From 6bf037fbd8fd9b31ecc45e09cf02267c271e6c49 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Sun, 9 Mar 2025 22:23:27 +0100 Subject: [PATCH 001/128] Add reference to main project from `TickAPI.Tests`. --- TickAPI/TickAPI.Tests/TickAPI.Tests.csproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/TickAPI/TickAPI.Tests/TickAPI.Tests.csproj b/TickAPI/TickAPI.Tests/TickAPI.Tests.csproj index 95d873d..d15cfe8 100644 --- a/TickAPI/TickAPI.Tests/TickAPI.Tests.csproj +++ b/TickAPI/TickAPI.Tests/TickAPI.Tests.csproj @@ -18,4 +18,8 @@ + + + + From d6da69b784d36d9c29db96b9d0a3d3ad49c506aa Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Sun, 9 Mar 2025 22:24:55 +0100 Subject: [PATCH 002/128] Implement result pattern --- TickAPI/TickAPI/Common/Result/Result.cs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/TickAPI/TickAPI/Common/Result/Result.cs b/TickAPI/TickAPI/Common/Result/Result.cs index 9e196f8..553bad6 100644 --- a/TickAPI/TickAPI/Common/Result/Result.cs +++ b/TickAPI/TickAPI/Common/Result/Result.cs @@ -1,6 +1,28 @@ namespace TickAPI.Common.Result; -public class Result +public record Result { + public bool IsSuccess { get; } + public bool IsError => !IsSuccess; + public T? Value { get; } + public int StatusCode { get; } + public string ErrorMsg { get; } + private Result(bool isSuccess, T? value = default, int statusCode = StatusCodes.Status200OK, string errorMsg = "") + { + IsSuccess = isSuccess; + Value = value; + StatusCode = statusCode; + ErrorMsg = errorMsg; + } + + public static Result Success(T value) + { + return new Result(true, value); + } + + public static Result Failure(int statusCode, string errorMsg) + { + return new Result(false, default, statusCode, errorMsg); + } } \ No newline at end of file From ea39aad95452af619ac356caec7c03fe5689ae02 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Sun, 9 Mar 2025 22:25:28 +0100 Subject: [PATCH 003/128] Create tests for `Result` --- .../Common/Result/ResultTests.cs | 34 +++++++++++++++++++ TickAPI/TickAPI.Tests/UnitTest1.cs | 9 ----- 2 files changed, 34 insertions(+), 9 deletions(-) create mode 100644 TickAPI/TickAPI.Tests/Common/Result/ResultTests.cs delete mode 100644 TickAPI/TickAPI.Tests/UnitTest1.cs diff --git a/TickAPI/TickAPI.Tests/Common/Result/ResultTests.cs b/TickAPI/TickAPI.Tests/Common/Result/ResultTests.cs new file mode 100644 index 0000000..d0da236 --- /dev/null +++ b/TickAPI/TickAPI.Tests/Common/Result/ResultTests.cs @@ -0,0 +1,34 @@ +using TickAPI.Common.Result; + +namespace TickAPI.Tests.Common.Result; + +public class ResultTests +{ + [Fact] + public void Success_ShouldReturnResultWithValue() + { + const int value = 123; + + var result = Result.Success(value); + + Assert.Equal(value, result.Value); + Assert.True(result.IsSuccess); + Assert.False(result.IsError); + Assert.Equal("", result.ErrorMsg); + Assert.Equal(200, result.StatusCode); + } + + [Fact] + public void Failure_ShouldReturnResultWithError() + { + const int statusCode = 500; + const string errorMsg = "example error msg"; + + var result = Result.Failure(500, errorMsg); + + Assert.True(result.IsError); + Assert.False(result.IsSuccess); + Assert.Equal(errorMsg, result.ErrorMsg); + Assert.Equal(statusCode, result.StatusCode); + } +} \ No newline at end of file diff --git a/TickAPI/TickAPI.Tests/UnitTest1.cs b/TickAPI/TickAPI.Tests/UnitTest1.cs deleted file mode 100644 index a06b7b3..0000000 --- a/TickAPI/TickAPI.Tests/UnitTest1.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace TickAPI.Tests; - -public class UnitTest1 -{ - [Fact] - public void Test1() - { - } -} \ No newline at end of file From 1430f959c652d3102cc9139d3ae4b7d36f665e4d Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Mon, 10 Mar 2025 13:39:04 +0100 Subject: [PATCH 004/128] Add information about external services to `README.md` --- README.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 05eaf91..a869df0 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,15 @@ Resellio API is the backend service for the Resellio application, providing an i - dotnet version >= `9.0` - docker version >= `28.0` +### External Services + +This project uses the following external services: + +- **PostgreSQL** – as the main relational database. +- **Redis** – for caching and other fast in-memory operations. + +Both services are managed using **Docker Compose**, and are defined in the `docker-compose.yml` file. When running locally, Docker will automatically provision and run these services. + ### Running locally 1. Clone the repository: @@ -23,13 +32,12 @@ Resellio API is the backend service for the Resellio application, providing an i ```bash docker compose up ``` - + 3. Set up environment variables: Create an `appsettings.json` file in the root of the project, following the structure of `appsettings.example.json` found in `TickAPI/TickAPI/appsettings.example.json`. - 4. Run application: ```bash From 1f9ee2a023d08447c66039bb28aa97e6deaffc91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Mon, 10 Mar 2025 21:24:06 +0100 Subject: [PATCH 005/128] Add EF migrations to models, add address, category and ticketType model, update connectionstring in appsettings --- TickAPI/TickAPI.Tests/TickAPI.Tests.csproj | 6 + TickAPI/TickAPI/Admins/Models/Admin.cs | 8 +- TickAPI/TickAPI/Categories/Models/Category.cs | 10 + .../TickApiDbContext/TickApiDbContext.cs | 9 +- TickAPI/TickAPI/Customers/Models/Customer.cs | 13 +- TickAPI/TickAPI/Events/Models/Address.cs | 13 + TickAPI/TickAPI/Events/Models/Event.cs | 25 +- ...0250310201301_InitialMigration.Designer.cs | 392 ++++++++++++++++++ .../20250310201301_InitialMigration.cs | 263 ++++++++++++ .../TickApiDbContextModelSnapshot.cs | 389 +++++++++++++++++ .../TickAPI/Organizers/Models/Organizer.cs | 16 +- TickAPI/TickAPI/Program.cs | 7 + TickAPI/TickAPI/TickAPI.csproj | 6 + .../TickAPI/TicketTypes/Models/TicketType.cs | 16 + TickAPI/TickAPI/Tickets/Models/Ticket.cs | 12 +- TickAPI/TickAPI/appsettings.example.json | 6 +- 16 files changed, 1180 insertions(+), 11 deletions(-) create mode 100644 TickAPI/TickAPI/Categories/Models/Category.cs create mode 100644 TickAPI/TickAPI/Events/Models/Address.cs create mode 100644 TickAPI/TickAPI/Migrations/20250310201301_InitialMigration.Designer.cs create mode 100644 TickAPI/TickAPI/Migrations/20250310201301_InitialMigration.cs create mode 100644 TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs create mode 100644 TickAPI/TickAPI/TicketTypes/Models/TicketType.cs diff --git a/TickAPI/TickAPI.Tests/TickAPI.Tests.csproj b/TickAPI/TickAPI.Tests/TickAPI.Tests.csproj index 95d873d..0496e4e 100644 --- a/TickAPI/TickAPI.Tests/TickAPI.Tests.csproj +++ b/TickAPI/TickAPI.Tests/TickAPI.Tests.csproj @@ -9,7 +9,13 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/TickAPI/TickAPI/Admins/Models/Admin.cs b/TickAPI/TickAPI/Admins/Models/Admin.cs index e629518..ea27dad 100644 --- a/TickAPI/TickAPI/Admins/Models/Admin.cs +++ b/TickAPI/TickAPI/Admins/Models/Admin.cs @@ -2,5 +2,11 @@ public class Admin { - + public Guid Id { get; set; } + public String Email { get; set; } + public String Login { get; set; } + public String PasswordHash { get; set; } + public String FirstName { get; set; } + public String LastName { get; set; } + public DateTime CreationDate { get; set; } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Categories/Models/Category.cs b/TickAPI/TickAPI/Categories/Models/Category.cs new file mode 100644 index 0000000..649e446 --- /dev/null +++ b/TickAPI/TickAPI/Categories/Models/Category.cs @@ -0,0 +1,10 @@ +using TickAPI.Events.Models; + +namespace TickAPI.Categories.Models; + +public class Category +{ + public Guid Id { get; set; } + public String CategoryName { get; set; } + public ICollection Events { get; set; } +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/TickApiDbContext/TickApiDbContext.cs b/TickAPI/TickAPI/Common/TickApiDbContext/TickApiDbContext.cs index 10d1a6c..51ad757 100644 --- a/TickAPI/TickAPI/Common/TickApiDbContext/TickApiDbContext.cs +++ b/TickAPI/TickAPI/Common/TickApiDbContext/TickApiDbContext.cs @@ -1,19 +1,26 @@ using Microsoft.EntityFrameworkCore; using TickAPI.Admins.Models; +using TickAPI.Categories.Models; using TickAPI.Customers.Models; using TickAPI.Events.Models; using TickAPI.Organizers.Models; using TickAPI.Tickets.Models; +using TickAPI.TicketTypes.Models; namespace TickAPI.Common.TickApiDbContext; public class TickApiDbContext : DbContext { - public TickApiDbContext(DbContextOptions options) : base(options) { } + public TickApiDbContext(DbContextOptions options) : base(options) + { } + public DbSet Admins { get; set; } public DbSet Customers { get; set; } public DbSet Events { get; set; } public DbSet Organizers { get; set; } public DbSet Tickets { get; set; } + public DbSet TicketTypes { get; set; } + public DbSet
Addresses { get; set; } + public DbSet Categories { get; set; } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Customers/Models/Customer.cs b/TickAPI/TickAPI/Customers/Models/Customer.cs index 7b19737..575dd93 100644 --- a/TickAPI/TickAPI/Customers/Models/Customer.cs +++ b/TickAPI/TickAPI/Customers/Models/Customer.cs @@ -1,6 +1,15 @@ -namespace TickAPI.Customers.Models; +using TickAPI.Tickets.Models; + +namespace TickAPI.Customers.Models; public class Customer { - + public String Id { get; set; } + public String Email { get; set; } + public String Login { get; set; } + public String PasswordHash { get; set; } + public String FirstName { get; set; } + public String LastName { get; set; } + public DateTime CreationDate { get; set; } + public ICollection Tickets { get; set; } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/Models/Address.cs b/TickAPI/TickAPI/Events/Models/Address.cs new file mode 100644 index 0000000..89e1cf6 --- /dev/null +++ b/TickAPI/TickAPI/Events/Models/Address.cs @@ -0,0 +1,13 @@ +namespace TickAPI.Events.Models; + +public class Address +{ + public Guid Id { get; set; } + public String Country { get; set; } + public String City { get; set; } + public String Street { get; set; } + public uint HouseNumber { get; set; } + public uint FlatNumber { get; set; } + public String PostalCode { get; set; } + public Event Event { get; set; } +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/Models/Event.cs b/TickAPI/TickAPI/Events/Models/Event.cs index 2008893..2237b9f 100644 --- a/TickAPI/TickAPI/Events/Models/Event.cs +++ b/TickAPI/TickAPI/Events/Models/Event.cs @@ -1,6 +1,27 @@ -namespace TickAPI.Events.Models; +using TickAPI.Organizers.Models; +using TickAPI.Categories.Models; +using TickAPI.TicketTypes.Models; + +namespace TickAPI.Events.Models; public class Event { - + public Guid Id { get; set; } + public String Name { get; set; } + public String Description { get; set; } + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } + public uint MinimumAge { get; set; } + public Organizer Organizer { get; set; } + public ICollection Categories { get; set; } + public ICollection TicketTypes { get; set; } + public EventStatus EventStatus { get; set; } +} + +public enum EventStatus +{ + TICKETS_AVAILABE, + SOLD_OUT, + IN_PROGRESS, + FINISHED } \ No newline at end of file diff --git a/TickAPI/TickAPI/Migrations/20250310201301_InitialMigration.Designer.cs b/TickAPI/TickAPI/Migrations/20250310201301_InitialMigration.Designer.cs new file mode 100644 index 0000000..f4a65a5 --- /dev/null +++ b/TickAPI/TickAPI/Migrations/20250310201301_InitialMigration.Designer.cs @@ -0,0 +1,392 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TickAPI.Common.TickApiDbContext; + +#nullable disable + +namespace TickAPI.Migrations +{ + [DbContext(typeof(TickApiDbContext))] + [Migration("20250310201301_InitialMigration")] + partial class InitialMigration + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("CategoryEvent", b => + { + b.Property("CategoriesId") + .HasColumnType("uuid"); + + b.Property("EventsId") + .HasColumnType("uuid"); + + b.HasKey("CategoriesId", "EventsId"); + + b.HasIndex("EventsId"); + + b.ToTable("CategoryEvent"); + }); + + modelBuilder.Entity("TickAPI.Admins.Models.Admin", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Login") + .IsRequired() + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Admins"); + }); + + modelBuilder.Entity("TickAPI.Categories.Models.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CategoryName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("TickAPI.Customers.Models.Customer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Login") + .IsRequired() + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("City") + .IsRequired() + .HasColumnType("text"); + + b.Property("Country") + .IsRequired() + .HasColumnType("text"); + + b.Property("EventId") + .HasColumnType("uuid"); + + b.Property("FlatNumber") + .HasColumnType("bigint"); + + b.Property("HouseNumber") + .HasColumnType("bigint"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("Street") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EventId"); + + b.ToTable("Addresses"); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EventStatus") + .HasColumnType("integer"); + + b.Property("MinimumAge") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrganizerId") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizerId"); + + b.ToTable("Events"); + }); + + modelBuilder.Entity("TickAPI.Organizers.Models.Organizer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Login") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrganizerName") + .IsRequired() + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Organizers"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AvailableForm") + .HasColumnType("timestamp with time zone"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("EventId") + .HasColumnType("uuid"); + + b.Property("MaxCount") + .HasColumnType("bigint"); + + b.Property("Price") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("EventId"); + + b.ToTable("TicketTypes"); + }); + + modelBuilder.Entity("TickAPI.Tickets.Models.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ForResell") + .HasColumnType("boolean"); + + b.Property("NameOnTicket") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Seats") + .IsRequired() + .HasColumnType("text"); + + b.Property("TypeId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("TypeId"); + + b.ToTable("Tickets"); + }); + + modelBuilder.Entity("CategoryEvent", b => + { + b.HasOne("TickAPI.Categories.Models.Category", null) + .WithMany() + .HasForeignKey("CategoriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TickAPI.Events.Models.Event", null) + .WithMany() + .HasForeignKey("EventsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Address", b => + { + b.HasOne("TickAPI.Events.Models.Event", "Event") + .WithMany() + .HasForeignKey("EventId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Event"); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.HasOne("TickAPI.Organizers.Models.Organizer", "Organizer") + .WithMany("Events") + .HasForeignKey("OrganizerId"); + + b.Navigation("Organizer"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.HasOne("TickAPI.Events.Models.Event", "Event") + .WithMany("TicketTypes") + .HasForeignKey("EventId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Event"); + }); + + modelBuilder.Entity("TickAPI.Tickets.Models.Ticket", b => + { + b.HasOne("TickAPI.Customers.Models.Customer", "Owner") + .WithMany("Tickets") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TickAPI.TicketTypes.Models.TicketType", "Type") + .WithMany("Tickets") + .HasForeignKey("TypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("Type"); + }); + + modelBuilder.Entity("TickAPI.Customers.Models.Customer", b => + { + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.Navigation("TicketTypes"); + }); + + modelBuilder.Entity("TickAPI.Organizers.Models.Organizer", b => + { + b.Navigation("Events"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.Navigation("Tickets"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TickAPI/TickAPI/Migrations/20250310201301_InitialMigration.cs b/TickAPI/TickAPI/Migrations/20250310201301_InitialMigration.cs new file mode 100644 index 0000000..af588d7 --- /dev/null +++ b/TickAPI/TickAPI/Migrations/20250310201301_InitialMigration.cs @@ -0,0 +1,263 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TickAPI.Migrations +{ + /// + public partial class InitialMigration : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Admins", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Email = table.Column(type: "text", nullable: false), + Login = table.Column(type: "text", nullable: false), + PasswordHash = table.Column(type: "text", nullable: false), + FirstName = table.Column(type: "text", nullable: false), + LastName = table.Column(type: "text", nullable: false), + CreationDate = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Admins", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Categories", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CategoryName = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Categories", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Customers", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + Email = table.Column(type: "text", nullable: false), + Login = table.Column(type: "text", nullable: false), + PasswordHash = table.Column(type: "text", nullable: false), + FirstName = table.Column(type: "text", nullable: false), + LastName = table.Column(type: "text", nullable: false), + CreationDate = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Customers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Organizers", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + Email = table.Column(type: "text", nullable: false), + Login = table.Column(type: "text", nullable: false), + PasswordHash = table.Column(type: "text", nullable: false), + FirstName = table.Column(type: "text", nullable: false), + LastName = table.Column(type: "text", nullable: false), + CreationDate = table.Column(type: "timestamp with time zone", nullable: false), + OrganizerName = table.Column(type: "text", nullable: false), + IsVerified = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Organizers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Events", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: false), + Description = table.Column(type: "text", nullable: false), + StartDate = table.Column(type: "timestamp with time zone", nullable: false), + EndDate = table.Column(type: "timestamp with time zone", nullable: false), + MinimumAge = table.Column(type: "bigint", nullable: false), + OrganizerId = table.Column(type: "text", nullable: true), + EventStatus = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Events", x => x.Id); + table.ForeignKey( + name: "FK_Events_Organizers_OrganizerId", + column: x => x.OrganizerId, + principalTable: "Organizers", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "Addresses", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Country = table.Column(type: "text", nullable: false), + City = table.Column(type: "text", nullable: false), + Street = table.Column(type: "text", nullable: false), + HouseNumber = table.Column(type: "bigint", nullable: false), + FlatNumber = table.Column(type: "bigint", nullable: false), + PostalCode = table.Column(type: "text", nullable: false), + EventId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Addresses", x => x.Id); + table.ForeignKey( + name: "FK_Addresses_Events_EventId", + column: x => x.EventId, + principalTable: "Events", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "CategoryEvent", + columns: table => new + { + CategoriesId = table.Column(type: "uuid", nullable: false), + EventsId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CategoryEvent", x => new { x.CategoriesId, x.EventsId }); + table.ForeignKey( + name: "FK_CategoryEvent_Categories_CategoriesId", + column: x => x.CategoriesId, + principalTable: "Categories", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_CategoryEvent_Events_EventsId", + column: x => x.EventsId, + principalTable: "Events", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "TicketTypes", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + EventId = table.Column(type: "uuid", nullable: false), + Description = table.Column(type: "text", nullable: false), + MaxCount = table.Column(type: "bigint", nullable: false), + Price = table.Column(type: "bigint", nullable: false), + Currency = table.Column(type: "text", nullable: false), + AvailableForm = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TicketTypes", x => x.Id); + table.ForeignKey( + name: "FK_TicketTypes_Events_EventId", + column: x => x.EventId, + principalTable: "Events", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Tickets", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + TypeId = table.Column(type: "uuid", nullable: false), + OwnerId = table.Column(type: "text", nullable: false), + NameOnTicket = table.Column(type: "text", nullable: false), + Seats = table.Column(type: "text", nullable: false), + ForResell = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Tickets", x => x.Id); + table.ForeignKey( + name: "FK_Tickets_Customers_OwnerId", + column: x => x.OwnerId, + principalTable: "Customers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Tickets_TicketTypes_TypeId", + column: x => x.TypeId, + principalTable: "TicketTypes", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Addresses_EventId", + table: "Addresses", + column: "EventId"); + + migrationBuilder.CreateIndex( + name: "IX_CategoryEvent_EventsId", + table: "CategoryEvent", + column: "EventsId"); + + migrationBuilder.CreateIndex( + name: "IX_Events_OrganizerId", + table: "Events", + column: "OrganizerId"); + + migrationBuilder.CreateIndex( + name: "IX_Tickets_OwnerId", + table: "Tickets", + column: "OwnerId"); + + migrationBuilder.CreateIndex( + name: "IX_Tickets_TypeId", + table: "Tickets", + column: "TypeId"); + + migrationBuilder.CreateIndex( + name: "IX_TicketTypes_EventId", + table: "TicketTypes", + column: "EventId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Addresses"); + + migrationBuilder.DropTable( + name: "Admins"); + + migrationBuilder.DropTable( + name: "CategoryEvent"); + + migrationBuilder.DropTable( + name: "Tickets"); + + migrationBuilder.DropTable( + name: "Categories"); + + migrationBuilder.DropTable( + name: "Customers"); + + migrationBuilder.DropTable( + name: "TicketTypes"); + + migrationBuilder.DropTable( + name: "Events"); + + migrationBuilder.DropTable( + name: "Organizers"); + } + } +} diff --git a/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs b/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs new file mode 100644 index 0000000..2ab53f2 --- /dev/null +++ b/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs @@ -0,0 +1,389 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TickAPI.Common.TickApiDbContext; + +#nullable disable + +namespace TickAPI.Migrations +{ + [DbContext(typeof(TickApiDbContext))] + partial class TickApiDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("CategoryEvent", b => + { + b.Property("CategoriesId") + .HasColumnType("uuid"); + + b.Property("EventsId") + .HasColumnType("uuid"); + + b.HasKey("CategoriesId", "EventsId"); + + b.HasIndex("EventsId"); + + b.ToTable("CategoryEvent"); + }); + + modelBuilder.Entity("TickAPI.Admins.Models.Admin", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Login") + .IsRequired() + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Admins"); + }); + + modelBuilder.Entity("TickAPI.Categories.Models.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CategoryName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("TickAPI.Customers.Models.Customer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Login") + .IsRequired() + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("City") + .IsRequired() + .HasColumnType("text"); + + b.Property("Country") + .IsRequired() + .HasColumnType("text"); + + b.Property("EventId") + .HasColumnType("uuid"); + + b.Property("FlatNumber") + .HasColumnType("bigint"); + + b.Property("HouseNumber") + .HasColumnType("bigint"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("Street") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EventId"); + + b.ToTable("Addresses"); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EventStatus") + .HasColumnType("integer"); + + b.Property("MinimumAge") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrganizerId") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizerId"); + + b.ToTable("Events"); + }); + + modelBuilder.Entity("TickAPI.Organizers.Models.Organizer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Login") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrganizerName") + .IsRequired() + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Organizers"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AvailableForm") + .HasColumnType("timestamp with time zone"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("EventId") + .HasColumnType("uuid"); + + b.Property("MaxCount") + .HasColumnType("bigint"); + + b.Property("Price") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("EventId"); + + b.ToTable("TicketTypes"); + }); + + modelBuilder.Entity("TickAPI.Tickets.Models.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ForResell") + .HasColumnType("boolean"); + + b.Property("NameOnTicket") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Seats") + .IsRequired() + .HasColumnType("text"); + + b.Property("TypeId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("TypeId"); + + b.ToTable("Tickets"); + }); + + modelBuilder.Entity("CategoryEvent", b => + { + b.HasOne("TickAPI.Categories.Models.Category", null) + .WithMany() + .HasForeignKey("CategoriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TickAPI.Events.Models.Event", null) + .WithMany() + .HasForeignKey("EventsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Address", b => + { + b.HasOne("TickAPI.Events.Models.Event", "Event") + .WithMany() + .HasForeignKey("EventId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Event"); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.HasOne("TickAPI.Organizers.Models.Organizer", "Organizer") + .WithMany("Events") + .HasForeignKey("OrganizerId"); + + b.Navigation("Organizer"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.HasOne("TickAPI.Events.Models.Event", "Event") + .WithMany("TicketTypes") + .HasForeignKey("EventId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Event"); + }); + + modelBuilder.Entity("TickAPI.Tickets.Models.Ticket", b => + { + b.HasOne("TickAPI.Customers.Models.Customer", "Owner") + .WithMany("Tickets") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TickAPI.TicketTypes.Models.TicketType", "Type") + .WithMany("Tickets") + .HasForeignKey("TypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("Type"); + }); + + modelBuilder.Entity("TickAPI.Customers.Models.Customer", b => + { + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.Navigation("TicketTypes"); + }); + + modelBuilder.Entity("TickAPI.Organizers.Models.Organizer", b => + { + b.Navigation("Events"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.Navigation("Tickets"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TickAPI/TickAPI/Organizers/Models/Organizer.cs b/TickAPI/TickAPI/Organizers/Models/Organizer.cs index 56df9ab..237eb70 100644 --- a/TickAPI/TickAPI/Organizers/Models/Organizer.cs +++ b/TickAPI/TickAPI/Organizers/Models/Organizer.cs @@ -1,6 +1,18 @@ -namespace TickAPI.Organizers.Models; +using System.Reflection.Metadata.Ecma335; +using TickAPI.Events.Models; + +namespace TickAPI.Organizers.Models; public class Organizer { - + public String Id { get; set; } + public String Email { get; set; } + public String Login { get; set; } + public String PasswordHash { get; set; } + public String FirstName { get; set; } + public String LastName { get; set; } + public DateTime CreationDate { get; set; } + public String OrganizerName { get; set; } + public bool IsVerified { get; set; } + public ICollection Events { get; set; } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Program.cs b/TickAPI/TickAPI/Program.cs index 3b4c236..1562524 100644 --- a/TickAPI/TickAPI/Program.cs +++ b/TickAPI/TickAPI/Program.cs @@ -1,3 +1,4 @@ +using Microsoft.EntityFrameworkCore; using TickAPI; using TickAPI.Admins.Abstractions; using TickAPI.Admins.Repositories; @@ -6,6 +7,7 @@ using TickAPI.Common.Auth.Services; using TickAPI.Common.Pagination.Abstractions; using TickAPI.Common.Pagination.Services; +using TickAPI.Common.TickApiDbContext; using TickAPI.Customers.Abstractions; using TickAPI.Customers.Repositories; using TickAPI.Customers.Services; @@ -59,6 +61,11 @@ builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); +builder.Services.AddDbContext(options => +{ + options.UseNpgsql(builder.Configuration.GetConnectionString("ResellioDatabase")); +}); + // Create CORS policy builder.Services.AddCors(options => { diff --git a/TickAPI/TickAPI/TickAPI.csproj b/TickAPI/TickAPI/TickAPI.csproj index 66af13d..8fbb6b2 100644 --- a/TickAPI/TickAPI/TickAPI.csproj +++ b/TickAPI/TickAPI/TickAPI.csproj @@ -8,6 +8,12 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/TickAPI/TickAPI/TicketTypes/Models/TicketType.cs b/TickAPI/TickAPI/TicketTypes/Models/TicketType.cs new file mode 100644 index 0000000..561a6b4 --- /dev/null +++ b/TickAPI/TickAPI/TicketTypes/Models/TicketType.cs @@ -0,0 +1,16 @@ +using TickAPI.Events.Models; +using TickAPI.Tickets.Models; + +namespace TickAPI.TicketTypes.Models; + +public class TicketType +{ + public Guid Id { get; set; } + public Event Event { get; set; } + public String Description { get; set; } + public uint MaxCount { get; set; } + public uint Price { get; set; } + public String Currency { get; set; } + public DateTime AvailableForm { get; set; } + public ICollection Tickets { get; set; } +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Models/Ticket.cs b/TickAPI/TickAPI/Tickets/Models/Ticket.cs index a72e26b..e81f5c3 100644 --- a/TickAPI/TickAPI/Tickets/Models/Ticket.cs +++ b/TickAPI/TickAPI/Tickets/Models/Ticket.cs @@ -1,6 +1,14 @@ -namespace TickAPI.Tickets.Models; +using TickAPI.Customers.Models; +using TickAPI.TicketTypes.Models; + +namespace TickAPI.Tickets.Models; public class Ticket { - + public Guid Id { get; set; } + public TicketType Type { get; set; } + public Customer Owner { get; set; } + public String NameOnTicket { get; set; } + public String Seats { get; set; } + public bool ForResell { get; set; } } \ No newline at end of file diff --git a/TickAPI/TickAPI/appsettings.example.json b/TickAPI/TickAPI/appsettings.example.json index 0602d10..8612a3e 100644 --- a/TickAPI/TickAPI/appsettings.example.json +++ b/TickAPI/TickAPI/appsettings.example.json @@ -9,5 +9,9 @@ "AllowedOrigins": [ "https://example.com", "https://another-site.com" - ] + ], + + "ConnectionStrings": { + "ResellioDatabase": "Host=localhost; Database=resellioDB; Username=postgres; Password=password; Port=5432" + } } From 552c60bb1dad7d1d8084b5897a2a2805b7a3fdd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Mon, 10 Mar 2025 22:10:12 +0100 Subject: [PATCH 006/128] removed passwordHashes form Models --- TickAPI/TickAPI/Admins/Models/Admin.cs | 1 - TickAPI/TickAPI/Customers/Models/Customer.cs | 3 +- TickAPI/TickAPI/Events/Models/Address.cs | 1 - TickAPI/TickAPI/Events/Models/Event.cs | 1 + ...25_UpdatedPasswordHashAnGuids.Designer.cs} | 66 +++++++-------- ...50310210825_UpdatedPasswordHashAnGuids.cs} | 80 +++++++++---------- .../TickApiDbContextModelSnapshot.cs | 62 ++++++-------- .../TickAPI/Organizers/Models/Organizer.cs | 3 +- .../TickAPI/TicketTypes/Models/TicketType.cs | 2 +- 9 files changed, 95 insertions(+), 124 deletions(-) rename TickAPI/TickAPI/Migrations/{20250310201301_InitialMigration.Designer.cs => 20250310210825_UpdatedPasswordHashAnGuids.Designer.cs} (88%) rename TickAPI/TickAPI/Migrations/{20250310201301_InitialMigration.cs => 20250310210825_UpdatedPasswordHashAnGuids.cs} (90%) diff --git a/TickAPI/TickAPI/Admins/Models/Admin.cs b/TickAPI/TickAPI/Admins/Models/Admin.cs index ea27dad..eef3333 100644 --- a/TickAPI/TickAPI/Admins/Models/Admin.cs +++ b/TickAPI/TickAPI/Admins/Models/Admin.cs @@ -5,7 +5,6 @@ public class Admin public Guid Id { get; set; } public String Email { get; set; } public String Login { get; set; } - public String PasswordHash { get; set; } public String FirstName { get; set; } public String LastName { get; set; } public DateTime CreationDate { get; set; } diff --git a/TickAPI/TickAPI/Customers/Models/Customer.cs b/TickAPI/TickAPI/Customers/Models/Customer.cs index 575dd93..e245fd0 100644 --- a/TickAPI/TickAPI/Customers/Models/Customer.cs +++ b/TickAPI/TickAPI/Customers/Models/Customer.cs @@ -4,10 +4,9 @@ namespace TickAPI.Customers.Models; public class Customer { - public String Id { get; set; } + public Guid Id { get; set; } public String Email { get; set; } public String Login { get; set; } - public String PasswordHash { get; set; } public String FirstName { get; set; } public String LastName { get; set; } public DateTime CreationDate { get; set; } diff --git a/TickAPI/TickAPI/Events/Models/Address.cs b/TickAPI/TickAPI/Events/Models/Address.cs index 89e1cf6..40dd2da 100644 --- a/TickAPI/TickAPI/Events/Models/Address.cs +++ b/TickAPI/TickAPI/Events/Models/Address.cs @@ -9,5 +9,4 @@ public class Address public uint HouseNumber { get; set; } public uint FlatNumber { get; set; } public String PostalCode { get; set; } - public Event Event { get; set; } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/Models/Event.cs b/TickAPI/TickAPI/Events/Models/Event.cs index 2237b9f..02cc904 100644 --- a/TickAPI/TickAPI/Events/Models/Event.cs +++ b/TickAPI/TickAPI/Events/Models/Event.cs @@ -16,6 +16,7 @@ public class Event public ICollection Categories { get; set; } public ICollection TicketTypes { get; set; } public EventStatus EventStatus { get; set; } + public Address Address { get; set; } } public enum EventStatus diff --git a/TickAPI/TickAPI/Migrations/20250310201301_InitialMigration.Designer.cs b/TickAPI/TickAPI/Migrations/20250310210825_UpdatedPasswordHashAnGuids.Designer.cs similarity index 88% rename from TickAPI/TickAPI/Migrations/20250310201301_InitialMigration.Designer.cs rename to TickAPI/TickAPI/Migrations/20250310210825_UpdatedPasswordHashAnGuids.Designer.cs index f4a65a5..e4ee6c5 100644 --- a/TickAPI/TickAPI/Migrations/20250310201301_InitialMigration.Designer.cs +++ b/TickAPI/TickAPI/Migrations/20250310210825_UpdatedPasswordHashAnGuids.Designer.cs @@ -12,8 +12,8 @@ namespace TickAPI.Migrations { [DbContext(typeof(TickApiDbContext))] - [Migration("20250310201301_InitialMigration")] - partial class InitialMigration + [Migration("20250310210825_UpdatedPasswordHashAnGuids")] + partial class UpdatedPasswordHashAnGuids { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -65,10 +65,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); - b.Property("PasswordHash") - .IsRequired() - .HasColumnType("text"); - b.HasKey("Id"); b.ToTable("Admins"); @@ -91,8 +87,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("TickAPI.Customers.Models.Customer", b => { - b.Property("Id") - .HasColumnType("text"); + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); b.Property("CreationDate") .HasColumnType("timestamp with time zone"); @@ -113,10 +110,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); - b.Property("PasswordHash") - .IsRequired() - .HasColumnType("text"); - b.HasKey("Id"); b.ToTable("Customers"); @@ -136,9 +129,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); - b.Property("EventId") - .HasColumnType("uuid"); - b.Property("FlatNumber") .HasColumnType("bigint"); @@ -155,8 +145,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.HasIndex("EventId"); - b.ToTable("Addresses"); }); @@ -166,6 +154,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("uuid"); + b.Property("AddressId") + .HasColumnType("uuid"); + b.Property("Description") .IsRequired() .HasColumnType("text"); @@ -183,14 +174,16 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); - b.Property("OrganizerId") - .HasColumnType("text"); + b.Property("OrganizerId") + .HasColumnType("uuid"); b.Property("StartDate") .HasColumnType("timestamp with time zone"); b.HasKey("Id"); + b.HasIndex("AddressId"); + b.HasIndex("OrganizerId"); b.ToTable("Events"); @@ -198,8 +191,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("TickAPI.Organizers.Models.Organizer", b => { - b.Property("Id") - .HasColumnType("text"); + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); b.Property("CreationDate") .HasColumnType("timestamp with time zone"); @@ -227,10 +221,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); - b.Property("PasswordHash") - .IsRequired() - .HasColumnType("text"); - b.HasKey("Id"); b.ToTable("Organizers"); @@ -259,8 +249,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("MaxCount") .HasColumnType("bigint"); - b.Property("Price") - .HasColumnType("bigint"); + b.Property("Price") + .HasColumnType("numeric"); b.HasKey("Id"); @@ -282,9 +272,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); - b.Property("OwnerId") - .IsRequired() - .HasColumnType("text"); + b.Property("OwnerId") + .HasColumnType("uuid"); b.Property("Seats") .IsRequired() @@ -317,22 +306,21 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .IsRequired(); }); - modelBuilder.Entity("TickAPI.Events.Models.Address", b => + modelBuilder.Entity("TickAPI.Events.Models.Event", b => { - b.HasOne("TickAPI.Events.Models.Event", "Event") + b.HasOne("TickAPI.Events.Models.Address", "Address") .WithMany() - .HasForeignKey("EventId") + .HasForeignKey("AddressId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.Navigation("Event"); - }); - - modelBuilder.Entity("TickAPI.Events.Models.Event", b => - { b.HasOne("TickAPI.Organizers.Models.Organizer", "Organizer") .WithMany("Events") - .HasForeignKey("OrganizerId"); + .HasForeignKey("OrganizerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Address"); b.Navigation("Organizer"); }); diff --git a/TickAPI/TickAPI/Migrations/20250310201301_InitialMigration.cs b/TickAPI/TickAPI/Migrations/20250310210825_UpdatedPasswordHashAnGuids.cs similarity index 90% rename from TickAPI/TickAPI/Migrations/20250310201301_InitialMigration.cs rename to TickAPI/TickAPI/Migrations/20250310210825_UpdatedPasswordHashAnGuids.cs index af588d7..b2120b9 100644 --- a/TickAPI/TickAPI/Migrations/20250310201301_InitialMigration.cs +++ b/TickAPI/TickAPI/Migrations/20250310210825_UpdatedPasswordHashAnGuids.cs @@ -6,11 +6,28 @@ namespace TickAPI.Migrations { /// - public partial class InitialMigration : Migration + public partial class UpdatedPasswordHashAnGuids : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) { + migrationBuilder.CreateTable( + name: "Addresses", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Country = table.Column(type: "text", nullable: false), + City = table.Column(type: "text", nullable: false), + Street = table.Column(type: "text", nullable: false), + HouseNumber = table.Column(type: "bigint", nullable: false), + FlatNumber = table.Column(type: "bigint", nullable: false), + PostalCode = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Addresses", x => x.Id); + }); + migrationBuilder.CreateTable( name: "Admins", columns: table => new @@ -18,7 +35,6 @@ protected override void Up(MigrationBuilder migrationBuilder) Id = table.Column(type: "uuid", nullable: false), Email = table.Column(type: "text", nullable: false), Login = table.Column(type: "text", nullable: false), - PasswordHash = table.Column(type: "text", nullable: false), FirstName = table.Column(type: "text", nullable: false), LastName = table.Column(type: "text", nullable: false), CreationDate = table.Column(type: "timestamp with time zone", nullable: false) @@ -44,10 +60,9 @@ protected override void Up(MigrationBuilder migrationBuilder) name: "Customers", columns: table => new { - Id = table.Column(type: "text", nullable: false), + Id = table.Column(type: "uuid", nullable: false), Email = table.Column(type: "text", nullable: false), Login = table.Column(type: "text", nullable: false), - PasswordHash = table.Column(type: "text", nullable: false), FirstName = table.Column(type: "text", nullable: false), LastName = table.Column(type: "text", nullable: false), CreationDate = table.Column(type: "timestamp with time zone", nullable: false) @@ -61,10 +76,9 @@ protected override void Up(MigrationBuilder migrationBuilder) name: "Organizers", columns: table => new { - Id = table.Column(type: "text", nullable: false), + Id = table.Column(type: "uuid", nullable: false), Email = table.Column(type: "text", nullable: false), Login = table.Column(type: "text", nullable: false), - PasswordHash = table.Column(type: "text", nullable: false), FirstName = table.Column(type: "text", nullable: false), LastName = table.Column(type: "text", nullable: false), CreationDate = table.Column(type: "timestamp with time zone", nullable: false), @@ -86,39 +100,23 @@ protected override void Up(MigrationBuilder migrationBuilder) StartDate = table.Column(type: "timestamp with time zone", nullable: false), EndDate = table.Column(type: "timestamp with time zone", nullable: false), MinimumAge = table.Column(type: "bigint", nullable: false), - OrganizerId = table.Column(type: "text", nullable: true), - EventStatus = table.Column(type: "integer", nullable: false) + OrganizerId = table.Column(type: "uuid", nullable: false), + EventStatus = table.Column(type: "integer", nullable: false), + AddressId = table.Column(type: "uuid", nullable: false) }, constraints: table => { table.PrimaryKey("PK_Events", x => x.Id); + table.ForeignKey( + name: "FK_Events_Addresses_AddressId", + column: x => x.AddressId, + principalTable: "Addresses", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); table.ForeignKey( name: "FK_Events_Organizers_OrganizerId", column: x => x.OrganizerId, principalTable: "Organizers", - principalColumn: "Id"); - }); - - migrationBuilder.CreateTable( - name: "Addresses", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - Country = table.Column(type: "text", nullable: false), - City = table.Column(type: "text", nullable: false), - Street = table.Column(type: "text", nullable: false), - HouseNumber = table.Column(type: "bigint", nullable: false), - FlatNumber = table.Column(type: "bigint", nullable: false), - PostalCode = table.Column(type: "text", nullable: false), - EventId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Addresses", x => x.Id); - table.ForeignKey( - name: "FK_Addresses_Events_EventId", - column: x => x.EventId, - principalTable: "Events", principalColumn: "Id", onDelete: ReferentialAction.Cascade); }); @@ -155,7 +153,7 @@ protected override void Up(MigrationBuilder migrationBuilder) EventId = table.Column(type: "uuid", nullable: false), Description = table.Column(type: "text", nullable: false), MaxCount = table.Column(type: "bigint", nullable: false), - Price = table.Column(type: "bigint", nullable: false), + Price = table.Column(type: "numeric", nullable: false), Currency = table.Column(type: "text", nullable: false), AvailableForm = table.Column(type: "timestamp with time zone", nullable: false) }, @@ -176,7 +174,7 @@ protected override void Up(MigrationBuilder migrationBuilder) { Id = table.Column(type: "uuid", nullable: false), TypeId = table.Column(type: "uuid", nullable: false), - OwnerId = table.Column(type: "text", nullable: false), + OwnerId = table.Column(type: "uuid", nullable: false), NameOnTicket = table.Column(type: "text", nullable: false), Seats = table.Column(type: "text", nullable: false), ForResell = table.Column(type: "boolean", nullable: false) @@ -198,16 +196,16 @@ protected override void Up(MigrationBuilder migrationBuilder) onDelete: ReferentialAction.Cascade); }); - migrationBuilder.CreateIndex( - name: "IX_Addresses_EventId", - table: "Addresses", - column: "EventId"); - migrationBuilder.CreateIndex( name: "IX_CategoryEvent_EventsId", table: "CategoryEvent", column: "EventsId"); + migrationBuilder.CreateIndex( + name: "IX_Events_AddressId", + table: "Events", + column: "AddressId"); + migrationBuilder.CreateIndex( name: "IX_Events_OrganizerId", table: "Events", @@ -232,9 +230,6 @@ protected override void Up(MigrationBuilder migrationBuilder) /// protected override void Down(MigrationBuilder migrationBuilder) { - migrationBuilder.DropTable( - name: "Addresses"); - migrationBuilder.DropTable( name: "Admins"); @@ -256,6 +251,9 @@ protected override void Down(MigrationBuilder migrationBuilder) migrationBuilder.DropTable( name: "Events"); + migrationBuilder.DropTable( + name: "Addresses"); + migrationBuilder.DropTable( name: "Organizers"); } diff --git a/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs b/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs index 2ab53f2..1c09cce 100644 --- a/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs +++ b/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs @@ -62,10 +62,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); - b.Property("PasswordHash") - .IsRequired() - .HasColumnType("text"); - b.HasKey("Id"); b.ToTable("Admins"); @@ -88,8 +84,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("TickAPI.Customers.Models.Customer", b => { - b.Property("Id") - .HasColumnType("text"); + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); b.Property("CreationDate") .HasColumnType("timestamp with time zone"); @@ -110,10 +107,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); - b.Property("PasswordHash") - .IsRequired() - .HasColumnType("text"); - b.HasKey("Id"); b.ToTable("Customers"); @@ -133,9 +126,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); - b.Property("EventId") - .HasColumnType("uuid"); - b.Property("FlatNumber") .HasColumnType("bigint"); @@ -152,8 +142,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.HasIndex("EventId"); - b.ToTable("Addresses"); }); @@ -163,6 +151,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("uuid"); + b.Property("AddressId") + .HasColumnType("uuid"); + b.Property("Description") .IsRequired() .HasColumnType("text"); @@ -180,14 +171,16 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); - b.Property("OrganizerId") - .HasColumnType("text"); + b.Property("OrganizerId") + .HasColumnType("uuid"); b.Property("StartDate") .HasColumnType("timestamp with time zone"); b.HasKey("Id"); + b.HasIndex("AddressId"); + b.HasIndex("OrganizerId"); b.ToTable("Events"); @@ -195,8 +188,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("TickAPI.Organizers.Models.Organizer", b => { - b.Property("Id") - .HasColumnType("text"); + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); b.Property("CreationDate") .HasColumnType("timestamp with time zone"); @@ -224,10 +218,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); - b.Property("PasswordHash") - .IsRequired() - .HasColumnType("text"); - b.HasKey("Id"); b.ToTable("Organizers"); @@ -256,8 +246,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("MaxCount") .HasColumnType("bigint"); - b.Property("Price") - .HasColumnType("bigint"); + b.Property("Price") + .HasColumnType("numeric"); b.HasKey("Id"); @@ -279,9 +269,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); - b.Property("OwnerId") - .IsRequired() - .HasColumnType("text"); + b.Property("OwnerId") + .HasColumnType("uuid"); b.Property("Seats") .IsRequired() @@ -314,22 +303,21 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired(); }); - modelBuilder.Entity("TickAPI.Events.Models.Address", b => + modelBuilder.Entity("TickAPI.Events.Models.Event", b => { - b.HasOne("TickAPI.Events.Models.Event", "Event") + b.HasOne("TickAPI.Events.Models.Address", "Address") .WithMany() - .HasForeignKey("EventId") + .HasForeignKey("AddressId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.Navigation("Event"); - }); - - modelBuilder.Entity("TickAPI.Events.Models.Event", b => - { b.HasOne("TickAPI.Organizers.Models.Organizer", "Organizer") .WithMany("Events") - .HasForeignKey("OrganizerId"); + .HasForeignKey("OrganizerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Address"); b.Navigation("Organizer"); }); diff --git a/TickAPI/TickAPI/Organizers/Models/Organizer.cs b/TickAPI/TickAPI/Organizers/Models/Organizer.cs index 237eb70..f62e01b 100644 --- a/TickAPI/TickAPI/Organizers/Models/Organizer.cs +++ b/TickAPI/TickAPI/Organizers/Models/Organizer.cs @@ -5,10 +5,9 @@ namespace TickAPI.Organizers.Models; public class Organizer { - public String Id { get; set; } + public Guid Id { get; set; } public String Email { get; set; } public String Login { get; set; } - public String PasswordHash { get; set; } public String FirstName { get; set; } public String LastName { get; set; } public DateTime CreationDate { get; set; } diff --git a/TickAPI/TickAPI/TicketTypes/Models/TicketType.cs b/TickAPI/TickAPI/TicketTypes/Models/TicketType.cs index 561a6b4..2b4f0df 100644 --- a/TickAPI/TickAPI/TicketTypes/Models/TicketType.cs +++ b/TickAPI/TickAPI/TicketTypes/Models/TicketType.cs @@ -9,7 +9,7 @@ public class TicketType public Event Event { get; set; } public String Description { get; set; } public uint MaxCount { get; set; } - public uint Price { get; set; } + public decimal Price { get; set; } public String Currency { get; set; } public DateTime AvailableForm { get; set; } public ICollection Tickets { get; set; } From 6370a95a1cd63a5022e68e31a901af1a911ccffa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Tue, 11 Mar 2025 08:36:30 +0100 Subject: [PATCH 007/128] Fixed changes from PR comments --- TickAPI/TickAPI.Tests/TickAPI.Tests.csproj | 2 -- TickAPI/TickAPI/Admins/Models/Admin.cs | 3 --- TickAPI/TickAPI/Customers/Models/Customer.cs | 1 - TickAPI/TickAPI/Events/Models/Address.cs | 6 ++--- TickAPI/TickAPI/Events/Models/Event.cs | 8 +++--- ...250311073446_InitialMigration.Designer.cs} | 26 ++++--------------- ....cs => 20250311073446_InitialMigration.cs} | 16 +++++------- .../TickApiDbContextModelSnapshot.cs | 22 +++------------- .../TickAPI/TicketTypes/Models/TicketType.cs | 2 +- 9 files changed, 22 insertions(+), 64 deletions(-) rename TickAPI/TickAPI/Migrations/{20250310210825_UpdatedPasswordHashAnGuids.Designer.cs => 20250311073446_InitialMigration.Designer.cs} (93%) rename TickAPI/TickAPI/Migrations/{20250310210825_UpdatedPasswordHashAnGuids.cs => 20250311073446_InitialMigration.cs} (95%) diff --git a/TickAPI/TickAPI.Tests/TickAPI.Tests.csproj b/TickAPI/TickAPI.Tests/TickAPI.Tests.csproj index 0496e4e..2424eb9 100644 --- a/TickAPI/TickAPI.Tests/TickAPI.Tests.csproj +++ b/TickAPI/TickAPI.Tests/TickAPI.Tests.csproj @@ -14,8 +14,6 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - diff --git a/TickAPI/TickAPI/Admins/Models/Admin.cs b/TickAPI/TickAPI/Admins/Models/Admin.cs index eef3333..d7dd686 100644 --- a/TickAPI/TickAPI/Admins/Models/Admin.cs +++ b/TickAPI/TickAPI/Admins/Models/Admin.cs @@ -5,7 +5,4 @@ public class Admin public Guid Id { get; set; } public String Email { get; set; } public String Login { get; set; } - public String FirstName { get; set; } - public String LastName { get; set; } - public DateTime CreationDate { get; set; } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Customers/Models/Customer.cs b/TickAPI/TickAPI/Customers/Models/Customer.cs index e245fd0..619e62d 100644 --- a/TickAPI/TickAPI/Customers/Models/Customer.cs +++ b/TickAPI/TickAPI/Customers/Models/Customer.cs @@ -6,7 +6,6 @@ public class Customer { public Guid Id { get; set; } public String Email { get; set; } - public String Login { get; set; } public String FirstName { get; set; } public String LastName { get; set; } public DateTime CreationDate { get; set; } diff --git a/TickAPI/TickAPI/Events/Models/Address.cs b/TickAPI/TickAPI/Events/Models/Address.cs index 40dd2da..8c2cef5 100644 --- a/TickAPI/TickAPI/Events/Models/Address.cs +++ b/TickAPI/TickAPI/Events/Models/Address.cs @@ -5,8 +5,8 @@ public class Address public Guid Id { get; set; } public String Country { get; set; } public String City { get; set; } - public String Street { get; set; } - public uint HouseNumber { get; set; } - public uint FlatNumber { get; set; } + public String? Street { get; set; } + public uint? HouseNumber { get; set; } + public uint? FlatNumber { get; set; } public String PostalCode { get; set; } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/Models/Event.cs b/TickAPI/TickAPI/Events/Models/Event.cs index 02cc904..f6b85a5 100644 --- a/TickAPI/TickAPI/Events/Models/Event.cs +++ b/TickAPI/TickAPI/Events/Models/Event.cs @@ -21,8 +21,8 @@ public class Event public enum EventStatus { - TICKETS_AVAILABE, - SOLD_OUT, - IN_PROGRESS, - FINISHED + TicketsAvailable, + SoldOut, + InProgress, + Finished } \ No newline at end of file diff --git a/TickAPI/TickAPI/Migrations/20250310210825_UpdatedPasswordHashAnGuids.Designer.cs b/TickAPI/TickAPI/Migrations/20250311073446_InitialMigration.Designer.cs similarity index 93% rename from TickAPI/TickAPI/Migrations/20250310210825_UpdatedPasswordHashAnGuids.Designer.cs rename to TickAPI/TickAPI/Migrations/20250311073446_InitialMigration.Designer.cs index e4ee6c5..9bc4485 100644 --- a/TickAPI/TickAPI/Migrations/20250310210825_UpdatedPasswordHashAnGuids.Designer.cs +++ b/TickAPI/TickAPI/Migrations/20250311073446_InitialMigration.Designer.cs @@ -12,8 +12,8 @@ namespace TickAPI.Migrations { [DbContext(typeof(TickApiDbContext))] - [Migration("20250310210825_UpdatedPasswordHashAnGuids")] - partial class UpdatedPasswordHashAnGuids + [Migration("20250311073446_InitialMigration")] + partial class InitialMigration { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -46,21 +46,10 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("uuid"); - b.Property("CreationDate") - .HasColumnType("timestamp with time zone"); - b.Property("Email") .IsRequired() .HasColumnType("text"); - b.Property("FirstName") - .IsRequired() - .HasColumnType("text"); - - b.Property("LastName") - .IsRequired() - .HasColumnType("text"); - b.Property("Login") .IsRequired() .HasColumnType("text"); @@ -106,10 +95,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); - b.Property("Login") - .IsRequired() - .HasColumnType("text"); - b.HasKey("Id"); b.ToTable("Customers"); @@ -129,10 +114,10 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); - b.Property("FlatNumber") + b.Property("FlatNumber") .HasColumnType("bigint"); - b.Property("HouseNumber") + b.Property("HouseNumber") .HasColumnType("bigint"); b.Property("PostalCode") @@ -140,7 +125,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("text"); b.Property("Street") - .IsRequired() .HasColumnType("text"); b.HasKey("Id"); @@ -232,7 +216,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("uuid"); - b.Property("AvailableForm") + b.Property("AvailableFrom") .HasColumnType("timestamp with time zone"); b.Property("Currency") diff --git a/TickAPI/TickAPI/Migrations/20250310210825_UpdatedPasswordHashAnGuids.cs b/TickAPI/TickAPI/Migrations/20250311073446_InitialMigration.cs similarity index 95% rename from TickAPI/TickAPI/Migrations/20250310210825_UpdatedPasswordHashAnGuids.cs rename to TickAPI/TickAPI/Migrations/20250311073446_InitialMigration.cs index b2120b9..ead764e 100644 --- a/TickAPI/TickAPI/Migrations/20250310210825_UpdatedPasswordHashAnGuids.cs +++ b/TickAPI/TickAPI/Migrations/20250311073446_InitialMigration.cs @@ -6,7 +6,7 @@ namespace TickAPI.Migrations { /// - public partial class UpdatedPasswordHashAnGuids : Migration + public partial class InitialMigration : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) @@ -18,9 +18,9 @@ protected override void Up(MigrationBuilder migrationBuilder) Id = table.Column(type: "uuid", nullable: false), Country = table.Column(type: "text", nullable: false), City = table.Column(type: "text", nullable: false), - Street = table.Column(type: "text", nullable: false), - HouseNumber = table.Column(type: "bigint", nullable: false), - FlatNumber = table.Column(type: "bigint", nullable: false), + Street = table.Column(type: "text", nullable: true), + HouseNumber = table.Column(type: "bigint", nullable: true), + FlatNumber = table.Column(type: "bigint", nullable: true), PostalCode = table.Column(type: "text", nullable: false) }, constraints: table => @@ -34,10 +34,7 @@ protected override void Up(MigrationBuilder migrationBuilder) { Id = table.Column(type: "uuid", nullable: false), Email = table.Column(type: "text", nullable: false), - Login = table.Column(type: "text", nullable: false), - FirstName = table.Column(type: "text", nullable: false), - LastName = table.Column(type: "text", nullable: false), - CreationDate = table.Column(type: "timestamp with time zone", nullable: false) + Login = table.Column(type: "text", nullable: false) }, constraints: table => { @@ -62,7 +59,6 @@ protected override void Up(MigrationBuilder migrationBuilder) { Id = table.Column(type: "uuid", nullable: false), Email = table.Column(type: "text", nullable: false), - Login = table.Column(type: "text", nullable: false), FirstName = table.Column(type: "text", nullable: false), LastName = table.Column(type: "text", nullable: false), CreationDate = table.Column(type: "timestamp with time zone", nullable: false) @@ -155,7 +151,7 @@ protected override void Up(MigrationBuilder migrationBuilder) MaxCount = table.Column(type: "bigint", nullable: false), Price = table.Column(type: "numeric", nullable: false), Currency = table.Column(type: "text", nullable: false), - AvailableForm = table.Column(type: "timestamp with time zone", nullable: false) + AvailableFrom = table.Column(type: "timestamp with time zone", nullable: false) }, constraints: table => { diff --git a/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs b/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs index 1c09cce..9e328a3 100644 --- a/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs +++ b/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs @@ -43,21 +43,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("uuid"); - b.Property("CreationDate") - .HasColumnType("timestamp with time zone"); - b.Property("Email") .IsRequired() .HasColumnType("text"); - b.Property("FirstName") - .IsRequired() - .HasColumnType("text"); - - b.Property("LastName") - .IsRequired() - .HasColumnType("text"); - b.Property("Login") .IsRequired() .HasColumnType("text"); @@ -103,10 +92,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); - b.Property("Login") - .IsRequired() - .HasColumnType("text"); - b.HasKey("Id"); b.ToTable("Customers"); @@ -126,10 +111,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); - b.Property("FlatNumber") + b.Property("FlatNumber") .HasColumnType("bigint"); - b.Property("HouseNumber") + b.Property("HouseNumber") .HasColumnType("bigint"); b.Property("PostalCode") @@ -137,7 +122,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("text"); b.Property("Street") - .IsRequired() .HasColumnType("text"); b.HasKey("Id"); @@ -229,7 +213,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("uuid"); - b.Property("AvailableForm") + b.Property("AvailableFrom") .HasColumnType("timestamp with time zone"); b.Property("Currency") diff --git a/TickAPI/TickAPI/TicketTypes/Models/TicketType.cs b/TickAPI/TickAPI/TicketTypes/Models/TicketType.cs index 2b4f0df..7d41919 100644 --- a/TickAPI/TickAPI/TicketTypes/Models/TicketType.cs +++ b/TickAPI/TickAPI/TicketTypes/Models/TicketType.cs @@ -11,6 +11,6 @@ public class TicketType public uint MaxCount { get; set; } public decimal Price { get; set; } public String Currency { get; set; } - public DateTime AvailableForm { get; set; } + public DateTime AvailableFrom { get; set; } public ICollection Tickets { get; set; } } \ No newline at end of file From 4e29f6314f8ae2d7758e79f708dd4b1b87145005 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Tue, 11 Mar 2025 08:39:21 +0100 Subject: [PATCH 008/128] Remove EF from testing project --- TickAPI/TickAPI.Tests/TickAPI.Tests.csproj | 4 ---- 1 file changed, 4 deletions(-) diff --git a/TickAPI/TickAPI.Tests/TickAPI.Tests.csproj b/TickAPI/TickAPI.Tests/TickAPI.Tests.csproj index 2424eb9..95d873d 100644 --- a/TickAPI/TickAPI.Tests/TickAPI.Tests.csproj +++ b/TickAPI/TickAPI.Tests/TickAPI.Tests.csproj @@ -9,10 +9,6 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - From 5ac0d1c454478d2ee69aa7bf6a11038fda1c9ba3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Tue, 11 Mar 2025 12:13:26 +0100 Subject: [PATCH 009/128] Change strings to lower case ones --- TickAPI/TickAPI/Admins/Models/Admin.cs | 4 ++-- TickAPI/TickAPI/Categories/Models/Category.cs | 2 +- TickAPI/TickAPI/Customers/Models/Customer.cs | 6 +++--- TickAPI/TickAPI/Events/Models/Address.cs | 8 ++++---- TickAPI/TickAPI/Events/Models/Event.cs | 6 +++--- ....cs => 20250311110321_InitialMigration.Designer.cs} | 5 ++--- ...Migration.cs => 20250311110321_InitialMigration.cs} | 4 ++-- .../Migrations/TickApiDbContextModelSnapshot.cs | 3 +-- TickAPI/TickAPI/Organizers/Models/Organizer.cs | 10 +++++----- TickAPI/TickAPI/TicketTypes/Models/TicketType.cs | 6 +++--- TickAPI/TickAPI/Tickets/Models/Ticket.cs | 4 ++-- 11 files changed, 28 insertions(+), 30 deletions(-) rename TickAPI/TickAPI/Migrations/{20250311073446_InitialMigration.Designer.cs => 20250311110321_InitialMigration.Designer.cs} (98%) rename TickAPI/TickAPI/Migrations/{20250311073446_InitialMigration.cs => 20250311110321_InitialMigration.cs} (99%) diff --git a/TickAPI/TickAPI/Admins/Models/Admin.cs b/TickAPI/TickAPI/Admins/Models/Admin.cs index d7dd686..7c2d230 100644 --- a/TickAPI/TickAPI/Admins/Models/Admin.cs +++ b/TickAPI/TickAPI/Admins/Models/Admin.cs @@ -3,6 +3,6 @@ public class Admin { public Guid Id { get; set; } - public String Email { get; set; } - public String Login { get; set; } + public string Email { get; set; } + public string Login { get; set; } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Categories/Models/Category.cs b/TickAPI/TickAPI/Categories/Models/Category.cs index 649e446..053c6e1 100644 --- a/TickAPI/TickAPI/Categories/Models/Category.cs +++ b/TickAPI/TickAPI/Categories/Models/Category.cs @@ -5,6 +5,6 @@ namespace TickAPI.Categories.Models; public class Category { public Guid Id { get; set; } - public String CategoryName { get; set; } + public string CategoryName { get; set; } public ICollection Events { get; set; } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Customers/Models/Customer.cs b/TickAPI/TickAPI/Customers/Models/Customer.cs index 619e62d..8a78532 100644 --- a/TickAPI/TickAPI/Customers/Models/Customer.cs +++ b/TickAPI/TickAPI/Customers/Models/Customer.cs @@ -5,9 +5,9 @@ namespace TickAPI.Customers.Models; public class Customer { public Guid Id { get; set; } - public String Email { get; set; } - public String FirstName { get; set; } - public String LastName { get; set; } + public string Email { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } public DateTime CreationDate { get; set; } public ICollection Tickets { get; set; } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/Models/Address.cs b/TickAPI/TickAPI/Events/Models/Address.cs index 8c2cef5..faa88d2 100644 --- a/TickAPI/TickAPI/Events/Models/Address.cs +++ b/TickAPI/TickAPI/Events/Models/Address.cs @@ -3,10 +3,10 @@ public class Address { public Guid Id { get; set; } - public String Country { get; set; } - public String City { get; set; } - public String? Street { get; set; } + public string Country { get; set; } + public string City { get; set; } + public string? Street { get; set; } public uint? HouseNumber { get; set; } public uint? FlatNumber { get; set; } - public String PostalCode { get; set; } + public string PostalCode { get; set; } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/Models/Event.cs b/TickAPI/TickAPI/Events/Models/Event.cs index f6b85a5..c16b37f 100644 --- a/TickAPI/TickAPI/Events/Models/Event.cs +++ b/TickAPI/TickAPI/Events/Models/Event.cs @@ -7,11 +7,11 @@ namespace TickAPI.Events.Models; public class Event { public Guid Id { get; set; } - public String Name { get; set; } - public String Description { get; set; } + public string Name { get; set; } + public string Description { get; set; } public DateTime StartDate { get; set; } public DateTime EndDate { get; set; } - public uint MinimumAge { get; set; } + public uint? MinimumAge { get; set; } public Organizer Organizer { get; set; } public ICollection Categories { get; set; } public ICollection TicketTypes { get; set; } diff --git a/TickAPI/TickAPI/Migrations/20250311073446_InitialMigration.Designer.cs b/TickAPI/TickAPI/Migrations/20250311110321_InitialMigration.Designer.cs similarity index 98% rename from TickAPI/TickAPI/Migrations/20250311073446_InitialMigration.Designer.cs rename to TickAPI/TickAPI/Migrations/20250311110321_InitialMigration.Designer.cs index 9bc4485..6a3e0b9 100644 --- a/TickAPI/TickAPI/Migrations/20250311073446_InitialMigration.Designer.cs +++ b/TickAPI/TickAPI/Migrations/20250311110321_InitialMigration.Designer.cs @@ -12,7 +12,7 @@ namespace TickAPI.Migrations { [DbContext(typeof(TickApiDbContext))] - [Migration("20250311073446_InitialMigration")] + [Migration("20250311110321_InitialMigration")] partial class InitialMigration { /// @@ -151,7 +151,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("EventStatus") .HasColumnType("integer"); - b.Property("MinimumAge") + b.Property("MinimumAge") .HasColumnType("bigint"); b.Property("Name") @@ -260,7 +260,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("uuid"); b.Property("Seats") - .IsRequired() .HasColumnType("text"); b.Property("TypeId") diff --git a/TickAPI/TickAPI/Migrations/20250311073446_InitialMigration.cs b/TickAPI/TickAPI/Migrations/20250311110321_InitialMigration.cs similarity index 99% rename from TickAPI/TickAPI/Migrations/20250311073446_InitialMigration.cs rename to TickAPI/TickAPI/Migrations/20250311110321_InitialMigration.cs index ead764e..eb817bd 100644 --- a/TickAPI/TickAPI/Migrations/20250311073446_InitialMigration.cs +++ b/TickAPI/TickAPI/Migrations/20250311110321_InitialMigration.cs @@ -95,7 +95,7 @@ protected override void Up(MigrationBuilder migrationBuilder) Description = table.Column(type: "text", nullable: false), StartDate = table.Column(type: "timestamp with time zone", nullable: false), EndDate = table.Column(type: "timestamp with time zone", nullable: false), - MinimumAge = table.Column(type: "bigint", nullable: false), + MinimumAge = table.Column(type: "bigint", nullable: true), OrganizerId = table.Column(type: "uuid", nullable: false), EventStatus = table.Column(type: "integer", nullable: false), AddressId = table.Column(type: "uuid", nullable: false) @@ -172,7 +172,7 @@ protected override void Up(MigrationBuilder migrationBuilder) TypeId = table.Column(type: "uuid", nullable: false), OwnerId = table.Column(type: "uuid", nullable: false), NameOnTicket = table.Column(type: "text", nullable: false), - Seats = table.Column(type: "text", nullable: false), + Seats = table.Column(type: "text", nullable: true), ForResell = table.Column(type: "boolean", nullable: false) }, constraints: table => diff --git a/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs b/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs index 9e328a3..d1d044f 100644 --- a/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs +++ b/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs @@ -148,7 +148,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("EventStatus") .HasColumnType("integer"); - b.Property("MinimumAge") + b.Property("MinimumAge") .HasColumnType("bigint"); b.Property("Name") @@ -257,7 +257,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("uuid"); b.Property("Seats") - .IsRequired() .HasColumnType("text"); b.Property("TypeId") diff --git a/TickAPI/TickAPI/Organizers/Models/Organizer.cs b/TickAPI/TickAPI/Organizers/Models/Organizer.cs index f62e01b..df43b4c 100644 --- a/TickAPI/TickAPI/Organizers/Models/Organizer.cs +++ b/TickAPI/TickAPI/Organizers/Models/Organizer.cs @@ -6,12 +6,12 @@ namespace TickAPI.Organizers.Models; public class Organizer { public Guid Id { get; set; } - public String Email { get; set; } - public String Login { get; set; } - public String FirstName { get; set; } - public String LastName { get; set; } + public string Email { get; set; } + public string Login { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } public DateTime CreationDate { get; set; } - public String OrganizerName { get; set; } + public string OrganizerName { get; set; } public bool IsVerified { get; set; } public ICollection Events { get; set; } } \ No newline at end of file diff --git a/TickAPI/TickAPI/TicketTypes/Models/TicketType.cs b/TickAPI/TickAPI/TicketTypes/Models/TicketType.cs index 7d41919..b861fb5 100644 --- a/TickAPI/TickAPI/TicketTypes/Models/TicketType.cs +++ b/TickAPI/TickAPI/TicketTypes/Models/TicketType.cs @@ -7,10 +7,10 @@ public class TicketType { public Guid Id { get; set; } public Event Event { get; set; } - public String Description { get; set; } + public string Description { get; set; } public uint MaxCount { get; set; } public decimal Price { get; set; } - public String Currency { get; set; } + public string Currency { get; set; } public DateTime AvailableFrom { get; set; } - public ICollection Tickets { get; set; } + public ICollection Tickets { get; set; } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Models/Ticket.cs b/TickAPI/TickAPI/Tickets/Models/Ticket.cs index e81f5c3..21f765f 100644 --- a/TickAPI/TickAPI/Tickets/Models/Ticket.cs +++ b/TickAPI/TickAPI/Tickets/Models/Ticket.cs @@ -8,7 +8,7 @@ public class Ticket public Guid Id { get; set; } public TicketType Type { get; set; } public Customer Owner { get; set; } - public String NameOnTicket { get; set; } - public String Seats { get; set; } + public string NameOnTicket { get; set; } + public string? Seats { get; set; } public bool ForResell { get; set; } } \ No newline at end of file From 51bab9c5b947c5939d12a8d0624ad2068e459d5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Tue, 11 Mar 2025 12:39:33 +0100 Subject: [PATCH 010/128] Delete .idea and restart always setting for postgres --- .idea/.gitignore | 13 ------------- .idea/encodings.xml | 4 ---- .idea/indexLayout.xml | 8 -------- .idea/vcs.xml | 4 ---- TickAPI/TickAPI/Tickets/Models/Ticket.cs | 5 +++-- docker-compose.yml | 1 - 6 files changed, 3 insertions(+), 32 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/encodings.xml delete mode 100644 .idea/indexLayout.xml delete mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index f1978cd..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Rider ignored files -/contentModel.xml -/projectSettingsUpdater.xml -/.idea.api.iml -/modules.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/encodings.xml b/.idea/encodings.xml deleted file mode 100644 index df87cf9..0000000 --- a/.idea/encodings.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/indexLayout.xml b/.idea/indexLayout.xml deleted file mode 100644 index 7b08163..0000000 --- a/.idea/indexLayout.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index d843f34..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Models/Ticket.cs b/TickAPI/TickAPI/Tickets/Models/Ticket.cs index 21f765f..d2199bb 100644 --- a/TickAPI/TickAPI/Tickets/Models/Ticket.cs +++ b/TickAPI/TickAPI/Tickets/Models/Ticket.cs @@ -1,11 +1,12 @@ -using TickAPI.Customers.Models; +using Microsoft.EntityFrameworkCore; +using TickAPI.Customers.Models; using TickAPI.TicketTypes.Models; namespace TickAPI.Tickets.Models; public class Ticket { - public Guid Id { get; set; } + public Guid Id { get; set; } public TicketType Type { get; set; } public Customer Owner { get; set; } public string NameOnTicket { get; set; } diff --git a/docker-compose.yml b/docker-compose.yml index 728575d..88c76c8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,6 @@ services: postgres: image: postgres:latest container_name: resellio_postgres - restart: always environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: password From 68358a7bc91c046a05f1fc59fa288b145f7c3263 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Tue, 11 Mar 2025 12:43:56 +0100 Subject: [PATCH 011/128] Remove unnecessary import form ticket --- TickAPI/TickAPI/Tickets/Models/Ticket.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/TickAPI/TickAPI/Tickets/Models/Ticket.cs b/TickAPI/TickAPI/Tickets/Models/Ticket.cs index d2199bb..21f765f 100644 --- a/TickAPI/TickAPI/Tickets/Models/Ticket.cs +++ b/TickAPI/TickAPI/Tickets/Models/Ticket.cs @@ -1,12 +1,11 @@ -using Microsoft.EntityFrameworkCore; -using TickAPI.Customers.Models; +using TickAPI.Customers.Models; using TickAPI.TicketTypes.Models; namespace TickAPI.Tickets.Models; public class Ticket { - public Guid Id { get; set; } + public Guid Id { get; set; } public TicketType Type { get; set; } public Customer Owner { get; set; } public string NameOnTicket { get; set; } From 47accc5810e6a2e7a6aa7b38b51a7e0b6a9c6fde Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Tue, 11 Mar 2025 21:02:05 +0100 Subject: [PATCH 012/128] Add `PaginationService` implementation --- .../Abstractions/IPaginationService.cs | 6 +++-- .../Common/Pagination/PaginatedData.cs | 10 ++++++++ .../Pagination/Services/PaginationService.cs | 23 ++++++++++++++++++- 3 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 TickAPI/TickAPI/Common/Pagination/PaginatedData.cs diff --git a/TickAPI/TickAPI/Common/Pagination/Abstractions/IPaginationService.cs b/TickAPI/TickAPI/Common/Pagination/Abstractions/IPaginationService.cs index 618b966..c49b781 100644 --- a/TickAPI/TickAPI/Common/Pagination/Abstractions/IPaginationService.cs +++ b/TickAPI/TickAPI/Common/Pagination/Abstractions/IPaginationService.cs @@ -1,6 +1,8 @@ -namespace TickAPI.Common.Pagination.Abstractions; +using TickAPI.Common.Result; + +namespace TickAPI.Common.Pagination.Abstractions; public interface IPaginationService { - + public Result> Paginate(ICollection collection, int pageSize, int page); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Pagination/PaginatedData.cs b/TickAPI/TickAPI/Common/Pagination/PaginatedData.cs new file mode 100644 index 0000000..e95e020 --- /dev/null +++ b/TickAPI/TickAPI/Common/Pagination/PaginatedData.cs @@ -0,0 +1,10 @@ +namespace TickAPI.Common.Pagination; + +public record PaginatedData( + List Data, + // First page should have number '0' + int PageNumber, + int PageSize, + bool HasNextPage, + bool HasPreviousPage +); \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Pagination/Services/PaginationService.cs b/TickAPI/TickAPI/Common/Pagination/Services/PaginationService.cs index f4ba3ef..28432e0 100644 --- a/TickAPI/TickAPI/Common/Pagination/Services/PaginationService.cs +++ b/TickAPI/TickAPI/Common/Pagination/Services/PaginationService.cs @@ -1,8 +1,29 @@ using TickAPI.Common.Pagination.Abstractions; +using TickAPI.Common.Result; namespace TickAPI.Common.Pagination.Services; public class PaginationService : IPaginationService { - + public Result> Paginate(ICollection collection, int pageSize, int page) + { + if (pageSize <= 0) + { + return Result>.Failure(StatusCodes.Status400BadRequest, $"'pageSize' param must be > 0, got: {pageSize}"); + } + + if (page < 0) + { + return Result>.Failure(StatusCodes.Status400BadRequest, $"'page' param must be >= 0, got: {page}"); + } + + var totalCount = collection.Count; + var data = collection.Skip(page * pageSize).Take(pageSize).ToList(); + var hasPreviousPage = page > 0 && ((page - 1) * pageSize) < totalCount; + var hasNextPage = ((page + 1) * pageSize) < totalCount; + + var paginatedData = new PaginatedData(data, page, pageSize, hasNextPage, hasPreviousPage); + + return Result>.Success(paginatedData); + } } \ No newline at end of file From 2d52d89a2880c8d36dfe01f164ebeaf8d5e8a0f3 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Tue, 11 Mar 2025 21:02:19 +0100 Subject: [PATCH 013/128] Add tests for `PaginationService` --- .../Services/PaginationServiceTests.cs | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 TickAPI/TickAPI.Tests/Common/Pagination/Services/PaginationServiceTests.cs diff --git a/TickAPI/TickAPI.Tests/Common/Pagination/Services/PaginationServiceTests.cs b/TickAPI/TickAPI.Tests/Common/Pagination/Services/PaginationServiceTests.cs new file mode 100644 index 0000000..7961377 --- /dev/null +++ b/TickAPI/TickAPI.Tests/Common/Pagination/Services/PaginationServiceTests.cs @@ -0,0 +1,141 @@ +using Microsoft.AspNetCore.Http; +using TickAPI.Common.Pagination.Services; + +namespace TickAPI.Tests.Common.Pagination.Services; + +public class PaginationServiceTests +{ + private readonly PaginationService _paginationService = new(); + + [Fact] + public void Paginate_WhenPageSizeNegative_ShouldReturnFailure() + { + var result = _paginationService.Paginate(new List(), -5, 0); + + Assert.True(result.IsError); + Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); + Assert.Equal("'pageSize' param must be > 0, got: -5", result.ErrorMsg); + } + + [Fact] + public void Paginate_WhenPageSizeZero_ShouldReturnFailure() + { + var result = _paginationService.Paginate(new List(), 0, 0); + + Assert.True(result.IsError); + Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); + Assert.Equal("'pageSize' param must be > 0, got: 0", result.ErrorMsg); + } + + [Fact] + public void Paginate_WhenPageNegative_ShouldReturnFailure() + { + var result = _paginationService.Paginate(new List(), 1, -12); + + Assert.True(result.IsError); + Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); + Assert.Equal("'page' param must be >= 0, got: -12", result.ErrorMsg); + } + + [Fact] + public void Paginate_WhenCollectionLengthSmallerThanPageSize_ShouldReturnAllElements() + { + var data = new List { 1, 2, 3, 4, 5 }; + int pageSize = data.Count + 1; + const int pageNumber = 0; + + var result = _paginationService.Paginate(data, pageSize, pageNumber); + + Assert.True(result.IsSuccess); + Assert.Equal(data, result.Value?.Data); + Assert.Equal(pageNumber, result.Value?.PageNumber); + Assert.Equal(pageSize, result.Value?.PageSize); + Assert.False(result.Value?.HasNextPage); + Assert.False(result.Value?.HasPreviousPage); + } + + [Fact] + public void Paginate_WhenCollectionLengthBiggerThanPageSize_ShouldReturnPartOfCollection() + { + var data = new List { 1, 2, 3, 4, 5 }; + const int pageSize = 2; + const int pageNumber = 0; + + var result = _paginationService.Paginate(data, pageSize, pageNumber); + + Assert.True(result.IsSuccess); + Assert.Equal(new List {1, 2}, result.Value?.Data); + Assert.Equal(pageNumber, result.Value?.PageNumber); + Assert.Equal(pageSize, result.Value?.PageSize); + Assert.True(result.Value?.HasNextPage); + Assert.False(result.Value?.HasPreviousPage); + } + + [Fact] + public void Paginate_WhenTakingElementsFromTheMiddle_ShouldReturnPaginatedDataWithBothBooleansTrue() + { + var data = new List { 1, 2, 3, 4, 5 }; + const int pageSize = 2; + const int pageNumber = 1; + + var result = _paginationService.Paginate(data, pageSize, pageNumber); + + Assert.True(result.IsSuccess); + Assert.Equal(new List {3, 4}, result.Value?.Data); + Assert.Equal(pageNumber, result.Value?.PageNumber); + Assert.Equal(pageSize, result.Value?.PageSize); + Assert.True(result.Value?.HasNextPage); + Assert.True(result.Value?.HasPreviousPage); + } + + [Fact] + public void Paginate_WhenPageNumberTimesPageSizeIsMoreThanCollectionLength_ShouldReturnEmptyList() + { + var data = new List { 1, 2, 3, 4, 5 }; + const int pageSize = 2; + const int pageNumber = 5; + + var result = _paginationService.Paginate(data, pageSize, pageNumber); + + Assert.True(result.IsSuccess); + Assert.Equal(new List(), result.Value?.Data); + Assert.Equal(pageNumber, result.Value?.PageNumber); + Assert.Equal(pageSize, result.Value?.PageSize); + Assert.False(result.Value?.HasNextPage); + Assert.False(result.Value?.HasPreviousPage); + } + + [Fact] + public void Paginate_WhenOnOneAfterLastPage_ShouldReturnHasPreviousPageSetToTrue() + { + var data = new List { 1, 2, 3, 4, 5 }; + const int pageSize = 2; + const int pageNumber = 3; + + var result = _paginationService.Paginate(data, pageSize, pageNumber); + + Assert.True(result.IsSuccess); + Assert.Equal(new List(), result.Value?.Data); + Assert.Equal(pageNumber, result.Value?.PageNumber); + Assert.Equal(pageSize, result.Value?.PageSize); + Assert.False(result.Value?.HasNextPage); + Assert.True(result.Value?.HasPreviousPage); + } + + [Fact] + public void Paginate_WhenOnLastPage_ShouldReturnHasNextPageSetToFalse() + { + var data = new List { 1, 2, 3, 4, 5 }; + const int pageSize = 2; + const int pageNumber = 2; + + var result = _paginationService.Paginate(data, pageSize, pageNumber); + + Assert.True(result.IsSuccess); + Assert.Equal(new List() { 5 }, result.Value?.Data); + Assert.Equal(pageNumber, result.Value?.PageNumber); + Assert.Equal(pageSize, result.Value?.PageSize); + Assert.False(result.Value?.HasNextPage); + Assert.True(result.Value?.HasPreviousPage); + } +} \ No newline at end of file From b91b66abb6bfc6dd41cae0f865985de363fbc37b Mon Sep 17 00:00:00 2001 From: kubapoke Date: Wed, 12 Mar 2025 18:02:14 +0100 Subject: [PATCH 014/128] added Google authentication setup --- TickAPI/TickAPI/Program.cs | 20 +++++++++++++++++++- TickAPI/TickAPI/TickAPI.csproj | 3 +++ TickAPI/TickAPI/appsettings.example.json | 7 +++++-- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/TickAPI/TickAPI/Program.cs b/TickAPI/TickAPI/Program.cs index 1562524..75ec413 100644 --- a/TickAPI/TickAPI/Program.cs +++ b/TickAPI/TickAPI/Program.cs @@ -1,5 +1,9 @@ +using System.Text; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.Google; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore; -using TickAPI; +using Microsoft.IdentityModel.Tokens; using TickAPI.Admins.Abstractions; using TickAPI.Admins.Repositories; using TickAPI.Admins.Services; @@ -32,6 +36,20 @@ // Add controllers to the container. builder.Services.AddControllers(); +// Add Google authentication. +builder.Services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme; + options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = GoogleDefaults.AuthenticationScheme; + }) + .AddCookie() + .AddGoogle(options => + { + options.ClientId = builder.Configuration["Google:ClientId"]; + options.ClientSecret = builder.Configuration["Google:ClientSecret"]; + }); + // Add admin services. builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/TickAPI/TickAPI/TickAPI.csproj b/TickAPI/TickAPI/TickAPI.csproj index 8fbb6b2..5b4320c 100644 --- a/TickAPI/TickAPI/TickAPI.csproj +++ b/TickAPI/TickAPI/TickAPI.csproj @@ -7,6 +7,9 @@ + + + all diff --git a/TickAPI/TickAPI/appsettings.example.json b/TickAPI/TickAPI/appsettings.example.json index 8612a3e..f40a3b8 100644 --- a/TickAPI/TickAPI/appsettings.example.json +++ b/TickAPI/TickAPI/appsettings.example.json @@ -10,8 +10,11 @@ "https://example.com", "https://another-site.com" ], - "ConnectionStrings": { - "ResellioDatabase": "Host=localhost; Database=resellioDB; Username=postgres; Password=password; Port=5432" + "ResellioDatabase": "Host=localhost; Database=ResellioDB; Username=postgres; Password=password; Port=5432" + }, + "Google": { + "ClientId": "your-google-client-id-here", + "ClientSecret": "your-google-client-secret-here" } } From 1ae8c33834c12b6365de6da5260bdbf8a9d4776d Mon Sep 17 00:00:00 2001 From: kubapoke Date: Wed, 12 Mar 2025 20:08:26 +0100 Subject: [PATCH 015/128] set up jwt authentication --- TickAPI/TickAPI/Program.cs | 16 ++++++++++++++++ TickAPI/TickAPI/appsettings.example.json | 12 +++++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/TickAPI/TickAPI/Program.cs b/TickAPI/TickAPI/Program.cs index 75ec413..6595acc 100644 --- a/TickAPI/TickAPI/Program.cs +++ b/TickAPI/TickAPI/Program.cs @@ -50,6 +50,22 @@ options.ClientSecret = builder.Configuration["Google:ClientSecret"]; }); +// Add JWT authentication. +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = builder.Configuration["JwtIssuer"], + ValidAudience = builder.Configuration["JwtIssuer"], + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["JwtSecurityKey"])) + }; + }); + // Add admin services. builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/TickAPI/TickAPI/appsettings.example.json b/TickAPI/TickAPI/appsettings.example.json index f40a3b8..d7f1471 100644 --- a/TickAPI/TickAPI/appsettings.example.json +++ b/TickAPI/TickAPI/appsettings.example.json @@ -13,8 +13,14 @@ "ConnectionStrings": { "ResellioDatabase": "Host=localhost; Database=ResellioDB; Username=postgres; Password=password; Port=5432" }, - "Google": { - "ClientId": "your-google-client-id-here", - "ClientSecret": "your-google-client-secret-here" + "Authentication": { + "Google": { + "ClientId": "your-google-client-id-here", + "ClientSecret": "your-google-client-secret-here" + }, + "Jwt": { + "Issuer": "your-api-issuer-here", + "SecurityKey": "your-jwt-security-key-here" + } } } From 8bed9cdeab82c36d7573d307a04d7f7ff6846af5 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Wed, 12 Mar 2025 20:08:26 +0100 Subject: [PATCH 016/128] set up jwt authentication --- TickAPI/TickAPI/Program.cs | 20 ++++++++++++++++++-- TickAPI/TickAPI/appsettings.example.json | 12 +++++++++--- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/TickAPI/TickAPI/Program.cs b/TickAPI/TickAPI/Program.cs index 75ec413..0c799c5 100644 --- a/TickAPI/TickAPI/Program.cs +++ b/TickAPI/TickAPI/Program.cs @@ -46,8 +46,24 @@ .AddCookie() .AddGoogle(options => { - options.ClientId = builder.Configuration["Google:ClientId"]; - options.ClientSecret = builder.Configuration["Google:ClientSecret"]; + options.ClientId = builder.Configuration["Authentication:Google:ClientId"]; + options.ClientSecret = builder.Configuration["Authentication:Google:ClientSecret"]; + }); + +// Add JWT authentication. +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = builder.Configuration["Authentication:Jwt:Issuer"], + ValidAudience = builder.Configuration["Authentication:Jwt:Issuer"], + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Authentication:Jwt:SecurityKey"])) + }; }); // Add admin services. diff --git a/TickAPI/TickAPI/appsettings.example.json b/TickAPI/TickAPI/appsettings.example.json index f40a3b8..d7f1471 100644 --- a/TickAPI/TickAPI/appsettings.example.json +++ b/TickAPI/TickAPI/appsettings.example.json @@ -13,8 +13,14 @@ "ConnectionStrings": { "ResellioDatabase": "Host=localhost; Database=ResellioDB; Username=postgres; Password=password; Port=5432" }, - "Google": { - "ClientId": "your-google-client-id-here", - "ClientSecret": "your-google-client-secret-here" + "Authentication": { + "Google": { + "ClientId": "your-google-client-id-here", + "ClientSecret": "your-google-client-secret-here" + }, + "Jwt": { + "Issuer": "your-api-issuer-here", + "SecurityKey": "your-jwt-security-key-here" + } } } From 9561571aab9e2d3bd9307fcada45ab2fe6c76721 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Wed, 12 Mar 2025 20:30:55 +0100 Subject: [PATCH 017/128] Add utility function for propagating errors using `Result` (transforming `Result` into `Result` when both should be errors) --- .../Common/Result/ResultTests.cs | 25 +++++++++++++++++++ TickAPI/TickAPI/Common/Result/Result.cs | 10 ++++++++ 2 files changed, 35 insertions(+) diff --git a/TickAPI/TickAPI.Tests/Common/Result/ResultTests.cs b/TickAPI/TickAPI.Tests/Common/Result/ResultTests.cs index d0da236..ee8e056 100644 --- a/TickAPI/TickAPI.Tests/Common/Result/ResultTests.cs +++ b/TickAPI/TickAPI.Tests/Common/Result/ResultTests.cs @@ -31,4 +31,29 @@ public void Failure_ShouldReturnResultWithError() Assert.Equal(errorMsg, result.ErrorMsg); Assert.Equal(statusCode, result.StatusCode); } + + [Fact] + public void PropagateError_WhenResultWithErrorPassed_ShouldReturnResultWithError() + { + const int statusCode = 500; + const string errorMsg = "error message"; + var resultWithError = Result.Failure(statusCode, errorMsg); + + var result = Result.PropagateError(resultWithError); + + Assert.True(result.IsError); + Assert.False(result.IsSuccess); + Assert.Equal(errorMsg, result.ErrorMsg); + Assert.Equal(statusCode, result.StatusCode); + } + + [Fact] + public void PropagateError_WhenResultWithSuccessPassed_ShouldThrowArgumentException() + { + var resultWithSuccess = Result.Success("abc"); + + var act = () => Result.PropagateError(resultWithSuccess); + + Assert.Throws(act); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Result/Result.cs b/TickAPI/TickAPI/Common/Result/Result.cs index 553bad6..411938b 100644 --- a/TickAPI/TickAPI/Common/Result/Result.cs +++ b/TickAPI/TickAPI/Common/Result/Result.cs @@ -25,4 +25,14 @@ public static Result Failure(int statusCode, string errorMsg) { return new Result(false, default, statusCode, errorMsg); } + + public static Result PropagateError(Result other) + { + if (other.IsSuccess) + { + throw new ArgumentException("Trying to propagate error from successful value"); + } + + return Failure(other.StatusCode, other.ErrorMsg); + } } \ No newline at end of file From f9adc122228ecce8a21cc587dac3fd2f9089fcb1 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Wed, 12 Mar 2025 20:31:44 +0100 Subject: [PATCH 018/128] Update records used in pagination --- .../Common/Pagination/{ => Responses}/PaginatedData.cs | 5 +++-- .../Common/Pagination/Responses/PaginationDetails.cs | 6 ++++++ 2 files changed, 9 insertions(+), 2 deletions(-) rename TickAPI/TickAPI/Common/Pagination/{ => Responses}/PaginatedData.cs (56%) create mode 100644 TickAPI/TickAPI/Common/Pagination/Responses/PaginationDetails.cs diff --git a/TickAPI/TickAPI/Common/Pagination/PaginatedData.cs b/TickAPI/TickAPI/Common/Pagination/Responses/PaginatedData.cs similarity index 56% rename from TickAPI/TickAPI/Common/Pagination/PaginatedData.cs rename to TickAPI/TickAPI/Common/Pagination/Responses/PaginatedData.cs index e95e020..75ae745 100644 --- a/TickAPI/TickAPI/Common/Pagination/PaginatedData.cs +++ b/TickAPI/TickAPI/Common/Pagination/Responses/PaginatedData.cs @@ -1,4 +1,4 @@ -namespace TickAPI.Common.Pagination; +namespace TickAPI.Common.Pagination.Responses; public record PaginatedData( List Data, @@ -6,5 +6,6 @@ public record PaginatedData( int PageNumber, int PageSize, bool HasNextPage, - bool HasPreviousPage + bool HasPreviousPage, + PaginationDetails PaginationDetails ); \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Pagination/Responses/PaginationDetails.cs b/TickAPI/TickAPI/Common/Pagination/Responses/PaginationDetails.cs new file mode 100644 index 0000000..ed44c09 --- /dev/null +++ b/TickAPI/TickAPI/Common/Pagination/Responses/PaginationDetails.cs @@ -0,0 +1,6 @@ +namespace TickAPI.Common.Pagination.Responses; + +public record PaginationDetails( + int MaxPageNumber, + int AllElementsCount +); \ No newline at end of file From 291ee2e5a2da28f2133a4f6eefa111eaec064ee5 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Wed, 12 Mar 2025 20:32:52 +0100 Subject: [PATCH 019/128] Add method for getting pagination details + update implementation of `Paginate` --- .../Abstractions/IPaginationService.cs | 4 +- .../Pagination/Services/PaginationService.cs | 37 +++++++++++++++++-- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/TickAPI/TickAPI/Common/Pagination/Abstractions/IPaginationService.cs b/TickAPI/TickAPI/Common/Pagination/Abstractions/IPaginationService.cs index c49b781..7a3e285 100644 --- a/TickAPI/TickAPI/Common/Pagination/Abstractions/IPaginationService.cs +++ b/TickAPI/TickAPI/Common/Pagination/Abstractions/IPaginationService.cs @@ -1,8 +1,10 @@ -using TickAPI.Common.Result; +using TickAPI.Common.Pagination.Responses; +using TickAPI.Common.Result; namespace TickAPI.Common.Pagination.Abstractions; public interface IPaginationService { + public Result GetPaginationDetails(ICollection collection, int pageSize); public Result> Paginate(ICollection collection, int pageSize, int page); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Pagination/Services/PaginationService.cs b/TickAPI/TickAPI/Common/Pagination/Services/PaginationService.cs index 28432e0..cec0872 100644 --- a/TickAPI/TickAPI/Common/Pagination/Services/PaginationService.cs +++ b/TickAPI/TickAPI/Common/Pagination/Services/PaginationService.cs @@ -1,10 +1,26 @@ using TickAPI.Common.Pagination.Abstractions; +using TickAPI.Common.Pagination.Responses; using TickAPI.Common.Result; namespace TickAPI.Common.Pagination.Services; public class PaginationService : IPaginationService { + public Result GetPaginationDetails(ICollection collection, int pageSize) + { + if (pageSize <= 0) + { + return Result.Failure(StatusCodes.Status400BadRequest, $"'pageSize' param must be > 0, got: {pageSize}"); + } + + var allElementsCount = collection.Count; + var maxPageNumber = Math.Max((int)Math.Ceiling(1.0 * allElementsCount / pageSize) - 1, 0); + + var paginationDetails = new PaginationDetails(maxPageNumber, allElementsCount); + + return Result.Success(paginationDetails); + } + public Result> Paginate(ICollection collection, int pageSize, int page) { if (pageSize <= 0) @@ -17,12 +33,25 @@ public Result> Paginate(ICollection collection, int pageS return Result>.Failure(StatusCodes.Status400BadRequest, $"'page' param must be >= 0, got: {page}"); } - var totalCount = collection.Count; + var paginationDetailsResult = GetPaginationDetails(collection, pageSize); + if (paginationDetailsResult.IsError) + { + return Result>.PropagateError(paginationDetailsResult); + } + + var paginationDetails = paginationDetailsResult.Value!; + + if (page > paginationDetails.MaxPageNumber) + { + return Result>.Failure(StatusCodes.Status400BadRequest, + $"'page' param must be <= {paginationDetails.MaxPageNumber}, got: {page}"); + } + var data = collection.Skip(page * pageSize).Take(pageSize).ToList(); - var hasPreviousPage = page > 0 && ((page - 1) * pageSize) < totalCount; - var hasNextPage = ((page + 1) * pageSize) < totalCount; + var hasPreviousPage = page > 0 && ((page - 1) * pageSize) < paginationDetails.AllElementsCount; + var hasNextPage = ((page + 1) * pageSize) < paginationDetails.AllElementsCount; - var paginatedData = new PaginatedData(data, page, pageSize, hasNextPage, hasPreviousPage); + var paginatedData = new PaginatedData(data, page, pageSize, hasNextPage, hasPreviousPage, paginationDetails); return Result>.Success(paginatedData); } From 0b39397804d7f57790ca8cf4c4efb4c99d8db037 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Wed, 12 Mar 2025 20:33:30 +0100 Subject: [PATCH 020/128] Update tests for `PaginationService` --- .../Services/PaginationServiceTests.cs | 43 +++++++++++-------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/TickAPI/TickAPI.Tests/Common/Pagination/Services/PaginationServiceTests.cs b/TickAPI/TickAPI.Tests/Common/Pagination/Services/PaginationServiceTests.cs index 7961377..e234b47 100644 --- a/TickAPI/TickAPI.Tests/Common/Pagination/Services/PaginationServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Common/Pagination/Services/PaginationServiceTests.cs @@ -52,6 +52,8 @@ public void Paginate_WhenCollectionLengthSmallerThanPageSize_ShouldReturnAllElem Assert.Equal(pageSize, result.Value?.PageSize); Assert.False(result.Value?.HasNextPage); Assert.False(result.Value?.HasPreviousPage); + Assert.Equal(data.Count, result.Value?.PaginationDetails.AllElementsCount); + Assert.Equal(0, result.Value?.PaginationDetails.MaxPageNumber); } [Fact] @@ -69,6 +71,8 @@ public void Paginate_WhenCollectionLengthBiggerThanPageSize_ShouldReturnPartOfCo Assert.Equal(pageSize, result.Value?.PageSize); Assert.True(result.Value?.HasNextPage); Assert.False(result.Value?.HasPreviousPage); + Assert.Equal(data.Count, result.Value?.PaginationDetails.AllElementsCount); + Assert.Equal(2, result.Value?.PaginationDetails.MaxPageNumber); } [Fact] @@ -86,56 +90,59 @@ public void Paginate_WhenTakingElementsFromTheMiddle_ShouldReturnPaginatedDataWi Assert.Equal(pageSize, result.Value?.PageSize); Assert.True(result.Value?.HasNextPage); Assert.True(result.Value?.HasPreviousPage); + Assert.Equal(data.Count, result.Value?.PaginationDetails.AllElementsCount); + Assert.Equal(2, result.Value?.PaginationDetails.MaxPageNumber); } [Fact] - public void Paginate_WhenPageNumberTimesPageSizeIsMoreThanCollectionLength_ShouldReturnEmptyList() + public void Paginate_WhenExceededMaxPageNumber_ShouldReturnFailure() { var data = new List { 1, 2, 3, 4, 5 }; const int pageSize = 2; - const int pageNumber = 5; + const int pageNumber = 3; var result = _paginationService.Paginate(data, pageSize, pageNumber); - Assert.True(result.IsSuccess); - Assert.Equal(new List(), result.Value?.Data); - Assert.Equal(pageNumber, result.Value?.PageNumber); - Assert.Equal(pageSize, result.Value?.PageSize); - Assert.False(result.Value?.HasNextPage); - Assert.False(result.Value?.HasPreviousPage); + Assert.True(result.IsError); + Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); + Assert.Equal("'page' param must be <= 2, got: 3", result.ErrorMsg); } - + [Fact] - public void Paginate_WhenOnOneAfterLastPage_ShouldReturnHasPreviousPageSetToTrue() + public void Paginate_WhenOnLastPage_ShouldReturnHasNextPageSetToFalse() { var data = new List { 1, 2, 3, 4, 5 }; const int pageSize = 2; - const int pageNumber = 3; + const int pageNumber = 2; var result = _paginationService.Paginate(data, pageSize, pageNumber); Assert.True(result.IsSuccess); - Assert.Equal(new List(), result.Value?.Data); + Assert.Equal(new List() { 5 }, result.Value?.Data); Assert.Equal(pageNumber, result.Value?.PageNumber); Assert.Equal(pageSize, result.Value?.PageSize); Assert.False(result.Value?.HasNextPage); Assert.True(result.Value?.HasPreviousPage); + Assert.Equal(data.Count, result.Value?.PaginationDetails.AllElementsCount); + Assert.Equal(2, result.Value?.PaginationDetails.MaxPageNumber); } [Fact] - public void Paginate_WhenOnLastPage_ShouldReturnHasNextPageSetToFalse() + public void Paginate_WhenCollectionEmptyAndFirstPageIsRequested_ShouldReturnSuccess() { - var data = new List { 1, 2, 3, 4, 5 }; + var data = new List(); const int pageSize = 2; - const int pageNumber = 2; + const int pageNumber = 0; var result = _paginationService.Paginate(data, pageSize, pageNumber); - + Assert.True(result.IsSuccess); - Assert.Equal(new List() { 5 }, result.Value?.Data); + Assert.Equal(new List(), result.Value?.Data); Assert.Equal(pageNumber, result.Value?.PageNumber); Assert.Equal(pageSize, result.Value?.PageSize); Assert.False(result.Value?.HasNextPage); - Assert.True(result.Value?.HasPreviousPage); + Assert.False(result.Value?.HasPreviousPage); + Assert.Equal(data.Count, result.Value?.PaginationDetails.AllElementsCount); + Assert.Equal(0, result.Value?.PaginationDetails.MaxPageNumber); } } \ No newline at end of file From b377e1287570dc43d2d6e03aa28d3ad0d411a072 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Wed, 12 Mar 2025 20:43:32 +0100 Subject: [PATCH 021/128] implemented initial version of GoogleAuthService.cs --- .../Common/Auth/Abstractions/IAuthService.cs | 6 ++-- .../Common/Auth/Services/GoogleAuthService.cs | 29 +++++++++++++++++-- TickAPI/TickAPI/Program.cs | 3 +- TickAPI/TickAPI/TickAPI.csproj | 1 + 4 files changed, 33 insertions(+), 6 deletions(-) diff --git a/TickAPI/TickAPI/Common/Auth/Abstractions/IAuthService.cs b/TickAPI/TickAPI/Common/Auth/Abstractions/IAuthService.cs index dab82c6..df0f33f 100644 --- a/TickAPI/TickAPI/Common/Auth/Abstractions/IAuthService.cs +++ b/TickAPI/TickAPI/Common/Auth/Abstractions/IAuthService.cs @@ -1,6 +1,8 @@ -namespace TickAPI.Common.Auth.Abstractions; +using TickAPI.Common.Result; + +namespace TickAPI.Common.Auth.Abstractions; public interface IAuthService { - + Task> LoginAsync(string idToken); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Auth/Services/GoogleAuthService.cs b/TickAPI/TickAPI/Common/Auth/Services/GoogleAuthService.cs index 147beb3..7f6bb87 100644 --- a/TickAPI/TickAPI/Common/Auth/Services/GoogleAuthService.cs +++ b/TickAPI/TickAPI/Common/Auth/Services/GoogleAuthService.cs @@ -1,8 +1,33 @@ -using TickAPI.Common.Auth.Abstractions; +using Google.Apis.Auth; +using TickAPI.Common.Auth.Abstractions; +using TickAPI.Common.Result; namespace TickAPI.Common.Auth.Services; public class GoogleAuthService : IAuthService { - + private readonly IConfiguration _configuration; + + public GoogleAuthService(IConfiguration configuration) + { + _configuration = configuration; + } + + public async Task> LoginAsync(string idToken) + { + try + { + var payload = await GoogleJsonWebSignature.ValidateAsync(idToken, + new GoogleJsonWebSignature.ValidationSettings + { + Audience = [_configuration["Authentication:Google:ClientId"]] + }); + + return Result.Success(payload.Email); + } + catch (Exception ex) + { + return Result.Failure(StatusCodes.Status401Unauthorized, "Invalid Google token"); + } + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Program.cs b/TickAPI/TickAPI/Program.cs index e39491b..8fe1b27 100644 --- a/TickAPI/TickAPI/Program.cs +++ b/TickAPI/TickAPI/Program.cs @@ -57,11 +57,10 @@ options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, - ValidateAudience = true, + ValidateAudience = false, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = builder.Configuration["Authentication:Jwt:Issuer"], - ValidAudience = builder.Configuration["Authentication:Jwt:Issuer"], IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Authentication:Jwt:SecurityKey"])) }; }); diff --git a/TickAPI/TickAPI/TickAPI.csproj b/TickAPI/TickAPI/TickAPI.csproj index 5b4320c..5e3c377 100644 --- a/TickAPI/TickAPI/TickAPI.csproj +++ b/TickAPI/TickAPI/TickAPI.csproj @@ -7,6 +7,7 @@ + From 2146f0eac222d12ec57d744f3644e844a948b743 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Wed, 12 Mar 2025 20:47:46 +0100 Subject: [PATCH 022/128] implemented initial version of JwtService.cs --- .../Common/Auth/Abstractions/IJwtService.cs | 2 +- .../Common/Auth/Services/JwtService.cs | 32 ++++++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/TickAPI/TickAPI/Common/Auth/Abstractions/IJwtService.cs b/TickAPI/TickAPI/Common/Auth/Abstractions/IJwtService.cs index 2965c65..cc79f7c 100644 --- a/TickAPI/TickAPI/Common/Auth/Abstractions/IJwtService.cs +++ b/TickAPI/TickAPI/Common/Auth/Abstractions/IJwtService.cs @@ -2,5 +2,5 @@ public interface IJwtService { - + public string GenerateJwtToken(string userEmail, string role); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Auth/Services/JwtService.cs b/TickAPI/TickAPI/Common/Auth/Services/JwtService.cs index 30008b8..a7acc2a 100644 --- a/TickAPI/TickAPI/Common/Auth/Services/JwtService.cs +++ b/TickAPI/TickAPI/Common/Auth/Services/JwtService.cs @@ -1,8 +1,38 @@ -using TickAPI.Common.Auth.Abstractions; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using Microsoft.IdentityModel.Tokens; +using TickAPI.Common.Auth.Abstractions; namespace TickAPI.Common.Auth.Services; public class JwtService : IJwtService { + private readonly IConfiguration _configuration; + + public JwtService(IConfiguration configuration) + { + _configuration = configuration; + } + public string GenerateJwtToken(string userEmail, string role) + { + var claims = new List + { + new Claim(JwtRegisteredClaimNames.Email, userEmail), + new Claim(ClaimTypes.Role, role) + }; + + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Authentication:Jwt:SecurityKey"])); + var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + + var token = new JwtSecurityToken( + issuer: _configuration["Authentication:Jwt:Issuer"], + claims: claims, + expires: DateTime.Now.AddHours(1), + signingCredentials: creds + ); + + return new JwtSecurityTokenHandler().WriteToken(token); + } } \ No newline at end of file From 8b49453208f18d9c891d100207d1aa13d2a78a6d Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Wed, 12 Mar 2025 22:35:47 +0100 Subject: [PATCH 023/128] Simplify calculating `HasPreviousPage` and `HasNextPage` --- .../TickAPI/Common/Pagination/Services/PaginationService.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TickAPI/TickAPI/Common/Pagination/Services/PaginationService.cs b/TickAPI/TickAPI/Common/Pagination/Services/PaginationService.cs index cec0872..662e510 100644 --- a/TickAPI/TickAPI/Common/Pagination/Services/PaginationService.cs +++ b/TickAPI/TickAPI/Common/Pagination/Services/PaginationService.cs @@ -48,8 +48,8 @@ public Result> Paginate(ICollection collection, int pageS } var data = collection.Skip(page * pageSize).Take(pageSize).ToList(); - var hasPreviousPage = page > 0 && ((page - 1) * pageSize) < paginationDetails.AllElementsCount; - var hasNextPage = ((page + 1) * pageSize) < paginationDetails.AllElementsCount; + var hasPreviousPage = page > 0; + var hasNextPage = page < paginationDetails.MaxPageNumber; var paginatedData = new PaginatedData(data, page, pageSize, hasNextPage, hasPreviousPage, paginationDetails); From cc140b6f8594de9a68509f4f20831cc48ecbb321 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Thu, 13 Mar 2025 13:44:56 +0100 Subject: [PATCH 024/128] Setup healthcheck --- TickAPI/TickAPI/Program.cs | 6 ++++++ TickAPI/TickAPI/TickAPI.csproj | 1 + 2 files changed, 7 insertions(+) diff --git a/TickAPI/TickAPI/Program.cs b/TickAPI/TickAPI/Program.cs index 1562524..f915afb 100644 --- a/TickAPI/TickAPI/Program.cs +++ b/TickAPI/TickAPI/Program.cs @@ -80,6 +80,10 @@ }); }); +// TODO: when we start using redis we should probably also check here if we can connect to it +// Setup healtcheck +builder.Services.AddHealthChecks().AddNpgSql(connectionString: builder.Configuration.GetConnectionString("ResellioDatabase") ?? ""); + var app = builder.Build(); // Configure the HTTP request pipeline. @@ -95,4 +99,6 @@ app.UseCors(allowClientPolicyName); +app.MapHealthChecks("/health"); + app.Run(); \ No newline at end of file diff --git a/TickAPI/TickAPI/TickAPI.csproj b/TickAPI/TickAPI/TickAPI.csproj index 8fbb6b2..9aa8722 100644 --- a/TickAPI/TickAPI/TickAPI.csproj +++ b/TickAPI/TickAPI/TickAPI.csproj @@ -7,6 +7,7 @@ + all From 69580817fbc0eb1fcad180e47acb7a4fca7d5a88 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Thu, 13 Mar 2025 19:25:34 +0100 Subject: [PATCH 025/128] removed repeated fragment of code --- TickAPI/TickAPI/Program.cs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/TickAPI/TickAPI/Program.cs b/TickAPI/TickAPI/Program.cs index 3d9fb23..1a173a9 100644 --- a/TickAPI/TickAPI/Program.cs +++ b/TickAPI/TickAPI/Program.cs @@ -65,22 +65,6 @@ }; }); -// Add JWT authentication. -builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) - .AddJwtBearer(options => - { - options.TokenValidationParameters = new TokenValidationParameters - { - ValidateIssuer = true, - ValidateAudience = true, - ValidateLifetime = true, - ValidateIssuerSigningKey = true, - ValidIssuer = builder.Configuration["JwtIssuer"], - ValidAudience = builder.Configuration["JwtIssuer"], - IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["JwtSecurityKey"])) - }; - }); - // Add admin services. builder.Services.AddScoped(); builder.Services.AddScoped(); From 1851e223ff8bcf422f1b098173130e3745eb461e Mon Sep 17 00:00:00 2001 From: kubapoke Date: Thu, 13 Mar 2025 19:31:56 +0100 Subject: [PATCH 026/128] merged the .AddAuthentication calls --- TickAPI/TickAPI/Program.cs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/TickAPI/TickAPI/Program.cs b/TickAPI/TickAPI/Program.cs index 1a173a9..4b4b0a0 100644 --- a/TickAPI/TickAPI/Program.cs +++ b/TickAPI/TickAPI/Program.cs @@ -36,22 +36,17 @@ // Add controllers to the container. builder.Services.AddControllers(); -// Add Google authentication. +// Add authentication. builder.Services.AddAuthentication(options => { - options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme; - options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = GoogleDefaults.AuthenticationScheme; }) - .AddCookie() .AddGoogle(options => { options.ClientId = builder.Configuration["Authentication:Google:ClientId"]; options.ClientSecret = builder.Configuration["Authentication:Google:ClientSecret"]; - }); - -// Add JWT authentication. -builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + }) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters @@ -61,7 +56,8 @@ ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = builder.Configuration["Authentication:Jwt:Issuer"], - IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Authentication:Jwt:SecurityKey"])) + IssuerSigningKey = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(builder.Configuration["Authentication:Jwt:SecurityKey"])) }; }); From 34e4c2f7bb2949d346c91f60be008d04e02ea184 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Thu, 13 Mar 2025 20:38:11 +0100 Subject: [PATCH 027/128] Added AuthController.cs --- .../Common/Auth/Controllers/AuthController.cs | 38 +++++++++++++++++++ .../Common/Auth/Services/GoogleAuthService.cs | 2 +- 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 TickAPI/TickAPI/Common/Auth/Controllers/AuthController.cs diff --git a/TickAPI/TickAPI/Common/Auth/Controllers/AuthController.cs b/TickAPI/TickAPI/Common/Auth/Controllers/AuthController.cs new file mode 100644 index 0000000..c6f28ef --- /dev/null +++ b/TickAPI/TickAPI/Common/Auth/Controllers/AuthController.cs @@ -0,0 +1,38 @@ +using Microsoft.AspNetCore.Mvc; +using TickAPI.Common.Auth.Abstractions; + +namespace TickAPI.Common.Auth.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class AuthController : ControllerBase +{ + private readonly IAuthService _authService; + private readonly IJwtService _jwtService; + + public AuthController(IAuthService authService, IJwtService jwtService) + { + _authService = authService; + _jwtService = jwtService; + } + + // TODO: this is a placeholder method that shows of the general structure of how logging in through Google works + // in the application. It should be replaced with appropriate login/register endpoints. + [HttpPost("google-login")] + public async Task GoogleLogin([FromBody] GoogleLoginRequest request) + { + var result = await _authService.LoginAsync(request.IdToken); + + if(!result.IsSuccess) + return Unauthorized(result.ErrorMsg); + + var jwtToken = _jwtService.GenerateJwtToken(result.Value, "Customer"); + + return Ok(new { token = jwtToken }); + } + + public class GoogleLoginRequest + { + public string IdToken { get; set; } + } +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Auth/Services/GoogleAuthService.cs b/TickAPI/TickAPI/Common/Auth/Services/GoogleAuthService.cs index 7f6bb87..ff7dce5 100644 --- a/TickAPI/TickAPI/Common/Auth/Services/GoogleAuthService.cs +++ b/TickAPI/TickAPI/Common/Auth/Services/GoogleAuthService.cs @@ -25,7 +25,7 @@ public async Task> LoginAsync(string idToken) return Result.Success(payload.Email); } - catch (Exception ex) + catch (Exception) { return Result.Failure(StatusCodes.Status401Unauthorized, "Invalid Google token"); } From e09aeeec826284dc5116f920c29532a25aabf3c7 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Thu, 13 Mar 2025 21:37:04 +0100 Subject: [PATCH 028/128] added GoogleTokenValidator.cs and written GoogleAuthService.cs tests --- .../Auth/Services/GoogleAuthServiceTests.cs | 42 +++++++++++++++++++ TickAPI/TickAPI.Tests/TickAPI.Tests.csproj | 1 + .../Abstractions/IGoogleTokenValidator.cs | 9 ++++ .../Common/Auth/Services/GoogleAuthService.cs | 15 +++---- .../Auth/Services/GoogleTokenValidator.cs | 20 +++++++++ TickAPI/TickAPI/Program.cs | 1 + 6 files changed, 78 insertions(+), 10 deletions(-) create mode 100644 TickAPI/TickAPI.Tests/Common/Auth/Services/GoogleAuthServiceTests.cs create mode 100644 TickAPI/TickAPI/Common/Auth/Abstractions/IGoogleTokenValidator.cs create mode 100644 TickAPI/TickAPI/Common/Auth/Services/GoogleTokenValidator.cs diff --git a/TickAPI/TickAPI.Tests/Common/Auth/Services/GoogleAuthServiceTests.cs b/TickAPI/TickAPI.Tests/Common/Auth/Services/GoogleAuthServiceTests.cs new file mode 100644 index 0000000..ad5e695 --- /dev/null +++ b/TickAPI/TickAPI.Tests/Common/Auth/Services/GoogleAuthServiceTests.cs @@ -0,0 +1,42 @@ +using Google.Apis.Auth; +using Microsoft.AspNetCore.Http; +using Moq; +using TickAPI.Common.Auth.Abstractions; +using TickAPI.Common.Auth.Services; +using TickAPI.Common.Result; + +namespace TickAPI.Tests.Common.Auth.Services; + +public class GoogleAuthServiceTests +{ + [Fact] + public async Task LoginAsync_WhenTokenValidatorReturnsPayload_ShouldReturnEmailFromPayload() + { + var googleTokenValidatorMock = new Mock(); + googleTokenValidatorMock + .Setup(m => m.ValidateAsync("validToken")) + .ReturnsAsync(new GoogleJsonWebSignature.Payload { Email = "example@test.com" }); + var sut = new GoogleAuthService(googleTokenValidatorMock.Object); + + var result = await sut.LoginAsync("validToken"); + + Assert.True(result.IsSuccess); + Assert.Equal("example@test.com", result.Value); + } + + [Fact] + public async Task LoginAsync_WhenTokenValidatorThrowsException_ShouldReturnFailure() + { + var googleTokenValidatorMock = new Mock(); + googleTokenValidatorMock + .Setup(m => m.ValidateAsync("invalidToken")) + .Throws(new InvalidJwtException("Invalid Google ID token")); + var sut = new GoogleAuthService(googleTokenValidatorMock.Object); + + var result = await sut.LoginAsync("invalidToken"); + + Assert.True(result.IsError); + Assert.Equal(StatusCodes.Status401Unauthorized, result.StatusCode); + Assert.Equal("Invalid Google ID token", result.ErrorMsg); + } +} \ No newline at end of file diff --git a/TickAPI/TickAPI.Tests/TickAPI.Tests.csproj b/TickAPI/TickAPI.Tests/TickAPI.Tests.csproj index d15cfe8..5617d62 100644 --- a/TickAPI/TickAPI.Tests/TickAPI.Tests.csproj +++ b/TickAPI/TickAPI.Tests/TickAPI.Tests.csproj @@ -10,6 +10,7 @@ + diff --git a/TickAPI/TickAPI/Common/Auth/Abstractions/IGoogleTokenValidator.cs b/TickAPI/TickAPI/Common/Auth/Abstractions/IGoogleTokenValidator.cs new file mode 100644 index 0000000..f944e24 --- /dev/null +++ b/TickAPI/TickAPI/Common/Auth/Abstractions/IGoogleTokenValidator.cs @@ -0,0 +1,9 @@ +using Google.Apis.Auth; +using TickAPI.Common.Result; + +namespace TickAPI.Common.Auth.Abstractions; + +public interface IGoogleTokenValidator +{ + Task ValidateAsync(string idToken); +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Auth/Services/GoogleAuthService.cs b/TickAPI/TickAPI/Common/Auth/Services/GoogleAuthService.cs index ff7dce5..4abf2ad 100644 --- a/TickAPI/TickAPI/Common/Auth/Services/GoogleAuthService.cs +++ b/TickAPI/TickAPI/Common/Auth/Services/GoogleAuthService.cs @@ -6,28 +6,23 @@ namespace TickAPI.Common.Auth.Services; public class GoogleAuthService : IAuthService { - private readonly IConfiguration _configuration; + private readonly IGoogleTokenValidator _googleTokenValidator; - public GoogleAuthService(IConfiguration configuration) + public GoogleAuthService(IGoogleTokenValidator googleTokenValidator) { - _configuration = configuration; + _googleTokenValidator = googleTokenValidator; } public async Task> LoginAsync(string idToken) { try { - var payload = await GoogleJsonWebSignature.ValidateAsync(idToken, - new GoogleJsonWebSignature.ValidationSettings - { - Audience = [_configuration["Authentication:Google:ClientId"]] - }); - + var payload = await _googleTokenValidator.ValidateAsync(idToken); return Result.Success(payload.Email); } catch (Exception) { - return Result.Failure(StatusCodes.Status401Unauthorized, "Invalid Google token"); + return Result.Failure(StatusCodes.Status401Unauthorized, "Invalid Google ID token"); } } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Auth/Services/GoogleTokenValidator.cs b/TickAPI/TickAPI/Common/Auth/Services/GoogleTokenValidator.cs new file mode 100644 index 0000000..060a2f0 --- /dev/null +++ b/TickAPI/TickAPI/Common/Auth/Services/GoogleTokenValidator.cs @@ -0,0 +1,20 @@ +using Google.Apis.Auth; +using TickAPI.Common.Auth.Abstractions; + +public class GoogleTokenValidator : IGoogleTokenValidator +{ + private readonly IConfiguration _configuration; + + public GoogleTokenValidator(IConfiguration configuration) + { + _configuration = configuration; + } + + public async Task ValidateAsync(string idToken) + { + return await GoogleJsonWebSignature.ValidateAsync(idToken, new GoogleJsonWebSignature.ValidationSettings + { + Audience = [_configuration["Authentication:Google:ClientId"]] + }); + } +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Program.cs b/TickAPI/TickAPI/Program.cs index 4b4b0a0..a030932 100644 --- a/TickAPI/TickAPI/Program.cs +++ b/TickAPI/TickAPI/Program.cs @@ -84,6 +84,7 @@ // Add common services. builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle From 344f131ff9aaa474f1b58c95d9528296e5bb0ef4 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Thu, 13 Mar 2025 21:42:04 +0100 Subject: [PATCH 029/128] small stylistic changes in AuthController.cs --- TickAPI/TickAPI/Common/Auth/Controllers/AuthController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TickAPI/TickAPI/Common/Auth/Controllers/AuthController.cs b/TickAPI/TickAPI/Common/Auth/Controllers/AuthController.cs index c6f28ef..eec1938 100644 --- a/TickAPI/TickAPI/Common/Auth/Controllers/AuthController.cs +++ b/TickAPI/TickAPI/Common/Auth/Controllers/AuthController.cs @@ -16,14 +16,14 @@ public AuthController(IAuthService authService, IJwtService jwtService) _jwtService = jwtService; } - // TODO: this is a placeholder method that shows of the general structure of how logging in through Google works + // TODO: this is a placeholder method that shows off the general structure of how logging in through Google works // in the application. It should be replaced with appropriate login/register endpoints. [HttpPost("google-login")] public async Task GoogleLogin([FromBody] GoogleLoginRequest request) { var result = await _authService.LoginAsync(request.IdToken); - if(!result.IsSuccess) + if(result.IsError) return Unauthorized(result.ErrorMsg); var jwtToken = _jwtService.GenerateJwtToken(result.Value, "Customer"); From 0fd96c84a6757fe36053ec15f8ce057a13e6294e Mon Sep 17 00:00:00 2001 From: kubapoke Date: Sun, 16 Mar 2025 01:53:52 +0100 Subject: [PATCH 030/128] added JwtServiceTests.cs --- .../Common/Auth/Services/JwtServiceTests.cs | 40 +++++++++++++++++++ .../Common/Auth/Services/JwtService.cs | 2 + 2 files changed, 42 insertions(+) create mode 100644 TickAPI/TickAPI.Tests/Common/Auth/Services/JwtServiceTests.cs diff --git a/TickAPI/TickAPI.Tests/Common/Auth/Services/JwtServiceTests.cs b/TickAPI/TickAPI.Tests/Common/Auth/Services/JwtServiceTests.cs new file mode 100644 index 0000000..bc420f2 --- /dev/null +++ b/TickAPI/TickAPI.Tests/Common/Auth/Services/JwtServiceTests.cs @@ -0,0 +1,40 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using Microsoft.Extensions.Configuration; +using Microsoft.IdentityModel.Tokens; +using Moq; +using TickAPI.Common.Auth.Services; + +namespace TickAPI.Tests.Common.Auth.Services; + +public class JwtServiceTests +{ + private readonly Mock _mockConfiguration; + + public JwtServiceTests() + { + _mockConfiguration = new Mock(); + _mockConfiguration + .Setup(m => m["Authentication:Jwt:SecurityKey"]) + .Returns("ExampleSecurityKey-01234567890123456789"); + _mockConfiguration + .Setup(m => m["Authentication:Jwt:Issuer"]) + .Returns("Issuer"); + } + + [Fact] + public void GenerateJwtToken_WhenGivenValidData_ShouldReturnJwtToken() + { + JwtService sut = new JwtService(_mockConfiguration.Object); + + var tokenString = sut.GenerateJwtToken("example@test.com", "role"); + var handler = new JwtSecurityTokenHandler(); + var jwt = handler.ReadJwtToken(tokenString); + + Assert.NotNull(jwt); + Assert.Equal("Issuer", jwt.Issuer); + Assert.Contains(jwt.Claims, c => c.Type == JwtRegisteredClaimNames.Email && c.Value == "example@test.com"); + Assert.Contains(jwt.Claims, c => c.Type == ClaimTypes.Role && c.Value == "role"); + } +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Auth/Services/JwtService.cs b/TickAPI/TickAPI/Common/Auth/Services/JwtService.cs index a7acc2a..8f39fdc 100644 --- a/TickAPI/TickAPI/Common/Auth/Services/JwtService.cs +++ b/TickAPI/TickAPI/Common/Auth/Services/JwtService.cs @@ -17,6 +17,8 @@ public JwtService(IConfiguration configuration) public string GenerateJwtToken(string userEmail, string role) { + // TODO: add some sort of userEmail/Role validation after adding new users is implemented + appropriate tests + var claims = new List { new Claim(JwtRegisteredClaimNames.Email, userEmail), From eb1227496c2ed2847a447376529dac9342aac2db Mon Sep 17 00:00:00 2001 From: kubapoke Date: Sun, 16 Mar 2025 02:01:20 +0100 Subject: [PATCH 031/128] added authorization --- TickAPI/TickAPI/Program.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/TickAPI/TickAPI/Program.cs b/TickAPI/TickAPI/Program.cs index a030932..4727464 100644 --- a/TickAPI/TickAPI/Program.cs +++ b/TickAPI/TickAPI/Program.cs @@ -61,6 +61,17 @@ }; }); +// Add authorization. +builder.Services.AddAuthorization(options => +{ + options.AddPolicy("AdminPolicy", policy => policy.RequireRole("Admin")); + options.AddPolicy("OrganizerPolicy", policy => policy.RequireRole("Organizer")); + options.AddPolicy("UserPolicy", policy => policy.RequireRole("User")); + + options.AddPolicy("NewOrganizerPolicy", policy => policy.RequireRole("NewOrganizer")); + options.AddPolicy("NewUserPolicy", policy => policy.RequireRole("NewUser")); +}); + // Add admin services. builder.Services.AddScoped(); builder.Services.AddScoped(); From 72d82f4bf1a788615c71acf5a1ef504540478cb6 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Sun, 16 Mar 2025 02:29:32 +0100 Subject: [PATCH 032/128] added auth to swagger --- TickAPI/TickAPI/Program.cs | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/TickAPI/TickAPI/Program.cs b/TickAPI/TickAPI/Program.cs index 4727464..21f62f1 100644 --- a/TickAPI/TickAPI/Program.cs +++ b/TickAPI/TickAPI/Program.cs @@ -100,7 +100,30 @@ // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); +builder.Services.AddSwaggerGen(c => +{ + c.AddSecurityDefinition("Bearer", new Microsoft.OpenApi.Models.OpenApiSecurityScheme + { + In = Microsoft.OpenApi.Models.ParameterLocation.Header, + Description = "JWT Authorization header using the Bearer scheme", + Name = "Authorization", + Type = Microsoft.OpenApi.Models.SecuritySchemeType.ApiKey + }); + c.AddSecurityRequirement(new Microsoft.OpenApi.Models.OpenApiSecurityRequirement + { + { + new Microsoft.OpenApi.Models.OpenApiSecurityScheme + { + Reference = new Microsoft.OpenApi.Models.OpenApiReference + { + Type = Microsoft.OpenApi.Models.ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + new string[] {} + } + }); +}); builder.Services.AddDbContext(options => { From 66b832c20846f1c7eee24ed336e3d1351ca73cd3 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Sun, 16 Mar 2025 02:33:46 +0100 Subject: [PATCH 033/128] added app.MapControllers to fix swagger --- TickAPI/TickAPI/Program.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TickAPI/TickAPI/Program.cs b/TickAPI/TickAPI/Program.cs index 21f62f1..d4bc978 100644 --- a/TickAPI/TickAPI/Program.cs +++ b/TickAPI/TickAPI/Program.cs @@ -165,4 +165,6 @@ app.MapHealthChecks("/health"); +app.MapControllers(); + app.Run(); \ No newline at end of file From f103c3d71fd26eedd8d4c89fecba7679df444c30 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Sun, 16 Mar 2025 03:04:49 +0100 Subject: [PATCH 034/128] added an example of an actual, working jwt security key (the previous placeholder wasn't long enough) --- TickAPI/TickAPI/appsettings.example.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TickAPI/TickAPI/appsettings.example.json b/TickAPI/TickAPI/appsettings.example.json index d7f1471..fcc7098 100644 --- a/TickAPI/TickAPI/appsettings.example.json +++ b/TickAPI/TickAPI/appsettings.example.json @@ -20,7 +20,7 @@ }, "Jwt": { "Issuer": "your-api-issuer-here", - "SecurityKey": "your-jwt-security-key-here" + "SecurityKey": "IH4xhBUKl3z51Gig5MFfg4kl0yLOulGk" } } } From e63eaa0b89836a509175641db624c21f2f09a3b0 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Sun, 16 Mar 2025 03:07:43 +0100 Subject: [PATCH 035/128] reverted a name change for the PR --- TickAPI/TickAPI/appsettings.example.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TickAPI/TickAPI/appsettings.example.json b/TickAPI/TickAPI/appsettings.example.json index fcc7098..895c495 100644 --- a/TickAPI/TickAPI/appsettings.example.json +++ b/TickAPI/TickAPI/appsettings.example.json @@ -11,7 +11,7 @@ "https://another-site.com" ], "ConnectionStrings": { - "ResellioDatabase": "Host=localhost; Database=ResellioDB; Username=postgres; Password=password; Port=5432" + "ResellioDatabase": "Host=localhost; Database=resellioDB; Username=postgres; Password=password; Port=5432" }, "Authentication": { "Google": { From cf318e164c11d2515066b558576a382b721e14ea Mon Sep 17 00:00:00 2001 From: kubapoke Date: Sun, 16 Mar 2025 13:23:34 +0100 Subject: [PATCH 036/128] introduced an enum for storing user roles --- .../Common/Auth/Services/JwtServiceTests.cs | 5 +++-- .../TickAPI/Common/Auth/Abstractions/IJwtService.cs | 6 ++++-- .../TickAPI/Common/Auth/Controllers/AuthController.cs | 3 ++- TickAPI/TickAPI/Common/Auth/Enums/UserRole.cs | 10 ++++++++++ TickAPI/TickAPI/Common/Auth/Services/JwtService.cs | 5 +++-- TickAPI/TickAPI/Program.cs | 11 ++++++----- 6 files changed, 28 insertions(+), 12 deletions(-) create mode 100644 TickAPI/TickAPI/Common/Auth/Enums/UserRole.cs diff --git a/TickAPI/TickAPI.Tests/Common/Auth/Services/JwtServiceTests.cs b/TickAPI/TickAPI.Tests/Common/Auth/Services/JwtServiceTests.cs index bc420f2..3f756ca 100644 --- a/TickAPI/TickAPI.Tests/Common/Auth/Services/JwtServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Common/Auth/Services/JwtServiceTests.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.IdentityModel.Tokens; using Moq; +using TickAPI.Common.Auth.Enums; using TickAPI.Common.Auth.Services; namespace TickAPI.Tests.Common.Auth.Services; @@ -28,13 +29,13 @@ public void GenerateJwtToken_WhenGivenValidData_ShouldReturnJwtToken() { JwtService sut = new JwtService(_mockConfiguration.Object); - var tokenString = sut.GenerateJwtToken("example@test.com", "role"); + var tokenString = sut.GenerateJwtToken("example@test.com", UserRole.Customer); var handler = new JwtSecurityTokenHandler(); var jwt = handler.ReadJwtToken(tokenString); Assert.NotNull(jwt); Assert.Equal("Issuer", jwt.Issuer); Assert.Contains(jwt.Claims, c => c.Type == JwtRegisteredClaimNames.Email && c.Value == "example@test.com"); - Assert.Contains(jwt.Claims, c => c.Type == ClaimTypes.Role && c.Value == "role"); + Assert.Contains(jwt.Claims, c => c.Type == ClaimTypes.Role && c.Value == "Customer"); } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Auth/Abstractions/IJwtService.cs b/TickAPI/TickAPI/Common/Auth/Abstractions/IJwtService.cs index cc79f7c..017fc6a 100644 --- a/TickAPI/TickAPI/Common/Auth/Abstractions/IJwtService.cs +++ b/TickAPI/TickAPI/Common/Auth/Abstractions/IJwtService.cs @@ -1,6 +1,8 @@ -namespace TickAPI.Common.Auth.Abstractions; +using TickAPI.Common.Auth.Enums; + +namespace TickAPI.Common.Auth.Abstractions; public interface IJwtService { - public string GenerateJwtToken(string userEmail, string role); + public string GenerateJwtToken(string userEmail, UserRole role); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Auth/Controllers/AuthController.cs b/TickAPI/TickAPI/Common/Auth/Controllers/AuthController.cs index eec1938..30f8c61 100644 --- a/TickAPI/TickAPI/Common/Auth/Controllers/AuthController.cs +++ b/TickAPI/TickAPI/Common/Auth/Controllers/AuthController.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Mvc; using TickAPI.Common.Auth.Abstractions; +using TickAPI.Common.Auth.Enums; namespace TickAPI.Common.Auth.Controllers; @@ -26,7 +27,7 @@ public async Task GoogleLogin([FromBody] GoogleLoginRequest reque if(result.IsError) return Unauthorized(result.ErrorMsg); - var jwtToken = _jwtService.GenerateJwtToken(result.Value, "Customer"); + var jwtToken = _jwtService.GenerateJwtToken(result.Value, UserRole.Customer); return Ok(new { token = jwtToken }); } diff --git a/TickAPI/TickAPI/Common/Auth/Enums/UserRole.cs b/TickAPI/TickAPI/Common/Auth/Enums/UserRole.cs new file mode 100644 index 0000000..93e3677 --- /dev/null +++ b/TickAPI/TickAPI/Common/Auth/Enums/UserRole.cs @@ -0,0 +1,10 @@ +namespace TickAPI.Common.Auth.Enums; + +public enum UserRole +{ + Admin, + Organizer, + Customer, + NewOrganizer, + NewCustomer, +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Auth/Services/JwtService.cs b/TickAPI/TickAPI/Common/Auth/Services/JwtService.cs index 8f39fdc..66a2911 100644 --- a/TickAPI/TickAPI/Common/Auth/Services/JwtService.cs +++ b/TickAPI/TickAPI/Common/Auth/Services/JwtService.cs @@ -3,6 +3,7 @@ using System.Text; using Microsoft.IdentityModel.Tokens; using TickAPI.Common.Auth.Abstractions; +using TickAPI.Common.Auth.Enums; namespace TickAPI.Common.Auth.Services; @@ -15,14 +16,14 @@ public JwtService(IConfiguration configuration) _configuration = configuration; } - public string GenerateJwtToken(string userEmail, string role) + public string GenerateJwtToken(string userEmail, UserRole role) { // TODO: add some sort of userEmail/Role validation after adding new users is implemented + appropriate tests var claims = new List { new Claim(JwtRegisteredClaimNames.Email, userEmail), - new Claim(ClaimTypes.Role, role) + new Claim(ClaimTypes.Role, role.ToString()) }; var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Authentication:Jwt:SecurityKey"])); diff --git a/TickAPI/TickAPI/Program.cs b/TickAPI/TickAPI/Program.cs index d4bc978..cc43c1b 100644 --- a/TickAPI/TickAPI/Program.cs +++ b/TickAPI/TickAPI/Program.cs @@ -8,6 +8,7 @@ using TickAPI.Admins.Repositories; using TickAPI.Admins.Services; using TickAPI.Common.Auth.Abstractions; +using TickAPI.Common.Auth.Enums; using TickAPI.Common.Auth.Services; using TickAPI.Common.Pagination.Abstractions; using TickAPI.Common.Pagination.Services; @@ -64,12 +65,12 @@ // Add authorization. builder.Services.AddAuthorization(options => { - options.AddPolicy("AdminPolicy", policy => policy.RequireRole("Admin")); - options.AddPolicy("OrganizerPolicy", policy => policy.RequireRole("Organizer")); - options.AddPolicy("UserPolicy", policy => policy.RequireRole("User")); + options.AddPolicy("AdminPolicy", policy => policy.RequireRole(UserRole.Admin.ToString())); + options.AddPolicy("OrganizerPolicy", policy => policy.RequireRole(UserRole.Organizer.ToString())); + options.AddPolicy("CustomerPolicy", policy => policy.RequireRole(UserRole.Customer.ToString())); - options.AddPolicy("NewOrganizerPolicy", policy => policy.RequireRole("NewOrganizer")); - options.AddPolicy("NewUserPolicy", policy => policy.RequireRole("NewUser")); + options.AddPolicy("NewOrganizerPolicy", policy => policy.RequireRole(UserRole.NewOrganizer.ToString())); + options.AddPolicy("NewCustomerPolicy", policy => policy.RequireRole(UserRole.NewCustomer.ToString())); }); // Add admin services. From 0115877fc2e77fb1c65094a36aae50b5d70d66b1 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Sun, 16 Mar 2025 13:46:19 +0100 Subject: [PATCH 037/128] added datetimeservice for proper testing and modified JwtService.cs to allow changing expiry date via a secret --- .../Common/Auth/Services/JwtServiceTests.cs | 14 +++++++++++++- TickAPI/TickAPI/Common/Auth/Services/JwtService.cs | 9 +++++++-- .../Common/Time/Abstractions/IDateTimeService.cs | 6 ++++++ .../Common/Time/Services/DateTimeService.cs | 11 +++++++++++ TickAPI/TickAPI/appsettings.example.json | 3 ++- 5 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 TickAPI/TickAPI/Common/Time/Abstractions/IDateTimeService.cs create mode 100644 TickAPI/TickAPI/Common/Time/Services/DateTimeService.cs diff --git a/TickAPI/TickAPI.Tests/Common/Auth/Services/JwtServiceTests.cs b/TickAPI/TickAPI.Tests/Common/Auth/Services/JwtServiceTests.cs index 3f756ca..efe6034 100644 --- a/TickAPI/TickAPI.Tests/Common/Auth/Services/JwtServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Common/Auth/Services/JwtServiceTests.cs @@ -6,12 +6,14 @@ using Moq; using TickAPI.Common.Auth.Enums; using TickAPI.Common.Auth.Services; +using TickAPI.Common.Time.Abstractions; namespace TickAPI.Tests.Common.Auth.Services; public class JwtServiceTests { private readonly Mock _mockConfiguration; + private readonly Mock _mockDateTimeService; public JwtServiceTests() { @@ -22,12 +24,21 @@ public JwtServiceTests() _mockConfiguration .Setup(m => m["Authentication:Jwt:Issuer"]) .Returns("Issuer"); + _mockConfiguration + .Setup(m => m["Authentication:Jwt:ExpirySeconds"]) + .Returns("3600"); + + _mockDateTimeService = new Mock(); + _mockDateTimeService + .Setup(m => m.GetCurrentDateTime()) + .Returns(new DateTime(1970, 1, 1, 8, 0, 0, DateTimeKind.Utc)); + } [Fact] public void GenerateJwtToken_WhenGivenValidData_ShouldReturnJwtToken() { - JwtService sut = new JwtService(_mockConfiguration.Object); + JwtService sut = new JwtService(_mockConfiguration.Object, _mockDateTimeService.Object); var tokenString = sut.GenerateJwtToken("example@test.com", UserRole.Customer); var handler = new JwtSecurityTokenHandler(); @@ -35,6 +46,7 @@ public void GenerateJwtToken_WhenGivenValidData_ShouldReturnJwtToken() Assert.NotNull(jwt); Assert.Equal("Issuer", jwt.Issuer); + Assert.Equal(new DateTime(1970, 1, 1, 9, 0, 0, DateTimeKind.Utc), jwt.ValidTo); Assert.Contains(jwt.Claims, c => c.Type == JwtRegisteredClaimNames.Email && c.Value == "example@test.com"); Assert.Contains(jwt.Claims, c => c.Type == ClaimTypes.Role && c.Value == "Customer"); } diff --git a/TickAPI/TickAPI/Common/Auth/Services/JwtService.cs b/TickAPI/TickAPI/Common/Auth/Services/JwtService.cs index 66a2911..90b92da 100644 --- a/TickAPI/TickAPI/Common/Auth/Services/JwtService.cs +++ b/TickAPI/TickAPI/Common/Auth/Services/JwtService.cs @@ -4,16 +4,19 @@ using Microsoft.IdentityModel.Tokens; using TickAPI.Common.Auth.Abstractions; using TickAPI.Common.Auth.Enums; +using TickAPI.Common.Time.Abstractions; namespace TickAPI.Common.Auth.Services; public class JwtService : IJwtService { private readonly IConfiguration _configuration; + private readonly IDateTimeService _dateTimeService; - public JwtService(IConfiguration configuration) + public JwtService(IConfiguration configuration, IDateTimeService dateTimeService) { _configuration = configuration; + _dateTimeService = dateTimeService; } public string GenerateJwtToken(string userEmail, UserRole role) @@ -29,10 +32,12 @@ public string GenerateJwtToken(string userEmail, UserRole role) var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Authentication:Jwt:SecurityKey"])); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + int.TryParse(_configuration["Authentication:Jwt:ExpirySeconds"], out var addedSeconds); + var token = new JwtSecurityToken( issuer: _configuration["Authentication:Jwt:Issuer"], claims: claims, - expires: DateTime.Now.AddHours(1), + expires: _dateTimeService.GetCurrentDateTime().AddSeconds(addedSeconds), signingCredentials: creds ); diff --git a/TickAPI/TickAPI/Common/Time/Abstractions/IDateTimeService.cs b/TickAPI/TickAPI/Common/Time/Abstractions/IDateTimeService.cs new file mode 100644 index 0000000..7ee11dc --- /dev/null +++ b/TickAPI/TickAPI/Common/Time/Abstractions/IDateTimeService.cs @@ -0,0 +1,6 @@ +namespace TickAPI.Common.Time.Abstractions; + +public interface IDateTimeService +{ + public DateTime GetCurrentDateTime(); +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Time/Services/DateTimeService.cs b/TickAPI/TickAPI/Common/Time/Services/DateTimeService.cs new file mode 100644 index 0000000..a286224 --- /dev/null +++ b/TickAPI/TickAPI/Common/Time/Services/DateTimeService.cs @@ -0,0 +1,11 @@ +using TickAPI.Common.Time.Abstractions; + +namespace TickAPI.Common.Time.Services; + +public class DateTimeService : IDateTimeService +{ + public DateTime GetCurrentDateTime() + { + return DateTime.UtcNow; + } +} \ No newline at end of file diff --git a/TickAPI/TickAPI/appsettings.example.json b/TickAPI/TickAPI/appsettings.example.json index 895c495..1c31e13 100644 --- a/TickAPI/TickAPI/appsettings.example.json +++ b/TickAPI/TickAPI/appsettings.example.json @@ -20,7 +20,8 @@ }, "Jwt": { "Issuer": "your-api-issuer-here", - "SecurityKey": "IH4xhBUKl3z51Gig5MFfg4kl0yLOulGk" + "SecurityKey": "IH4xhBUKl3z51Gig5MFfg4kl0yLOulGk", + "ExpirySeconds" : "3600" } } } From fd50ec23ae059ab7db665283b7ada89e24136a6b Mon Sep 17 00:00:00 2001 From: kubapoke Date: Sun, 16 Mar 2025 14:01:28 +0100 Subject: [PATCH 038/128] added AuthPolicies.cs enum --- TickAPI/TickAPI/Common/Auth/Enums/AuthPolicies.cs | 10 ++++++++++ TickAPI/TickAPI/Program.cs | 10 +++++----- 2 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 TickAPI/TickAPI/Common/Auth/Enums/AuthPolicies.cs diff --git a/TickAPI/TickAPI/Common/Auth/Enums/AuthPolicies.cs b/TickAPI/TickAPI/Common/Auth/Enums/AuthPolicies.cs new file mode 100644 index 0000000..ec06e2e --- /dev/null +++ b/TickAPI/TickAPI/Common/Auth/Enums/AuthPolicies.cs @@ -0,0 +1,10 @@ +namespace TickAPI.Common.Auth.Enums; + +public enum AuthPolicies +{ + AdminPolicy, + OrganizerPolicy, + CustomerPolicy, + NewOrganizerPolicy, + NewCustomerPolicy, +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Program.cs b/TickAPI/TickAPI/Program.cs index cc43c1b..0f89ba7 100644 --- a/TickAPI/TickAPI/Program.cs +++ b/TickAPI/TickAPI/Program.cs @@ -65,12 +65,12 @@ // Add authorization. builder.Services.AddAuthorization(options => { - options.AddPolicy("AdminPolicy", policy => policy.RequireRole(UserRole.Admin.ToString())); - options.AddPolicy("OrganizerPolicy", policy => policy.RequireRole(UserRole.Organizer.ToString())); - options.AddPolicy("CustomerPolicy", policy => policy.RequireRole(UserRole.Customer.ToString())); + options.AddPolicy(AuthPolicies.AdminPolicy.ToString(), policy => policy.RequireRole(UserRole.Admin.ToString())); + options.AddPolicy(AuthPolicies.OrganizerPolicy.ToString(), policy => policy.RequireRole(UserRole.Organizer.ToString())); + options.AddPolicy(AuthPolicies.CustomerPolicy.ToString(), policy => policy.RequireRole(UserRole.Customer.ToString())); - options.AddPolicy("NewOrganizerPolicy", policy => policy.RequireRole(UserRole.NewOrganizer.ToString())); - options.AddPolicy("NewCustomerPolicy", policy => policy.RequireRole(UserRole.NewCustomer.ToString())); + options.AddPolicy(AuthPolicies.NewOrganizerPolicy.ToString(), policy => policy.RequireRole(UserRole.NewOrganizer.ToString())); + options.AddPolicy(AuthPolicies.NewCustomerPolicy.ToString(), policy => policy.RequireRole(UserRole.NewCustomer.ToString())); }); // Add admin services. From 1bf05aef105a2eeb724d85c60dc3313699c8d7b8 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Sun, 16 Mar 2025 14:03:53 +0100 Subject: [PATCH 039/128] added DateTimeService to builder --- TickAPI/TickAPI/Program.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/TickAPI/TickAPI/Program.cs b/TickAPI/TickAPI/Program.cs index 0f89ba7..68c7b06 100644 --- a/TickAPI/TickAPI/Program.cs +++ b/TickAPI/TickAPI/Program.cs @@ -13,6 +13,8 @@ using TickAPI.Common.Pagination.Abstractions; using TickAPI.Common.Pagination.Services; using TickAPI.Common.TickApiDbContext; +using TickAPI.Common.Time.Abstractions; +using TickAPI.Common.Time.Services; using TickAPI.Customers.Abstractions; using TickAPI.Customers.Repositories; using TickAPI.Customers.Services; @@ -98,6 +100,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); From 556aa0646d676c30d0f4b164903ec86b0083dc20 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Sun, 16 Mar 2025 15:09:46 +0100 Subject: [PATCH 040/128] added some validation to JwtService.cs, complete with appropriate tests --- .../Common/Auth/Services/JwtServiceTests.cs | 80 ++++++++++++++++++- .../Common/Auth/Abstractions/IJwtService.cs | 3 +- .../Common/Auth/Controllers/AuthController.cs | 13 +-- .../Common/Auth/Services/JwtService.cs | 29 +++++-- 4 files changed, 111 insertions(+), 14 deletions(-) diff --git a/TickAPI/TickAPI.Tests/Common/Auth/Services/JwtServiceTests.cs b/TickAPI/TickAPI.Tests/Common/Auth/Services/JwtServiceTests.cs index efe6034..ef57062 100644 --- a/TickAPI/TickAPI.Tests/Common/Auth/Services/JwtServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Common/Auth/Services/JwtServiceTests.cs @@ -1,6 +1,7 @@ using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.IdentityModel.Tokens; using Moq; @@ -38,16 +39,89 @@ public JwtServiceTests() [Fact] public void GenerateJwtToken_WhenGivenValidData_ShouldReturnJwtToken() { - JwtService sut = new JwtService(_mockConfiguration.Object, _mockDateTimeService.Object); + var sut = new JwtService(_mockConfiguration.Object, _mockDateTimeService.Object); - var tokenString = sut.GenerateJwtToken("example@test.com", UserRole.Customer); + var result = sut.GenerateJwtToken("example@test.com", UserRole.Customer); var handler = new JwtSecurityTokenHandler(); - var jwt = handler.ReadJwtToken(tokenString); + var jwt = handler.ReadJwtToken(result.Value); + Assert.True(result.IsSuccess); Assert.NotNull(jwt); Assert.Equal("Issuer", jwt.Issuer); Assert.Equal(new DateTime(1970, 1, 1, 9, 0, 0, DateTimeKind.Utc), jwt.ValidTo); Assert.Contains(jwt.Claims, c => c.Type == JwtRegisteredClaimNames.Email && c.Value == "example@test.com"); Assert.Contains(jwt.Claims, c => c.Type == ClaimTypes.Role && c.Value == "Customer"); } + + [Fact] + public void GenerateJwtToken_WhenUserEmailIsEmpty_ShouldReturnError() + { + var sut = new JwtService(_mockConfiguration.Object, _mockDateTimeService.Object); + + var result = sut.GenerateJwtToken(null, UserRole.Customer); + + Assert.True(result.IsError); + Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); + Assert.Equal("'userEmail' parameter cannot be null or empty", result.ErrorMsg); + } + + [Fact] + public void GenerateJwtToken_WhenSecurityKeyIsTooShort_ShouldReturnError() + { + _mockConfiguration + .Setup(m => m["Authentication:Jwt:SecurityKey"]) + .Returns("too-short"); + var sut = new JwtService(_mockConfiguration.Object, _mockDateTimeService.Object); + + var result = sut.GenerateJwtToken("example@test.com", UserRole.Customer); + + Assert.True(result.IsError); + Assert.Equal(StatusCodes.Status500InternalServerError, result.StatusCode); + Assert.Equal("'SecurityKey' must be at least 256 bits", result.ErrorMsg); + } + + [Fact] + public void GenerateJwtToken_WhenExpirySecondsIsZero_ShouldReturnError() + { + _mockConfiguration + .Setup(m => m["Authentication:Jwt:ExpirySeconds"]) + .Returns("0"); + var sut = new JwtService(_mockConfiguration.Object, _mockDateTimeService.Object); + + var result = sut.GenerateJwtToken("example@test.com", UserRole.Customer); + + Assert.True(result.IsError); + Assert.Equal(StatusCodes.Status500InternalServerError, result.StatusCode); + Assert.Equal("'ExpirySeconds' must be a positive integer", result.ErrorMsg); + } + + [Fact] + public void GenerateJwtToken_WhenExpirySecondsIsNegative_ShouldReturnError() + { + _mockConfiguration + .Setup(m => m["Authentication:Jwt:ExpirySeconds"]) + .Returns("-3600"); + var sut = new JwtService(_mockConfiguration.Object, _mockDateTimeService.Object); + + var result = sut.GenerateJwtToken("example@test.com", UserRole.Customer); + + Assert.True(result.IsError); + Assert.Equal(StatusCodes.Status500InternalServerError, result.StatusCode); + Assert.Equal("'ExpirySeconds' must be a positive integer", result.ErrorMsg); + } + + [Fact] + public void GenerateJwtToken_WhenExpirySecondsIsNotANumber_ShouldReturnError() + { + _mockConfiguration + .Setup(m => m["Authentication:Jwt:ExpirySeconds"]) + .Returns("not-a-number"); + var sut = new JwtService(_mockConfiguration.Object, _mockDateTimeService.Object); + + var result = sut.GenerateJwtToken("example@test.com", UserRole.Customer); + + Assert.True(result.IsError); + Assert.Equal(StatusCodes.Status500InternalServerError, result.StatusCode); + Assert.Equal("'ExpirySeconds' must be a positive integer", result.ErrorMsg); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Auth/Abstractions/IJwtService.cs b/TickAPI/TickAPI/Common/Auth/Abstractions/IJwtService.cs index 017fc6a..a382372 100644 --- a/TickAPI/TickAPI/Common/Auth/Abstractions/IJwtService.cs +++ b/TickAPI/TickAPI/Common/Auth/Abstractions/IJwtService.cs @@ -1,8 +1,9 @@ using TickAPI.Common.Auth.Enums; +using TickAPI.Common.Result; namespace TickAPI.Common.Auth.Abstractions; public interface IJwtService { - public string GenerateJwtToken(string userEmail, UserRole role); + public Result GenerateJwtToken(string? userEmail, UserRole role); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Auth/Controllers/AuthController.cs b/TickAPI/TickAPI/Common/Auth/Controllers/AuthController.cs index 30f8c61..bdeedc5 100644 --- a/TickAPI/TickAPI/Common/Auth/Controllers/AuthController.cs +++ b/TickAPI/TickAPI/Common/Auth/Controllers/AuthController.cs @@ -22,14 +22,17 @@ public AuthController(IAuthService authService, IJwtService jwtService) [HttpPost("google-login")] public async Task GoogleLogin([FromBody] GoogleLoginRequest request) { - var result = await _authService.LoginAsync(request.IdToken); + var loginResult = await _authService.LoginAsync(request.IdToken); - if(result.IsError) - return Unauthorized(result.ErrorMsg); + if(loginResult.IsError) + return StatusCode(loginResult.StatusCode, loginResult.ErrorMsg); - var jwtToken = _jwtService.GenerateJwtToken(result.Value, UserRole.Customer); + var jwtTokenResult = _jwtService.GenerateJwtToken(loginResult.Value, UserRole.Customer); - return Ok(new { token = jwtToken }); + if(jwtTokenResult.IsError) + return StatusCode(jwtTokenResult.StatusCode, jwtTokenResult.ErrorMsg); + + return Ok(new { token = jwtTokenResult.Value }); } public class GoogleLoginRequest diff --git a/TickAPI/TickAPI/Common/Auth/Services/JwtService.cs b/TickAPI/TickAPI/Common/Auth/Services/JwtService.cs index 90b92da..f09709b 100644 --- a/TickAPI/TickAPI/Common/Auth/Services/JwtService.cs +++ b/TickAPI/TickAPI/Common/Auth/Services/JwtService.cs @@ -4,6 +4,7 @@ using Microsoft.IdentityModel.Tokens; using TickAPI.Common.Auth.Abstractions; using TickAPI.Common.Auth.Enums; +using TickAPI.Common.Result; using TickAPI.Common.Time.Abstractions; namespace TickAPI.Common.Auth.Services; @@ -19,10 +20,14 @@ public JwtService(IConfiguration configuration, IDateTimeService dateTimeService _dateTimeService = dateTimeService; } - public string GenerateJwtToken(string userEmail, UserRole role) + public Result GenerateJwtToken(string? userEmail, UserRole role) { // TODO: add some sort of userEmail/Role validation after adding new users is implemented + appropriate tests + var validationResult = ValidateJwtSettings(userEmail); + if (validationResult.IsError) + return Result.PropagateError(validationResult); + var claims = new List { new Claim(JwtRegisteredClaimNames.Email, userEmail), @@ -31,9 +36,8 @@ public string GenerateJwtToken(string userEmail, UserRole role) var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Authentication:Jwt:SecurityKey"])); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); - - int.TryParse(_configuration["Authentication:Jwt:ExpirySeconds"], out var addedSeconds); - + var addedSeconds = int.Parse(_configuration["Authentication:Jwt:ExpirySeconds"]); + var token = new JwtSecurityToken( issuer: _configuration["Authentication:Jwt:Issuer"], claims: claims, @@ -41,6 +45,21 @@ public string GenerateJwtToken(string userEmail, UserRole role) signingCredentials: creds ); - return new JwtSecurityTokenHandler().WriteToken(token); + return Result.Success(new JwtSecurityTokenHandler().WriteToken(token)); + } + + private Result ValidateJwtSettings(string? userEmail) + { + if (string.IsNullOrWhiteSpace(userEmail)) + return Result.Failure(StatusCodes.Status400BadRequest, "'userEmail' parameter cannot be null or empty"); + + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Authentication:Jwt:SecurityKey"]!)); + if (key.KeySize < 256) + return Result.Failure(StatusCodes.Status500InternalServerError, "'SecurityKey' must be at least 256 bits"); + + if (!int.TryParse(_configuration["Authentication:Jwt:ExpirySeconds"], out var addedSeconds) || addedSeconds <= 0) + return Result.Failure(StatusCodes.Status500InternalServerError, "'ExpirySeconds' must be a positive integer"); + + return Result.Success(string.Empty); } } \ No newline at end of file From ddac411724a56df2d58ab9f42660f327d33e88dc Mon Sep 17 00:00:00 2001 From: kubapoke Date: Sun, 16 Mar 2025 15:54:45 +0100 Subject: [PATCH 041/128] simplified JwtService.cs to prevent unnecessary configuration calls --- .../Common/Auth/Services/JwtService.cs | 38 +++++++++++++------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/TickAPI/TickAPI/Common/Auth/Services/JwtService.cs b/TickAPI/TickAPI/Common/Auth/Services/JwtService.cs index f09709b..86816f2 100644 --- a/TickAPI/TickAPI/Common/Auth/Services/JwtService.cs +++ b/TickAPI/TickAPI/Common/Auth/Services/JwtService.cs @@ -24,9 +24,9 @@ public Result GenerateJwtToken(string? userEmail, UserRole role) { // TODO: add some sort of userEmail/Role validation after adding new users is implemented + appropriate tests - var validationResult = ValidateJwtSettings(userEmail); - if (validationResult.IsError) - return Result.PropagateError(validationResult); + var configurationDataResult = ValidateConfigurationData(userEmail); + if (configurationDataResult.IsError) + return Result.PropagateError(configurationDataResult); var claims = new List { @@ -34,12 +34,12 @@ public Result GenerateJwtToken(string? userEmail, UserRole role) new Claim(ClaimTypes.Role, role.ToString()) }; - var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Authentication:Jwt:SecurityKey"])); + var key = configurationDataResult.Value.SecurityKey; var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); - var addedSeconds = int.Parse(_configuration["Authentication:Jwt:ExpirySeconds"]); + var addedSeconds = configurationDataResult.Value.ExpirySeconds; var token = new JwtSecurityToken( - issuer: _configuration["Authentication:Jwt:Issuer"], + issuer: configurationDataResult.Value.Issuer, claims: claims, expires: _dateTimeService.GetCurrentDateTime().AddSeconds(addedSeconds), signingCredentials: creds @@ -48,18 +48,32 @@ public Result GenerateJwtToken(string? userEmail, UserRole role) return Result.Success(new JwtSecurityTokenHandler().WriteToken(token)); } - private Result ValidateJwtSettings(string? userEmail) + private Result ValidateConfigurationData(string? userEmail) { if (string.IsNullOrWhiteSpace(userEmail)) - return Result.Failure(StatusCodes.Status400BadRequest, "'userEmail' parameter cannot be null or empty"); + return Result.Failure(StatusCodes.Status400BadRequest, "'userEmail' parameter cannot be null or empty"); var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Authentication:Jwt:SecurityKey"]!)); if (key.KeySize < 256) - return Result.Failure(StatusCodes.Status500InternalServerError, "'SecurityKey' must be at least 256 bits"); + return Result.Failure(StatusCodes.Status500InternalServerError, "'SecurityKey' must be at least 256 bits"); - if (!int.TryParse(_configuration["Authentication:Jwt:ExpirySeconds"], out var addedSeconds) || addedSeconds <= 0) - return Result.Failure(StatusCodes.Status500InternalServerError, "'ExpirySeconds' must be a positive integer"); + if (!int.TryParse(_configuration["Authentication:Jwt:ExpirySeconds"], out var expirySeconds) || expirySeconds <= 0) + return Result.Failure(StatusCodes.Status500InternalServerError, "'ExpirySeconds' must be a positive integer"); + + var issuer = _configuration["Authentication:Jwt:Issuer"]; + + return Result.Success(new ConfigurationData + { + SecurityKey = key, + ExpirySeconds = expirySeconds, + Issuer = issuer, + }); + } - return Result.Success(string.Empty); + private struct ConfigurationData + { + public SymmetricSecurityKey SecurityKey; + public int ExpirySeconds; + public string Issuer; } } \ No newline at end of file From 912f602e571348351968dc2e6e6e87653ef9aa50 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Sun, 16 Mar 2025 20:13:35 +0100 Subject: [PATCH 042/128] removed redundant AddAuthorization() call in Program.cs (overriden by later use) --- TickAPI/TickAPI/Program.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/TickAPI/TickAPI/Program.cs b/TickAPI/TickAPI/Program.cs index 68c7b06..5f71a00 100644 --- a/TickAPI/TickAPI/Program.cs +++ b/TickAPI/TickAPI/Program.cs @@ -33,9 +33,6 @@ var builder = WebApplication.CreateBuilder(args); -// Add services to the container. -builder.Services.AddAuthorization(); - // Add controllers to the container. builder.Services.AddControllers(); From f1881154192811f6dda4a776575205331895a565 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Sun, 16 Mar 2025 23:25:26 +0100 Subject: [PATCH 043/128] Implement logic for getting customer by email --- .../Abstractions/ICustomerRepository.cs | 7 ++++-- .../Abstractions/ICustomerService.cs | 7 ++++-- .../Repositories/CustomerRepository.cs | 25 +++++++++++++++++-- .../Customers/Services/CustomerService.cs | 16 ++++++++++-- 4 files changed, 47 insertions(+), 8 deletions(-) diff --git a/TickAPI/TickAPI/Customers/Abstractions/ICustomerRepository.cs b/TickAPI/TickAPI/Customers/Abstractions/ICustomerRepository.cs index 8dc6310..326b3eb 100644 --- a/TickAPI/TickAPI/Customers/Abstractions/ICustomerRepository.cs +++ b/TickAPI/TickAPI/Customers/Abstractions/ICustomerRepository.cs @@ -1,6 +1,9 @@ -namespace TickAPI.Customers.Abstractions; +using TickAPI.Common.Result; +using TickAPI.Customers.Models; + +namespace TickAPI.Customers.Abstractions; public interface ICustomerRepository { - + Task> GetCustomerByEmailAsync(string customerEmail); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Customers/Abstractions/ICustomerService.cs b/TickAPI/TickAPI/Customers/Abstractions/ICustomerService.cs index 8371ac2..e48ab11 100644 --- a/TickAPI/TickAPI/Customers/Abstractions/ICustomerService.cs +++ b/TickAPI/TickAPI/Customers/Abstractions/ICustomerService.cs @@ -1,6 +1,9 @@ -namespace TickAPI.Customers.Abstractions; +using TickAPI.Common.Result; +using TickAPI.Customers.Models; + +namespace TickAPI.Customers.Abstractions; public interface ICustomerService { - + Task> GetCustomerByEmailAsync(string customerEmail); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Customers/Repositories/CustomerRepository.cs b/TickAPI/TickAPI/Customers/Repositories/CustomerRepository.cs index 8b21f7e..5dcaff7 100644 --- a/TickAPI/TickAPI/Customers/Repositories/CustomerRepository.cs +++ b/TickAPI/TickAPI/Customers/Repositories/CustomerRepository.cs @@ -1,8 +1,29 @@ -using TickAPI.Customers.Abstractions; +using Microsoft.EntityFrameworkCore; +using TickAPI.Common.Result; +using TickAPI.Common.TickApiDbContext; +using TickAPI.Customers.Abstractions; +using TickAPI.Customers.Models; namespace TickAPI.Customers.Repositories; public class CustomerRepository : ICustomerRepository { - + private readonly TickApiDbContext _tickApiDbContext; + + public CustomerRepository(TickApiDbContext tickApiDbContext) + { + _tickApiDbContext = tickApiDbContext; + } + + public async Task> GetCustomerByEmailAsync(string customerEmail) + { + var customer = await _tickApiDbContext.Customers.FirstOrDefaultAsync(customer => customer.Email == customerEmail); + + if (customer == null) + { + return Result.Failure(StatusCodes.Status404NotFound, $"customer with email '{customerEmail}' not found"); + } + + return Result.Success(customer); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Customers/Services/CustomerService.cs b/TickAPI/TickAPI/Customers/Services/CustomerService.cs index 4b75299..a4bfe77 100644 --- a/TickAPI/TickAPI/Customers/Services/CustomerService.cs +++ b/TickAPI/TickAPI/Customers/Services/CustomerService.cs @@ -1,8 +1,20 @@ -using TickAPI.Customers.Abstractions; +using TickAPI.Common.Result; +using TickAPI.Customers.Abstractions; +using TickAPI.Customers.Models; namespace TickAPI.Customers.Services; public class CustomerService : ICustomerService { - + private readonly ICustomerRepository _customerRepository; + + public CustomerService(ICustomerRepository customerRepository) + { + _customerRepository = customerRepository; + } + + public async Task> GetCustomerByEmailAsync(string customerEmail) + { + return await _customerRepository.GetCustomerByEmailAsync(customerEmail); + } } \ No newline at end of file From 1cd6bfa9fef77d77a6baf234510c015cdec40aff Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Sun, 16 Mar 2025 23:26:24 +0100 Subject: [PATCH 044/128] Create DTOs for login via google request for customer. --- TickAPI/TickAPI/Customers/DTOs/Request/GoogleLoginDto.cs | 5 +++++ .../Customers/DTOs/Response/GoogleLoginResponseDto.cs | 6 ++++++ 2 files changed, 11 insertions(+) create mode 100644 TickAPI/TickAPI/Customers/DTOs/Request/GoogleLoginDto.cs create mode 100644 TickAPI/TickAPI/Customers/DTOs/Response/GoogleLoginResponseDto.cs diff --git a/TickAPI/TickAPI/Customers/DTOs/Request/GoogleLoginDto.cs b/TickAPI/TickAPI/Customers/DTOs/Request/GoogleLoginDto.cs new file mode 100644 index 0000000..043b7ce --- /dev/null +++ b/TickAPI/TickAPI/Customers/DTOs/Request/GoogleLoginDto.cs @@ -0,0 +1,5 @@ +namespace TickAPI.Customers.DTOs.Request; + +public record GoogleLoginDto( + string IdToken +); \ No newline at end of file diff --git a/TickAPI/TickAPI/Customers/DTOs/Response/GoogleLoginResponseDto.cs b/TickAPI/TickAPI/Customers/DTOs/Response/GoogleLoginResponseDto.cs new file mode 100644 index 0000000..b2d5434 --- /dev/null +++ b/TickAPI/TickAPI/Customers/DTOs/Response/GoogleLoginResponseDto.cs @@ -0,0 +1,6 @@ +namespace TickAPI.Customers.DTOs.Response; + +public record GoogleLoginResponseDto( + string Token, + bool IsNewCustomer +); \ No newline at end of file From 6a3d0bad15c994619037002f8e4d5424baa232b2 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Sun, 16 Mar 2025 23:27:16 +0100 Subject: [PATCH 045/128] Create endpoint for login via google for customer --- .../Controllers/CustomerController.cs | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/TickAPI/TickAPI/Customers/Controllers/CustomerController.cs b/TickAPI/TickAPI/Customers/Controllers/CustomerController.cs index 44599d3..5ff2c17 100644 --- a/TickAPI/TickAPI/Customers/Controllers/CustomerController.cs +++ b/TickAPI/TickAPI/Customers/Controllers/CustomerController.cs @@ -1,4 +1,9 @@ using Microsoft.AspNetCore.Mvc; +using TickAPI.Common.Auth.Abstractions; +using TickAPI.Common.Auth.Enums; +using TickAPI.Customers.Abstractions; +using TickAPI.Customers.DTOs.Request; +using TickAPI.Customers.DTOs.Response; namespace TickAPI.Customers.Controllers; @@ -6,5 +11,43 @@ namespace TickAPI.Customers.Controllers; [Route("api/[controller]")] public class CustomerController : ControllerBase { + private readonly IAuthService _authService; + private readonly IJwtService _jwtService; + private readonly ICustomerService _customerService; + public CustomerController(IAuthService authService, IJwtService jwtService, ICustomerService customerService) + { + _authService = authService; + _jwtService = jwtService; + _customerService = customerService; + } + + [HttpPost("google-login")] + public async Task> GoogleLogin([FromBody] GoogleLoginDto request) + { + var loginResult = await _authService.LoginAsync(request.IdToken); + if(loginResult.IsError) + return StatusCode(loginResult.StatusCode, loginResult.ErrorMsg); + + UserRole role; + bool isNewCustomer; + + var existingCustomerResult = await _customerService.GetCustomerByEmailAsync(loginResult.Value!); + if (existingCustomerResult.IsSuccess) + { + role = UserRole.Customer; + isNewCustomer = false; + } + else + { + role = UserRole.NewCustomer; + isNewCustomer = true; + } + + var jwtTokenResult = _jwtService.GenerateJwtToken(loginResult.Value, role); + if(jwtTokenResult.IsError) + return StatusCode(jwtTokenResult.StatusCode, jwtTokenResult.ErrorMsg); + + return new ActionResult(new GoogleLoginResponseDto(jwtTokenResult.Value!, isNewCustomer)); + } } \ No newline at end of file From b679fe50e6fbd7d249a5b798ed0b8514467d183f Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Sun, 16 Mar 2025 23:41:40 +0100 Subject: [PATCH 046/128] Add our own `Authorize` attribute built on top of the original one to ensure enum type-safety --- .../Common/Auth/Attributes/AuthorizeWithPolicy.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 TickAPI/TickAPI/Common/Auth/Attributes/AuthorizeWithPolicy.cs diff --git a/TickAPI/TickAPI/Common/Auth/Attributes/AuthorizeWithPolicy.cs b/TickAPI/TickAPI/Common/Auth/Attributes/AuthorizeWithPolicy.cs new file mode 100644 index 0000000..eba7fe9 --- /dev/null +++ b/TickAPI/TickAPI/Common/Auth/Attributes/AuthorizeWithPolicy.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Authorization; +using TickAPI.Common.Auth.Enums; + +namespace TickAPI.Common.Auth.Attributes; + +public class AuthorizeWithPolicy : AuthorizeAttribute +{ + public AuthorizeWithPolicy(AuthPolicies policy) + { + Policy = policy.ToString(); + } +} \ No newline at end of file From f1e4fe929eebeb03ba185b5fa8befc3eb4267a33 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Sun, 16 Mar 2025 23:57:48 +0100 Subject: [PATCH 047/128] Implement creating new customer --- .../Abstractions/ICustomerRepository.cs | 1 + .../Abstractions/ICustomerService.cs | 1 + .../Repositories/CustomerRepository.cs | 6 +++++ .../Customers/Services/CustomerService.cs | 24 ++++++++++++++++++- 4 files changed, 31 insertions(+), 1 deletion(-) diff --git a/TickAPI/TickAPI/Customers/Abstractions/ICustomerRepository.cs b/TickAPI/TickAPI/Customers/Abstractions/ICustomerRepository.cs index 326b3eb..5177854 100644 --- a/TickAPI/TickAPI/Customers/Abstractions/ICustomerRepository.cs +++ b/TickAPI/TickAPI/Customers/Abstractions/ICustomerRepository.cs @@ -6,4 +6,5 @@ namespace TickAPI.Customers.Abstractions; public interface ICustomerRepository { Task> GetCustomerByEmailAsync(string customerEmail); + Task AddNewCustomerAsync(Customer customer); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Customers/Abstractions/ICustomerService.cs b/TickAPI/TickAPI/Customers/Abstractions/ICustomerService.cs index e48ab11..ea4e7b8 100644 --- a/TickAPI/TickAPI/Customers/Abstractions/ICustomerService.cs +++ b/TickAPI/TickAPI/Customers/Abstractions/ICustomerService.cs @@ -6,4 +6,5 @@ namespace TickAPI.Customers.Abstractions; public interface ICustomerService { Task> GetCustomerByEmailAsync(string customerEmail); + Task> CreateNewCustomerAsync(string email, string firstName, string lastName); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Customers/Repositories/CustomerRepository.cs b/TickAPI/TickAPI/Customers/Repositories/CustomerRepository.cs index 5dcaff7..9adb003 100644 --- a/TickAPI/TickAPI/Customers/Repositories/CustomerRepository.cs +++ b/TickAPI/TickAPI/Customers/Repositories/CustomerRepository.cs @@ -26,4 +26,10 @@ public async Task> GetCustomerByEmailAsync(string customerEmail return Result.Success(customer); } + + public async Task AddNewCustomerAsync(Customer customer) + { + _tickApiDbContext.Customers.Add(customer); + await _tickApiDbContext.SaveChangesAsync(); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Customers/Services/CustomerService.cs b/TickAPI/TickAPI/Customers/Services/CustomerService.cs index a4bfe77..33289c7 100644 --- a/TickAPI/TickAPI/Customers/Services/CustomerService.cs +++ b/TickAPI/TickAPI/Customers/Services/CustomerService.cs @@ -1,20 +1,42 @@ using TickAPI.Common.Result; +using TickAPI.Common.Time.Abstractions; using TickAPI.Customers.Abstractions; using TickAPI.Customers.Models; +using TickAPI.Tickets.Models; namespace TickAPI.Customers.Services; public class CustomerService : ICustomerService { private readonly ICustomerRepository _customerRepository; + private readonly IDateTimeService _dateTimeService; - public CustomerService(ICustomerRepository customerRepository) + public CustomerService(ICustomerRepository customerRepository, IDateTimeService dateTimeService) { _customerRepository = customerRepository; + _dateTimeService = dateTimeService; } public async Task> GetCustomerByEmailAsync(string customerEmail) { return await _customerRepository.GetCustomerByEmailAsync(customerEmail); } + + public async Task> CreateNewCustomerAsync(string email, string firstName, string lastName) + { + var alreadyExistingResult = await GetCustomerByEmailAsync(email); + if (alreadyExistingResult.IsSuccess) + return Result.Failure(StatusCodes.Status400BadRequest, + $"customer with email '{email}' already exists"); + var customer = new Customer + { + Email = email, + FirstName = firstName, + LastName = lastName, + CreationDate = _dateTimeService.GetCurrentDateTime(), + Tickets = new List() + }; + await _customerRepository.AddNewCustomerAsync(customer); + return Result.Success(customer); + } } \ No newline at end of file From 2dfa5a082561779ddbc5343996b4340de768dc03 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Sun, 16 Mar 2025 23:58:23 +0100 Subject: [PATCH 048/128] Add dtos for creating new customer account via google request --- .../Customers/DTOs/Request/GoogleCreateNewAccountDto.cs | 6 ++++++ .../DTOs/Response/GoogleCreateNewAccountResponseDto.cs | 5 +++++ 2 files changed, 11 insertions(+) create mode 100644 TickAPI/TickAPI/Customers/DTOs/Request/GoogleCreateNewAccountDto.cs create mode 100644 TickAPI/TickAPI/Customers/DTOs/Response/GoogleCreateNewAccountResponseDto.cs diff --git a/TickAPI/TickAPI/Customers/DTOs/Request/GoogleCreateNewAccountDto.cs b/TickAPI/TickAPI/Customers/DTOs/Request/GoogleCreateNewAccountDto.cs new file mode 100644 index 0000000..f896265 --- /dev/null +++ b/TickAPI/TickAPI/Customers/DTOs/Request/GoogleCreateNewAccountDto.cs @@ -0,0 +1,6 @@ +namespace TickAPI.Customers.DTOs.Request; + +public record GoogleCreateNewAccountDto( + string FirstName, + string LastName +); \ No newline at end of file diff --git a/TickAPI/TickAPI/Customers/DTOs/Response/GoogleCreateNewAccountResponseDto.cs b/TickAPI/TickAPI/Customers/DTOs/Response/GoogleCreateNewAccountResponseDto.cs new file mode 100644 index 0000000..b49ac83 --- /dev/null +++ b/TickAPI/TickAPI/Customers/DTOs/Response/GoogleCreateNewAccountResponseDto.cs @@ -0,0 +1,5 @@ +namespace TickAPI.Customers.DTOs.Response; + +public record GoogleCreateNewAccountResponseDto( + string Token +); \ No newline at end of file From 257d1f3b7d129f5a4d7d207e1abc5568818f2d0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Mon, 17 Mar 2025 08:52:54 +0100 Subject: [PATCH 049/128] Change postgres to sql server --- ...250317073945_InitialMigration.Designer.cs} | 98 +++++++++---------- ....cs => 20250317073945_InitialMigration.cs} | 90 ++++++++--------- .../TickApiDbContextModelSnapshot.cs | 96 +++++++++--------- TickAPI/TickAPI/Program.cs | 4 +- TickAPI/TickAPI/TickAPI.csproj | 2 + TickAPI/TickAPI/appsettings.example.json | 3 +- docker-compose.yml | 25 +++-- 7 files changed, 163 insertions(+), 155 deletions(-) rename TickAPI/TickAPI/Migrations/{20250311110321_InitialMigration.Designer.cs => 20250317073945_InitialMigration.Designer.cs} (77%) rename TickAPI/TickAPI/Migrations/{20250311110321_InitialMigration.cs => 20250317073945_InitialMigration.cs} (66%) diff --git a/TickAPI/TickAPI/Migrations/20250311110321_InitialMigration.Designer.cs b/TickAPI/TickAPI/Migrations/20250317073945_InitialMigration.Designer.cs similarity index 77% rename from TickAPI/TickAPI/Migrations/20250311110321_InitialMigration.Designer.cs rename to TickAPI/TickAPI/Migrations/20250317073945_InitialMigration.Designer.cs index 6a3e0b9..319de56 100644 --- a/TickAPI/TickAPI/Migrations/20250311110321_InitialMigration.Designer.cs +++ b/TickAPI/TickAPI/Migrations/20250317073945_InitialMigration.Designer.cs @@ -2,9 +2,9 @@ using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; using TickAPI.Common.TickApiDbContext; #nullable disable @@ -12,7 +12,7 @@ namespace TickAPI.Migrations { [DbContext(typeof(TickApiDbContext))] - [Migration("20250311110321_InitialMigration")] + [Migration("20250317073945_InitialMigration")] partial class InitialMigration { /// @@ -21,17 +21,17 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder .HasAnnotation("ProductVersion", "9.0.2") - .HasAnnotation("Relational:MaxIdentifierLength", 63); + .HasAnnotation("Relational:MaxIdentifierLength", 128); - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); modelBuilder.Entity("CategoryEvent", b => { b.Property("CategoriesId") - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("EventsId") - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.HasKey("CategoriesId", "EventsId"); @@ -44,15 +44,15 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("Email") .IsRequired() - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.Property("Login") .IsRequired() - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.HasKey("Id"); @@ -63,11 +63,11 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("CategoryName") .IsRequired() - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.HasKey("Id"); @@ -78,22 +78,22 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("CreationDate") - .HasColumnType("timestamp with time zone"); + .HasColumnType("datetime2"); b.Property("Email") .IsRequired() - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.Property("FirstName") .IsRequired() - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.Property("LastName") .IsRequired() - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.HasKey("Id"); @@ -104,15 +104,15 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("City") .IsRequired() - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.Property("Country") .IsRequired() - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.Property("FlatNumber") .HasColumnType("bigint"); @@ -122,10 +122,10 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("PostalCode") .IsRequired() - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.Property("Street") - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.HasKey("Id"); @@ -136,33 +136,33 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("AddressId") - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("Description") .IsRequired() - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.Property("EndDate") - .HasColumnType("timestamp with time zone"); + .HasColumnType("datetime2"); b.Property("EventStatus") - .HasColumnType("integer"); + .HasColumnType("int"); b.Property("MinimumAge") .HasColumnType("bigint"); b.Property("Name") .IsRequired() - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.Property("OrganizerId") - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("StartDate") - .HasColumnType("timestamp with time zone"); + .HasColumnType("datetime2"); b.HasKey("Id"); @@ -177,33 +177,33 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("CreationDate") - .HasColumnType("timestamp with time zone"); + .HasColumnType("datetime2"); b.Property("Email") .IsRequired() - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.Property("FirstName") .IsRequired() - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.Property("IsVerified") - .HasColumnType("boolean"); + .HasColumnType("bit"); b.Property("LastName") .IsRequired() - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.Property("Login") .IsRequired() - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.Property("OrganizerName") .IsRequired() - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.HasKey("Id"); @@ -214,27 +214,27 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("AvailableFrom") - .HasColumnType("timestamp with time zone"); + .HasColumnType("datetime2"); b.Property("Currency") .IsRequired() - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.Property("Description") .IsRequired() - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.Property("EventId") - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("MaxCount") .HasColumnType("bigint"); b.Property("Price") - .HasColumnType("numeric"); + .HasColumnType("decimal(18,2)"); b.HasKey("Id"); @@ -247,23 +247,23 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("ForResell") - .HasColumnType("boolean"); + .HasColumnType("bit"); b.Property("NameOnTicket") .IsRequired() - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.Property("OwnerId") - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("Seats") - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.Property("TypeId") - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.HasKey("Id"); diff --git a/TickAPI/TickAPI/Migrations/20250311110321_InitialMigration.cs b/TickAPI/TickAPI/Migrations/20250317073945_InitialMigration.cs similarity index 66% rename from TickAPI/TickAPI/Migrations/20250311110321_InitialMigration.cs rename to TickAPI/TickAPI/Migrations/20250317073945_InitialMigration.cs index eb817bd..542f32d 100644 --- a/TickAPI/TickAPI/Migrations/20250311110321_InitialMigration.cs +++ b/TickAPI/TickAPI/Migrations/20250317073945_InitialMigration.cs @@ -15,13 +15,13 @@ protected override void Up(MigrationBuilder migrationBuilder) name: "Addresses", columns: table => new { - Id = table.Column(type: "uuid", nullable: false), - Country = table.Column(type: "text", nullable: false), - City = table.Column(type: "text", nullable: false), - Street = table.Column(type: "text", nullable: true), + Id = table.Column(type: "uniqueidentifier", nullable: false), + Country = table.Column(type: "nvarchar(max)", nullable: false), + City = table.Column(type: "nvarchar(max)", nullable: false), + Street = table.Column(type: "nvarchar(max)", nullable: true), HouseNumber = table.Column(type: "bigint", nullable: true), FlatNumber = table.Column(type: "bigint", nullable: true), - PostalCode = table.Column(type: "text", nullable: false) + PostalCode = table.Column(type: "nvarchar(max)", nullable: false) }, constraints: table => { @@ -32,9 +32,9 @@ protected override void Up(MigrationBuilder migrationBuilder) name: "Admins", columns: table => new { - Id = table.Column(type: "uuid", nullable: false), - Email = table.Column(type: "text", nullable: false), - Login = table.Column(type: "text", nullable: false) + Id = table.Column(type: "uniqueidentifier", nullable: false), + Email = table.Column(type: "nvarchar(max)", nullable: false), + Login = table.Column(type: "nvarchar(max)", nullable: false) }, constraints: table => { @@ -45,8 +45,8 @@ protected override void Up(MigrationBuilder migrationBuilder) name: "Categories", columns: table => new { - Id = table.Column(type: "uuid", nullable: false), - CategoryName = table.Column(type: "text", nullable: false) + Id = table.Column(type: "uniqueidentifier", nullable: false), + CategoryName = table.Column(type: "nvarchar(max)", nullable: false) }, constraints: table => { @@ -57,11 +57,11 @@ protected override void Up(MigrationBuilder migrationBuilder) name: "Customers", columns: table => new { - Id = table.Column(type: "uuid", nullable: false), - Email = table.Column(type: "text", nullable: false), - FirstName = table.Column(type: "text", nullable: false), - LastName = table.Column(type: "text", nullable: false), - CreationDate = table.Column(type: "timestamp with time zone", nullable: false) + Id = table.Column(type: "uniqueidentifier", nullable: false), + Email = table.Column(type: "nvarchar(max)", nullable: false), + FirstName = table.Column(type: "nvarchar(max)", nullable: false), + LastName = table.Column(type: "nvarchar(max)", nullable: false), + CreationDate = table.Column(type: "datetime2", nullable: false) }, constraints: table => { @@ -72,14 +72,14 @@ protected override void Up(MigrationBuilder migrationBuilder) name: "Organizers", columns: table => new { - Id = table.Column(type: "uuid", nullable: false), - Email = table.Column(type: "text", nullable: false), - Login = table.Column(type: "text", nullable: false), - FirstName = table.Column(type: "text", nullable: false), - LastName = table.Column(type: "text", nullable: false), - CreationDate = table.Column(type: "timestamp with time zone", nullable: false), - OrganizerName = table.Column(type: "text", nullable: false), - IsVerified = table.Column(type: "boolean", nullable: false) + Id = table.Column(type: "uniqueidentifier", nullable: false), + Email = table.Column(type: "nvarchar(max)", nullable: false), + Login = table.Column(type: "nvarchar(max)", nullable: false), + FirstName = table.Column(type: "nvarchar(max)", nullable: false), + LastName = table.Column(type: "nvarchar(max)", nullable: false), + CreationDate = table.Column(type: "datetime2", nullable: false), + OrganizerName = table.Column(type: "nvarchar(max)", nullable: false), + IsVerified = table.Column(type: "bit", nullable: false) }, constraints: table => { @@ -90,15 +90,15 @@ protected override void Up(MigrationBuilder migrationBuilder) name: "Events", columns: table => new { - Id = table.Column(type: "uuid", nullable: false), - Name = table.Column(type: "text", nullable: false), - Description = table.Column(type: "text", nullable: false), - StartDate = table.Column(type: "timestamp with time zone", nullable: false), - EndDate = table.Column(type: "timestamp with time zone", nullable: false), + Id = table.Column(type: "uniqueidentifier", nullable: false), + Name = table.Column(type: "nvarchar(max)", nullable: false), + Description = table.Column(type: "nvarchar(max)", nullable: false), + StartDate = table.Column(type: "datetime2", nullable: false), + EndDate = table.Column(type: "datetime2", nullable: false), MinimumAge = table.Column(type: "bigint", nullable: true), - OrganizerId = table.Column(type: "uuid", nullable: false), - EventStatus = table.Column(type: "integer", nullable: false), - AddressId = table.Column(type: "uuid", nullable: false) + OrganizerId = table.Column(type: "uniqueidentifier", nullable: false), + EventStatus = table.Column(type: "int", nullable: false), + AddressId = table.Column(type: "uniqueidentifier", nullable: false) }, constraints: table => { @@ -121,8 +121,8 @@ protected override void Up(MigrationBuilder migrationBuilder) name: "CategoryEvent", columns: table => new { - CategoriesId = table.Column(type: "uuid", nullable: false), - EventsId = table.Column(type: "uuid", nullable: false) + CategoriesId = table.Column(type: "uniqueidentifier", nullable: false), + EventsId = table.Column(type: "uniqueidentifier", nullable: false) }, constraints: table => { @@ -145,13 +145,13 @@ protected override void Up(MigrationBuilder migrationBuilder) name: "TicketTypes", columns: table => new { - Id = table.Column(type: "uuid", nullable: false), - EventId = table.Column(type: "uuid", nullable: false), - Description = table.Column(type: "text", nullable: false), + Id = table.Column(type: "uniqueidentifier", nullable: false), + EventId = table.Column(type: "uniqueidentifier", nullable: false), + Description = table.Column(type: "nvarchar(max)", nullable: false), MaxCount = table.Column(type: "bigint", nullable: false), - Price = table.Column(type: "numeric", nullable: false), - Currency = table.Column(type: "text", nullable: false), - AvailableFrom = table.Column(type: "timestamp with time zone", nullable: false) + Price = table.Column(type: "decimal(18,2)", nullable: false), + Currency = table.Column(type: "nvarchar(max)", nullable: false), + AvailableFrom = table.Column(type: "datetime2", nullable: false) }, constraints: table => { @@ -168,12 +168,12 @@ protected override void Up(MigrationBuilder migrationBuilder) name: "Tickets", columns: table => new { - Id = table.Column(type: "uuid", nullable: false), - TypeId = table.Column(type: "uuid", nullable: false), - OwnerId = table.Column(type: "uuid", nullable: false), - NameOnTicket = table.Column(type: "text", nullable: false), - Seats = table.Column(type: "text", nullable: true), - ForResell = table.Column(type: "boolean", nullable: false) + Id = table.Column(type: "uniqueidentifier", nullable: false), + TypeId = table.Column(type: "uniqueidentifier", nullable: false), + OwnerId = table.Column(type: "uniqueidentifier", nullable: false), + NameOnTicket = table.Column(type: "nvarchar(max)", nullable: false), + Seats = table.Column(type: "nvarchar(max)", nullable: true), + ForResell = table.Column(type: "bit", nullable: false) }, constraints: table => { diff --git a/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs b/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs index d1d044f..7925b4d 100644 --- a/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs +++ b/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs @@ -2,8 +2,8 @@ using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; using TickAPI.Common.TickApiDbContext; #nullable disable @@ -18,17 +18,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder .HasAnnotation("ProductVersion", "9.0.2") - .HasAnnotation("Relational:MaxIdentifierLength", 63); + .HasAnnotation("Relational:MaxIdentifierLength", 128); - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); modelBuilder.Entity("CategoryEvent", b => { b.Property("CategoriesId") - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("EventsId") - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.HasKey("CategoriesId", "EventsId"); @@ -41,15 +41,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("Email") .IsRequired() - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.Property("Login") .IsRequired() - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.HasKey("Id"); @@ -60,11 +60,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("CategoryName") .IsRequired() - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.HasKey("Id"); @@ -75,22 +75,22 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("CreationDate") - .HasColumnType("timestamp with time zone"); + .HasColumnType("datetime2"); b.Property("Email") .IsRequired() - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.Property("FirstName") .IsRequired() - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.Property("LastName") .IsRequired() - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.HasKey("Id"); @@ -101,15 +101,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("City") .IsRequired() - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.Property("Country") .IsRequired() - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.Property("FlatNumber") .HasColumnType("bigint"); @@ -119,10 +119,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("PostalCode") .IsRequired() - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.Property("Street") - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.HasKey("Id"); @@ -133,33 +133,33 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("AddressId") - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("Description") .IsRequired() - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.Property("EndDate") - .HasColumnType("timestamp with time zone"); + .HasColumnType("datetime2"); b.Property("EventStatus") - .HasColumnType("integer"); + .HasColumnType("int"); b.Property("MinimumAge") .HasColumnType("bigint"); b.Property("Name") .IsRequired() - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.Property("OrganizerId") - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("StartDate") - .HasColumnType("timestamp with time zone"); + .HasColumnType("datetime2"); b.HasKey("Id"); @@ -174,33 +174,33 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("CreationDate") - .HasColumnType("timestamp with time zone"); + .HasColumnType("datetime2"); b.Property("Email") .IsRequired() - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.Property("FirstName") .IsRequired() - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.Property("IsVerified") - .HasColumnType("boolean"); + .HasColumnType("bit"); b.Property("LastName") .IsRequired() - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.Property("Login") .IsRequired() - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.Property("OrganizerName") .IsRequired() - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.HasKey("Id"); @@ -211,27 +211,27 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("AvailableFrom") - .HasColumnType("timestamp with time zone"); + .HasColumnType("datetime2"); b.Property("Currency") .IsRequired() - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.Property("Description") .IsRequired() - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.Property("EventId") - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("MaxCount") .HasColumnType("bigint"); b.Property("Price") - .HasColumnType("numeric"); + .HasColumnType("decimal(18,2)"); b.HasKey("Id"); @@ -244,23 +244,23 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("ForResell") - .HasColumnType("boolean"); + .HasColumnType("bit"); b.Property("NameOnTicket") .IsRequired() - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.Property("OwnerId") - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("Seats") - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.Property("TypeId") - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.HasKey("Id"); diff --git a/TickAPI/TickAPI/Program.cs b/TickAPI/TickAPI/Program.cs index 68c7b06..91adbd0 100644 --- a/TickAPI/TickAPI/Program.cs +++ b/TickAPI/TickAPI/Program.cs @@ -131,7 +131,7 @@ builder.Services.AddDbContext(options => { - options.UseNpgsql(builder.Configuration.GetConnectionString("ResellioDatabase")); + options.UseSqlServer(builder.Configuration.GetConnectionString("ResellioLocalDB")); }); // Create CORS policy @@ -150,7 +150,7 @@ // TODO: when we start using redis we should probably also check here if we can connect to it // Setup healtcheck -builder.Services.AddHealthChecks().AddNpgSql(connectionString: builder.Configuration.GetConnectionString("ResellioDatabase") ?? ""); +builder.Services.AddHealthChecks().AddSqlServer(connectionString: builder.Configuration.GetConnectionString("ResellioDatabase") ?? ""); var app = builder.Build(); diff --git a/TickAPI/TickAPI/TickAPI.csproj b/TickAPI/TickAPI/TickAPI.csproj index 3b7e4f1..ce0ca58 100644 --- a/TickAPI/TickAPI/TickAPI.csproj +++ b/TickAPI/TickAPI/TickAPI.csproj @@ -7,6 +7,7 @@ + @@ -17,6 +18,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/TickAPI/TickAPI/appsettings.example.json b/TickAPI/TickAPI/appsettings.example.json index 1c31e13..6896968 100644 --- a/TickAPI/TickAPI/appsettings.example.json +++ b/TickAPI/TickAPI/appsettings.example.json @@ -11,7 +11,8 @@ "https://another-site.com" ], "ConnectionStrings": { - "ResellioDatabase": "Host=localhost; Database=resellioDB; Username=postgres; Password=password; Port=5432" + "ResellioDatabase": "Host=localhost; Database=resellioDB; Username=postgres; Password=password; Port=5432", + "ResellioLocalDB": "Server=localhost,1433;Database=master;User Id=sa;Password=your_password;TrustServerCertificate=True;" }, "Authentication": { "Google": { diff --git a/docker-compose.yml b/docker-compose.yml index 88c76c8..f9f4fa6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,17 +1,22 @@ version: '3.8' services: - postgres: - image: postgres:latest - container_name: resellio_postgres + azuresqledge: + image: mcr.microsoft.com/azure-sql-edge:latest + container_name: resellio_azuresqledge environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: password - POSTGRES_DB: resellioDB + ACCEPT_EULA: Y + MSSQL_SA_PASSWORD: Rese11io + MSSQL_PID: Developer + MSSQL_DATABASE: resellioDB + MSSQL_USER: sqluser + MSSQL_PASSWORD: Rese11io volumes: - - pg_data:/var/lib/postgresql/data + - azuresqledge_data:/var/opt/mssql ports: - - 5432:5432 + - 1433:1433 + cap_add: + - SYS_PTRACE redis: image: redis:latest @@ -23,7 +28,7 @@ services: - 6379:6379 volumes: - pg_data: - driver: local redis_data: + driver: local + azuresqledge_data: driver: local \ No newline at end of file From 7e48f34b37d3b2d5947f4ba9d481ee64681fb8e4 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Mon, 17 Mar 2025 08:54:33 +0100 Subject: [PATCH 050/128] Create endpoint for creating new account via google --- .../Controllers/CustomerController.cs | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/TickAPI/TickAPI/Customers/Controllers/CustomerController.cs b/TickAPI/TickAPI/Customers/Controllers/CustomerController.cs index 5ff2c17..28433f9 100644 --- a/TickAPI/TickAPI/Customers/Controllers/CustomerController.cs +++ b/TickAPI/TickAPI/Customers/Controllers/CustomerController.cs @@ -1,5 +1,7 @@ using Microsoft.AspNetCore.Mvc; +using Microsoft.IdentityModel.JsonWebTokens; using TickAPI.Common.Auth.Abstractions; +using TickAPI.Common.Auth.Attributes; using TickAPI.Common.Auth.Enums; using TickAPI.Customers.Abstractions; using TickAPI.Customers.DTOs.Request; @@ -45,9 +47,27 @@ public async Task> GoogleLogin([FromBody] G } var jwtTokenResult = _jwtService.GenerateJwtToken(loginResult.Value, role); - if(jwtTokenResult.IsError) + if (jwtTokenResult.IsError) return StatusCode(jwtTokenResult.StatusCode, jwtTokenResult.ErrorMsg); return new ActionResult(new GoogleLoginResponseDto(jwtTokenResult.Value!, isNewCustomer)); } + + [AuthorizeWithPolicy(AuthPolicies.NewCustomerPolicy)] + public async Task> GoogleCreateNewAccount([FromBody] GoogleCreateNewAccountDto request) + { + var email = User.Claims.FirstOrDefault(c => c.Type == JwtRegisteredClaimNames.Email)?.Value; + if (email == null) + return StatusCode(StatusCodes.Status400BadRequest, "missing email claim"); + + var newCustomerResult = await _customerService.CreateNewCustomerAsync(email, request.FirstName, request.LastName); + if (newCustomerResult.IsError) + return StatusCode(newCustomerResult.StatusCode, newCustomerResult.ErrorMsg); + + var jwtTokenResult = _jwtService.GenerateJwtToken(newCustomerResult.Value!.Email, UserRole.Customer); + if (jwtTokenResult.IsError) + return StatusCode(jwtTokenResult.StatusCode, jwtTokenResult.ErrorMsg); + + return new ActionResult(new GoogleCreateNewAccountResponseDto(jwtTokenResult.Value!)); + } } \ No newline at end of file From 3edcb014c384ba898bd91fd7c7c627b8a922debb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Mon, 17 Mar 2025 09:28:23 +0100 Subject: [PATCH 051/128] Fixed trzcinksik comments --- TickAPI/TickAPI/Program.cs | 2 +- TickAPI/TickAPI/TickAPI.csproj | 3 --- TickAPI/TickAPI/appsettings.example.json | 3 +-- docker-compose.yml | 5 ----- 4 files changed, 2 insertions(+), 11 deletions(-) diff --git a/TickAPI/TickAPI/Program.cs b/TickAPI/TickAPI/Program.cs index 503cb9c..725812a 100644 --- a/TickAPI/TickAPI/Program.cs +++ b/TickAPI/TickAPI/Program.cs @@ -128,7 +128,7 @@ builder.Services.AddDbContext(options => { - options.UseSqlServer(builder.Configuration.GetConnectionString("ResellioLocalDB")); + options.UseSqlServer(builder.Configuration.GetConnectionString("ResellioDatabase")); }); // Create CORS policy diff --git a/TickAPI/TickAPI/TickAPI.csproj b/TickAPI/TickAPI/TickAPI.csproj index ce0ca58..d89ca81 100644 --- a/TickAPI/TickAPI/TickAPI.csproj +++ b/TickAPI/TickAPI/TickAPI.csproj @@ -12,15 +12,12 @@ - all runtime; build; native; contentfiles; analyzers; buildtransitive - - diff --git a/TickAPI/TickAPI/appsettings.example.json b/TickAPI/TickAPI/appsettings.example.json index 6896968..1e34d53 100644 --- a/TickAPI/TickAPI/appsettings.example.json +++ b/TickAPI/TickAPI/appsettings.example.json @@ -11,8 +11,7 @@ "https://another-site.com" ], "ConnectionStrings": { - "ResellioDatabase": "Host=localhost; Database=resellioDB; Username=postgres; Password=password; Port=5432", - "ResellioLocalDB": "Server=localhost,1433;Database=master;User Id=sa;Password=your_password;TrustServerCertificate=True;" + "ResellioDatabase": "Server=tcp:localhost,1433;Initial Catalog=your_db_name;Persist Security Info=False;User ID=sa;Password=your_password;TrustServerCertificate=True" }, "Authentication": { "Google": { diff --git a/docker-compose.yml b/docker-compose.yml index f9f4fa6..cf8ef14 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,19 +9,14 @@ services: MSSQL_SA_PASSWORD: Rese11io MSSQL_PID: Developer MSSQL_DATABASE: resellioDB - MSSQL_USER: sqluser - MSSQL_PASSWORD: Rese11io volumes: - azuresqledge_data:/var/opt/mssql ports: - 1433:1433 - cap_add: - - SYS_PTRACE redis: image: redis:latest container_name: resellio_redis - restart: always volumes: - redis_data:/data ports: From 3469ccd1954ca83be29c7dc2e67822840bc4eb37 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Mon, 17 Mar 2025 09:36:50 +0100 Subject: [PATCH 052/128] Small adjustments in endpoint for creating new account --- .../TickAPI/Customers/Controllers/CustomerController.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/TickAPI/TickAPI/Customers/Controllers/CustomerController.cs b/TickAPI/TickAPI/Customers/Controllers/CustomerController.cs index 28433f9..fabb150 100644 --- a/TickAPI/TickAPI/Customers/Controllers/CustomerController.cs +++ b/TickAPI/TickAPI/Customers/Controllers/CustomerController.cs @@ -1,5 +1,5 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.IdentityModel.JsonWebTokens; +using System.Security.Claims; +using Microsoft.AspNetCore.Mvc; using TickAPI.Common.Auth.Abstractions; using TickAPI.Common.Auth.Attributes; using TickAPI.Common.Auth.Enums; @@ -54,9 +54,10 @@ public async Task> GoogleLogin([FromBody] G } [AuthorizeWithPolicy(AuthPolicies.NewCustomerPolicy)] + [HttpPost("google-create-new-account")] public async Task> GoogleCreateNewAccount([FromBody] GoogleCreateNewAccountDto request) { - var email = User.Claims.FirstOrDefault(c => c.Type == JwtRegisteredClaimNames.Email)?.Value; + var email = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value; if (email == null) return StatusCode(StatusCodes.Status400BadRequest, "missing email claim"); From f442b18d4704a81557a9a31790d1176a604a5adc Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Mon, 17 Mar 2025 11:40:14 +0100 Subject: [PATCH 053/128] Create tests for `CustomerService` --- .../Services/CustomerServiceTests.cs | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 TickAPI/TickAPI.Tests/Customers/Services/CustomerServiceTests.cs diff --git a/TickAPI/TickAPI.Tests/Customers/Services/CustomerServiceTests.cs b/TickAPI/TickAPI.Tests/Customers/Services/CustomerServiceTests.cs new file mode 100644 index 0000000..ba0960e --- /dev/null +++ b/TickAPI/TickAPI.Tests/Customers/Services/CustomerServiceTests.cs @@ -0,0 +1,93 @@ +using Microsoft.AspNetCore.Http; +using Moq; +using TickAPI.Common.Result; +using TickAPI.Common.Time.Abstractions; +using TickAPI.Customers.Abstractions; +using TickAPI.Customers.Models; +using TickAPI.Customers.Services; + +namespace TickAPI.Tests.Customers.Services; + +public class CustomerServiceTests +{ + [Fact] + public async Task GetCustomerByEmailAsync_WhenCustomerWithEmailIsReturnedFromRepository_ShouldReturnUser() + { + var customer = new Customer + { + Email = "example@test.com" + }; + var dateTimeServiceMock = new Mock(); + var customerRepositoryMock = new Mock(); + customerRepositoryMock.Setup(m => m.GetCustomerByEmailAsync(customer.Email)).ReturnsAsync(Result.Success(customer)); + var sut = new CustomerService(customerRepositoryMock.Object, dateTimeServiceMock.Object); + + var result = await sut.GetCustomerByEmailAsync(customer.Email); + + Assert.True(result.IsSuccess); + Assert.Equal(customer, result.Value); + } + + [Fact] + public async Task GetCustomerByEmailAsync_WhenCustomerWithEmailIsNotReturnedFromRepository_ShouldReturnFailure() + { + const string email = "not@existing.com"; + var dateTimeServiceMock = new Mock(); + var customerRepositoryMock = new Mock(); + customerRepositoryMock.Setup(m => m.GetCustomerByEmailAsync(email)).ReturnsAsync(Result.Failure(StatusCodes.Status404NotFound, $"customer with email '{email}' not found")); + var sut = new CustomerService(customerRepositoryMock.Object, dateTimeServiceMock.Object); + + var result = await sut.GetCustomerByEmailAsync(email); + + Assert.True(result.IsError); + Assert.Equal(StatusCodes.Status404NotFound, result.StatusCode); + Assert.Equal($"customer with email '{email}' not found", result.ErrorMsg); + } + + [Fact] + public async Task CreateNewCustomerAsync_WhenCustomerWithUniqueEmail_ShouldReturnNewCustomer() + { + const string email = "new@customer.com"; + const string firstName = "First"; + const string lastName = "Last"; + Guid id = Guid.NewGuid(); + DateTime createdAt = new DateTime(1970, 1, 1, 8, 0, 0, DateTimeKind.Utc); + var dateTimeServiceMock = new Mock(); + dateTimeServiceMock.Setup(m => m.GetCurrentDateTime()).Returns(createdAt); + var customerRepositoryMock = new Mock(); + customerRepositoryMock.Setup(m => m.GetCustomerByEmailAsync(email)).ReturnsAsync(Result.Failure(StatusCodes.Status404NotFound, $"customer with email '{email}' not found")); + customerRepositoryMock + .Setup(m => m.AddNewCustomerAsync(It.IsAny())) + .Callback(c => c.Id = id) + .Returns(Task.CompletedTask); + var sut = new CustomerService(customerRepositoryMock.Object, dateTimeServiceMock.Object); + + var result = await sut.CreateNewCustomerAsync(email, firstName, lastName); + + Assert.True(result.IsSuccess); + Assert.Equal(email, result.Value!.Email); + Assert.Equal(firstName, result.Value!.FirstName); + Assert.Equal(lastName, result.Value!.LastName); + Assert.Equal(createdAt, result.Value!.CreationDate); + Assert.Equal(id, result.Value!.Id); + } + + [Fact] + public async Task CreateNewCustomerAsync_WhenCustomerWithNotUniqueEmail_ShouldReturnFailure() + { + var customer = new Customer + { + Email = "already@exists.com" + }; + var dateTimeServiceMock = new Mock(); + var customerRepositoryMock = new Mock(); + customerRepositoryMock.Setup(m => m.GetCustomerByEmailAsync(customer.Email)).ReturnsAsync(Result.Success(customer)); + var sut = new CustomerService(customerRepositoryMock.Object, dateTimeServiceMock.Object); + + var result = await sut.CreateNewCustomerAsync(customer.Email, "First", "Last"); + + Assert.True(result.IsError); + Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); + Assert.Equal($"customer with email '{customer.Email}' already exists", result.ErrorMsg); + } +} \ No newline at end of file From d5a5f5c14bc1427a0be549a681bf0776bc25e3e2 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Mon, 17 Mar 2025 22:53:14 +0100 Subject: [PATCH 054/128] Create tests for `CustomerController` --- .../Controllers/CustomerControllerTests.cs | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 TickAPI/TickAPI.Tests/Customers/Controllers/CustomerControllerTests.cs diff --git a/TickAPI/TickAPI.Tests/Customers/Controllers/CustomerControllerTests.cs b/TickAPI/TickAPI.Tests/Customers/Controllers/CustomerControllerTests.cs new file mode 100644 index 0000000..d36ac0f --- /dev/null +++ b/TickAPI/TickAPI.Tests/Customers/Controllers/CustomerControllerTests.cs @@ -0,0 +1,150 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Moq; +using TickAPI.Common.Auth.Abstractions; +using TickAPI.Common.Auth.Enums; +using TickAPI.Common.Result; +using TickAPI.Customers.Abstractions; +using TickAPI.Customers.Controllers; +using TickAPI.Customers.DTOs.Request; +using TickAPI.Customers.Models; + +namespace TickAPI.Tests.Customers.Controllers; + +public class CustomerControllerTests +{ + [Fact] + public async Task GoogleLogin_WhenAuthSuccessAndCustomerExists_ShouldReturnTokenAndNotNewCustomer() + { + const string email = "existing@test.com"; + const string idToken = "valid-google-token"; + const string jwtToken = "valid-jwt-token"; + + var authServiceMock = new Mock(); + authServiceMock.Setup(m => m.LoginAsync(idToken)) + .ReturnsAsync(Result.Success(email)); + + var customerServiceMock = new Mock(); + customerServiceMock.Setup(m => m.GetCustomerByEmailAsync(email)) + .ReturnsAsync(Result.Success(new Customer { Email = email })); + + var jwtServiceMock = new Mock(); + jwtServiceMock.Setup(m => m.GenerateJwtToken(email, UserRole.Customer)) + .Returns(Result.Success(jwtToken)); + + var sut = new CustomerController( + authServiceMock.Object, + jwtServiceMock.Object, + customerServiceMock.Object); + + var actionResult = await sut.GoogleLogin(new GoogleLoginDto(idToken)); + + Assert.Equal(jwtToken, actionResult.Value?.Token); + Assert.False(actionResult.Value?.IsNewCustomer); + } + + [Fact] + public async Task GoogleLogin_WhenAuthSuccessAndCustomerDoesNotExist_ShouldReturnTokenAndNewCustomer() + { + const string email = "new@test.com"; + const string idToken = "valid-google-token"; + const string jwtToken = "valid-jwt-token"; + + var authServiceMock = new Mock(); + authServiceMock.Setup(m => m.LoginAsync(idToken)) + .ReturnsAsync(Result.Success(email)); + + var customerServiceMock = new Mock(); + customerServiceMock.Setup(m => m.GetCustomerByEmailAsync(email)) + .ReturnsAsync(Result.Failure(StatusCodes.Status404NotFound, $"customer with email '{email}' not found")); + + var jwtServiceMock = new Mock(); + jwtServiceMock.Setup(m => m.GenerateJwtToken(email, UserRole.NewCustomer)) + .Returns(Result.Success(jwtToken)); + + var sut = new CustomerController( + authServiceMock.Object, + jwtServiceMock.Object, + customerServiceMock.Object); + + var result = await sut.GoogleLogin(new GoogleLoginDto(idToken)); + + Assert.Equal(jwtToken, result.Value?.Token); + Assert.True(result.Value?.IsNewCustomer); + } + + [Fact] + public async Task GoogleCreateNewAccount_WhenCreatingAccountIsSuccessful_ShouldReturnToken() + { + const string email = "new@test.com"; + const string firstName = "First"; + const string lastName = "Last"; + const string jwtToken = "valid-jwt-token"; + + var authServiceMock = new Mock(); + var customerServiceMock = new Mock(); + customerServiceMock.Setup(m => m.CreateNewCustomerAsync(email, firstName, lastName)) + .ReturnsAsync(Result.Success(new Customer + { + Id = Guid.NewGuid(), + Email = email, + FirstName = firstName, + LastName = lastName + })); + + var jwtServiceMock = new Mock(); + jwtServiceMock.Setup(m => m.GenerateJwtToken(email, UserRole.Customer)) + .Returns(Result.Success(jwtToken)); + + var sut = new CustomerController( + authServiceMock.Object, + jwtServiceMock.Object, + customerServiceMock.Object); + + var claims = new List + { + new Claim(ClaimTypes.Email, email) + }; + sut.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity(claims)) + } + }; + + var result = await sut.GoogleCreateNewAccount( + new GoogleCreateNewAccountDto( firstName, lastName )); + + Assert.Equal(jwtToken, result.Value?.Token); + } + + [Fact] + public async Task GoogleCreateNewAccount_WhenEmailClaimIsMissing_ShouldReturnBadRequest() + { + var authServiceMock = new Mock(); + var jwtServiceMock = new Mock(); + var customerServiceMock = new Mock(); + + var sut = new CustomerController( + authServiceMock.Object, + jwtServiceMock.Object, + customerServiceMock.Object); + + sut.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity(new List())) + } + }; + + var result = await sut.GoogleCreateNewAccount( + new GoogleCreateNewAccountDto("First","Last")); + + var objectResult = Assert.IsType(result.Result); + Assert.Equal(StatusCodes.Status400BadRequest, objectResult.StatusCode); + Assert.Equal("missing email claim", objectResult.Value); + } +} \ No newline at end of file From 1fe63fdc4fd12dc1a0d1e8ef921a9c9ea929961b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Tue, 18 Mar 2025 12:05:22 +0100 Subject: [PATCH 055/128] Add encrypt false to constring --- TickAPI/TickAPI/appsettings.example.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TickAPI/TickAPI/appsettings.example.json b/TickAPI/TickAPI/appsettings.example.json index 1e34d53..020e0e4 100644 --- a/TickAPI/TickAPI/appsettings.example.json +++ b/TickAPI/TickAPI/appsettings.example.json @@ -11,7 +11,7 @@ "https://another-site.com" ], "ConnectionStrings": { - "ResellioDatabase": "Server=tcp:localhost,1433;Initial Catalog=your_db_name;Persist Security Info=False;User ID=sa;Password=your_password;TrustServerCertificate=True" + "ResellioDatabase": "Server=tcp:localhost,1433;Initial Catalog=resellioDB;Persist Security Info=False;User ID=sa;Password=Rese11io;TrustServerCertificate=True; Encrypt=False" }, "Authentication": { "Google": { From 70d0abf595a607c5a0157167956329eea5b41e47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Tue, 18 Mar 2025 12:08:28 +0100 Subject: [PATCH 056/128] Jakub Paczka space fix --- TickAPI/TickAPI/appsettings.example.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TickAPI/TickAPI/appsettings.example.json b/TickAPI/TickAPI/appsettings.example.json index 020e0e4..d24bd13 100644 --- a/TickAPI/TickAPI/appsettings.example.json +++ b/TickAPI/TickAPI/appsettings.example.json @@ -11,7 +11,7 @@ "https://another-site.com" ], "ConnectionStrings": { - "ResellioDatabase": "Server=tcp:localhost,1433;Initial Catalog=resellioDB;Persist Security Info=False;User ID=sa;Password=Rese11io;TrustServerCertificate=True; Encrypt=False" + "ResellioDatabase": "Server=tcp:localhost,1433;Initial Catalog=resellioDB;Persist Security Info=False;User ID=sa;Password=Rese11io;TrustServerCertificate=True;Encrypt=False" }, "Authentication": { "Google": { From 0f5d817fc3386281ba3b4b1d6f0cc5e5317b3c56 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Tue, 18 Mar 2025 17:56:11 +0100 Subject: [PATCH 057/128] Change `IAuthService` to `IGoogleAuthService` --- .../Common/Auth/Abstractions/IAuthService.cs | 8 -------- .../Common/Auth/Abstractions/IGoogleAuthService.cs | 9 +++++++++ .../Common/Auth/Controllers/AuthController.cs | 14 +++++++------- .../Common/Auth/Responses/GoogleUserData.cs | 7 +++++++ .../Common/Auth/Services/GoogleAuthService.cs | 13 +++++++------ 5 files changed, 30 insertions(+), 21 deletions(-) delete mode 100644 TickAPI/TickAPI/Common/Auth/Abstractions/IAuthService.cs create mode 100644 TickAPI/TickAPI/Common/Auth/Abstractions/IGoogleAuthService.cs create mode 100644 TickAPI/TickAPI/Common/Auth/Responses/GoogleUserData.cs diff --git a/TickAPI/TickAPI/Common/Auth/Abstractions/IAuthService.cs b/TickAPI/TickAPI/Common/Auth/Abstractions/IAuthService.cs deleted file mode 100644 index df0f33f..0000000 --- a/TickAPI/TickAPI/Common/Auth/Abstractions/IAuthService.cs +++ /dev/null @@ -1,8 +0,0 @@ -using TickAPI.Common.Result; - -namespace TickAPI.Common.Auth.Abstractions; - -public interface IAuthService -{ - Task> LoginAsync(string idToken); -} \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Auth/Abstractions/IGoogleAuthService.cs b/TickAPI/TickAPI/Common/Auth/Abstractions/IGoogleAuthService.cs new file mode 100644 index 0000000..039e75c --- /dev/null +++ b/TickAPI/TickAPI/Common/Auth/Abstractions/IGoogleAuthService.cs @@ -0,0 +1,9 @@ +using TickAPI.Common.Auth.Responses; +using TickAPI.Common.Result; + +namespace TickAPI.Common.Auth.Abstractions; + +public interface IGoogleAuthService +{ + Task> GetUserDataFromToken(string token); +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Auth/Controllers/AuthController.cs b/TickAPI/TickAPI/Common/Auth/Controllers/AuthController.cs index bdeedc5..6483535 100644 --- a/TickAPI/TickAPI/Common/Auth/Controllers/AuthController.cs +++ b/TickAPI/TickAPI/Common/Auth/Controllers/AuthController.cs @@ -8,12 +8,12 @@ namespace TickAPI.Common.Auth.Controllers; [Route("api/[controller]")] public class AuthController : ControllerBase { - private readonly IAuthService _authService; + private readonly IGoogleAuthService _googleAuthService; private readonly IJwtService _jwtService; - public AuthController(IAuthService authService, IJwtService jwtService) + public AuthController(IGoogleAuthService googleAuthService, IJwtService jwtService) { - _authService = authService; + _googleAuthService = googleAuthService; _jwtService = jwtService; } @@ -22,12 +22,12 @@ public AuthController(IAuthService authService, IJwtService jwtService) [HttpPost("google-login")] public async Task GoogleLogin([FromBody] GoogleLoginRequest request) { - var loginResult = await _authService.LoginAsync(request.IdToken); + var userDataResult = await _googleAuthService.GetUserDataFromToken(request.IdToken); - if(loginResult.IsError) - return StatusCode(loginResult.StatusCode, loginResult.ErrorMsg); + if(userDataResult.IsError) + return StatusCode(userDataResult.StatusCode, userDataResult.ErrorMsg); - var jwtTokenResult = _jwtService.GenerateJwtToken(loginResult.Value, UserRole.Customer); + var jwtTokenResult = _jwtService.GenerateJwtToken(userDataResult.Value?.Email, UserRole.Customer); if(jwtTokenResult.IsError) return StatusCode(jwtTokenResult.StatusCode, jwtTokenResult.ErrorMsg); diff --git a/TickAPI/TickAPI/Common/Auth/Responses/GoogleUserData.cs b/TickAPI/TickAPI/Common/Auth/Responses/GoogleUserData.cs new file mode 100644 index 0000000..d317e07 --- /dev/null +++ b/TickAPI/TickAPI/Common/Auth/Responses/GoogleUserData.cs @@ -0,0 +1,7 @@ +namespace TickAPI.Common.Auth.Responses; + +public record GoogleUserData( + string Email, + string FirstName, + string LastName +); \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Auth/Services/GoogleAuthService.cs b/TickAPI/TickAPI/Common/Auth/Services/GoogleAuthService.cs index 4abf2ad..b8fc876 100644 --- a/TickAPI/TickAPI/Common/Auth/Services/GoogleAuthService.cs +++ b/TickAPI/TickAPI/Common/Auth/Services/GoogleAuthService.cs @@ -1,10 +1,10 @@ -using Google.Apis.Auth; -using TickAPI.Common.Auth.Abstractions; +using TickAPI.Common.Auth.Abstractions; +using TickAPI.Common.Auth.Responses; using TickAPI.Common.Result; namespace TickAPI.Common.Auth.Services; -public class GoogleAuthService : IAuthService +public class GoogleAuthService : IGoogleAuthService { private readonly IGoogleTokenValidator _googleTokenValidator; @@ -13,16 +13,17 @@ public GoogleAuthService(IGoogleTokenValidator googleTokenValidator) _googleTokenValidator = googleTokenValidator; } - public async Task> LoginAsync(string idToken) + public async Task> GetUserDataFromToken(string idToken) { try { var payload = await _googleTokenValidator.ValidateAsync(idToken); - return Result.Success(payload.Email); + var userData = new GoogleUserData(payload.Email, payload.GivenName, payload.FamilyName); + return Result.Success(userData); } catch (Exception) { - return Result.Failure(StatusCodes.Status401Unauthorized, "Invalid Google ID token"); + return Result.Failure(StatusCodes.Status401Unauthorized, "Invalid Google ID token"); } } } \ No newline at end of file From 43adeff0805323555f031e7469930d96da6f909d Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Tue, 18 Mar 2025 17:57:42 +0100 Subject: [PATCH 058/128] Use only one endpoint for customer google auth --- .../Controllers/CustomerController.cs | 56 +++++-------------- .../DTOs/Request/GoogleCreateNewAccountDto.cs | 6 -- .../GoogleCreateNewAccountResponseDto.cs | 5 -- .../DTOs/Response/GoogleLoginResponseDto.cs | 3 +- 4 files changed, 16 insertions(+), 54 deletions(-) delete mode 100644 TickAPI/TickAPI/Customers/DTOs/Request/GoogleCreateNewAccountDto.cs delete mode 100644 TickAPI/TickAPI/Customers/DTOs/Response/GoogleCreateNewAccountResponseDto.cs diff --git a/TickAPI/TickAPI/Customers/Controllers/CustomerController.cs b/TickAPI/TickAPI/Customers/Controllers/CustomerController.cs index fabb150..d18d01d 100644 --- a/TickAPI/TickAPI/Customers/Controllers/CustomerController.cs +++ b/TickAPI/TickAPI/Customers/Controllers/CustomerController.cs @@ -1,7 +1,5 @@ -using System.Security.Claims; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using TickAPI.Common.Auth.Abstractions; -using TickAPI.Common.Auth.Attributes; using TickAPI.Common.Auth.Enums; using TickAPI.Customers.Abstractions; using TickAPI.Customers.DTOs.Request; @@ -13,13 +11,13 @@ namespace TickAPI.Customers.Controllers; [Route("api/[controller]")] public class CustomerController : ControllerBase { - private readonly IAuthService _authService; + private readonly IGoogleAuthService _googleAuthService; private readonly IJwtService _jwtService; private readonly ICustomerService _customerService; - public CustomerController(IAuthService authService, IJwtService jwtService, ICustomerService customerService) + public CustomerController(IGoogleAuthService googleAuthService, IJwtService jwtService, ICustomerService customerService) { - _authService = authService; + _googleAuthService = googleAuthService; _jwtService = jwtService; _customerService = customerService; } @@ -27,48 +25,24 @@ public CustomerController(IAuthService authService, IJwtService jwtService, ICus [HttpPost("google-login")] public async Task> GoogleLogin([FromBody] GoogleLoginDto request) { - var loginResult = await _authService.LoginAsync(request.IdToken); - if(loginResult.IsError) - return StatusCode(loginResult.StatusCode, loginResult.ErrorMsg); + var userDataResult = await _googleAuthService.GetUserDataFromToken(request.IdToken); + if(userDataResult.IsError) + return StatusCode(userDataResult.StatusCode, userDataResult.ErrorMsg); - UserRole role; - bool isNewCustomer; + var userData = userDataResult.Value!; - var existingCustomerResult = await _customerService.GetCustomerByEmailAsync(loginResult.Value!); - if (existingCustomerResult.IsSuccess) + var existingCustomerResult = await _customerService.GetCustomerByEmailAsync(userData.Email); + if (existingCustomerResult.IsError) { - role = UserRole.Customer; - isNewCustomer = false; + var newCustomerResult = await _customerService.CreateNewCustomerAsync(userData.Email, userData.FirstName, userData.LastName); + if (newCustomerResult.IsError) + return StatusCode(newCustomerResult.StatusCode, newCustomerResult.ErrorMsg); } - else - { - role = UserRole.NewCustomer; - isNewCustomer = true; - } - - var jwtTokenResult = _jwtService.GenerateJwtToken(loginResult.Value, role); - if (jwtTokenResult.IsError) - return StatusCode(jwtTokenResult.StatusCode, jwtTokenResult.ErrorMsg); - - return new ActionResult(new GoogleLoginResponseDto(jwtTokenResult.Value!, isNewCustomer)); - } - - [AuthorizeWithPolicy(AuthPolicies.NewCustomerPolicy)] - [HttpPost("google-create-new-account")] - public async Task> GoogleCreateNewAccount([FromBody] GoogleCreateNewAccountDto request) - { - var email = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value; - if (email == null) - return StatusCode(StatusCodes.Status400BadRequest, "missing email claim"); - - var newCustomerResult = await _customerService.CreateNewCustomerAsync(email, request.FirstName, request.LastName); - if (newCustomerResult.IsError) - return StatusCode(newCustomerResult.StatusCode, newCustomerResult.ErrorMsg); - var jwtTokenResult = _jwtService.GenerateJwtToken(newCustomerResult.Value!.Email, UserRole.Customer); + var jwtTokenResult = _jwtService.GenerateJwtToken(userData.Email, UserRole.Customer); if (jwtTokenResult.IsError) return StatusCode(jwtTokenResult.StatusCode, jwtTokenResult.ErrorMsg); - return new ActionResult(new GoogleCreateNewAccountResponseDto(jwtTokenResult.Value!)); + return new ActionResult(new GoogleLoginResponseDto(jwtTokenResult.Value!)); } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Customers/DTOs/Request/GoogleCreateNewAccountDto.cs b/TickAPI/TickAPI/Customers/DTOs/Request/GoogleCreateNewAccountDto.cs deleted file mode 100644 index f896265..0000000 --- a/TickAPI/TickAPI/Customers/DTOs/Request/GoogleCreateNewAccountDto.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace TickAPI.Customers.DTOs.Request; - -public record GoogleCreateNewAccountDto( - string FirstName, - string LastName -); \ No newline at end of file diff --git a/TickAPI/TickAPI/Customers/DTOs/Response/GoogleCreateNewAccountResponseDto.cs b/TickAPI/TickAPI/Customers/DTOs/Response/GoogleCreateNewAccountResponseDto.cs deleted file mode 100644 index b49ac83..0000000 --- a/TickAPI/TickAPI/Customers/DTOs/Response/GoogleCreateNewAccountResponseDto.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace TickAPI.Customers.DTOs.Response; - -public record GoogleCreateNewAccountResponseDto( - string Token -); \ No newline at end of file diff --git a/TickAPI/TickAPI/Customers/DTOs/Response/GoogleLoginResponseDto.cs b/TickAPI/TickAPI/Customers/DTOs/Response/GoogleLoginResponseDto.cs index b2d5434..0090b70 100644 --- a/TickAPI/TickAPI/Customers/DTOs/Response/GoogleLoginResponseDto.cs +++ b/TickAPI/TickAPI/Customers/DTOs/Response/GoogleLoginResponseDto.cs @@ -1,6 +1,5 @@ namespace TickAPI.Customers.DTOs.Response; public record GoogleLoginResponseDto( - string Token, - bool IsNewCustomer + string Token ); \ No newline at end of file From bbf0644f1f2be40e87ad3cee191fdab3e6e27929 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Tue, 18 Mar 2025 17:58:24 +0100 Subject: [PATCH 059/128] Remove `NewCustomer` role and related policy + clean up `Program.cs` --- TickAPI/TickAPI/Common/Auth/Enums/AuthPolicies.cs | 1 - TickAPI/TickAPI/Common/Auth/Enums/UserRole.cs | 1 - TickAPI/TickAPI/Program.cs | 3 +-- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/TickAPI/TickAPI/Common/Auth/Enums/AuthPolicies.cs b/TickAPI/TickAPI/Common/Auth/Enums/AuthPolicies.cs index ec06e2e..c5d6408 100644 --- a/TickAPI/TickAPI/Common/Auth/Enums/AuthPolicies.cs +++ b/TickAPI/TickAPI/Common/Auth/Enums/AuthPolicies.cs @@ -6,5 +6,4 @@ public enum AuthPolicies OrganizerPolicy, CustomerPolicy, NewOrganizerPolicy, - NewCustomerPolicy, } \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Auth/Enums/UserRole.cs b/TickAPI/TickAPI/Common/Auth/Enums/UserRole.cs index 93e3677..d382a54 100644 --- a/TickAPI/TickAPI/Common/Auth/Enums/UserRole.cs +++ b/TickAPI/TickAPI/Common/Auth/Enums/UserRole.cs @@ -6,5 +6,4 @@ public enum UserRole Organizer, Customer, NewOrganizer, - NewCustomer, } \ No newline at end of file diff --git a/TickAPI/TickAPI/Program.cs b/TickAPI/TickAPI/Program.cs index 725812a..0faecf6 100644 --- a/TickAPI/TickAPI/Program.cs +++ b/TickAPI/TickAPI/Program.cs @@ -69,7 +69,6 @@ options.AddPolicy(AuthPolicies.CustomerPolicy.ToString(), policy => policy.RequireRole(UserRole.Customer.ToString())); options.AddPolicy(AuthPolicies.NewOrganizerPolicy.ToString(), policy => policy.RequireRole(UserRole.NewOrganizer.ToString())); - options.AddPolicy(AuthPolicies.NewCustomerPolicy.ToString(), policy => policy.RequireRole(UserRole.NewCustomer.ToString())); }); // Add admin services. @@ -93,7 +92,7 @@ builder.Services.AddScoped(); // Add common services. -builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); From a6fb00216f3ef372441469767d7e4511a8afb10c Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Tue, 18 Mar 2025 18:05:39 +0100 Subject: [PATCH 060/128] Add proper namespace to `GoogleTokenValidator` --- TickAPI/TickAPI/Common/Auth/Services/GoogleTokenValidator.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TickAPI/TickAPI/Common/Auth/Services/GoogleTokenValidator.cs b/TickAPI/TickAPI/Common/Auth/Services/GoogleTokenValidator.cs index 060a2f0..a6bd4d4 100644 --- a/TickAPI/TickAPI/Common/Auth/Services/GoogleTokenValidator.cs +++ b/TickAPI/TickAPI/Common/Auth/Services/GoogleTokenValidator.cs @@ -1,6 +1,8 @@ using Google.Apis.Auth; using TickAPI.Common.Auth.Abstractions; +namespace TickAPI.Common.Auth.Services; + public class GoogleTokenValidator : IGoogleTokenValidator { private readonly IConfiguration _configuration; From e7e822e8ad973e7c05bb35a11488fdbf184ab0b4 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Tue, 18 Mar 2025 18:06:57 +0100 Subject: [PATCH 061/128] Add `about-me` endpoint to `CustomerController` --- .../Controllers/CustomerController.cs | 24 ++++++++++++++++++- .../DTOs/Response/AboutMeResponseDto.cs | 8 +++++++ 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 TickAPI/TickAPI/Customers/DTOs/Response/AboutMeResponseDto.cs diff --git a/TickAPI/TickAPI/Customers/Controllers/CustomerController.cs b/TickAPI/TickAPI/Customers/Controllers/CustomerController.cs index d18d01d..ea02b83 100644 --- a/TickAPI/TickAPI/Customers/Controllers/CustomerController.cs +++ b/TickAPI/TickAPI/Customers/Controllers/CustomerController.cs @@ -1,5 +1,7 @@ -using Microsoft.AspNetCore.Mvc; +using System.Security.Claims; +using Microsoft.AspNetCore.Mvc; using TickAPI.Common.Auth.Abstractions; +using TickAPI.Common.Auth.Attributes; using TickAPI.Common.Auth.Enums; using TickAPI.Customers.Abstractions; using TickAPI.Customers.DTOs.Request; @@ -45,4 +47,24 @@ public async Task> GoogleLogin([FromBody] G return new ActionResult(new GoogleLoginResponseDto(jwtTokenResult.Value!)); } + + [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] + [HttpGet("about-me")] + public async Task> AboutMe() + { + var email = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value; + if (email == null) + return StatusCode(StatusCodes.Status400BadRequest, "missing email claim"); + + var customerResult = await _customerService.GetCustomerByEmailAsync(email); + if (customerResult.IsError) + return StatusCode(StatusCodes.Status500InternalServerError, + "cannot find customer in database for authorized customer request"); + + var customer = customerResult.Value!; + + var aboutMeResponse = + new AboutMeResponseDto(customer.Email, customer.FirstName, customer.LastName, customer.CreationDate); + return new ActionResult(aboutMeResponse); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Customers/DTOs/Response/AboutMeResponseDto.cs b/TickAPI/TickAPI/Customers/DTOs/Response/AboutMeResponseDto.cs new file mode 100644 index 0000000..c813f55 --- /dev/null +++ b/TickAPI/TickAPI/Customers/DTOs/Response/AboutMeResponseDto.cs @@ -0,0 +1,8 @@ +namespace TickAPI.Customers.DTOs.Response; + +public record AboutMeResponseDto( + string Email, + string FirstName, + string LastName, + DateTime CreationDate +); \ No newline at end of file From f6bfd7b229f9394be556a2f59c882c588a2e5a09 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Tue, 18 Mar 2025 18:19:36 +0100 Subject: [PATCH 062/128] Fix tests for `GoogleAuthService` --- .../Common/Auth/Services/GoogleAuthServiceTests.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/TickAPI/TickAPI.Tests/Common/Auth/Services/GoogleAuthServiceTests.cs b/TickAPI/TickAPI.Tests/Common/Auth/Services/GoogleAuthServiceTests.cs index ad5e695..c9c3824 100644 --- a/TickAPI/TickAPI.Tests/Common/Auth/Services/GoogleAuthServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Common/Auth/Services/GoogleAuthServiceTests.cs @@ -10,22 +10,24 @@ namespace TickAPI.Tests.Common.Auth.Services; public class GoogleAuthServiceTests { [Fact] - public async Task LoginAsync_WhenTokenValidatorReturnsPayload_ShouldReturnEmailFromPayload() + public async Task GetUserDataFromToken_WhenTokenValidatorReturnsPayload_ShouldReturnEmailFromPayload() { var googleTokenValidatorMock = new Mock(); googleTokenValidatorMock .Setup(m => m.ValidateAsync("validToken")) - .ReturnsAsync(new GoogleJsonWebSignature.Payload { Email = "example@test.com" }); + .ReturnsAsync(new GoogleJsonWebSignature.Payload { Email = "example@test.com", GivenName = "First", FamilyName = "Last"}); var sut = new GoogleAuthService(googleTokenValidatorMock.Object); - var result = await sut.LoginAsync("validToken"); + var result = await sut.GetUserDataFromToken("validToken"); Assert.True(result.IsSuccess); - Assert.Equal("example@test.com", result.Value); + Assert.Equal("example@test.com", result.Value?.Email); + Assert.Equal("First", result.Value?.FirstName); + Assert.Equal("Last", result.Value?.LastName); } [Fact] - public async Task LoginAsync_WhenTokenValidatorThrowsException_ShouldReturnFailure() + public async Task GetUserDataFromToken_WhenTokenValidatorThrowsException_ShouldReturnFailure() { var googleTokenValidatorMock = new Mock(); googleTokenValidatorMock @@ -33,7 +35,7 @@ public async Task LoginAsync_WhenTokenValidatorThrowsException_ShouldReturnFailu .Throws(new InvalidJwtException("Invalid Google ID token")); var sut = new GoogleAuthService(googleTokenValidatorMock.Object); - var result = await sut.LoginAsync("invalidToken"); + var result = await sut.GetUserDataFromToken("invalidToken"); Assert.True(result.IsError); Assert.Equal(StatusCodes.Status401Unauthorized, result.StatusCode); From 403608314632e82fed4b93310ca83d2774159fdf Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Tue, 18 Mar 2025 18:33:29 +0100 Subject: [PATCH 063/128] Fix tests for `CustomerContoller` --- .../Controllers/CustomerControllerTests.cs | 103 ++++-------------- 1 file changed, 21 insertions(+), 82 deletions(-) diff --git a/TickAPI/TickAPI.Tests/Customers/Controllers/CustomerControllerTests.cs b/TickAPI/TickAPI.Tests/Customers/Controllers/CustomerControllerTests.cs index d36ac0f..dc6b52e 100644 --- a/TickAPI/TickAPI.Tests/Customers/Controllers/CustomerControllerTests.cs +++ b/TickAPI/TickAPI.Tests/Customers/Controllers/CustomerControllerTests.cs @@ -1,9 +1,8 @@ -using System.Security.Claims; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Http; using Moq; using TickAPI.Common.Auth.Abstractions; using TickAPI.Common.Auth.Enums; +using TickAPI.Common.Auth.Responses; using TickAPI.Common.Result; using TickAPI.Customers.Abstractions; using TickAPI.Customers.Controllers; @@ -15,15 +14,16 @@ namespace TickAPI.Tests.Customers.Controllers; public class CustomerControllerTests { [Fact] - public async Task GoogleLogin_WhenAuthSuccessAndCustomerExists_ShouldReturnTokenAndNotNewCustomer() + public async Task GoogleLogin_WhenAuthSuccessAndCustomerExists_ShouldReturnToken() { + // Arrange const string email = "existing@test.com"; const string idToken = "valid-google-token"; const string jwtToken = "valid-jwt-token"; - var authServiceMock = new Mock(); - authServiceMock.Setup(m => m.LoginAsync(idToken)) - .ReturnsAsync(Result.Success(email)); + var googleAuthServiceMock = new Mock(); + googleAuthServiceMock.Setup(m => m.GetUserDataFromToken(idToken)) + .ReturnsAsync(Result.Success(new GoogleUserData(email, "First", "Last"))); var customerServiceMock = new Mock(); customerServiceMock.Setup(m => m.GetCustomerByEmailAsync(email)) @@ -34,56 +34,34 @@ public async Task GoogleLogin_WhenAuthSuccessAndCustomerExists_ShouldReturnToken .Returns(Result.Success(jwtToken)); var sut = new CustomerController( - authServiceMock.Object, + googleAuthServiceMock.Object, jwtServiceMock.Object, customerServiceMock.Object); + // Act var actionResult = await sut.GoogleLogin(new GoogleLoginDto(idToken)); + // Assert Assert.Equal(jwtToken, actionResult.Value?.Token); - Assert.False(actionResult.Value?.IsNewCustomer); } [Fact] - public async Task GoogleLogin_WhenAuthSuccessAndCustomerDoesNotExist_ShouldReturnTokenAndNewCustomer() + public async Task GoogleLogin_WhenAuthSuccessAndCustomerDoesNotExist_ShouldCreateCustomerAndReturnToken() { + // Arrange const string email = "new@test.com"; const string idToken = "valid-google-token"; + const string firstName = "First"; + const string lastName = "Last"; const string jwtToken = "valid-jwt-token"; - var authServiceMock = new Mock(); - authServiceMock.Setup(m => m.LoginAsync(idToken)) - .ReturnsAsync(Result.Success(email)); + var googleAuthServiceMock = new Mock(); + googleAuthServiceMock.Setup(m => m.GetUserDataFromToken(idToken)) + .ReturnsAsync(Result.Success(new GoogleUserData(email, "First", "Last"))); var customerServiceMock = new Mock(); customerServiceMock.Setup(m => m.GetCustomerByEmailAsync(email)) .ReturnsAsync(Result.Failure(StatusCodes.Status404NotFound, $"customer with email '{email}' not found")); - - var jwtServiceMock = new Mock(); - jwtServiceMock.Setup(m => m.GenerateJwtToken(email, UserRole.NewCustomer)) - .Returns(Result.Success(jwtToken)); - - var sut = new CustomerController( - authServiceMock.Object, - jwtServiceMock.Object, - customerServiceMock.Object); - - var result = await sut.GoogleLogin(new GoogleLoginDto(idToken)); - - Assert.Equal(jwtToken, result.Value?.Token); - Assert.True(result.Value?.IsNewCustomer); - } - - [Fact] - public async Task GoogleCreateNewAccount_WhenCreatingAccountIsSuccessful_ShouldReturnToken() - { - const string email = "new@test.com"; - const string firstName = "First"; - const string lastName = "Last"; - const string jwtToken = "valid-jwt-token"; - - var authServiceMock = new Mock(); - var customerServiceMock = new Mock(); customerServiceMock.Setup(m => m.CreateNewCustomerAsync(email, firstName, lastName)) .ReturnsAsync(Result.Success(new Customer { @@ -98,53 +76,14 @@ public async Task GoogleCreateNewAccount_WhenCreatingAccountIsSuccessful_ShouldR .Returns(Result.Success(jwtToken)); var sut = new CustomerController( - authServiceMock.Object, + googleAuthServiceMock.Object, jwtServiceMock.Object, customerServiceMock.Object); - var claims = new List - { - new Claim(ClaimTypes.Email, email) - }; - sut.ControllerContext = new ControllerContext - { - HttpContext = new DefaultHttpContext - { - User = new ClaimsPrincipal(new ClaimsIdentity(claims)) - } - }; - - var result = await sut.GoogleCreateNewAccount( - new GoogleCreateNewAccountDto( firstName, lastName )); + // Act + var result = await sut.GoogleLogin(new GoogleLoginDto( idToken )); + // Assert Assert.Equal(jwtToken, result.Value?.Token); } - - [Fact] - public async Task GoogleCreateNewAccount_WhenEmailClaimIsMissing_ShouldReturnBadRequest() - { - var authServiceMock = new Mock(); - var jwtServiceMock = new Mock(); - var customerServiceMock = new Mock(); - - var sut = new CustomerController( - authServiceMock.Object, - jwtServiceMock.Object, - customerServiceMock.Object); - - sut.ControllerContext = new ControllerContext - { - HttpContext = new DefaultHttpContext - { - User = new ClaimsPrincipal(new ClaimsIdentity(new List())) - } - }; - - var result = await sut.GoogleCreateNewAccount( - new GoogleCreateNewAccountDto("First","Last")); - - var objectResult = Assert.IsType(result.Result); - Assert.Equal(StatusCodes.Status400BadRequest, objectResult.StatusCode); - Assert.Equal("missing email claim", objectResult.Value); - } } \ No newline at end of file From bd23e474c0997fd436c355b83af7b3d16d6a26c6 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Tue, 18 Mar 2025 18:42:23 +0100 Subject: [PATCH 064/128] Add tests for "about-me" customer endpoint --- .../Controllers/CustomerControllerTests.cs | 92 ++++++++++++++++++- 1 file changed, 91 insertions(+), 1 deletion(-) diff --git a/TickAPI/TickAPI.Tests/Customers/Controllers/CustomerControllerTests.cs b/TickAPI/TickAPI.Tests/Customers/Controllers/CustomerControllerTests.cs index dc6b52e..0c0270b 100644 --- a/TickAPI/TickAPI.Tests/Customers/Controllers/CustomerControllerTests.cs +++ b/TickAPI/TickAPI.Tests/Customers/Controllers/CustomerControllerTests.cs @@ -1,4 +1,6 @@ -using Microsoft.AspNetCore.Http; +using System.Security.Claims; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; using Moq; using TickAPI.Common.Auth.Abstractions; using TickAPI.Common.Auth.Enums; @@ -86,4 +88,92 @@ public async Task GoogleLogin_WhenAuthSuccessAndCustomerDoesNotExist_ShouldCreat // Assert Assert.Equal(jwtToken, result.Value?.Token); } + + [Fact] + public async Task AboutMe_WithValidEmailClaim_ShouldReturnCustomerDetails() + { + // Arrange + const string email = "test@example.com"; + const string firstName = "John"; + const string lastName = "Doe"; + var creationDate = DateTime.UtcNow.AddDays(-30); + + var customer = new Customer + { + Email = email, + FirstName = firstName, + LastName = lastName, + CreationDate = creationDate + }; + + var customerServiceMock = new Mock(); + customerServiceMock.Setup(m => m.GetCustomerByEmailAsync(email)) + .ReturnsAsync(Result.Success(customer)); + + var googleAuthServiceMock = new Mock(); + var jwtServiceMock = new Mock(); + + var sut = new CustomerController( + googleAuthServiceMock.Object, + jwtServiceMock.Object, + customerServiceMock.Object); + + var claims = new List + { + new Claim(ClaimTypes.Email, email) + }; + var identity = new ClaimsIdentity(claims); + var claimsPrincipal = new ClaimsPrincipal(identity); + + sut.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = claimsPrincipal + } + }; + + // Act + var result = await sut.AboutMe(); + + // Assert + Assert.Equal(email, result.Value?.Email); + Assert.Equal(firstName, result.Value?.FirstName); + Assert.Equal(lastName, result.Value?.LastName); + Assert.Equal(creationDate, result.Value?.CreationDate); + } + + [Fact] + public async Task AboutMe_WithMissingEmailClaim_ShouldReturnBadRequest() + { + // Arrange + var customerServiceMock = new Mock(); + var googleAuthServiceMock = new Mock(); + var jwtServiceMock = new Mock(); + + var sut = new CustomerController( + googleAuthServiceMock.Object, + jwtServiceMock.Object, + customerServiceMock.Object); + + var claims = new List(); + var identity = new ClaimsIdentity(claims); + var claimsPrincipal = new ClaimsPrincipal(identity); + + sut.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = claimsPrincipal + } + }; + + // Act + var result = await sut.AboutMe(); + + // Assert + var objectResult = Assert.IsType(result.Result); + Assert.Equal(StatusCodes.Status400BadRequest, objectResult.StatusCode); + Assert.Equal("missing email claim", objectResult.Value); + } } \ No newline at end of file From 4d8d8ea8cb1a327ef8ec01884466c43d41b6f414 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Tue, 18 Mar 2025 19:52:56 +0100 Subject: [PATCH 065/128] Assign the same value to the `creationDate` in every test run --- .../Customers/Controllers/CustomerControllerTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TickAPI/TickAPI.Tests/Customers/Controllers/CustomerControllerTests.cs b/TickAPI/TickAPI.Tests/Customers/Controllers/CustomerControllerTests.cs index 0c0270b..9c8079b 100644 --- a/TickAPI/TickAPI.Tests/Customers/Controllers/CustomerControllerTests.cs +++ b/TickAPI/TickAPI.Tests/Customers/Controllers/CustomerControllerTests.cs @@ -96,7 +96,7 @@ public async Task AboutMe_WithValidEmailClaim_ShouldReturnCustomerDetails() const string email = "test@example.com"; const string firstName = "John"; const string lastName = "Doe"; - var creationDate = DateTime.UtcNow.AddDays(-30); + var creationDate = new DateTime(1970, 1, 1, 8, 0, 0, DateTimeKind.Utc); var customer = new Customer { From 74f307f8dd27cb54dcc630707f5b72f6cae0396a Mon Sep 17 00:00:00 2001 From: kubapoke Date: Wed, 19 Mar 2025 22:01:47 +0100 Subject: [PATCH 066/128] changed user data retrieval to use access token instead of id token --- .../Auth/Abstractions/IGoogleAuthService.cs | 2 +- .../Common/Auth/Responses/GoogleUserData.cs | 10 +++-- .../Common/Auth/Services/GoogleAuthService.cs | 38 ++++++++++++++----- .../Controllers/CustomerController.cs | 4 +- .../Customers/DTOs/Request/GoogleLoginDto.cs | 2 +- TickAPI/TickAPI/Program.cs | 3 ++ TickAPI/TickAPI/appsettings.example.json | 3 +- 7 files changed, 43 insertions(+), 19 deletions(-) diff --git a/TickAPI/TickAPI/Common/Auth/Abstractions/IGoogleAuthService.cs b/TickAPI/TickAPI/Common/Auth/Abstractions/IGoogleAuthService.cs index 039e75c..3a3c5f5 100644 --- a/TickAPI/TickAPI/Common/Auth/Abstractions/IGoogleAuthService.cs +++ b/TickAPI/TickAPI/Common/Auth/Abstractions/IGoogleAuthService.cs @@ -5,5 +5,5 @@ namespace TickAPI.Common.Auth.Abstractions; public interface IGoogleAuthService { - Task> GetUserDataFromToken(string token); + Task> GetUserDataFromAccessToken(string accessToken); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Auth/Responses/GoogleUserData.cs b/TickAPI/TickAPI/Common/Auth/Responses/GoogleUserData.cs index d317e07..9512e88 100644 --- a/TickAPI/TickAPI/Common/Auth/Responses/GoogleUserData.cs +++ b/TickAPI/TickAPI/Common/Auth/Responses/GoogleUserData.cs @@ -1,7 +1,9 @@ -namespace TickAPI.Common.Auth.Responses; +using System.Text.Json.Serialization; + +namespace TickAPI.Common.Auth.Responses; public record GoogleUserData( - string Email, - string FirstName, - string LastName + [property :JsonPropertyName("email")] string Email, + [property :JsonPropertyName("given_name")] string GivenName, + [property :JsonPropertyName("family_name")] string FamilyName ); \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Auth/Services/GoogleAuthService.cs b/TickAPI/TickAPI/Common/Auth/Services/GoogleAuthService.cs index b8fc876..42e3dd1 100644 --- a/TickAPI/TickAPI/Common/Auth/Services/GoogleAuthService.cs +++ b/TickAPI/TickAPI/Common/Auth/Services/GoogleAuthService.cs @@ -1,4 +1,5 @@ -using TickAPI.Common.Auth.Abstractions; +using System.Text.Json; +using TickAPI.Common.Auth.Abstractions; using TickAPI.Common.Auth.Responses; using TickAPI.Common.Result; @@ -6,24 +7,41 @@ namespace TickAPI.Common.Auth.Services; public class GoogleAuthService : IGoogleAuthService { - private readonly IGoogleTokenValidator _googleTokenValidator; + private IConfiguration _configuration; + private IHttpClientFactory _httpClientFactory; - public GoogleAuthService(IGoogleTokenValidator googleTokenValidator) + public GoogleAuthService(IConfiguration configuration, IHttpClientFactory httpClientFactory) { - _googleTokenValidator = googleTokenValidator; + _configuration = configuration; + _httpClientFactory = httpClientFactory; } - public async Task> GetUserDataFromToken(string idToken) + public async Task> GetUserDataFromAccessToken(string accessToken) { try { - var payload = await _googleTokenValidator.ValidateAsync(idToken); - var userData = new GoogleUserData(payload.Email, payload.GivenName, payload.FamilyName); - return Result.Success(userData); + var client = _httpClientFactory.CreateClient(); + client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken); + + var response = client.GetAsync(_configuration["Authentication:Google:UserInfoEndpoint"]).Result; + if (!response.IsSuccessStatusCode) + { + return Result.Failure(StatusCodes.Status401Unauthorized, "Invalid Google access token"); + } + + var jsonResponse = await response.Content.ReadAsStringAsync(); + var userInfo = JsonSerializer.Deserialize(jsonResponse, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + + if (userInfo == null) + { + return Result.Failure(StatusCodes.Status500InternalServerError, "Failed to parse Google user info"); + } + + return Result.Success(userInfo); } - catch (Exception) + catch (Exception ex) { - return Result.Failure(StatusCodes.Status401Unauthorized, "Invalid Google ID token"); + return Result.Failure(StatusCodes.Status500InternalServerError, $"Error fetching user data: {ex.Message}"); } } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Customers/Controllers/CustomerController.cs b/TickAPI/TickAPI/Customers/Controllers/CustomerController.cs index ea02b83..2a4d48a 100644 --- a/TickAPI/TickAPI/Customers/Controllers/CustomerController.cs +++ b/TickAPI/TickAPI/Customers/Controllers/CustomerController.cs @@ -27,7 +27,7 @@ public CustomerController(IGoogleAuthService googleAuthService, IJwtService jwtS [HttpPost("google-login")] public async Task> GoogleLogin([FromBody] GoogleLoginDto request) { - var userDataResult = await _googleAuthService.GetUserDataFromToken(request.IdToken); + var userDataResult = await _googleAuthService.GetUserDataFromAccessToken(request.AccessToken); if(userDataResult.IsError) return StatusCode(userDataResult.StatusCode, userDataResult.ErrorMsg); @@ -36,7 +36,7 @@ public async Task> GoogleLogin([FromBody] G var existingCustomerResult = await _customerService.GetCustomerByEmailAsync(userData.Email); if (existingCustomerResult.IsError) { - var newCustomerResult = await _customerService.CreateNewCustomerAsync(userData.Email, userData.FirstName, userData.LastName); + var newCustomerResult = await _customerService.CreateNewCustomerAsync(userData.Email, userData.GivenName, userData.FamilyName); if (newCustomerResult.IsError) return StatusCode(newCustomerResult.StatusCode, newCustomerResult.ErrorMsg); } diff --git a/TickAPI/TickAPI/Customers/DTOs/Request/GoogleLoginDto.cs b/TickAPI/TickAPI/Customers/DTOs/Request/GoogleLoginDto.cs index 043b7ce..4d9561e 100644 --- a/TickAPI/TickAPI/Customers/DTOs/Request/GoogleLoginDto.cs +++ b/TickAPI/TickAPI/Customers/DTOs/Request/GoogleLoginDto.cs @@ -1,5 +1,5 @@ namespace TickAPI.Customers.DTOs.Request; public record GoogleLoginDto( - string IdToken + string AccessToken ); \ No newline at end of file diff --git a/TickAPI/TickAPI/Program.cs b/TickAPI/TickAPI/Program.cs index 0faecf6..c67c1e5 100644 --- a/TickAPI/TickAPI/Program.cs +++ b/TickAPI/TickAPI/Program.cs @@ -148,6 +148,9 @@ // Setup healtcheck builder.Services.AddHealthChecks().AddSqlServer(connectionString: builder.Configuration.GetConnectionString("ResellioDatabase") ?? ""); +// Add http client +builder.Services.AddHttpClient(); + var app = builder.Build(); // Configure the HTTP request pipeline. diff --git a/TickAPI/TickAPI/appsettings.example.json b/TickAPI/TickAPI/appsettings.example.json index d24bd13..9070fd3 100644 --- a/TickAPI/TickAPI/appsettings.example.json +++ b/TickAPI/TickAPI/appsettings.example.json @@ -16,7 +16,8 @@ "Authentication": { "Google": { "ClientId": "your-google-client-id-here", - "ClientSecret": "your-google-client-secret-here" + "ClientSecret": "your-google-client-secret-here", + "UserInfoEndpoint" : "https://www.googleapis.com/oauth2/v3/userinfo" }, "Jwt": { "Issuer": "your-api-issuer-here", From f1f72928705b9dc268d27fb50fc77fabc92cacdd Mon Sep 17 00:00:00 2001 From: kubapoke Date: Wed, 19 Mar 2025 22:07:34 +0100 Subject: [PATCH 067/128] removed AuthController.cs as it's redundant at this point --- .../Common/Auth/Controllers/AuthController.cs | 42 ------------------- 1 file changed, 42 deletions(-) delete mode 100644 TickAPI/TickAPI/Common/Auth/Controllers/AuthController.cs diff --git a/TickAPI/TickAPI/Common/Auth/Controllers/AuthController.cs b/TickAPI/TickAPI/Common/Auth/Controllers/AuthController.cs deleted file mode 100644 index 6483535..0000000 --- a/TickAPI/TickAPI/Common/Auth/Controllers/AuthController.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using TickAPI.Common.Auth.Abstractions; -using TickAPI.Common.Auth.Enums; - -namespace TickAPI.Common.Auth.Controllers; - -[ApiController] -[Route("api/[controller]")] -public class AuthController : ControllerBase -{ - private readonly IGoogleAuthService _googleAuthService; - private readonly IJwtService _jwtService; - - public AuthController(IGoogleAuthService googleAuthService, IJwtService jwtService) - { - _googleAuthService = googleAuthService; - _jwtService = jwtService; - } - - // TODO: this is a placeholder method that shows off the general structure of how logging in through Google works - // in the application. It should be replaced with appropriate login/register endpoints. - [HttpPost("google-login")] - public async Task GoogleLogin([FromBody] GoogleLoginRequest request) - { - var userDataResult = await _googleAuthService.GetUserDataFromToken(request.IdToken); - - if(userDataResult.IsError) - return StatusCode(userDataResult.StatusCode, userDataResult.ErrorMsg); - - var jwtTokenResult = _jwtService.GenerateJwtToken(userDataResult.Value?.Email, UserRole.Customer); - - if(jwtTokenResult.IsError) - return StatusCode(jwtTokenResult.StatusCode, jwtTokenResult.ErrorMsg); - - return Ok(new { token = jwtTokenResult.Value }); - } - - public class GoogleLoginRequest - { - public string IdToken { get; set; } - } -} \ No newline at end of file From f367f40fa6fa7735baebf9d4f36c80be32193e9a Mon Sep 17 00:00:00 2001 From: kubapoke Date: Wed, 19 Mar 2025 22:09:05 +0100 Subject: [PATCH 068/128] updated CustomerControllerTests.cs with appropriate names --- .../Customers/Controllers/CustomerControllerTests.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/TickAPI/TickAPI.Tests/Customers/Controllers/CustomerControllerTests.cs b/TickAPI/TickAPI.Tests/Customers/Controllers/CustomerControllerTests.cs index 9c8079b..6be9918 100644 --- a/TickAPI/TickAPI.Tests/Customers/Controllers/CustomerControllerTests.cs +++ b/TickAPI/TickAPI.Tests/Customers/Controllers/CustomerControllerTests.cs @@ -20,11 +20,11 @@ public async Task GoogleLogin_WhenAuthSuccessAndCustomerExists_ShouldReturnToken { // Arrange const string email = "existing@test.com"; - const string idToken = "valid-google-token"; + const string accessToken = "valid-google-token"; const string jwtToken = "valid-jwt-token"; var googleAuthServiceMock = new Mock(); - googleAuthServiceMock.Setup(m => m.GetUserDataFromToken(idToken)) + googleAuthServiceMock.Setup(m => m.GetUserDataFromAccessToken(accessToken)) .ReturnsAsync(Result.Success(new GoogleUserData(email, "First", "Last"))); var customerServiceMock = new Mock(); @@ -41,7 +41,7 @@ public async Task GoogleLogin_WhenAuthSuccessAndCustomerExists_ShouldReturnToken customerServiceMock.Object); // Act - var actionResult = await sut.GoogleLogin(new GoogleLoginDto(idToken)); + var actionResult = await sut.GoogleLogin(new GoogleLoginDto(accessToken)); // Assert Assert.Equal(jwtToken, actionResult.Value?.Token); @@ -52,13 +52,13 @@ public async Task GoogleLogin_WhenAuthSuccessAndCustomerDoesNotExist_ShouldCreat { // Arrange const string email = "new@test.com"; - const string idToken = "valid-google-token"; + const string accessToken = "valid-google-token"; const string firstName = "First"; const string lastName = "Last"; const string jwtToken = "valid-jwt-token"; var googleAuthServiceMock = new Mock(); - googleAuthServiceMock.Setup(m => m.GetUserDataFromToken(idToken)) + googleAuthServiceMock.Setup(m => m.GetUserDataFromAccessToken(accessToken)) .ReturnsAsync(Result.Success(new GoogleUserData(email, "First", "Last"))); var customerServiceMock = new Mock(); @@ -83,7 +83,7 @@ public async Task GoogleLogin_WhenAuthSuccessAndCustomerDoesNotExist_ShouldCreat customerServiceMock.Object); // Act - var result = await sut.GoogleLogin(new GoogleLoginDto( idToken )); + var result = await sut.GoogleLogin(new GoogleLoginDto( accessToken )); // Assert Assert.Equal(jwtToken, result.Value?.Token); From 67a199ad02ffd55b743a57dcc295488477a44ec1 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Wed, 19 Mar 2025 23:14:30 +0100 Subject: [PATCH 069/128] corrected the structure of GoogleAuthService.cs and added appropriate tests --- .../Auth/Services/GoogleAuthServiceTests.cs | 89 ++++++++++++++----- .../Auth/Abstractions/IGoogleDataFetcher.cs | 9 ++ .../Abstractions/IGoogleTokenValidator.cs | 9 -- .../Common/Auth/Services/GoogleAuthService.cs | 18 ++-- .../Common/Auth/Services/GoogleDataFetcher.cs | 27 ++++++ .../Auth/Services/GoogleTokenValidator.cs | 22 ----- TickAPI/TickAPI/Program.cs | 2 +- 7 files changed, 113 insertions(+), 63 deletions(-) create mode 100644 TickAPI/TickAPI/Common/Auth/Abstractions/IGoogleDataFetcher.cs delete mode 100644 TickAPI/TickAPI/Common/Auth/Abstractions/IGoogleTokenValidator.cs create mode 100644 TickAPI/TickAPI/Common/Auth/Services/GoogleDataFetcher.cs delete mode 100644 TickAPI/TickAPI/Common/Auth/Services/GoogleTokenValidator.cs diff --git a/TickAPI/TickAPI.Tests/Common/Auth/Services/GoogleAuthServiceTests.cs b/TickAPI/TickAPI.Tests/Common/Auth/Services/GoogleAuthServiceTests.cs index c9c3824..551b130 100644 --- a/TickAPI/TickAPI.Tests/Common/Auth/Services/GoogleAuthServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Common/Auth/Services/GoogleAuthServiceTests.cs @@ -1,44 +1,93 @@ -using Google.Apis.Auth; +using System.Net; using Microsoft.AspNetCore.Http; using Moq; +using System.Text.Json; using TickAPI.Common.Auth.Abstractions; +using TickAPI.Common.Auth.Responses; using TickAPI.Common.Auth.Services; -using TickAPI.Common.Result; namespace TickAPI.Tests.Common.Auth.Services; public class GoogleAuthServiceTests { + private readonly Mock _googleDataFetcherMock; + + public GoogleAuthServiceTests() + { + var validMessage = new HttpResponseMessage(HttpStatusCode.OK); + validMessage.Content = new StringContent(JsonSerializer.Serialize(new GoogleUserData("example@test.com", "Name", "Surname"))); + + var unauthorizedMessage = new HttpResponseMessage(HttpStatusCode.Unauthorized); + + var wrongContentMessage = new HttpResponseMessage(HttpStatusCode.OK); + wrongContentMessage.Content = new StringContent("This content is wrong"); + + _googleDataFetcherMock = new Mock(); + _googleDataFetcherMock + .Setup(m => m.FetchUserDataAsync("validToken")) + .ReturnsAsync(validMessage); + _googleDataFetcherMock + .Setup(m => m.FetchUserDataAsync("invalidToken")) + .ReturnsAsync(unauthorizedMessage); + _googleDataFetcherMock + .Setup(m => m.FetchUserDataAsync("wrongContentToken")) + .ReturnsAsync(wrongContentMessage); + _googleDataFetcherMock + .Setup(m => m.FetchUserDataAsync("throwToken")) + .ThrowsAsync(new Exception("An exception occurred")); + } + [Fact] - public async Task GetUserDataFromToken_WhenTokenValidatorReturnsPayload_ShouldReturnEmailFromPayload() + public async Task GetUserDataFromAccessToken_WhenDataFetcherReturnsValidResponse_ShouldReturnUserDataFromResponse() { - var googleTokenValidatorMock = new Mock(); - googleTokenValidatorMock - .Setup(m => m.ValidateAsync("validToken")) - .ReturnsAsync(new GoogleJsonWebSignature.Payload { Email = "example@test.com", GivenName = "First", FamilyName = "Last"}); - var sut = new GoogleAuthService(googleTokenValidatorMock.Object); + var sut = new GoogleAuthService(_googleDataFetcherMock.Object); - var result = await sut.GetUserDataFromToken("validToken"); + var result = await sut.GetUserDataFromAccessToken("validToken"); + Assert.NotNull(result); Assert.True(result.IsSuccess); - Assert.Equal("example@test.com", result.Value?.Email); - Assert.Equal("First", result.Value?.FirstName); - Assert.Equal("Last", result.Value?.LastName); + Assert.Equal("example@test.com", result.Value!.Email); + Assert.Equal("Name", result.Value!.GivenName); + Assert.Equal("Surname", result.Value!.FamilyName); } [Fact] - public async Task GetUserDataFromToken_WhenTokenValidatorThrowsException_ShouldReturnFailure() + public async Task + GetUserDataFromAccessToken_WhenDataFetcherReturnsResponseWithErrorStatusCode_ShouldReturnFailure() { - var googleTokenValidatorMock = new Mock(); - googleTokenValidatorMock - .Setup(m => m.ValidateAsync("invalidToken")) - .Throws(new InvalidJwtException("Invalid Google ID token")); - var sut = new GoogleAuthService(googleTokenValidatorMock.Object); + var sut = new GoogleAuthService(_googleDataFetcherMock.Object); - var result = await sut.GetUserDataFromToken("invalidToken"); + var result = await sut.GetUserDataFromAccessToken("invalidToken"); + Assert.NotNull(result); Assert.True(result.IsError); Assert.Equal(StatusCodes.Status401Unauthorized, result.StatusCode); - Assert.Equal("Invalid Google ID token", result.ErrorMsg); + Assert.Equal("Invalid Google access token", result.ErrorMsg); + } + + [Fact] + public async Task GetUserDataFromAccessToken_WhenDataFetcherReturnsResponseWithInvalidJson_ShouldReturnFailure() + { + var sut = new GoogleAuthService(_googleDataFetcherMock.Object); + + var result = await sut.GetUserDataFromAccessToken("wrongContentToken"); + + Assert.NotNull(result); + Assert.True(result.IsError); + Assert.Equal(StatusCodes.Status500InternalServerError, result.StatusCode); + Assert.Equal("Failed to parse Google user info", result.ErrorMsg); + } + + [Fact] + public async Task GetUserDataFromAccessToken_WhenDataFetcherThrowsAnException_ShouldReturnFailure() + { + var sut = new GoogleAuthService(_googleDataFetcherMock.Object); + + var result = await sut.GetUserDataFromAccessToken("wrongContentToken"); + + Assert.NotNull(result); + Assert.True(result.IsError); + Assert.Equal(StatusCodes.Status500InternalServerError, result.StatusCode); + Assert.Equal($"Error fetching user data: An exception occured", result.ErrorMsg); } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Auth/Abstractions/IGoogleDataFetcher.cs b/TickAPI/TickAPI/Common/Auth/Abstractions/IGoogleDataFetcher.cs new file mode 100644 index 0000000..79086c1 --- /dev/null +++ b/TickAPI/TickAPI/Common/Auth/Abstractions/IGoogleDataFetcher.cs @@ -0,0 +1,9 @@ +using Google.Apis.Auth; +using TickAPI.Common.Result; + +namespace TickAPI.Common.Auth.Abstractions; + +public interface IGoogleDataFetcher +{ + Task FetchUserDataAsync(string accessToken); +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Auth/Abstractions/IGoogleTokenValidator.cs b/TickAPI/TickAPI/Common/Auth/Abstractions/IGoogleTokenValidator.cs deleted file mode 100644 index f944e24..0000000 --- a/TickAPI/TickAPI/Common/Auth/Abstractions/IGoogleTokenValidator.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Google.Apis.Auth; -using TickAPI.Common.Result; - -namespace TickAPI.Common.Auth.Abstractions; - -public interface IGoogleTokenValidator -{ - Task ValidateAsync(string idToken); -} \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Auth/Services/GoogleAuthService.cs b/TickAPI/TickAPI/Common/Auth/Services/GoogleAuthService.cs index 42e3dd1..a0f9602 100644 --- a/TickAPI/TickAPI/Common/Auth/Services/GoogleAuthService.cs +++ b/TickAPI/TickAPI/Common/Auth/Services/GoogleAuthService.cs @@ -7,30 +7,26 @@ namespace TickAPI.Common.Auth.Services; public class GoogleAuthService : IGoogleAuthService { - private IConfiguration _configuration; - private IHttpClientFactory _httpClientFactory; + private IGoogleDataFetcher _googleDataFetcher; - public GoogleAuthService(IConfiguration configuration, IHttpClientFactory httpClientFactory) + public GoogleAuthService(IGoogleDataFetcher googleDataFetcher) { - _configuration = configuration; - _httpClientFactory = httpClientFactory; + _googleDataFetcher = googleDataFetcher; } public async Task> GetUserDataFromAccessToken(string accessToken) { try { - var client = _httpClientFactory.CreateClient(); - client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken); - - var response = client.GetAsync(_configuration["Authentication:Google:UserInfoEndpoint"]).Result; + var response = await _googleDataFetcher.FetchUserDataAsync(accessToken); + if (!response.IsSuccessStatusCode) { return Result.Failure(StatusCodes.Status401Unauthorized, "Invalid Google access token"); } - + var jsonResponse = await response.Content.ReadAsStringAsync(); - var userInfo = JsonSerializer.Deserialize(jsonResponse, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + var userInfo = JsonSerializer.Deserialize(jsonResponse, new JsonSerializerOptions()); if (userInfo == null) { diff --git a/TickAPI/TickAPI/Common/Auth/Services/GoogleDataFetcher.cs b/TickAPI/TickAPI/Common/Auth/Services/GoogleDataFetcher.cs new file mode 100644 index 0000000..8e6ece3 --- /dev/null +++ b/TickAPI/TickAPI/Common/Auth/Services/GoogleDataFetcher.cs @@ -0,0 +1,27 @@ +using Google.Apis.Auth; +using TickAPI.Common.Auth.Abstractions; +using TickAPI.Common.Result; + +namespace TickAPI.Common.Auth.Services; + +public class GoogleDataFetcher : IGoogleDataFetcher +{ + private readonly IConfiguration _configuration; + private readonly IHttpClientFactory _httpClientFactory; + + public GoogleDataFetcher(IConfiguration configuration, IHttpClientFactory httpClientFactory) + { + _configuration = configuration; + _httpClientFactory = httpClientFactory; + } + + public async Task FetchUserDataAsync(string accessToken) + { + var client = _httpClientFactory.CreateClient(); + client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken); + + var response = await client.GetAsync(_configuration["Authentication:Google:UserInfoEndpoint"]); + + return response; + } +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Auth/Services/GoogleTokenValidator.cs b/TickAPI/TickAPI/Common/Auth/Services/GoogleTokenValidator.cs deleted file mode 100644 index a6bd4d4..0000000 --- a/TickAPI/TickAPI/Common/Auth/Services/GoogleTokenValidator.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Google.Apis.Auth; -using TickAPI.Common.Auth.Abstractions; - -namespace TickAPI.Common.Auth.Services; - -public class GoogleTokenValidator : IGoogleTokenValidator -{ - private readonly IConfiguration _configuration; - - public GoogleTokenValidator(IConfiguration configuration) - { - _configuration = configuration; - } - - public async Task ValidateAsync(string idToken) - { - return await GoogleJsonWebSignature.ValidateAsync(idToken, new GoogleJsonWebSignature.ValidationSettings - { - Audience = [_configuration["Authentication:Google:ClientId"]] - }); - } -} \ No newline at end of file diff --git a/TickAPI/TickAPI/Program.cs b/TickAPI/TickAPI/Program.cs index c67c1e5..36d62a5 100644 --- a/TickAPI/TickAPI/Program.cs +++ b/TickAPI/TickAPI/Program.cs @@ -94,7 +94,7 @@ // Add common services. builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); From 0f8429b437ea736acd94f7d0dc5f7431fd7d0cd6 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Wed, 19 Mar 2025 23:54:44 +0100 Subject: [PATCH 070/128] test correction --- .../Common/Auth/Services/GoogleAuthServiceTests.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/TickAPI/TickAPI.Tests/Common/Auth/Services/GoogleAuthServiceTests.cs b/TickAPI/TickAPI.Tests/Common/Auth/Services/GoogleAuthServiceTests.cs index 551b130..70835f9 100644 --- a/TickAPI/TickAPI.Tests/Common/Auth/Services/GoogleAuthServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Common/Auth/Services/GoogleAuthServiceTests.cs @@ -20,7 +20,7 @@ public GoogleAuthServiceTests() var unauthorizedMessage = new HttpResponseMessage(HttpStatusCode.Unauthorized); var wrongContentMessage = new HttpResponseMessage(HttpStatusCode.OK); - wrongContentMessage.Content = new StringContent("This content is wrong"); + wrongContentMessage.Content = new StringContent("null"); _googleDataFetcherMock = new Mock(); _googleDataFetcherMock @@ -30,11 +30,11 @@ public GoogleAuthServiceTests() .Setup(m => m.FetchUserDataAsync("invalidToken")) .ReturnsAsync(unauthorizedMessage); _googleDataFetcherMock - .Setup(m => m.FetchUserDataAsync("wrongContentToken")) + .Setup(m => m.FetchUserDataAsync("nullToken")) .ReturnsAsync(wrongContentMessage); _googleDataFetcherMock .Setup(m => m.FetchUserDataAsync("throwToken")) - .ThrowsAsync(new Exception("An exception occurred")); + .ThrowsAsync(new Exception("An exception occured")); } [Fact] @@ -66,11 +66,11 @@ public async Task } [Fact] - public async Task GetUserDataFromAccessToken_WhenDataFetcherReturnsResponseWithInvalidJson_ShouldReturnFailure() + public async Task GetUserDataFromAccessToken_WhenDataFetcherReturnsNullResponse_ShouldReturnFailure() { var sut = new GoogleAuthService(_googleDataFetcherMock.Object); - var result = await sut.GetUserDataFromAccessToken("wrongContentToken"); + var result = await sut.GetUserDataFromAccessToken("nullToken"); Assert.NotNull(result); Assert.True(result.IsError); @@ -83,7 +83,7 @@ public async Task GetUserDataFromAccessToken_WhenDataFetcherThrowsAnException_Sh { var sut = new GoogleAuthService(_googleDataFetcherMock.Object); - var result = await sut.GetUserDataFromAccessToken("wrongContentToken"); + var result = await sut.GetUserDataFromAccessToken("throwToken"); Assert.NotNull(result); Assert.True(result.IsError); From 44270d115a4640ae8c468840f03e81fc236ef055 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Thu, 20 Mar 2025 13:09:13 +0100 Subject: [PATCH 071/128] updated organizer model and added appropriate migration --- ...20250320114817_UpdateOrganizer.Designer.cs | 359 ++++++++++++++++++ .../20250320114817_UpdateOrganizer.cs | 39 ++ .../TickApiDbContextModelSnapshot.cs | 12 +- .../TickAPI/Organizers/Models/Organizer.cs | 3 +- 4 files changed, 403 insertions(+), 10 deletions(-) create mode 100644 TickAPI/TickAPI/Migrations/20250320114817_UpdateOrganizer.Designer.cs create mode 100644 TickAPI/TickAPI/Migrations/20250320114817_UpdateOrganizer.cs diff --git a/TickAPI/TickAPI/Migrations/20250320114817_UpdateOrganizer.Designer.cs b/TickAPI/TickAPI/Migrations/20250320114817_UpdateOrganizer.Designer.cs new file mode 100644 index 0000000..d2a288b --- /dev/null +++ b/TickAPI/TickAPI/Migrations/20250320114817_UpdateOrganizer.Designer.cs @@ -0,0 +1,359 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using TickAPI.Common.TickApiDbContext; + +#nullable disable + +namespace TickAPI.Migrations +{ + [DbContext(typeof(TickApiDbContext))] + [Migration("20250320114817_UpdateOrganizer")] + partial class UpdateOrganizer + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("CategoryEvent", b => + { + b.Property("CategoriesId") + .HasColumnType("uniqueidentifier"); + + b.Property("EventsId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("CategoriesId", "EventsId"); + + b.HasIndex("EventsId"); + + b.ToTable("CategoryEvent"); + }); + + modelBuilder.Entity("TickAPI.Admins.Models.Admin", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Login") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Admins"); + }); + + modelBuilder.Entity("TickAPI.Categories.Models.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CategoryName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("TickAPI.Customers.Models.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("City") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Country") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FlatNumber") + .HasColumnType("bigint"); + + b.Property("HouseNumber") + .HasColumnType("bigint"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Street") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Addresses"); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AddressId") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("EventStatus") + .HasColumnType("int"); + + b.Property("MinimumAge") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OrganizerId") + .HasColumnType("uniqueidentifier"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("AddressId"); + + b.HasIndex("OrganizerId"); + + b.ToTable("Events"); + }); + + modelBuilder.Entity("TickAPI.Organizers.Models.Organizer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsVerified") + .HasColumnType("bit"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Organizers"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AvailableFrom") + .HasColumnType("datetime2"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EventId") + .HasColumnType("uniqueidentifier"); + + b.Property("MaxCount") + .HasColumnType("bigint"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("EventId"); + + b.ToTable("TicketTypes"); + }); + + modelBuilder.Entity("TickAPI.Tickets.Models.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ForResell") + .HasColumnType("bit"); + + b.Property("NameOnTicket") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("uniqueidentifier"); + + b.Property("Seats") + .HasColumnType("nvarchar(max)"); + + b.Property("TypeId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("TypeId"); + + b.ToTable("Tickets"); + }); + + modelBuilder.Entity("CategoryEvent", b => + { + b.HasOne("TickAPI.Categories.Models.Category", null) + .WithMany() + .HasForeignKey("CategoriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TickAPI.Events.Models.Event", null) + .WithMany() + .HasForeignKey("EventsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.HasOne("TickAPI.Events.Models.Address", "Address") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TickAPI.Organizers.Models.Organizer", "Organizer") + .WithMany("Events") + .HasForeignKey("OrganizerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Address"); + + b.Navigation("Organizer"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.HasOne("TickAPI.Events.Models.Event", "Event") + .WithMany("TicketTypes") + .HasForeignKey("EventId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Event"); + }); + + modelBuilder.Entity("TickAPI.Tickets.Models.Ticket", b => + { + b.HasOne("TickAPI.Customers.Models.Customer", "Owner") + .WithMany("Tickets") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TickAPI.TicketTypes.Models.TicketType", "Type") + .WithMany("Tickets") + .HasForeignKey("TypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("Type"); + }); + + modelBuilder.Entity("TickAPI.Customers.Models.Customer", b => + { + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.Navigation("TicketTypes"); + }); + + modelBuilder.Entity("TickAPI.Organizers.Models.Organizer", b => + { + b.Navigation("Events"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.Navigation("Tickets"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TickAPI/TickAPI/Migrations/20250320114817_UpdateOrganizer.cs b/TickAPI/TickAPI/Migrations/20250320114817_UpdateOrganizer.cs new file mode 100644 index 0000000..9998296 --- /dev/null +++ b/TickAPI/TickAPI/Migrations/20250320114817_UpdateOrganizer.cs @@ -0,0 +1,39 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TickAPI.Migrations +{ + /// + public partial class UpdateOrganizer : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Login", + table: "Organizers"); + + migrationBuilder.RenameColumn( + name: "OrganizerName", + table: "Organizers", + newName: "DisplayName"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "DisplayName", + table: "Organizers", + newName: "OrganizerName"); + + migrationBuilder.AddColumn( + name: "Login", + table: "Organizers", + type: "nvarchar(max)", + nullable: false, + defaultValue: ""); + } + } +} diff --git a/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs b/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs index 7925b4d..c622432 100644 --- a/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs +++ b/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs @@ -179,6 +179,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreationDate") .HasColumnType("datetime2"); + b.Property("DisplayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + b.Property("Email") .IsRequired() .HasColumnType("nvarchar(max)"); @@ -194,14 +198,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("nvarchar(max)"); - b.Property("Login") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("OrganizerName") - .IsRequired() - .HasColumnType("nvarchar(max)"); - b.HasKey("Id"); b.ToTable("Organizers"); diff --git a/TickAPI/TickAPI/Organizers/Models/Organizer.cs b/TickAPI/TickAPI/Organizers/Models/Organizer.cs index df43b4c..b7ed81e 100644 --- a/TickAPI/TickAPI/Organizers/Models/Organizer.cs +++ b/TickAPI/TickAPI/Organizers/Models/Organizer.cs @@ -7,11 +7,10 @@ public class Organizer { public Guid Id { get; set; } public string Email { get; set; } - public string Login { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public DateTime CreationDate { get; set; } - public string OrganizerName { get; set; } + public string DisplayName { get; set; } public bool IsVerified { get; set; } public ICollection Events { get; set; } } \ No newline at end of file From 40b0a8d49f138c0f9a2bafc656557ddbfea5c649 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Thu, 20 Mar 2025 15:23:36 +0100 Subject: [PATCH 072/128] separated result into generic and non-generic variations --- .../Generic}/ResultTests.cs | 30 ++++++- .../Common/Results/ResultTests.cs | 82 +++++++++++++++++++ .../{Result => Results/Generic}/Result.cs | 12 ++- TickAPI/TickAPI/Common/Results/Result.cs | 48 +++++++++++ 4 files changed, 169 insertions(+), 3 deletions(-) rename TickAPI/TickAPI.Tests/Common/{Result => Results/Generic}/ResultTests.cs (63%) create mode 100644 TickAPI/TickAPI.Tests/Common/Results/ResultTests.cs rename TickAPI/TickAPI/Common/{Result => Results/Generic}/Result.cs (75%) create mode 100644 TickAPI/TickAPI/Common/Results/Result.cs diff --git a/TickAPI/TickAPI.Tests/Common/Result/ResultTests.cs b/TickAPI/TickAPI.Tests/Common/Results/Generic/ResultTests.cs similarity index 63% rename from TickAPI/TickAPI.Tests/Common/Result/ResultTests.cs rename to TickAPI/TickAPI.Tests/Common/Results/Generic/ResultTests.cs index ee8e056..ffb1390 100644 --- a/TickAPI/TickAPI.Tests/Common/Result/ResultTests.cs +++ b/TickAPI/TickAPI.Tests/Common/Results/Generic/ResultTests.cs @@ -1,6 +1,7 @@ -using TickAPI.Common.Result; +using TickAPI.Common.Results; +using TickAPI.Common.Results.Generic; -namespace TickAPI.Tests.Common.Result; +namespace TickAPI.Tests.Common.Results.Generic; public class ResultTests { @@ -46,6 +47,21 @@ public void PropagateError_WhenResultWithErrorPassed_ShouldReturnResultWithError Assert.Equal(errorMsg, result.ErrorMsg); Assert.Equal(statusCode, result.StatusCode); } + + [Fact] + public void PropagateError_WhenNonGenericResultWithErrorPassed_ShouldReturnResultWithError() + { + const int statusCode = 500; + const string errorMsg = "error message"; + var resultWithError = Result.Failure(statusCode, errorMsg); + + var result = Result.PropagateError(resultWithError); + + Assert.True(result.IsError); + Assert.False(result.IsSuccess); + Assert.Equal(errorMsg, result.ErrorMsg); + Assert.Equal(statusCode, result.StatusCode); + } [Fact] public void PropagateError_WhenResultWithSuccessPassed_ShouldThrowArgumentException() @@ -56,4 +72,14 @@ public void PropagateError_WhenResultWithSuccessPassed_ShouldThrowArgumentExcept Assert.Throws(act); } + + [Fact] + public void PropagateError_WhenNonGenericResultWithSuccessPassed_ShouldThrowArgumentException() + { + var resultWithSuccess = Result.Success(); + + var act = () => Result.PropagateError(resultWithSuccess); + + Assert.Throws(act); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI.Tests/Common/Results/ResultTests.cs b/TickAPI/TickAPI.Tests/Common/Results/ResultTests.cs new file mode 100644 index 0000000..7e8c2d4 --- /dev/null +++ b/TickAPI/TickAPI.Tests/Common/Results/ResultTests.cs @@ -0,0 +1,82 @@ +using TickAPI.Common.Results; +using TickAPI.Common.Results.Generic; + +namespace TickAPI.Tests.Common.Results; + +public class ResultTests +{ + [Fact] + public void Success_ShouldReturnResultWithSuccess() + { + var result = Result.Success(); + + Assert.True(result.IsSuccess); + Assert.False(result.IsError); + Assert.Equal("", result.ErrorMsg); + Assert.Equal(200, result.StatusCode); + } + + [Fact] + public void Failure_ShouldReturnResultWithError() + { + const int statusCode = 500; + const string errorMsg = "example error msg"; + + var result = Result.Failure(500, errorMsg); + + Assert.True(result.IsError); + Assert.False(result.IsSuccess); + Assert.Equal(errorMsg, result.ErrorMsg); + Assert.Equal(statusCode, result.StatusCode); + } + + [Fact] + public void PropagateError_WhenResultWithErrorPassed_ShouldReturnResultWithError() + { + const int statusCode = 500; + const string errorMsg = "error message"; + var resultWithError = Result.Failure(statusCode, errorMsg); + + var result = Result.PropagateError(resultWithError); + + Assert.True(result.IsError); + Assert.False(result.IsSuccess); + Assert.Equal(errorMsg, result.ErrorMsg); + Assert.Equal(statusCode, result.StatusCode); + } + + [Fact] + public void PropagateError_WhenGenericResultWithErrorPassed_ShouldReturnResultWithError() + { + const int statusCode = 500; + const string errorMsg = "error message"; + var resultWithError = Result.Failure(statusCode, errorMsg); + + var result = Result.PropagateError(resultWithError); + + Assert.True(result.IsError); + Assert.False(result.IsSuccess); + Assert.Equal(errorMsg, result.ErrorMsg); + Assert.Equal(statusCode, result.StatusCode); + } + + [Fact] + public void PropagateError_WhenResultWithSuccessPassed_ShouldThrowArgumentException() + { + var resultWithSuccess = Result.Success(); + + var act = () => Result.PropagateError(resultWithSuccess); + + Assert.Throws(act); + } + + [Fact] + public void PropagateError_WhenGenericResultWithSuccessPassed_ShouldThrowArgumentException() + { + var resultWithSuccess = Result.Success(123); + + var act = () => Result.PropagateError(resultWithSuccess); + + Assert.Throws(act); + } +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Result/Result.cs b/TickAPI/TickAPI/Common/Results/Generic/Result.cs similarity index 75% rename from TickAPI/TickAPI/Common/Result/Result.cs rename to TickAPI/TickAPI/Common/Results/Generic/Result.cs index 411938b..10ccf62 100644 --- a/TickAPI/TickAPI/Common/Result/Result.cs +++ b/TickAPI/TickAPI/Common/Results/Generic/Result.cs @@ -1,4 +1,4 @@ -namespace TickAPI.Common.Result; +namespace TickAPI.Common.Results.Generic; public record Result { @@ -25,6 +25,16 @@ public static Result Failure(int statusCode, string errorMsg) { return new Result(false, default, statusCode, errorMsg); } + + public static Result PropagateError(Result other) + { + if (other.IsSuccess) + { + throw new ArgumentException("Trying to propagate error from successful value"); + } + + return Failure(other.StatusCode, other.ErrorMsg); + } public static Result PropagateError(Result other) { diff --git a/TickAPI/TickAPI/Common/Results/Result.cs b/TickAPI/TickAPI/Common/Results/Result.cs new file mode 100644 index 0000000..f9ba8ff --- /dev/null +++ b/TickAPI/TickAPI/Common/Results/Result.cs @@ -0,0 +1,48 @@ +using TickAPI.Common.Results.Generic; + +namespace TickAPI.Common.Results; + +public record Result +{ + public bool IsSuccess { get; } + public bool IsError => !IsSuccess; + public int StatusCode { get; } + public string ErrorMsg { get; } + + private Result(bool isSuccess, int statusCode = StatusCodes.Status200OK, string errorMsg = "") + { + IsSuccess = isSuccess; + StatusCode = statusCode; + ErrorMsg = errorMsg; + } + + public static Result Success() + { + return new Result(true); + } + + public static Result Failure(int statusCode, string errorMsg) + { + return new Result(false, statusCode, errorMsg); + } + + public static Result PropagateError(Result other) + { + if (other.IsSuccess) + { + throw new ArgumentException("Trying to propagate error from successful value"); + } + + return Failure(other.StatusCode, other.ErrorMsg); + } + + public static Result PropagateError(Result other) + { + if (other.IsSuccess) + { + throw new ArgumentException("Trying to propagate error from successful value"); + } + + return Failure(other.StatusCode, other.ErrorMsg); + } +} \ No newline at end of file From e171ab9dd6d4ac1ada44e6e1532361cecad026e8 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Thu, 20 Mar 2025 15:25:40 +0100 Subject: [PATCH 073/128] updated namespaces with Results.Generic --- .../Customers/Controllers/CustomerControllerTests.cs | 2 +- .../TickAPI.Tests/Customers/Services/CustomerServiceTests.cs | 2 +- TickAPI/TickAPI/Common/Auth/Abstractions/IGoogleAuthService.cs | 2 +- TickAPI/TickAPI/Common/Auth/Abstractions/IGoogleDataFetcher.cs | 1 - TickAPI/TickAPI/Common/Auth/Abstractions/IJwtService.cs | 2 +- TickAPI/TickAPI/Common/Auth/Services/GoogleAuthService.cs | 2 +- TickAPI/TickAPI/Common/Auth/Services/GoogleDataFetcher.cs | 1 - TickAPI/TickAPI/Common/Auth/Services/JwtService.cs | 2 +- .../Common/Pagination/Abstractions/IPaginationService.cs | 2 +- TickAPI/TickAPI/Common/Pagination/Services/PaginationService.cs | 2 +- TickAPI/TickAPI/Customers/Abstractions/ICustomerRepository.cs | 2 +- TickAPI/TickAPI/Customers/Abstractions/ICustomerService.cs | 2 +- TickAPI/TickAPI/Customers/Repositories/CustomerRepository.cs | 2 +- TickAPI/TickAPI/Customers/Services/CustomerService.cs | 2 +- 14 files changed, 12 insertions(+), 14 deletions(-) diff --git a/TickAPI/TickAPI.Tests/Customers/Controllers/CustomerControllerTests.cs b/TickAPI/TickAPI.Tests/Customers/Controllers/CustomerControllerTests.cs index 6be9918..d9d7e5b 100644 --- a/TickAPI/TickAPI.Tests/Customers/Controllers/CustomerControllerTests.cs +++ b/TickAPI/TickAPI.Tests/Customers/Controllers/CustomerControllerTests.cs @@ -5,7 +5,7 @@ using TickAPI.Common.Auth.Abstractions; using TickAPI.Common.Auth.Enums; using TickAPI.Common.Auth.Responses; -using TickAPI.Common.Result; +using TickAPI.Common.Results.Generic; using TickAPI.Customers.Abstractions; using TickAPI.Customers.Controllers; using TickAPI.Customers.DTOs.Request; diff --git a/TickAPI/TickAPI.Tests/Customers/Services/CustomerServiceTests.cs b/TickAPI/TickAPI.Tests/Customers/Services/CustomerServiceTests.cs index ba0960e..9a3eb79 100644 --- a/TickAPI/TickAPI.Tests/Customers/Services/CustomerServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Customers/Services/CustomerServiceTests.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Http; using Moq; -using TickAPI.Common.Result; +using TickAPI.Common.Results.Generic; using TickAPI.Common.Time.Abstractions; using TickAPI.Customers.Abstractions; using TickAPI.Customers.Models; diff --git a/TickAPI/TickAPI/Common/Auth/Abstractions/IGoogleAuthService.cs b/TickAPI/TickAPI/Common/Auth/Abstractions/IGoogleAuthService.cs index 3a3c5f5..351dc4a 100644 --- a/TickAPI/TickAPI/Common/Auth/Abstractions/IGoogleAuthService.cs +++ b/TickAPI/TickAPI/Common/Auth/Abstractions/IGoogleAuthService.cs @@ -1,5 +1,5 @@ using TickAPI.Common.Auth.Responses; -using TickAPI.Common.Result; +using TickAPI.Common.Results.Generic; namespace TickAPI.Common.Auth.Abstractions; diff --git a/TickAPI/TickAPI/Common/Auth/Abstractions/IGoogleDataFetcher.cs b/TickAPI/TickAPI/Common/Auth/Abstractions/IGoogleDataFetcher.cs index 79086c1..9876cda 100644 --- a/TickAPI/TickAPI/Common/Auth/Abstractions/IGoogleDataFetcher.cs +++ b/TickAPI/TickAPI/Common/Auth/Abstractions/IGoogleDataFetcher.cs @@ -1,5 +1,4 @@ using Google.Apis.Auth; -using TickAPI.Common.Result; namespace TickAPI.Common.Auth.Abstractions; diff --git a/TickAPI/TickAPI/Common/Auth/Abstractions/IJwtService.cs b/TickAPI/TickAPI/Common/Auth/Abstractions/IJwtService.cs index a382372..8dd4720 100644 --- a/TickAPI/TickAPI/Common/Auth/Abstractions/IJwtService.cs +++ b/TickAPI/TickAPI/Common/Auth/Abstractions/IJwtService.cs @@ -1,5 +1,5 @@ using TickAPI.Common.Auth.Enums; -using TickAPI.Common.Result; +using TickAPI.Common.Results.Generic; namespace TickAPI.Common.Auth.Abstractions; diff --git a/TickAPI/TickAPI/Common/Auth/Services/GoogleAuthService.cs b/TickAPI/TickAPI/Common/Auth/Services/GoogleAuthService.cs index a0f9602..a1476ec 100644 --- a/TickAPI/TickAPI/Common/Auth/Services/GoogleAuthService.cs +++ b/TickAPI/TickAPI/Common/Auth/Services/GoogleAuthService.cs @@ -1,7 +1,7 @@ using System.Text.Json; using TickAPI.Common.Auth.Abstractions; using TickAPI.Common.Auth.Responses; -using TickAPI.Common.Result; +using TickAPI.Common.Results.Generic; namespace TickAPI.Common.Auth.Services; diff --git a/TickAPI/TickAPI/Common/Auth/Services/GoogleDataFetcher.cs b/TickAPI/TickAPI/Common/Auth/Services/GoogleDataFetcher.cs index 8e6ece3..d3e8282 100644 --- a/TickAPI/TickAPI/Common/Auth/Services/GoogleDataFetcher.cs +++ b/TickAPI/TickAPI/Common/Auth/Services/GoogleDataFetcher.cs @@ -1,6 +1,5 @@ using Google.Apis.Auth; using TickAPI.Common.Auth.Abstractions; -using TickAPI.Common.Result; namespace TickAPI.Common.Auth.Services; diff --git a/TickAPI/TickAPI/Common/Auth/Services/JwtService.cs b/TickAPI/TickAPI/Common/Auth/Services/JwtService.cs index 86816f2..0e458de 100644 --- a/TickAPI/TickAPI/Common/Auth/Services/JwtService.cs +++ b/TickAPI/TickAPI/Common/Auth/Services/JwtService.cs @@ -4,7 +4,7 @@ using Microsoft.IdentityModel.Tokens; using TickAPI.Common.Auth.Abstractions; using TickAPI.Common.Auth.Enums; -using TickAPI.Common.Result; +using TickAPI.Common.Results.Generic; using TickAPI.Common.Time.Abstractions; namespace TickAPI.Common.Auth.Services; diff --git a/TickAPI/TickAPI/Common/Pagination/Abstractions/IPaginationService.cs b/TickAPI/TickAPI/Common/Pagination/Abstractions/IPaginationService.cs index 7a3e285..4aec7fc 100644 --- a/TickAPI/TickAPI/Common/Pagination/Abstractions/IPaginationService.cs +++ b/TickAPI/TickAPI/Common/Pagination/Abstractions/IPaginationService.cs @@ -1,5 +1,5 @@ using TickAPI.Common.Pagination.Responses; -using TickAPI.Common.Result; +using TickAPI.Common.Results.Generic; namespace TickAPI.Common.Pagination.Abstractions; diff --git a/TickAPI/TickAPI/Common/Pagination/Services/PaginationService.cs b/TickAPI/TickAPI/Common/Pagination/Services/PaginationService.cs index 662e510..c7670ac 100644 --- a/TickAPI/TickAPI/Common/Pagination/Services/PaginationService.cs +++ b/TickAPI/TickAPI/Common/Pagination/Services/PaginationService.cs @@ -1,6 +1,6 @@ using TickAPI.Common.Pagination.Abstractions; using TickAPI.Common.Pagination.Responses; -using TickAPI.Common.Result; +using TickAPI.Common.Results.Generic; namespace TickAPI.Common.Pagination.Services; diff --git a/TickAPI/TickAPI/Customers/Abstractions/ICustomerRepository.cs b/TickAPI/TickAPI/Customers/Abstractions/ICustomerRepository.cs index 5177854..4cd7bab 100644 --- a/TickAPI/TickAPI/Customers/Abstractions/ICustomerRepository.cs +++ b/TickAPI/TickAPI/Customers/Abstractions/ICustomerRepository.cs @@ -1,4 +1,4 @@ -using TickAPI.Common.Result; +using TickAPI.Common.Results.Generic; using TickAPI.Customers.Models; namespace TickAPI.Customers.Abstractions; diff --git a/TickAPI/TickAPI/Customers/Abstractions/ICustomerService.cs b/TickAPI/TickAPI/Customers/Abstractions/ICustomerService.cs index ea4e7b8..a27002b 100644 --- a/TickAPI/TickAPI/Customers/Abstractions/ICustomerService.cs +++ b/TickAPI/TickAPI/Customers/Abstractions/ICustomerService.cs @@ -1,4 +1,4 @@ -using TickAPI.Common.Result; +using TickAPI.Common.Results.Generic; using TickAPI.Customers.Models; namespace TickAPI.Customers.Abstractions; diff --git a/TickAPI/TickAPI/Customers/Repositories/CustomerRepository.cs b/TickAPI/TickAPI/Customers/Repositories/CustomerRepository.cs index 9adb003..ed131d5 100644 --- a/TickAPI/TickAPI/Customers/Repositories/CustomerRepository.cs +++ b/TickAPI/TickAPI/Customers/Repositories/CustomerRepository.cs @@ -1,5 +1,5 @@ using Microsoft.EntityFrameworkCore; -using TickAPI.Common.Result; +using TickAPI.Common.Results.Generic; using TickAPI.Common.TickApiDbContext; using TickAPI.Customers.Abstractions; using TickAPI.Customers.Models; diff --git a/TickAPI/TickAPI/Customers/Services/CustomerService.cs b/TickAPI/TickAPI/Customers/Services/CustomerService.cs index 33289c7..e0a9f80 100644 --- a/TickAPI/TickAPI/Customers/Services/CustomerService.cs +++ b/TickAPI/TickAPI/Customers/Services/CustomerService.cs @@ -1,4 +1,4 @@ -using TickAPI.Common.Result; +using TickAPI.Common.Results.Generic; using TickAPI.Common.Time.Abstractions; using TickAPI.Customers.Abstractions; using TickAPI.Customers.Models; From aa367c035072c1b542d61de9841dbd960265b082 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Thu, 20 Mar 2025 16:28:28 +0100 Subject: [PATCH 074/128] added organizer login/registration pipeline --- .../Controllers/CustomerControllerTests.cs | 4 +- .../TickAPI/Common/Auth/Enums/AuthPolicies.cs | 3 +- TickAPI/TickAPI/Common/Auth/Enums/UserRole.cs | 1 + .../Controllers/CustomerController.cs | 10 +- ...eLoginDto.cs => GoogleCustomerLoginDto.cs} | 2 +- ...seDto.cs => AboutMeCustomerResponseDto.cs} | 2 +- ...o.cs => GoogleCustomerLoginResponseDto.cs} | 2 +- .../Abstractions/IOrganizerRepository.cs | 10 +- .../Abstractions/IOrganizerService.cs | 10 +- .../Controllers/OrganizerController.cs | 113 +++++++++++++++++- .../DTOs/Request/CreateOrganizerDto.cs | 7 ++ .../DTOs/Request/GoogleOrganizerLoginDto.cs | 5 + .../DTOs/Request/VerifyOrganizerDto.cs | 5 + .../Response/AboutMeOrganizerResponseDto.cs | 10 ++ .../Response/CreateOrganizerResponseDto.cs | 5 + .../GoogleOrganizerLoginResponseDto.cs | 7 ++ .../Repositories/OrganizerRepository.cs | 50 +++++++- .../Organizers/Services/OrganizerService.cs | 45 ++++++- TickAPI/TickAPI/Program.cs | 3 +- 19 files changed, 276 insertions(+), 18 deletions(-) rename TickAPI/TickAPI/Customers/DTOs/Request/{GoogleLoginDto.cs => GoogleCustomerLoginDto.cs} (65%) rename TickAPI/TickAPI/Customers/DTOs/Response/{AboutMeResponseDto.cs => AboutMeCustomerResponseDto.cs} (76%) rename TickAPI/TickAPI/Customers/DTOs/Response/{GoogleLoginResponseDto.cs => GoogleCustomerLoginResponseDto.cs} (58%) create mode 100644 TickAPI/TickAPI/Organizers/DTOs/Request/CreateOrganizerDto.cs create mode 100644 TickAPI/TickAPI/Organizers/DTOs/Request/GoogleOrganizerLoginDto.cs create mode 100644 TickAPI/TickAPI/Organizers/DTOs/Request/VerifyOrganizerDto.cs create mode 100644 TickAPI/TickAPI/Organizers/DTOs/Response/AboutMeOrganizerResponseDto.cs create mode 100644 TickAPI/TickAPI/Organizers/DTOs/Response/CreateOrganizerResponseDto.cs create mode 100644 TickAPI/TickAPI/Organizers/DTOs/Response/GoogleOrganizerLoginResponseDto.cs diff --git a/TickAPI/TickAPI.Tests/Customers/Controllers/CustomerControllerTests.cs b/TickAPI/TickAPI.Tests/Customers/Controllers/CustomerControllerTests.cs index d9d7e5b..2548884 100644 --- a/TickAPI/TickAPI.Tests/Customers/Controllers/CustomerControllerTests.cs +++ b/TickAPI/TickAPI.Tests/Customers/Controllers/CustomerControllerTests.cs @@ -41,7 +41,7 @@ public async Task GoogleLogin_WhenAuthSuccessAndCustomerExists_ShouldReturnToken customerServiceMock.Object); // Act - var actionResult = await sut.GoogleLogin(new GoogleLoginDto(accessToken)); + var actionResult = await sut.GoogleLogin(new GoogleCustomerLoginDto(accessToken)); // Assert Assert.Equal(jwtToken, actionResult.Value?.Token); @@ -83,7 +83,7 @@ public async Task GoogleLogin_WhenAuthSuccessAndCustomerDoesNotExist_ShouldCreat customerServiceMock.Object); // Act - var result = await sut.GoogleLogin(new GoogleLoginDto( accessToken )); + var result = await sut.GoogleLogin(new GoogleCustomerLoginDto( accessToken )); // Assert Assert.Equal(jwtToken, result.Value?.Token); diff --git a/TickAPI/TickAPI/Common/Auth/Enums/AuthPolicies.cs b/TickAPI/TickAPI/Common/Auth/Enums/AuthPolicies.cs index c5d6408..33bf3d0 100644 --- a/TickAPI/TickAPI/Common/Auth/Enums/AuthPolicies.cs +++ b/TickAPI/TickAPI/Common/Auth/Enums/AuthPolicies.cs @@ -3,7 +3,8 @@ public enum AuthPolicies { AdminPolicy, - OrganizerPolicy, + VerifiedOrganizerPolicy, CustomerPolicy, NewOrganizerPolicy, + CreatedOrganizerPolicy, } \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Auth/Enums/UserRole.cs b/TickAPI/TickAPI/Common/Auth/Enums/UserRole.cs index d382a54..019b009 100644 --- a/TickAPI/TickAPI/Common/Auth/Enums/UserRole.cs +++ b/TickAPI/TickAPI/Common/Auth/Enums/UserRole.cs @@ -6,4 +6,5 @@ public enum UserRole Organizer, Customer, NewOrganizer, + UnverifiedOrganizer, } \ No newline at end of file diff --git a/TickAPI/TickAPI/Customers/Controllers/CustomerController.cs b/TickAPI/TickAPI/Customers/Controllers/CustomerController.cs index 2a4d48a..5302175 100644 --- a/TickAPI/TickAPI/Customers/Controllers/CustomerController.cs +++ b/TickAPI/TickAPI/Customers/Controllers/CustomerController.cs @@ -25,7 +25,7 @@ public CustomerController(IGoogleAuthService googleAuthService, IJwtService jwtS } [HttpPost("google-login")] - public async Task> GoogleLogin([FromBody] GoogleLoginDto request) + public async Task> GoogleLogin([FromBody] GoogleCustomerLoginDto request) { var userDataResult = await _googleAuthService.GetUserDataFromAccessToken(request.AccessToken); if(userDataResult.IsError) @@ -45,12 +45,12 @@ public async Task> GoogleLogin([FromBody] G if (jwtTokenResult.IsError) return StatusCode(jwtTokenResult.StatusCode, jwtTokenResult.ErrorMsg); - return new ActionResult(new GoogleLoginResponseDto(jwtTokenResult.Value!)); + return new ActionResult(new GoogleCustomerLoginResponseDto(jwtTokenResult.Value!)); } [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] [HttpGet("about-me")] - public async Task> AboutMe() + public async Task> AboutMe() { var email = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value; if (email == null) @@ -64,7 +64,7 @@ public async Task> AboutMe() var customer = customerResult.Value!; var aboutMeResponse = - new AboutMeResponseDto(customer.Email, customer.FirstName, customer.LastName, customer.CreationDate); - return new ActionResult(aboutMeResponse); + new AboutMeCustomerResponseDto(customer.Email, customer.FirstName, customer.LastName, customer.CreationDate); + return new ActionResult(aboutMeResponse); } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Customers/DTOs/Request/GoogleLoginDto.cs b/TickAPI/TickAPI/Customers/DTOs/Request/GoogleCustomerLoginDto.cs similarity index 65% rename from TickAPI/TickAPI/Customers/DTOs/Request/GoogleLoginDto.cs rename to TickAPI/TickAPI/Customers/DTOs/Request/GoogleCustomerLoginDto.cs index 4d9561e..8979a85 100644 --- a/TickAPI/TickAPI/Customers/DTOs/Request/GoogleLoginDto.cs +++ b/TickAPI/TickAPI/Customers/DTOs/Request/GoogleCustomerLoginDto.cs @@ -1,5 +1,5 @@ namespace TickAPI.Customers.DTOs.Request; -public record GoogleLoginDto( +public record GoogleCustomerLoginDto( string AccessToken ); \ No newline at end of file diff --git a/TickAPI/TickAPI/Customers/DTOs/Response/AboutMeResponseDto.cs b/TickAPI/TickAPI/Customers/DTOs/Response/AboutMeCustomerResponseDto.cs similarity index 76% rename from TickAPI/TickAPI/Customers/DTOs/Response/AboutMeResponseDto.cs rename to TickAPI/TickAPI/Customers/DTOs/Response/AboutMeCustomerResponseDto.cs index c813f55..cb4bdb3 100644 --- a/TickAPI/TickAPI/Customers/DTOs/Response/AboutMeResponseDto.cs +++ b/TickAPI/TickAPI/Customers/DTOs/Response/AboutMeCustomerResponseDto.cs @@ -1,6 +1,6 @@ namespace TickAPI.Customers.DTOs.Response; -public record AboutMeResponseDto( +public record AboutMeCustomerResponseDto( string Email, string FirstName, string LastName, diff --git a/TickAPI/TickAPI/Customers/DTOs/Response/GoogleLoginResponseDto.cs b/TickAPI/TickAPI/Customers/DTOs/Response/GoogleCustomerLoginResponseDto.cs similarity index 58% rename from TickAPI/TickAPI/Customers/DTOs/Response/GoogleLoginResponseDto.cs rename to TickAPI/TickAPI/Customers/DTOs/Response/GoogleCustomerLoginResponseDto.cs index 0090b70..6b5c7c8 100644 --- a/TickAPI/TickAPI/Customers/DTOs/Response/GoogleLoginResponseDto.cs +++ b/TickAPI/TickAPI/Customers/DTOs/Response/GoogleCustomerLoginResponseDto.cs @@ -1,5 +1,5 @@ namespace TickAPI.Customers.DTOs.Response; -public record GoogleLoginResponseDto( +public record GoogleCustomerLoginResponseDto( string Token ); \ No newline at end of file diff --git a/TickAPI/TickAPI/Organizers/Abstractions/IOrganizerRepository.cs b/TickAPI/TickAPI/Organizers/Abstractions/IOrganizerRepository.cs index 42e14a1..31b1c22 100644 --- a/TickAPI/TickAPI/Organizers/Abstractions/IOrganizerRepository.cs +++ b/TickAPI/TickAPI/Organizers/Abstractions/IOrganizerRepository.cs @@ -1,6 +1,12 @@ -namespace TickAPI.Organizers.Abstractions; +using TickAPI.Common.Results; +using TickAPI.Common.Results.Generic; +using TickAPI.Organizers.Models; + +namespace TickAPI.Organizers.Abstractions; public interface IOrganizerRepository { - + Task> GetOrganizerByEmailAsync(string organizerEmail); + Task AddNewOrganizerAsync(Organizer organizer); + Task VerifyOrganizerByEmailAsync(string organizerEmail); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Organizers/Abstractions/IOrganizerService.cs b/TickAPI/TickAPI/Organizers/Abstractions/IOrganizerService.cs index 572caad..0fb0bc6 100644 --- a/TickAPI/TickAPI/Organizers/Abstractions/IOrganizerService.cs +++ b/TickAPI/TickAPI/Organizers/Abstractions/IOrganizerService.cs @@ -1,6 +1,14 @@ -namespace TickAPI.Organizers.Abstractions; +using TickAPI.Common.Results; +using TickAPI.Common.Results.Generic; +using TickAPI.Organizers.Models; + +namespace TickAPI.Organizers.Abstractions; public interface IOrganizerService { + public Task> GetOrganizerByEmailAsync(string organizerEmail); + public Task> CreateNewOrganizerAsync(string email, string firstName, string lastName, string displayName); + + public Task VerifyOrganizerByEmailAsync(string organizerEmail); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Organizers/Controllers/OrganizerController.cs b/TickAPI/TickAPI/Organizers/Controllers/OrganizerController.cs index f6c89ff..f19ee92 100644 --- a/TickAPI/TickAPI/Organizers/Controllers/OrganizerController.cs +++ b/TickAPI/TickAPI/Organizers/Controllers/OrganizerController.cs @@ -1,4 +1,12 @@ -using Microsoft.AspNetCore.Mvc; +using System.Security.Claims; +using Microsoft.AspNetCore.Mvc; +using TickAPI.Common.Auth.Abstractions; +using TickAPI.Common.Auth.Attributes; +using TickAPI.Common.Auth.Enums; +using TickAPI.Common.Results.Generic; +using TickAPI.Organizers.Abstractions; +using TickAPI.Organizers.DTOs.Request; +using TickAPI.Organizers.DTOs.Response; namespace TickAPI.Organizers.Controllers; @@ -6,5 +14,108 @@ namespace TickAPI.Organizers.Controllers; [Route("api/[controller]")] public class OrganizerController : ControllerBase { + private readonly IGoogleAuthService _googleAuthService; + private readonly IJwtService _jwtService; + private readonly IOrganizerService _organizerService; + + public OrganizerController(IGoogleAuthService googleAuthService, IJwtService jwtService, + IOrganizerService organizerService) + { + _googleAuthService = googleAuthService; + _jwtService = jwtService; + _organizerService = organizerService; + } + + [HttpPost("google-login")] + public async Task> GoogleLogin([FromBody] GoogleOrganizerLoginDto request) + { + var userDataResult = await _googleAuthService.GetUserDataFromAccessToken(request.AccessToken); + if(userDataResult.IsError) + return StatusCode(userDataResult.StatusCode, userDataResult.ErrorMsg); + + var userData = userDataResult.Value!; + + Result jwtTokenResult; + var existingOrganizerResult = await _organizerService.GetOrganizerByEmailAsync(userData.Email); + if (existingOrganizerResult.IsError) + { + jwtTokenResult = _jwtService.GenerateJwtToken(userData.Email, UserRole.NewOrganizer); + + if(jwtTokenResult.IsError) + return StatusCode(jwtTokenResult.StatusCode, jwtTokenResult.ErrorMsg); + + return new ActionResult(new GoogleOrganizerLoginResponseDto(jwtTokenResult.Value!, true, false)); + } + + var isVerified = existingOrganizerResult.Value!.IsVerified; + + if (isVerified) + { + jwtTokenResult = _jwtService.GenerateJwtToken(userData.Email, UserRole.Organizer); + + if(jwtTokenResult.IsError) + return StatusCode(jwtTokenResult.StatusCode, jwtTokenResult.ErrorMsg); + } + else + { + jwtTokenResult = _jwtService.GenerateJwtToken(userData.Email, UserRole.UnverifiedOrganizer); + + if(jwtTokenResult.IsError) + return StatusCode(jwtTokenResult.StatusCode, jwtTokenResult.ErrorMsg); + } + + return new ActionResult(new GoogleOrganizerLoginResponseDto(jwtTokenResult.Value!, false, isVerified)); + } + + [AuthorizeWithPolicy(AuthPolicies.NewOrganizerPolicy)] + [HttpPost("create-organizer")] + public async Task> CreateOrganizer([FromBody] CreateOrganizerDto request) + { + var email = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value; + if (email == null) + return StatusCode(StatusCodes.Status400BadRequest, "missing email claim"); + + var newOrganizerResult = await _organizerService.CreateNewOrganizerAsync(email, request.FirstName, request.LastName, request.DisplayName); + if(newOrganizerResult.IsError) + return StatusCode(newOrganizerResult.StatusCode, newOrganizerResult.ErrorMsg); + + var jwtTokenResult = _jwtService.GenerateJwtToken(newOrganizerResult.Value!.Email, UserRole.UnverifiedOrganizer); + if(jwtTokenResult.IsError) + return StatusCode(jwtTokenResult.StatusCode, jwtTokenResult.ErrorMsg); + + return new ActionResult(new CreateOrganizerResponseDto(jwtTokenResult.Value!)); + } + // TODO: Add authorization with admin policy here + [HttpPost("verify-organizer")] + public async Task VerifyOrganizer([FromBody] VerifyOrganizerDto request) + { + var verifyOrganizerResult = await _organizerService.VerifyOrganizerByEmailAsync(request.Email); + + if(verifyOrganizerResult.IsError) + return StatusCode(verifyOrganizerResult.StatusCode, verifyOrganizerResult.ErrorMsg); + + return Ok(); + } + + [AuthorizeWithPolicy(AuthPolicies.CreatedOrganizerPolicy)] + [HttpGet("about-me")] + public async Task> AboutMe() + { + var email = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value; + + if (email == null) + return StatusCode(StatusCodes.Status400BadRequest, "missing email claim"); + + var organizerResult = await _organizerService.GetOrganizerByEmailAsync(email); + if (organizerResult.IsError) + return StatusCode(StatusCodes.Status500InternalServerError, + "cannot find organizer in database for authorized organizer request"); + + var organizer = organizerResult.Value!; + + var aboutMeResponse = + new AboutMeOrganizerResponseDto(organizer.Email, organizer.FirstName, organizer.LastName, organizer.DisplayName, organizer.IsVerified, organizer.CreationDate); + return new ActionResult(aboutMeResponse); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Organizers/DTOs/Request/CreateOrganizerDto.cs b/TickAPI/TickAPI/Organizers/DTOs/Request/CreateOrganizerDto.cs new file mode 100644 index 0000000..cb985f1 --- /dev/null +++ b/TickAPI/TickAPI/Organizers/DTOs/Request/CreateOrganizerDto.cs @@ -0,0 +1,7 @@ +namespace TickAPI.Organizers.DTOs.Request; + +public record CreateOrganizerDto( + string FirstName, + string LastName, + string DisplayName +); \ No newline at end of file diff --git a/TickAPI/TickAPI/Organizers/DTOs/Request/GoogleOrganizerLoginDto.cs b/TickAPI/TickAPI/Organizers/DTOs/Request/GoogleOrganizerLoginDto.cs new file mode 100644 index 0000000..52ddda3 --- /dev/null +++ b/TickAPI/TickAPI/Organizers/DTOs/Request/GoogleOrganizerLoginDto.cs @@ -0,0 +1,5 @@ +namespace TickAPI.Organizers.DTOs.Request; + +public record GoogleOrganizerLoginDto( + string AccessToken +); \ No newline at end of file diff --git a/TickAPI/TickAPI/Organizers/DTOs/Request/VerifyOrganizerDto.cs b/TickAPI/TickAPI/Organizers/DTOs/Request/VerifyOrganizerDto.cs new file mode 100644 index 0000000..80c9464 --- /dev/null +++ b/TickAPI/TickAPI/Organizers/DTOs/Request/VerifyOrganizerDto.cs @@ -0,0 +1,5 @@ +namespace TickAPI.Organizers.DTOs.Request; + +public record VerifyOrganizerDto( + string Email +); \ No newline at end of file diff --git a/TickAPI/TickAPI/Organizers/DTOs/Response/AboutMeOrganizerResponseDto.cs b/TickAPI/TickAPI/Organizers/DTOs/Response/AboutMeOrganizerResponseDto.cs new file mode 100644 index 0000000..5928d16 --- /dev/null +++ b/TickAPI/TickAPI/Organizers/DTOs/Response/AboutMeOrganizerResponseDto.cs @@ -0,0 +1,10 @@ +namespace TickAPI.Organizers.DTOs.Response; + +public record AboutMeOrganizerResponseDto( + string Email, + string FirstName, + string LastName, + string DisplayName, + bool IsVerified, + DateTime CreationDate +); \ No newline at end of file diff --git a/TickAPI/TickAPI/Organizers/DTOs/Response/CreateOrganizerResponseDto.cs b/TickAPI/TickAPI/Organizers/DTOs/Response/CreateOrganizerResponseDto.cs new file mode 100644 index 0000000..3c5c2b5 --- /dev/null +++ b/TickAPI/TickAPI/Organizers/DTOs/Response/CreateOrganizerResponseDto.cs @@ -0,0 +1,5 @@ +namespace TickAPI.Organizers.DTOs.Response; + +public record CreateOrganizerResponseDto( + string Token +); \ No newline at end of file diff --git a/TickAPI/TickAPI/Organizers/DTOs/Response/GoogleOrganizerLoginResponseDto.cs b/TickAPI/TickAPI/Organizers/DTOs/Response/GoogleOrganizerLoginResponseDto.cs new file mode 100644 index 0000000..eaae43b --- /dev/null +++ b/TickAPI/TickAPI/Organizers/DTOs/Response/GoogleOrganizerLoginResponseDto.cs @@ -0,0 +1,7 @@ +namespace TickAPI.Organizers.DTOs.Response; + +public record GoogleOrganizerLoginResponseDto( + string Token, + bool IsNewOrganizer, + bool IsVerified +); \ No newline at end of file diff --git a/TickAPI/TickAPI/Organizers/Repositories/OrganizerRepository.cs b/TickAPI/TickAPI/Organizers/Repositories/OrganizerRepository.cs index f99ebb9..636322b 100644 --- a/TickAPI/TickAPI/Organizers/Repositories/OrganizerRepository.cs +++ b/TickAPI/TickAPI/Organizers/Repositories/OrganizerRepository.cs @@ -1,8 +1,56 @@ -using TickAPI.Organizers.Abstractions; +using Microsoft.EntityFrameworkCore; +using TickAPI.Common.Results; +using TickAPI.Common.Results.Generic; +using TickAPI.Common.TickApiDbContext; +using TickAPI.Organizers.Abstractions; +using TickAPI.Organizers.Models; namespace TickAPI.Organizers.Repositories; public class OrganizerRepository : IOrganizerRepository { + private readonly TickApiDbContext _tickApiDbContext; + + public OrganizerRepository(TickApiDbContext tickApiDbContext) + { + _tickApiDbContext = tickApiDbContext; + } + public async Task> GetOrganizerByEmailAsync(string organizerEmail) + { + var organizer = await _tickApiDbContext.Organizers.FirstOrDefaultAsync(organizer => organizer.Email == organizerEmail); + + if (organizer == null) + { + return Result.Failure(StatusCodes.Status404NotFound, $"organizer with email '{organizerEmail}' not found"); + } + + return Result.Success(organizer); + } + + public async Task AddNewOrganizerAsync(Organizer organizer) + { + _tickApiDbContext.Organizers.Add(organizer); + await _tickApiDbContext.SaveChangesAsync(); + } + + public async Task VerifyOrganizerByEmailAsync(string organizerEmail) + { + var organizer = await _tickApiDbContext.Organizers.FirstOrDefaultAsync(organizer => organizer.Email == organizerEmail); + + if (organizer == null) + { + return Result.Failure(StatusCodes.Status404NotFound, $"organizer with email '{organizerEmail}' not found"); + } + + if (organizer.IsVerified) + { + return Result.Failure(StatusCodes.Status400BadRequest, $"organizer with email '{organizerEmail}' is already verified"); + } + + organizer.IsVerified = true; + await _tickApiDbContext.SaveChangesAsync(); + + return Result.Success(); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Organizers/Services/OrganizerService.cs b/TickAPI/TickAPI/Organizers/Services/OrganizerService.cs index 22e0c39..9ff72ce 100644 --- a/TickAPI/TickAPI/Organizers/Services/OrganizerService.cs +++ b/TickAPI/TickAPI/Organizers/Services/OrganizerService.cs @@ -1,8 +1,51 @@ -using TickAPI.Organizers.Abstractions; +using TickAPI.Common.Results; +using TickAPI.Common.Results.Generic; +using TickAPI.Common.Time.Abstractions; +using TickAPI.Events.Models; +using TickAPI.Organizers.Abstractions; +using TickAPI.Organizers.Models; namespace TickAPI.Organizers.Services; public class OrganizerService : IOrganizerService { + private readonly IOrganizerRepository _organizerRepository; + private readonly IDateTimeService _dateTimeService; + + public OrganizerService(IOrganizerRepository organizerRepository, IDateTimeService dateTimeService) + { + _organizerRepository = organizerRepository; + _dateTimeService = dateTimeService; + } + public async Task> GetOrganizerByEmailAsync(string organizerEmail) + { + return await _organizerRepository.GetOrganizerByEmailAsync(organizerEmail); + } + + public async Task> CreateNewOrganizerAsync(string email, string firstName, string lastName, string displayName) + { + var alreadyExistingResult = await GetOrganizerByEmailAsync(email); + if (alreadyExistingResult.IsSuccess) + return Result.Failure(StatusCodes.Status400BadRequest, + $"organizer with email '{email}' already exists"); + + var organizer = new Organizer + { + Email = email, + FirstName = firstName, + LastName = lastName, + DisplayName = displayName, + IsVerified = false, + CreationDate = _dateTimeService.GetCurrentDateTime(), + Events = new List() + }; + await _organizerRepository.AddNewOrganizerAsync(organizer); + return Result.Success(organizer); + } + + public async Task VerifyOrganizerByEmailAsync(string organizerEmail) + { + return await _organizerRepository.VerifyOrganizerByEmailAsync(organizerEmail); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Program.cs b/TickAPI/TickAPI/Program.cs index 36d62a5..f6f3d13 100644 --- a/TickAPI/TickAPI/Program.cs +++ b/TickAPI/TickAPI/Program.cs @@ -65,10 +65,11 @@ builder.Services.AddAuthorization(options => { options.AddPolicy(AuthPolicies.AdminPolicy.ToString(), policy => policy.RequireRole(UserRole.Admin.ToString())); - options.AddPolicy(AuthPolicies.OrganizerPolicy.ToString(), policy => policy.RequireRole(UserRole.Organizer.ToString())); + options.AddPolicy(AuthPolicies.VerifiedOrganizerPolicy.ToString(), policy => policy.RequireRole(UserRole.Organizer.ToString())); options.AddPolicy(AuthPolicies.CustomerPolicy.ToString(), policy => policy.RequireRole(UserRole.Customer.ToString())); options.AddPolicy(AuthPolicies.NewOrganizerPolicy.ToString(), policy => policy.RequireRole(UserRole.NewOrganizer.ToString())); + options.AddPolicy(AuthPolicies.CreatedOrganizerPolicy.ToString(), policy => policy.RequireRole(UserRole.UnverifiedOrganizer.ToString(), UserRole.Organizer.ToString())); }); // Add admin services. From 60cc6a9f6142e173594fd8ede073b335f107f273 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Thu, 20 Mar 2025 17:49:06 +0100 Subject: [PATCH 075/128] added OrganizerControllerTests.cs --- .../Controllers/OrganizerControllerTests.cs | 384 ++++++++++++++++++ 1 file changed, 384 insertions(+) create mode 100644 TickAPI/TickAPI.Tests/Organizers/Controllers/OrganizerControllerTests.cs diff --git a/TickAPI/TickAPI.Tests/Organizers/Controllers/OrganizerControllerTests.cs b/TickAPI/TickAPI.Tests/Organizers/Controllers/OrganizerControllerTests.cs new file mode 100644 index 0000000..9560686 --- /dev/null +++ b/TickAPI/TickAPI.Tests/Organizers/Controllers/OrganizerControllerTests.cs @@ -0,0 +1,384 @@ +using System.Security.Claims; +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Moq; +using TickAPI.Common.Auth.Abstractions; +using TickAPI.Common.Auth.Enums; +using TickAPI.Common.Auth.Responses; +using TickAPI.Common.Results; +using TickAPI.Common.Results.Generic; +using TickAPI.Organizers.Abstractions; +using TickAPI.Organizers.Controllers; +using TickAPI.Organizers.DTOs.Request; +using TickAPI.Organizers.DTOs.Response; +using TickAPI.Organizers.Models; + +namespace TickAPI.Tests.Organizers.Controllers; + +public class OrganizerControllerTests +{ + [Fact] + public async Task GoogleLogin_WhenAuthSuccessAndVerifiedOrganizerExists_ShouldReturnValidVerifiedLoginDto() + { + // Arrange + const string email = "existing@test.com"; + const string accessToken = "valid-google-token"; + const string jwtToken = "valid-jwt-token"; + + var googleAuthServiceMock = new Mock(); + googleAuthServiceMock + .Setup(m => m.GetUserDataFromAccessToken(accessToken)) + .ReturnsAsync(Result.Success(new GoogleUserData(email, "First", "Last"))); + + var organizerServiceMock = new Mock(); + organizerServiceMock + .Setup(m => m.GetOrganizerByEmailAsync(email)) + .ReturnsAsync(Result.Success(new Organizer { Email = email, IsVerified = true })); + + var jwtServiceMock = new Mock(); + jwtServiceMock + .Setup(m => m.GenerateJwtToken(email, UserRole.Organizer)) + .Returns(Result.Success(jwtToken)); + + var sut = new OrganizerController( + googleAuthServiceMock.Object, + jwtServiceMock.Object, + organizerServiceMock.Object + ); + + // Act + var actionResult = await sut.GoogleLogin(new GoogleOrganizerLoginDto(accessToken)); + + // Assert + Assert.Equal(jwtToken, actionResult.Value!.Token); + Assert.False(actionResult.Value!.IsNewOrganizer); + Assert.True(actionResult.Value!.IsVerified); + } + + [Fact] + public async Task GoogleLogin_WhenAuthSuccessAndUnverifiedOrganizerExists_ShouldReturnValidUnverifiedLoginDto() + { + // Arrange + const string email = "unverified@test.com"; + const string accessToken = "valid-google-token"; + const string jwtToken = "valid-jwt-token"; + + var googleAuthServiceMock = new Mock(); + googleAuthServiceMock + .Setup(m => m.GetUserDataFromAccessToken(accessToken)) + .ReturnsAsync(Result.Success(new GoogleUserData(email, "First", "Last"))); + + var organizerServiceMock = new Mock(); + organizerServiceMock + .Setup(m => m.GetOrganizerByEmailAsync(email)) + .ReturnsAsync(Result.Success(new Organizer { Email = email, IsVerified = false })); + + var jwtServiceMock = new Mock(); + jwtServiceMock + .Setup(m => m.GenerateJwtToken(email, UserRole.UnverifiedOrganizer)) + .Returns(Result.Success(jwtToken)); + + var sut = new OrganizerController( + googleAuthServiceMock.Object, + jwtServiceMock.Object, + organizerServiceMock.Object + ); + + // Act + var actionResult = await sut.GoogleLogin(new GoogleOrganizerLoginDto(accessToken)); + + // Assert + Assert.Equal(jwtToken, actionResult.Value!.Token); + Assert.False(actionResult.Value!.IsNewOrganizer); + Assert.False(actionResult.Value!.IsVerified); + } + + [Fact] + public async Task + GoogleLogin_WhenAuthSuccessAndOrganizerDoesNotExist_ShouldCreateValidNewOrganizerLoginDto() + { + // Arrange + const string email = "new@test.com"; + const string accessToken = "valid-google-token"; + const string jwtToken = "valid-jwt-token"; + + var googleAuthServiceMock = new Mock(); + googleAuthServiceMock + .Setup(m => m.GetUserDataFromAccessToken(accessToken)) + .ReturnsAsync(Result.Success(new GoogleUserData(email, "First", "Last"))); + + var organizerServiceMock = new Mock(); + organizerServiceMock + .Setup(m => m.GetOrganizerByEmailAsync(email)) + .ReturnsAsync(Result.Failure(StatusCodes.Status404NotFound, $"organizer with email '{email}' not found")); + + var jwtServiceMock = new Mock(); + jwtServiceMock + .Setup(m => m.GenerateJwtToken(email, UserRole.NewOrganizer)) + .Returns(Result.Success(jwtToken)); + + var sut = new OrganizerController( + googleAuthServiceMock.Object, + jwtServiceMock.Object, + organizerServiceMock.Object + ); + + // Act + var actionResult = await sut.GoogleLogin(new GoogleOrganizerLoginDto(accessToken)); + + // Assert + Assert.Equal(jwtToken, actionResult.Value!.Token); + Assert.True(actionResult.Value!.IsNewOrganizer); + Assert.False(actionResult.Value!.IsVerified); + } + + [Fact] + public async Task CreateOrganizer_WhenCreatingAccountIsSuccessful_ShouldReturnToken() + { + // Arrange + const string email = "new@test.com"; + const string firstName = "First"; + const string lastName = "Last"; + const string displayName = "Display"; + const string jwtToken = "valid-jwt-token"; + + var organizer = new Organizer + { + Id = Guid.NewGuid(), + Email = email, + FirstName = firstName, + LastName = lastName, + DisplayName = displayName, + IsVerified = false + }; + + var googleAuthServiceMock = new Mock(); + + var organizerServiceMock = new Mock(); + organizerServiceMock + .Setup(m => m.CreateNewOrganizerAsync(email, firstName, lastName, displayName)) + .ReturnsAsync(Result.Success(organizer)); + + var jwtServiceMock = new Mock(); + jwtServiceMock.Setup(m => m.GenerateJwtToken(email, UserRole.UnverifiedOrganizer)) + .Returns(Result.Success(jwtToken)); + + var sut = new OrganizerController( + googleAuthServiceMock.Object, + jwtServiceMock.Object, + organizerServiceMock.Object); + + var claims = new List + { + new Claim(ClaimTypes.Email, email) + }; + sut.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity(claims)) + } + }; + + // Act + var actionResult = await sut.CreateOrganizer(new CreateOrganizerDto(firstName, lastName, displayName)); + + // Assert + Assert.Equal(jwtToken, actionResult.Value!.Token); + } + + [Fact] + public async Task CreateOrganizer_WhenMissingEmailClaim_ShouldReturnBadRequest() + { + // Arrange + var googleAuthServiceMock = new Mock(); + + var organizerServiceMock = new Mock(); + + var jwtServiceMock = new Mock(); + + var sut = new OrganizerController( + googleAuthServiceMock.Object, + jwtServiceMock.Object, + organizerServiceMock.Object); + + sut.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity(new List())) + } + }; + + // Act + var actionResult = await sut.CreateOrganizer(new CreateOrganizerDto("First", "Last", "Display")); + + // Assert + var objectResult = Assert.IsType(actionResult.Result); + Assert.Equal(StatusCodes.Status400BadRequest, objectResult.StatusCode); + Assert.Equal("missing email claim", objectResult.Value); + } + + [Fact] + public async Task VerifyOrganizer_WhenVerificationSuccessful_ShouldReturnOk() + { + // Arrange + const string email = "new@test.com"; + + var googleAuthServiceMock = new Mock(); + + var organizerServiceMock = new Mock(); + organizerServiceMock + .Setup(m => m.VerifyOrganizerByEmailAsync(email)) + .ReturnsAsync(Result.Success()); + + var jwtServiceMock = new Mock(); + + var sut = new OrganizerController( + googleAuthServiceMock.Object, + jwtServiceMock.Object, + organizerServiceMock.Object); + + // Act + var actionResult = await sut.VerifyOrganizer(new VerifyOrganizerDto(email)); + + + // Assert + var result = Assert.IsType(actionResult); + Assert.Equal(StatusCodes.Status200OK, result.StatusCode); + } + + [Fact] + public async Task AboutMe_WithValidEmailClaim_ShouldReturnOrganizerDetails() + { + // Arrange + const string email = "example@test.com"; + const string firstName = "First"; + const string lastName = "Last"; + const string displayName = "Display"; + const bool isVerified = true; + DateTime creationDate = new DateTime(1970, 1, 1, 8, 0, 0, DateTimeKind.Utc); + + var organizer = new Organizer + { + Id = Guid.NewGuid(), + Email = email, + FirstName = firstName, + LastName = lastName, + DisplayName = displayName, + IsVerified = isVerified, + CreationDate = creationDate + }; + + var googleAuthServiceMock = new Mock(); + + var organizerServiceMock = new Mock(); + organizerServiceMock + .Setup(m => m.GetOrganizerByEmailAsync(email)) + .ReturnsAsync(Result.Success(organizer)); + + var jwtServiceMock = new Mock(); + + var sut = new OrganizerController( + googleAuthServiceMock.Object, + jwtServiceMock.Object, + organizerServiceMock.Object); + + var claims = new List + { + new Claim(ClaimTypes.Email, email) + }; + sut.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity(claims)) + } + }; + + // Act + var actionResult = await sut.AboutMe(); + + // Assert + Assert.Equal(email, actionResult.Value?.Email); + Assert.Equal(firstName, actionResult.Value?.FirstName); + Assert.Equal(lastName, actionResult.Value?.LastName); + Assert.Equal(displayName, actionResult.Value?.DisplayName); + Assert.Equal(isVerified, actionResult.Value?.IsVerified); + Assert.Equal(creationDate, actionResult.Value?.CreationDate); + } + + [Fact] + public async Task AboutMe_WithMissingEmailClaim_ShouldReturnBadRequest() + { + // Arrange + var googleAuthServiceMock = new Mock(); + + var organizerServiceMock = new Mock(); + + var jwtServiceMock = new Mock(); + + var sut = new OrganizerController( + googleAuthServiceMock.Object, + jwtServiceMock.Object, + organizerServiceMock.Object); + + sut.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity(new List())) + } + }; + + // Act + var actionResult = await sut.AboutMe(); + + // Assert + var objectResult = Assert.IsType(actionResult.Result); + Assert.Equal(StatusCodes.Status400BadRequest, objectResult.StatusCode); + Assert.Equal("missing email claim", objectResult.Value); + } + + [Fact] + public async Task AboutMe_WhenOrganizerNotFound_ShouldReturnInternalServerError() + { + // Arrange + const string email = "example@test.com"; + + var googleAuthServiceMock = new Mock(); + + var organizerServiceMock = new Mock(); + organizerServiceMock + .Setup(m => m.GetOrganizerByEmailAsync(email)) + .ReturnsAsync(Result.Failure(StatusCodes.Status404NotFound, $"organizer with email '{email}' not found")); + + var jwtServiceMock = new Mock(); + + var sut = new OrganizerController( + googleAuthServiceMock.Object, + jwtServiceMock.Object, + organizerServiceMock.Object); + + var claims = new List + { + new Claim(ClaimTypes.Email, email) + }; + sut.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity(claims)) + } + }; + + // Act + var actionResult = await sut.AboutMe(); + + // Assert + var objectResult = Assert.IsType(actionResult.Result); + Assert.Equal(StatusCodes.Status500InternalServerError, objectResult.StatusCode); + Assert.Equal("cannot find organizer in database for authorized organizer request", objectResult.Value); + } +} \ No newline at end of file From e001f96651d56deca0f9ec2c88d17a3f066983d9 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Thu, 20 Mar 2025 18:00:11 +0100 Subject: [PATCH 076/128] implemented suggested changes --- .../Common/Results/ResultTests.cs | 25 ------------------ TickAPI/TickAPI/Common/Results/Result.cs | 10 ------- .../Controllers/OrganizerController.cs | 26 +++++++------------ 3 files changed, 9 insertions(+), 52 deletions(-) diff --git a/TickAPI/TickAPI.Tests/Common/Results/ResultTests.cs b/TickAPI/TickAPI.Tests/Common/Results/ResultTests.cs index 7e8c2d4..f09668d 100644 --- a/TickAPI/TickAPI.Tests/Common/Results/ResultTests.cs +++ b/TickAPI/TickAPI.Tests/Common/Results/ResultTests.cs @@ -29,21 +29,6 @@ public void Failure_ShouldReturnResultWithError() Assert.Equal(errorMsg, result.ErrorMsg); Assert.Equal(statusCode, result.StatusCode); } - - [Fact] - public void PropagateError_WhenResultWithErrorPassed_ShouldReturnResultWithError() - { - const int statusCode = 500; - const string errorMsg = "error message"; - var resultWithError = Result.Failure(statusCode, errorMsg); - - var result = Result.PropagateError(resultWithError); - - Assert.True(result.IsError); - Assert.False(result.IsSuccess); - Assert.Equal(errorMsg, result.ErrorMsg); - Assert.Equal(statusCode, result.StatusCode); - } [Fact] public void PropagateError_WhenGenericResultWithErrorPassed_ShouldReturnResultWithError() @@ -59,16 +44,6 @@ public void PropagateError_WhenGenericResultWithErrorPassed_ShouldReturnResultWi Assert.Equal(errorMsg, result.ErrorMsg); Assert.Equal(statusCode, result.StatusCode); } - - [Fact] - public void PropagateError_WhenResultWithSuccessPassed_ShouldThrowArgumentException() - { - var resultWithSuccess = Result.Success(); - - var act = () => Result.PropagateError(resultWithSuccess); - - Assert.Throws(act); - } [Fact] public void PropagateError_WhenGenericResultWithSuccessPassed_ShouldThrowArgumentException() diff --git a/TickAPI/TickAPI/Common/Results/Result.cs b/TickAPI/TickAPI/Common/Results/Result.cs index f9ba8ff..d339858 100644 --- a/TickAPI/TickAPI/Common/Results/Result.cs +++ b/TickAPI/TickAPI/Common/Results/Result.cs @@ -26,16 +26,6 @@ public static Result Failure(int statusCode, string errorMsg) return new Result(false, statusCode, errorMsg); } - public static Result PropagateError(Result other) - { - if (other.IsSuccess) - { - throw new ArgumentException("Trying to propagate error from successful value"); - } - - return Failure(other.StatusCode, other.ErrorMsg); - } - public static Result PropagateError(Result other) { if (other.IsSuccess) diff --git a/TickAPI/TickAPI/Organizers/Controllers/OrganizerController.cs b/TickAPI/TickAPI/Organizers/Controllers/OrganizerController.cs index f19ee92..9465d73 100644 --- a/TickAPI/TickAPI/Organizers/Controllers/OrganizerController.cs +++ b/TickAPI/TickAPI/Organizers/Controllers/OrganizerController.cs @@ -46,24 +46,16 @@ public async Task> GoogleLogin([Fr return new ActionResult(new GoogleOrganizerLoginResponseDto(jwtTokenResult.Value!, true, false)); } - - var isVerified = existingOrganizerResult.Value!.IsVerified; - if (isVerified) - { - jwtTokenResult = _jwtService.GenerateJwtToken(userData.Email, UserRole.Organizer); - - if(jwtTokenResult.IsError) - return StatusCode(jwtTokenResult.StatusCode, jwtTokenResult.ErrorMsg); - } - else - { - jwtTokenResult = _jwtService.GenerateJwtToken(userData.Email, UserRole.UnverifiedOrganizer); - - if(jwtTokenResult.IsError) - return StatusCode(jwtTokenResult.StatusCode, jwtTokenResult.ErrorMsg); - } + var isVerified = existingOrganizerResult.Value!.IsVerified; + var role = isVerified ? UserRole.Organizer : UserRole.UnverifiedOrganizer; + + jwtTokenResult = _jwtService.GenerateJwtToken(userData.Email, role); + + if(jwtTokenResult.IsError) + return StatusCode(jwtTokenResult.StatusCode, jwtTokenResult.ErrorMsg); + return new ActionResult(new GoogleOrganizerLoginResponseDto(jwtTokenResult.Value!, false, isVerified)); } @@ -79,7 +71,7 @@ public async Task> CreateOrganizer([Fro if(newOrganizerResult.IsError) return StatusCode(newOrganizerResult.StatusCode, newOrganizerResult.ErrorMsg); - var jwtTokenResult = _jwtService.GenerateJwtToken(newOrganizerResult.Value!.Email, UserRole.UnverifiedOrganizer); + var jwtTokenResult = _jwtService.GenerateJwtToken(newOrganizerResult.Value!.Email, newOrganizerResult.Value!.IsVerified ? UserRole.Organizer : UserRole.UnverifiedOrganizer); if(jwtTokenResult.IsError) return StatusCode(jwtTokenResult.StatusCode, jwtTokenResult.ErrorMsg); From f84815a454bbed20ba47305e4011adfd3549531c Mon Sep 17 00:00:00 2001 From: kubapoke Date: Thu, 20 Mar 2025 18:13:00 +0100 Subject: [PATCH 077/128] small OrganizerRepository.cs change to reduce repeated code --- .../Organizers/Repositories/OrganizerRepository.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/TickAPI/TickAPI/Organizers/Repositories/OrganizerRepository.cs b/TickAPI/TickAPI/Organizers/Repositories/OrganizerRepository.cs index 636322b..1a01bce 100644 --- a/TickAPI/TickAPI/Organizers/Repositories/OrganizerRepository.cs +++ b/TickAPI/TickAPI/Organizers/Repositories/OrganizerRepository.cs @@ -36,13 +36,13 @@ public async Task AddNewOrganizerAsync(Organizer organizer) public async Task VerifyOrganizerByEmailAsync(string organizerEmail) { - var organizer = await _tickApiDbContext.Organizers.FirstOrDefaultAsync(organizer => organizer.Email == organizerEmail); - - if (organizer == null) - { - return Result.Failure(StatusCodes.Status404NotFound, $"organizer with email '{organizerEmail}' not found"); - } - + var organizerResult = await GetOrganizerByEmailAsync(organizerEmail); + + if(organizerResult.IsError) + return Result.PropagateError(organizerResult); + + var organizer = organizerResult.Value!; + if (organizer.IsVerified) { return Result.Failure(StatusCodes.Status400BadRequest, $"organizer with email '{organizerEmail}' is already verified"); From cb604fabecefbc62552ce1c61e4cd00fefb7c367 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Thu, 20 Mar 2025 18:40:58 +0100 Subject: [PATCH 078/128] added OrganizerServiceTests.cs --- .../Services/OrganizerServiceTests.cs | 197 ++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 TickAPI/TickAPI.Tests/Organizers/Services/OrganizerServiceTests.cs diff --git a/TickAPI/TickAPI.Tests/Organizers/Services/OrganizerServiceTests.cs b/TickAPI/TickAPI.Tests/Organizers/Services/OrganizerServiceTests.cs new file mode 100644 index 0000000..5ff8365 --- /dev/null +++ b/TickAPI/TickAPI.Tests/Organizers/Services/OrganizerServiceTests.cs @@ -0,0 +1,197 @@ +using Microsoft.AspNetCore.Http; +using Moq; +using TickAPI.Common.Results; +using TickAPI.Common.Results.Generic; +using TickAPI.Common.Time.Abstractions; +using TickAPI.Organizers.Abstractions; +using TickAPI.Organizers.Models; +using TickAPI.Organizers.Services; + +namespace TickAPI.Tests.Organizers.Services; + +public class OrganizerServiceTests +{ + [Fact] + public async Task GetOrganizerByEmailAsync_WhenOrganizerWithEmailIsReturnedFromRepository_ShouldReturnOrganizer() + { + // Arrange + const string email = "example@test.com"; + + var organizer = new Organizer + { + Email = email + }; + + var organizerRepositoryMock = new Mock(); + organizerRepositoryMock + .Setup(m => m.GetOrganizerByEmailAsync(email)) + .ReturnsAsync(Result.Success(organizer)); + + var dateTimeServiceMock = new Mock(); + + var sut = new OrganizerService( + organizerRepositoryMock.Object, + dateTimeServiceMock.Object + ); + + // Act + var result = await sut.GetOrganizerByEmailAsync(email); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(organizer, result.Value); + } + + [Fact] + public async Task GetOrganizerByEmailAsync_WhenOrganizerWithEmailIsNotReturnedFromRepository_ShouldReturnFailure() + { + // Arrange + Guid id = Guid.NewGuid(); + const string email = "example@test.com"; + + var organizerRepositoryMock = new Mock(); + organizerRepositoryMock + .Setup(m => m.GetOrganizerByEmailAsync(email)) + .ReturnsAsync(Result.Failure(StatusCodes.Status404NotFound, $"organizer with email '{email}' not found")); + + var dateTimeServiceMock = new Mock(); + + var sut = new OrganizerService( + organizerRepositoryMock.Object, + dateTimeServiceMock.Object + ); + + // Act + var result = await sut.GetOrganizerByEmailAsync(email); + + // Assert + Assert.True(result.IsError); + Assert.Equal(StatusCodes.Status404NotFound, result.StatusCode); + Assert.Equal($"organizer with email '{email}' not found", result.ErrorMsg); + } + + [Fact] + public async Task CreateNewOrganizerAsync_WhenOrganizerDataIsValid_ShouldReturnNewOrganizer() + { + // Arrange + Guid id = Guid.NewGuid(); + const string email = "example@test.com"; + const string firstName = "First"; + const string lastName = "Last"; + const string displayName = "Display"; + DateTime currentDate = new DateTime(1970, 1, 1, 8, 0, 0, DateTimeKind.Utc); + + var organizerRepositoryMock = new Mock(); + organizerRepositoryMock + .Setup(m => m.GetOrganizerByEmailAsync(email)) + .ReturnsAsync(Result.Failure(StatusCodes.Status404NotFound, $"organizer with email '{email}' not found")); + organizerRepositoryMock + .Setup(m => m.AddNewOrganizerAsync(It.IsAny())) + .Callback(o => o.Id = id) + .Returns(Task.CompletedTask); + + var dateTimeServiceMock = new Mock(); + dateTimeServiceMock + .Setup(m => m.GetCurrentDateTime()) + .Returns(currentDate); + + var sut = new OrganizerService( + organizerRepositoryMock.Object, + dateTimeServiceMock.Object + ); + + // Act + var result = await sut.CreateNewOrganizerAsync(email, firstName, lastName, displayName); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(id, result.Value!.Id); + Assert.Equal(email, result.Value!.Email); + Assert.Equal(firstName, result.Value!.FirstName); + Assert.Equal(lastName, result.Value!.LastName); + Assert.Equal(displayName, result.Value!.DisplayName); + Assert.False(result.Value!.IsVerified); + Assert.Equal(currentDate, result.Value!.CreationDate); + } + + [Fact] + public async Task CreateNewOrganizerAsync_WhenWithNotUniqueEmail_ShouldReturnFailure() + { + // Arrange + const string email = "example@test.com"; + const string firstName = "First"; + const string lastName = "Last"; + const string displayName = "Display"; + DateTime currentDate = new DateTime(1970, 1, 1, 8, 0, 0, DateTimeKind.Utc); + + var organizerRepositoryMock = new Mock(); + organizerRepositoryMock + .Setup(m => m.GetOrganizerByEmailAsync(email)) + .ReturnsAsync(Result.Success(new Organizer { Email = email })); + + var dateTimeServiceMock = new Mock(); + + var sut = new OrganizerService( + organizerRepositoryMock.Object, + dateTimeServiceMock.Object + ); + + // Act + var result = await sut.CreateNewOrganizerAsync(email, firstName, lastName, displayName); + + // Assert + Assert.True(result.IsError); + Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); + Assert.Equal($"organizer with email '{email}' already exists", result.ErrorMsg); + } + + [Fact] + public async Task VerifyOrganizerByEmailAsync_WhenVerificationSuccessful_ShouldReturnSuccess() + { + const string email = "example@test.com"; + + var organizerRepositoryMock = new Mock(); + organizerRepositoryMock + .Setup(m => m.VerifyOrganizerByEmailAsync(email)) + .ReturnsAsync(Result.Success()); + + var dateTimeServiceMock = new Mock(); + + var sut = new OrganizerService( + organizerRepositoryMock.Object, + dateTimeServiceMock.Object + ); + + // Act + var result = await sut.VerifyOrganizerByEmailAsync(email); + + // Assert + Assert.True(result.IsSuccess); + } + + [Fact] + public async Task VerifyOrganizerByEmailAsync_WhenVerificationNotSuccessful_ShouldReturnFailure() + { + const string email = "example@test.com"; + + var organizerRepositoryMock = new Mock(); + organizerRepositoryMock + .Setup(m => m.VerifyOrganizerByEmailAsync(email)) + .ReturnsAsync(Result.Failure(StatusCodes.Status404NotFound, $"organizer with email '{email}' not found")); + + var dateTimeServiceMock = new Mock(); + + var sut = new OrganizerService( + organizerRepositoryMock.Object, + dateTimeServiceMock.Object + ); + + // Act + var result = await sut.VerifyOrganizerByEmailAsync(email); + + // Assert + Assert.True(result.IsError); + Assert.Equal(StatusCodes.Status404NotFound, result.StatusCode); + Assert.Equal($"organizer with email '{email}' not found", result.ErrorMsg); + } +} \ No newline at end of file From fd1889eb83d71036e7d22434b7c93fde2c54e97d Mon Sep 17 00:00:00 2001 From: kubapoke Date: Thu, 20 Mar 2025 22:34:26 +0100 Subject: [PATCH 079/128] removed unnecessary lines from OrganizerServiceTests.cs --- .../TickAPI.Tests/Organizers/Services/OrganizerServiceTests.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/TickAPI/TickAPI.Tests/Organizers/Services/OrganizerServiceTests.cs b/TickAPI/TickAPI.Tests/Organizers/Services/OrganizerServiceTests.cs index 5ff8365..769ee7f 100644 --- a/TickAPI/TickAPI.Tests/Organizers/Services/OrganizerServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Organizers/Services/OrganizerServiceTests.cs @@ -46,7 +46,6 @@ public async Task GetOrganizerByEmailAsync_WhenOrganizerWithEmailIsReturnedFromR public async Task GetOrganizerByEmailAsync_WhenOrganizerWithEmailIsNotReturnedFromRepository_ShouldReturnFailure() { // Arrange - Guid id = Guid.NewGuid(); const string email = "example@test.com"; var organizerRepositoryMock = new Mock(); @@ -122,7 +121,6 @@ public async Task CreateNewOrganizerAsync_WhenWithNotUniqueEmail_ShouldReturnFai const string firstName = "First"; const string lastName = "Last"; const string displayName = "Display"; - DateTime currentDate = new DateTime(1970, 1, 1, 8, 0, 0, DateTimeKind.Utc); var organizerRepositoryMock = new Mock(); organizerRepositoryMock From 6de5c49920ffb8b2e05c332415f5d3207e325547 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Sat, 22 Mar 2025 09:32:37 +0100 Subject: [PATCH 080/128] Adding events to the DB --- .../Events/Abstractions/IEventRepository.cs | 6 ++- .../Events/Abstractions/IEventService.cs | 8 +++- .../Events/Controllers/EventController.cs | 39 +++++++++++++++++++ .../TickAPI/Events/DTOs/Request/AddressDto.cs | 12 ++++++ .../Events/DTOs/Request/CreateEventDto.cs | 18 ++++++--- TickAPI/TickAPI/Events/Models/Address.cs | 16 +++++++- .../Events/Repositories/EventRepository.cs | 16 +++++++- .../TickAPI/Events/Services/EventService.cs | 38 +++++++++++++++++- 8 files changed, 140 insertions(+), 13 deletions(-) create mode 100644 TickAPI/TickAPI/Events/DTOs/Request/AddressDto.cs diff --git a/TickAPI/TickAPI/Events/Abstractions/IEventRepository.cs b/TickAPI/TickAPI/Events/Abstractions/IEventRepository.cs index b3873c1..d7a690e 100644 --- a/TickAPI/TickAPI/Events/Abstractions/IEventRepository.cs +++ b/TickAPI/TickAPI/Events/Abstractions/IEventRepository.cs @@ -1,6 +1,8 @@ -namespace TickAPI.Events.Abstractions; +using TickAPI.Events.Models; + +namespace TickAPI.Events.Abstractions; public interface IEventRepository { - + public Task AddNewEventAsync(Event @event); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/Abstractions/IEventService.cs b/TickAPI/TickAPI/Events/Abstractions/IEventService.cs index 0a32eb2..5a53c8e 100644 --- a/TickAPI/TickAPI/Events/Abstractions/IEventService.cs +++ b/TickAPI/TickAPI/Events/Abstractions/IEventService.cs @@ -1,6 +1,10 @@ -namespace TickAPI.Events.Abstractions; +using TickAPI.Events.Models; +using TickAPI.Common.Results.Generic; + +namespace TickAPI.Events.Abstractions; public interface IEventService { - + public Task> CreateNewEventAsync(string name, string description, DateTime startDate, + DateTime endDate, uint? minimumAge, string organizerEmail, Address address); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/Controllers/EventController.cs b/TickAPI/TickAPI/Events/Controllers/EventController.cs index 3349977..d82e8e1 100644 --- a/TickAPI/TickAPI/Events/Controllers/EventController.cs +++ b/TickAPI/TickAPI/Events/Controllers/EventController.cs @@ -1,10 +1,49 @@ using Microsoft.AspNetCore.Mvc; +using TickAPI.Events.DTOs.Response; +using TickAPI.Events.DTOs.Request; +using System.Security.Claims; +using TickAPI.Common.Auth.Attributes; +using TickAPI.Common.Auth.Enums; +using TickAPI.Events.Models; +using TickAPI.Events.Abstractions; + namespace TickAPI.Events.Controllers; [ApiController] [Route("api/[controller]")] + +// TODO: Add lists of categories and tickettypes public class EventController : ControllerBase { + private readonly IEventService _eventService; + + public EventController(IEventService eventService) + { + _eventService = eventService; + } + [AuthorizeWithPolicy(AuthPolicies.VerifiedOrganizerPolicy)] + [HttpPost("create-event")] + public async Task> CreateEvent([FromBody] CreateEventDto request) + { + DateTime startDate, endDate; + var email = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value; + if (email == null) + return StatusCode(StatusCodes.Status400BadRequest, "missing email claim"); + Console.WriteLine(request.StartDate); + if (!DateTime.TryParse(request.StartDate, out startDate)) + return BadRequest("Invalid start date format"); + + if (!DateTime.TryParse(request.EndDate, out endDate)) + return BadRequest("Invalid end date format"); + + var newEventResult = await _eventService.CreateNewEventAsync(request.Name, request.Description, startDate, endDate, request.MinimumAge, email, Address.FromDto(request.Address)); + + if(newEventResult.IsError) + return StatusCode(newEventResult.StatusCode, newEventResult.ErrorMsg); + + + return new ActionResult(new CreateEventResponseDto()); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/DTOs/Request/AddressDto.cs b/TickAPI/TickAPI/Events/DTOs/Request/AddressDto.cs new file mode 100644 index 0000000..e2aaf5e --- /dev/null +++ b/TickAPI/TickAPI/Events/DTOs/Request/AddressDto.cs @@ -0,0 +1,12 @@ +using TickAPI.Events.Models; + +namespace TickAPI.Events.DTOs.Request; + +public record AddressDto( + + string Country, + string City, + string? Street, + uint? HouseNumber, + uint? FlatNumber, + string PostalCode); \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/DTOs/Request/CreateEventDto.cs b/TickAPI/TickAPI/Events/DTOs/Request/CreateEventDto.cs index f06c961..4e31d66 100644 --- a/TickAPI/TickAPI/Events/DTOs/Request/CreateEventDto.cs +++ b/TickAPI/TickAPI/Events/DTOs/Request/CreateEventDto.cs @@ -1,6 +1,14 @@ -namespace TickAPI.Events.DTOs.Request; +using TickAPI.Events.Models; -public class CreateEventDto -{ - -} \ No newline at end of file +namespace TickAPI.Events.DTOs.Request; + +public record CreateEventDto +( + string Name, + string Description, + string StartDate, + string EndDate, + uint? MinimumAge, + EventStatus EventStatus, + AddressDto Address +); \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/Models/Address.cs b/TickAPI/TickAPI/Events/Models/Address.cs index faa88d2..658b8fb 100644 --- a/TickAPI/TickAPI/Events/Models/Address.cs +++ b/TickAPI/TickAPI/Events/Models/Address.cs @@ -1,5 +1,5 @@ namespace TickAPI.Events.Models; - +using TickAPI.Events.DTOs.Request; public class Address { public Guid Id { get; set; } @@ -9,4 +9,18 @@ public class Address public uint? HouseNumber { get; set; } public uint? FlatNumber { get; set; } public string PostalCode { get; set; } + + + public static Address FromDto(AddressDto dto) + { + return new Address + { + City = dto.City, + HouseNumber = dto.HouseNumber, + FlatNumber = dto.FlatNumber, + PostalCode = dto.PostalCode, + Street = dto.Street, + Country = dto.Country + }; + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/Repositories/EventRepository.cs b/TickAPI/TickAPI/Events/Repositories/EventRepository.cs index ac0fab2..0ea8f73 100644 --- a/TickAPI/TickAPI/Events/Repositories/EventRepository.cs +++ b/TickAPI/TickAPI/Events/Repositories/EventRepository.cs @@ -1,8 +1,22 @@ -using TickAPI.Events.Abstractions; +using Microsoft.EntityFrameworkCore; +using TickAPI.Common.TickApiDbContext; +using TickAPI.Events.Abstractions; +using TickAPI.Events.Models; namespace TickAPI.Events.Repositories; public class EventRepository : IEventRepository { + private readonly TickApiDbContext _tickApiDbContext; + + public EventRepository(TickApiDbContext tickApiDbContext) + { + _tickApiDbContext = tickApiDbContext; + } + public async Task AddNewEventAsync(Event @event) + { + _tickApiDbContext.Events.Add(@event); + await _tickApiDbContext.SaveChangesAsync(); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/Services/EventService.cs b/TickAPI/TickAPI/Events/Services/EventService.cs index 95304bc..34f72ec 100644 --- a/TickAPI/TickAPI/Events/Services/EventService.cs +++ b/TickAPI/TickAPI/Events/Services/EventService.cs @@ -1,8 +1,42 @@ -using TickAPI.Events.Abstractions; +using TickAPI.Common.Time.Abstractions; +using TickAPI.Events.Abstractions; +using TickAPI.Events.Models; +using TickAPI.Common.Results.Generic; +using TickAPI.Organizers.Abstractions; namespace TickAPI.Events.Services; public class EventService : IEventService { - + private readonly IDateTimeService _dateTimeService; + private readonly IOrganizerService _organizerService; + private readonly IEventRepository _eventRepository; + + public EventService(IDateTimeService dateTimeService, IEventRepository eventRepository, IOrganizerService organizerService) + { + _dateTimeService = dateTimeService; + _eventRepository = eventRepository; + _organizerService = organizerService; + } + + public async Task> CreateNewEventAsync(string name, string description, DateTime startDate, DateTime endDate, uint? minimumAge, string organizerEmail, Address address) + { + var organizerResult = await _organizerService.GetOrganizerByEmailAsync(organizerEmail); + if (!organizerResult.IsSuccess) + return Result.Failure(StatusCodes.Status400BadRequest, + $"organizer with email '{organizerEmail}' doesn't exist"); + + var @event = new Event + { + Name = name, + Description = description, + StartDate = startDate, + EndDate = endDate, + MinimumAge = minimumAge, + Address = address, + Organizer = organizerResult.Value! + }; + await _eventRepository.AddNewEventAsync(@event); + return Result.Success(@event); + } } \ No newline at end of file From be3434e0cefb564d5fef7c5ce60dc43d7673167f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Sat, 22 Mar 2025 09:42:26 +0100 Subject: [PATCH 081/128] STart date preceding end date check and update return value --- .../TickAPI/Events/Controllers/EventController.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/TickAPI/TickAPI/Events/Controllers/EventController.cs b/TickAPI/TickAPI/Events/Controllers/EventController.cs index d82e8e1..0cd4037 100644 --- a/TickAPI/TickAPI/Events/Controllers/EventController.cs +++ b/TickAPI/TickAPI/Events/Controllers/EventController.cs @@ -27,23 +27,25 @@ public EventController(IEventService eventService) [HttpPost("create-event")] public async Task> CreateEvent([FromBody] CreateEventDto request) { - DateTime startDate, endDate; var email = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value; if (email == null) return StatusCode(StatusCodes.Status400BadRequest, "missing email claim"); - Console.WriteLine(request.StartDate); - if (!DateTime.TryParse(request.StartDate, out startDate)) + + if (!DateTime.TryParse(request.StartDate, out DateTime startDate)) return BadRequest("Invalid start date format"); - if (!DateTime.TryParse(request.EndDate, out endDate)) + if (!DateTime.TryParse(request.EndDate, out DateTime endDate)) return BadRequest("Invalid end date format"); + if(endDate < startDate) + return BadRequest("End date must be after start date"); + var newEventResult = await _eventService.CreateNewEventAsync(request.Name, request.Description, startDate, endDate, request.MinimumAge, email, Address.FromDto(request.Address)); if(newEventResult.IsError) return StatusCode(newEventResult.StatusCode, newEventResult.ErrorMsg); - return new ActionResult(new CreateEventResponseDto()); + return StatusCode(200, "Event created succesfully"); } } \ No newline at end of file From 85533f8322c91106aa7e8cb153601f15c47a4895 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Sat, 22 Mar 2025 10:18:15 +0100 Subject: [PATCH 082/128] Move DTO parsing logic to Service from Controller --- .../Events/Services/EventServicesTests.cs | 22 +++++++++++++++++++ TickAPI/TickAPI.Tests/TickAPI.Tests.csproj | 4 ++++ .../Events/Abstractions/IEventService.cs | 5 +++-- .../Events/Controllers/EventController.cs | 10 +-------- .../TickAPI/Events/Services/EventService.cs | 18 +++++++++++---- 5 files changed, 44 insertions(+), 15 deletions(-) create mode 100644 TickAPI/TickAPI.Tests/Events/Services/EventServicesTests.cs diff --git a/TickAPI/TickAPI.Tests/Events/Services/EventServicesTests.cs b/TickAPI/TickAPI.Tests/Events/Services/EventServicesTests.cs new file mode 100644 index 0000000..72802a2 --- /dev/null +++ b/TickAPI/TickAPI.Tests/Events/Services/EventServicesTests.cs @@ -0,0 +1,22 @@ +namespace TickAPI.Tests.Events.Services; + +public class EventServicesTests +{ + [Fact] + + public async Task CreateNewEventAsync_WhenEventDataIsValid_ShouldReturnNewEvent() + { + // arrange + // string Name = "Concert"; + // string Description = "Description of a concert"; + // DateTime StartDate = startDate, + // EndDate = endDate, + // MinimumAge = minimumAge, + // Address = address, + // Organizer = organizerResult.Value! + + // act + + // assert + } +} \ No newline at end of file diff --git a/TickAPI/TickAPI.Tests/TickAPI.Tests.csproj b/TickAPI/TickAPI.Tests/TickAPI.Tests.csproj index 5617d62..ee472dc 100644 --- a/TickAPI/TickAPI.Tests/TickAPI.Tests.csproj +++ b/TickAPI/TickAPI.Tests/TickAPI.Tests.csproj @@ -23,4 +23,8 @@ + + + + diff --git a/TickAPI/TickAPI/Events/Abstractions/IEventService.cs b/TickAPI/TickAPI/Events/Abstractions/IEventService.cs index 5a53c8e..fdbfd72 100644 --- a/TickAPI/TickAPI/Events/Abstractions/IEventService.cs +++ b/TickAPI/TickAPI/Events/Abstractions/IEventService.cs @@ -1,10 +1,11 @@ using TickAPI.Events.Models; using TickAPI.Common.Results.Generic; +using TickAPI.Events.DTOs.Request; namespace TickAPI.Events.Abstractions; public interface IEventService { - public Task> CreateNewEventAsync(string name, string description, DateTime startDate, - DateTime endDate, uint? minimumAge, string organizerEmail, Address address); + public Task> CreateNewEventAsync(string name, string description, string startDate, + string endDate, uint? minimumAge, AddressDto address, string organizerEmail); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/Controllers/EventController.cs b/TickAPI/TickAPI/Events/Controllers/EventController.cs index 0cd4037..56ca9d6 100644 --- a/TickAPI/TickAPI/Events/Controllers/EventController.cs +++ b/TickAPI/TickAPI/Events/Controllers/EventController.cs @@ -30,17 +30,9 @@ public async Task> CreateEvent([FromBody] C var email = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value; if (email == null) return StatusCode(StatusCodes.Status400BadRequest, "missing email claim"); - - if (!DateTime.TryParse(request.StartDate, out DateTime startDate)) - return BadRequest("Invalid start date format"); - - if (!DateTime.TryParse(request.EndDate, out DateTime endDate)) - return BadRequest("Invalid end date format"); - if(endDate < startDate) - return BadRequest("End date must be after start date"); - var newEventResult = await _eventService.CreateNewEventAsync(request.Name, request.Description, startDate, endDate, request.MinimumAge, email, Address.FromDto(request.Address)); + var newEventResult = await _eventService.CreateNewEventAsync(request.Name, request.Description, request.StartDate, request.EndDate, request.MinimumAge, request.Address, email); if(newEventResult.IsError) return StatusCode(newEventResult.StatusCode, newEventResult.ErrorMsg); diff --git a/TickAPI/TickAPI/Events/Services/EventService.cs b/TickAPI/TickAPI/Events/Services/EventService.cs index 34f72ec..a823b2b 100644 --- a/TickAPI/TickAPI/Events/Services/EventService.cs +++ b/TickAPI/TickAPI/Events/Services/EventService.cs @@ -3,6 +3,7 @@ using TickAPI.Events.Models; using TickAPI.Common.Results.Generic; using TickAPI.Organizers.Abstractions; +using TickAPI.Events.DTOs.Request; namespace TickAPI.Events.Services; @@ -19,21 +20,30 @@ public EventService(IDateTimeService dateTimeService, IEventRepository eventRepo _organizerService = organizerService; } - public async Task> CreateNewEventAsync(string name, string description, DateTime startDate, DateTime endDate, uint? minimumAge, string organizerEmail, Address address) + public async Task> CreateNewEventAsync(string name, string description, string startDate, string endDate, uint? minimumAge, AddressDto address, string organizerEmail) { var organizerResult = await _organizerService.GetOrganizerByEmailAsync(organizerEmail); if (!organizerResult.IsSuccess) return Result.Failure(StatusCodes.Status400BadRequest, $"organizer with email '{organizerEmail}' doesn't exist"); + + if (!DateTime.TryParse(startDate, out DateTime startDateParsed)) + return Result.Failure(StatusCodes.Status400BadRequest, "Invalid start date format"); + + if (!DateTime.TryParse(endDate, out DateTime endDateParsed)) + return Result.Failure(StatusCodes.Status400BadRequest, "Invalid end date format"); + + if (endDateParsed < startDateParsed) + return Result.Failure(StatusCodes.Status400BadRequest, "End date should be after start date"); var @event = new Event { Name = name, Description = description, - StartDate = startDate, - EndDate = endDate, + StartDate = startDateParsed, + EndDate = endDateParsed, MinimumAge = minimumAge, - Address = address, + Address = Models.Address.FromDto(address), Organizer = organizerResult.Value! }; await _eventRepository.AddNewEventAsync(@event); From a870a4704e815dfc9b16141eee0acb264d99032e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Sat, 22 Mar 2025 14:40:50 +0100 Subject: [PATCH 083/128] Add Event service tests --- .../Controllers/EventControllerTests.cs | 6 +++ .../Events/Services/EventServicesTests.cs | 50 ++++++++++++++++--- TickAPI/TickAPI.Tests/TickAPI.Tests.csproj | 4 -- .../Events/Abstractions/IEventService.cs | 2 +- .../Events/Controllers/EventController.cs | 2 +- .../TickAPI/Events/Services/EventService.cs | 9 ++-- 6 files changed, 54 insertions(+), 19 deletions(-) create mode 100644 TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs diff --git a/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs b/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs new file mode 100644 index 0000000..cee8cca --- /dev/null +++ b/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs @@ -0,0 +1,6 @@ +namespace TickAPI.Tests.Events.Controllers; + +public class EventControllerTests +{ + +} \ No newline at end of file diff --git a/TickAPI/TickAPI.Tests/Events/Services/EventServicesTests.cs b/TickAPI/TickAPI.Tests/Events/Services/EventServicesTests.cs index 72802a2..eb6ef27 100644 --- a/TickAPI/TickAPI.Tests/Events/Services/EventServicesTests.cs +++ b/TickAPI/TickAPI.Tests/Events/Services/EventServicesTests.cs @@ -1,4 +1,14 @@ -namespace TickAPI.Tests.Events.Services; +using System.Runtime.CompilerServices; +using TickAPI.Events.Abstractions; +using TickAPI.Events.DTOs.Request; +using Moq; +using TickAPI.Events.Models; +using TickAPI.Organizers.Abstractions; +using TickAPI.Organizers.Models; +using TickAPI.Common.Results.Generic; +using TickAPI.Events.Services; + +namespace TickAPI.Tests.Events.Services; public class EventServicesTests { @@ -7,16 +17,40 @@ public class EventServicesTests public async Task CreateNewEventAsync_WhenEventDataIsValid_ShouldReturnNewEvent() { // arrange - // string Name = "Concert"; - // string Description = "Description of a concert"; - // DateTime StartDate = startDate, - // EndDate = endDate, - // MinimumAge = minimumAge, - // Address = address, - // Organizer = organizerResult.Value! + string name = "Concert"; + string description = "Description of a concert"; + string startDate = "01.05.2025"; + string endDate = "01.06.2025"; + uint? minimumAge = 18; + string organizerEmail = "123@mail.com"; + EventStatus eventStatus = EventStatus.TicketsAvailable; + Guid id = Guid.NewGuid(); + AddressDto address = new AddressDto("United States", "New York", "Main st", 20, null, "00-000"); + + var eventServiceMock = new Mock(); + eventServiceMock.Setup(e => e.AddNewEventAsync(It.IsAny())).Callback(e => e.Id = id) + .Returns(Task.CompletedTask); + var organizerServiceMock = new Mock(); + organizerServiceMock + .Setup(m => m.GetOrganizerByEmailAsync(organizerEmail)) + .ReturnsAsync(Result.Success(new Organizer { Email = organizerEmail, IsVerified = true })); + + var sut = new EventService(eventServiceMock.Object, organizerServiceMock.Object); // act + var result = await sut.CreateNewEventAsync(name, description, startDate, endDate, minimumAge, address, eventStatus, organizerEmail); + // assert + + + Assert.True(result.IsSuccess); + Assert.Equal(new DateTime(2025, 5, 1), result.Value!.StartDate); + Assert.Equal(new DateTime(2025, 6, 1), result.Value!.EndDate); + Assert.Equal(name, result.Value!.Name); + Assert.Equal(description, result.Value!.Description); + Assert.Equal(eventStatus, result.Value!.EventStatus); + Assert.Equal(id, result.Value!.Id); + Assert.Equal(organizerEmail, result.Value!.Organizer.Email); } } \ No newline at end of file diff --git a/TickAPI/TickAPI.Tests/TickAPI.Tests.csproj b/TickAPI/TickAPI.Tests/TickAPI.Tests.csproj index ee472dc..5617d62 100644 --- a/TickAPI/TickAPI.Tests/TickAPI.Tests.csproj +++ b/TickAPI/TickAPI.Tests/TickAPI.Tests.csproj @@ -23,8 +23,4 @@ - - - - diff --git a/TickAPI/TickAPI/Events/Abstractions/IEventService.cs b/TickAPI/TickAPI/Events/Abstractions/IEventService.cs index fdbfd72..953e014 100644 --- a/TickAPI/TickAPI/Events/Abstractions/IEventService.cs +++ b/TickAPI/TickAPI/Events/Abstractions/IEventService.cs @@ -7,5 +7,5 @@ namespace TickAPI.Events.Abstractions; public interface IEventService { public Task> CreateNewEventAsync(string name, string description, string startDate, - string endDate, uint? minimumAge, AddressDto address, string organizerEmail); + string endDate, uint? minimumAge, AddressDto address, EventStatus eventStatus, string organizerEmail); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/Controllers/EventController.cs b/TickAPI/TickAPI/Events/Controllers/EventController.cs index 56ca9d6..841a275 100644 --- a/TickAPI/TickAPI/Events/Controllers/EventController.cs +++ b/TickAPI/TickAPI/Events/Controllers/EventController.cs @@ -32,7 +32,7 @@ public async Task> CreateEvent([FromBody] C return StatusCode(StatusCodes.Status400BadRequest, "missing email claim"); - var newEventResult = await _eventService.CreateNewEventAsync(request.Name, request.Description, request.StartDate, request.EndDate, request.MinimumAge, request.Address, email); + var newEventResult = await _eventService.CreateNewEventAsync(request.Name, request.Description, request.StartDate, request.EndDate, request.MinimumAge, request.Address, request.EventStatus, email); if(newEventResult.IsError) return StatusCode(newEventResult.StatusCode, newEventResult.ErrorMsg); diff --git a/TickAPI/TickAPI/Events/Services/EventService.cs b/TickAPI/TickAPI/Events/Services/EventService.cs index a823b2b..f2b9574 100644 --- a/TickAPI/TickAPI/Events/Services/EventService.cs +++ b/TickAPI/TickAPI/Events/Services/EventService.cs @@ -9,18 +9,16 @@ namespace TickAPI.Events.Services; public class EventService : IEventService { - private readonly IDateTimeService _dateTimeService; private readonly IOrganizerService _organizerService; private readonly IEventRepository _eventRepository; - public EventService(IDateTimeService dateTimeService, IEventRepository eventRepository, IOrganizerService organizerService) + public EventService(IEventRepository eventRepository, IOrganizerService organizerService) { - _dateTimeService = dateTimeService; _eventRepository = eventRepository; _organizerService = organizerService; } - public async Task> CreateNewEventAsync(string name, string description, string startDate, string endDate, uint? minimumAge, AddressDto address, string organizerEmail) + public async Task> CreateNewEventAsync(string name, string description, string startDate, string endDate, uint? minimumAge, AddressDto address, EventStatus eventStatus, string organizerEmail) { var organizerResult = await _organizerService.GetOrganizerByEmailAsync(organizerEmail); if (!organizerResult.IsSuccess) @@ -44,7 +42,8 @@ public async Task> CreateNewEventAsync(string name, string descri EndDate = endDateParsed, MinimumAge = minimumAge, Address = Models.Address.FromDto(address), - Organizer = organizerResult.Value! + Organizer = organizerResult.Value!, + EventStatus = eventStatus }; await _eventRepository.AddNewEventAsync(@event); return Result.Success(@event); From 71a2c4cc288a36da811cf69d72f2bc4b1ed31f0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Sat, 22 Mar 2025 15:09:06 +0100 Subject: [PATCH 084/128] Add Event controller test --- .../Controllers/EventControllerTests.cs | 64 ++++++++++++++++++- .../Events/Services/EventServicesTests.cs | 6 +- 2 files changed, 65 insertions(+), 5 deletions(-) diff --git a/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs b/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs index cee8cca..0878b19 100644 --- a/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs +++ b/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs @@ -1,6 +1,66 @@ -namespace TickAPI.Tests.Events.Controllers; +using TickAPI.Events.DTOs.Request; +using TickAPI.Events.Models; +using System.Security.Claims; +using Moq; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using TickAPI.Events.Controllers; +using TickAPI.Events.Abstractions; +using TickAPI.Common.Results; +using TickAPI.Common.Results.Generic; +using TickAPI.Organizers.Models; +using TickAPI.Events.DTOs.Response; + +namespace TickAPI.Tests.Events.Controllers; public class EventControllerTests { - + [Fact] + public async Task CreateEvent_WheneDataIsValid_ShouldReturnSuccess() + { + + //arrange + + string name = "Concert"; + string description = "Description of a concert"; + string startDate = "01.05.2025"; + string endDate = "01.06.2025"; + uint? minimumAge = 18; + string email = "123@mail.com"; + EventStatus eventStatus = EventStatus.TicketsAvailable; + Guid id = Guid.NewGuid(); + AddressDto address = new AddressDto("United States", "New York", "Main st", 20, null, "00-000"); + CreateEventDto eventDto = new CreateEventDto(name, description, startDate, endDate, minimumAge, eventStatus, address); + + var eventServiceMock = new Mock(); + eventServiceMock + .Setup(m => m.CreateNewEventAsync(name, description, startDate, endDate, minimumAge, address, eventStatus, email)) + .ReturnsAsync(Result.Success(new Event())); + + var sut = new EventController(eventServiceMock.Object); + + var claims = new List + { + new Claim(ClaimTypes.Email, email) + }; + sut.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity(claims)) + } + }; + + // act + var res = await sut.CreateEvent(eventDto); + + // assert + + + var result = Assert.IsType>(res); + var objectResult = Assert.IsType(result.Result); + Assert.Equal(200, objectResult.StatusCode); + Assert.Equal("Event created succesfully", objectResult.Value); + + } } \ No newline at end of file diff --git a/TickAPI/TickAPI.Tests/Events/Services/EventServicesTests.cs b/TickAPI/TickAPI.Tests/Events/Services/EventServicesTests.cs index eb6ef27..ea0c857 100644 --- a/TickAPI/TickAPI.Tests/Events/Services/EventServicesTests.cs +++ b/TickAPI/TickAPI.Tests/Events/Services/EventServicesTests.cs @@ -27,8 +27,8 @@ public async Task CreateNewEventAsync_WhenEventDataIsValid_ShouldReturnNewEvent( Guid id = Guid.NewGuid(); AddressDto address = new AddressDto("United States", "New York", "Main st", 20, null, "00-000"); - var eventServiceMock = new Mock(); - eventServiceMock.Setup(e => e.AddNewEventAsync(It.IsAny())).Callback(e => e.Id = id) + var eventRepositoryMock = new Mock(); + eventRepositoryMock.Setup(e => e.AddNewEventAsync(It.IsAny())).Callback(e => e.Id = id) .Returns(Task.CompletedTask); var organizerServiceMock = new Mock(); @@ -36,7 +36,7 @@ public async Task CreateNewEventAsync_WhenEventDataIsValid_ShouldReturnNewEvent( .Setup(m => m.GetOrganizerByEmailAsync(organizerEmail)) .ReturnsAsync(Result.Success(new Organizer { Email = organizerEmail, IsVerified = true })); - var sut = new EventService(eventServiceMock.Object, organizerServiceMock.Object); + var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object); // act var result = await sut.CreateNewEventAsync(name, description, startDate, endDate, minimumAge, address, eventStatus, organizerEmail); From 4701fad074d8fee44117e05303273f38890d3653 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Sat, 22 Mar 2025 15:16:13 +0100 Subject: [PATCH 085/128] Add european locale to datetime parsing --- TickAPI/TickAPI/Events/Services/EventService.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/TickAPI/TickAPI/Events/Services/EventService.cs b/TickAPI/TickAPI/Events/Services/EventService.cs index f2b9574..01bd109 100644 --- a/TickAPI/TickAPI/Events/Services/EventService.cs +++ b/TickAPI/TickAPI/Events/Services/EventService.cs @@ -25,10 +25,12 @@ public async Task> CreateNewEventAsync(string name, string descri return Result.Failure(StatusCodes.Status400BadRequest, $"organizer with email '{organizerEmail}' doesn't exist"); - if (!DateTime.TryParse(startDate, out DateTime startDateParsed)) + if (!DateTime.TryParse(startDate, new System.Globalization.CultureInfo("fr-FR"), + System.Globalization.DateTimeStyles.None, out DateTime startDateParsed)) return Result.Failure(StatusCodes.Status400BadRequest, "Invalid start date format"); - if (!DateTime.TryParse(endDate, out DateTime endDateParsed)) + if (!DateTime.TryParse(endDate, new System.Globalization.CultureInfo("fr-FR"), + System.Globalization.DateTimeStyles.None, out DateTime endDateParsed)) return Result.Failure(StatusCodes.Status400BadRequest, "Invalid end date format"); if (endDateParsed < startDateParsed) From 64429e50c7aeae678f66eb52d6c5037a8294bbfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Fri, 28 Mar 2025 13:47:35 +0100 Subject: [PATCH 086/128] Add address services --- .../Controllers/EventControllerTests.cs | 8 ++-- .../Events/Services/EventServicesTests.cs | 19 +++++++-- .../Events/Abstractions/IAddressRepository.cs | 10 +++++ .../Events/Abstractions/IAddressService.cs | 10 +++++ .../Events/Abstractions/IEventService.cs | 2 +- .../Events/Controllers/EventController.cs | 4 +- .../{AddressDto.cs => CreateAddressDto.cs} | 2 +- .../Events/DTOs/Request/CreateEventDto.cs | 2 +- TickAPI/TickAPI/Events/Models/Address.cs | 15 +------ .../Events/Repositories/AddressRepository.cs | 38 +++++++++++++++++ .../TickAPI/Events/Services/AddressService.cs | 42 +++++++++++++++++++ .../TickAPI/Events/Services/EventService.cs | 13 +++--- TickAPI/TickAPI/Program.cs | 4 ++ 13 files changed, 138 insertions(+), 31 deletions(-) create mode 100644 TickAPI/TickAPI/Events/Abstractions/IAddressRepository.cs create mode 100644 TickAPI/TickAPI/Events/Abstractions/IAddressService.cs rename TickAPI/TickAPI/Events/DTOs/Request/{AddressDto.cs => CreateAddressDto.cs} (86%) create mode 100644 TickAPI/TickAPI/Events/Repositories/AddressRepository.cs create mode 100644 TickAPI/TickAPI/Events/Services/AddressService.cs diff --git a/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs b/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs index 0878b19..943d5a2 100644 --- a/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs +++ b/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs @@ -29,12 +29,12 @@ public async Task CreateEvent_WheneDataIsValid_ShouldReturnSuccess() string email = "123@mail.com"; EventStatus eventStatus = EventStatus.TicketsAvailable; Guid id = Guid.NewGuid(); - AddressDto address = new AddressDto("United States", "New York", "Main st", 20, null, "00-000"); - CreateEventDto eventDto = new CreateEventDto(name, description, startDate, endDate, minimumAge, eventStatus, address); + CreateAddressDto createAddress = new CreateAddressDto("United States", "New York", "Main st", 20, null, "00-000"); + CreateEventDto eventDto = new CreateEventDto(name, description, startDate, endDate, minimumAge, eventStatus, createAddress); var eventServiceMock = new Mock(); eventServiceMock - .Setup(m => m.CreateNewEventAsync(name, description, startDate, endDate, minimumAge, address, eventStatus, email)) + .Setup(m => m.CreateNewEventAsync(name, description, startDate, endDate, minimumAge, createAddress, eventStatus, email)) .ReturnsAsync(Result.Success(new Event())); var sut = new EventController(eventServiceMock.Object); @@ -58,7 +58,7 @@ public async Task CreateEvent_WheneDataIsValid_ShouldReturnSuccess() var result = Assert.IsType>(res); - var objectResult = Assert.IsType(result.Result); + var objectResult = Assert.IsType(result.Result); Assert.Equal(200, objectResult.StatusCode); Assert.Equal("Event created succesfully", objectResult.Value); diff --git a/TickAPI/TickAPI.Tests/Events/Services/EventServicesTests.cs b/TickAPI/TickAPI.Tests/Events/Services/EventServicesTests.cs index ea0c857..24fdec6 100644 --- a/TickAPI/TickAPI.Tests/Events/Services/EventServicesTests.cs +++ b/TickAPI/TickAPI.Tests/Events/Services/EventServicesTests.cs @@ -25,7 +25,7 @@ public async Task CreateNewEventAsync_WhenEventDataIsValid_ShouldReturnNewEvent( string organizerEmail = "123@mail.com"; EventStatus eventStatus = EventStatus.TicketsAvailable; Guid id = Guid.NewGuid(); - AddressDto address = new AddressDto("United States", "New York", "Main st", 20, null, "00-000"); + CreateAddressDto createAddress = new CreateAddressDto("United States", "New York", "Main st", 20, null, "00-000"); var eventRepositoryMock = new Mock(); eventRepositoryMock.Setup(e => e.AddNewEventAsync(It.IsAny())).Callback(e => e.Id = id) @@ -35,11 +35,24 @@ public async Task CreateNewEventAsync_WhenEventDataIsValid_ShouldReturnNewEvent( organizerServiceMock .Setup(m => m.GetOrganizerByEmailAsync(organizerEmail)) .ReturnsAsync(Result.Success(new Organizer { Email = organizerEmail, IsVerified = true })); + + var addressServiceMock = new Mock(); + addressServiceMock.Setup(m => m.GetAddressAsync(createAddress)).ReturnsAsync( + Result
.Success(new Address + { + City = createAddress.City, + Country = createAddress.Country, + FlatNumber = createAddress.FlatNumber, + HouseNumber = createAddress.HouseNumber, + PostalCode = createAddress.PostalCode, + Street = createAddress.Street, + }) + ); - var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object); + var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object); // act - var result = await sut.CreateNewEventAsync(name, description, startDate, endDate, minimumAge, address, eventStatus, organizerEmail); + var result = await sut.CreateNewEventAsync(name, description, startDate, endDate, minimumAge, createAddress, eventStatus, organizerEmail); // assert diff --git a/TickAPI/TickAPI/Events/Abstractions/IAddressRepository.cs b/TickAPI/TickAPI/Events/Abstractions/IAddressRepository.cs new file mode 100644 index 0000000..dba9625 --- /dev/null +++ b/TickAPI/TickAPI/Events/Abstractions/IAddressRepository.cs @@ -0,0 +1,10 @@ +using TickAPI.Common.Results.Generic; +using TickAPI.Events.DTOs.Request; +using TickAPI.Events.Models; + +namespace TickAPI.Events.Abstractions; + +public interface IAddressRepository +{ + public Task> GetAddressAsync(CreateAddressDto createAddress); +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/Abstractions/IAddressService.cs b/TickAPI/TickAPI/Events/Abstractions/IAddressService.cs new file mode 100644 index 0000000..5b2313c --- /dev/null +++ b/TickAPI/TickAPI/Events/Abstractions/IAddressService.cs @@ -0,0 +1,10 @@ +using TickAPI.Common.Results.Generic; +using TickAPI.Events.DTOs.Request; +using TickAPI.Events.Models; + +namespace TickAPI.Events.Abstractions; + +public interface IAddressService +{ + public Task> GetAddressAsync(CreateAddressDto createAddress); +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/Abstractions/IEventService.cs b/TickAPI/TickAPI/Events/Abstractions/IEventService.cs index 953e014..ccf8a91 100644 --- a/TickAPI/TickAPI/Events/Abstractions/IEventService.cs +++ b/TickAPI/TickAPI/Events/Abstractions/IEventService.cs @@ -7,5 +7,5 @@ namespace TickAPI.Events.Abstractions; public interface IEventService { public Task> CreateNewEventAsync(string name, string description, string startDate, - string endDate, uint? minimumAge, AddressDto address, EventStatus eventStatus, string organizerEmail); + string endDate, uint? minimumAge, CreateAddressDto createAddress, EventStatus eventStatus, string organizerEmail); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/Controllers/EventController.cs b/TickAPI/TickAPI/Events/Controllers/EventController.cs index 841a275..fb5f65d 100644 --- a/TickAPI/TickAPI/Events/Controllers/EventController.cs +++ b/TickAPI/TickAPI/Events/Controllers/EventController.cs @@ -32,12 +32,12 @@ public async Task> CreateEvent([FromBody] C return StatusCode(StatusCodes.Status400BadRequest, "missing email claim"); - var newEventResult = await _eventService.CreateNewEventAsync(request.Name, request.Description, request.StartDate, request.EndDate, request.MinimumAge, request.Address, request.EventStatus, email); + var newEventResult = await _eventService.CreateNewEventAsync(request.Name, request.Description, request.StartDate, request.EndDate, request.MinimumAge, request.CreateAddress, request.EventStatus, email); if(newEventResult.IsError) return StatusCode(newEventResult.StatusCode, newEventResult.ErrorMsg); - return StatusCode(200, "Event created succesfully"); + return Ok("Event created succesfully"); } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/DTOs/Request/AddressDto.cs b/TickAPI/TickAPI/Events/DTOs/Request/CreateAddressDto.cs similarity index 86% rename from TickAPI/TickAPI/Events/DTOs/Request/AddressDto.cs rename to TickAPI/TickAPI/Events/DTOs/Request/CreateAddressDto.cs index e2aaf5e..e06d179 100644 --- a/TickAPI/TickAPI/Events/DTOs/Request/AddressDto.cs +++ b/TickAPI/TickAPI/Events/DTOs/Request/CreateAddressDto.cs @@ -2,7 +2,7 @@ namespace TickAPI.Events.DTOs.Request; -public record AddressDto( +public record CreateAddressDto( string Country, string City, diff --git a/TickAPI/TickAPI/Events/DTOs/Request/CreateEventDto.cs b/TickAPI/TickAPI/Events/DTOs/Request/CreateEventDto.cs index 4e31d66..16d8044 100644 --- a/TickAPI/TickAPI/Events/DTOs/Request/CreateEventDto.cs +++ b/TickAPI/TickAPI/Events/DTOs/Request/CreateEventDto.cs @@ -10,5 +10,5 @@ public record CreateEventDto string EndDate, uint? MinimumAge, EventStatus EventStatus, - AddressDto Address + CreateAddressDto CreateAddress ); \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/Models/Address.cs b/TickAPI/TickAPI/Events/Models/Address.cs index 658b8fb..b38931c 100644 --- a/TickAPI/TickAPI/Events/Models/Address.cs +++ b/TickAPI/TickAPI/Events/Models/Address.cs @@ -9,18 +9,5 @@ public class Address public uint? HouseNumber { get; set; } public uint? FlatNumber { get; set; } public string PostalCode { get; set; } - - - public static Address FromDto(AddressDto dto) - { - return new Address - { - City = dto.City, - HouseNumber = dto.HouseNumber, - FlatNumber = dto.FlatNumber, - PostalCode = dto.PostalCode, - Street = dto.Street, - Country = dto.Country - }; - } + } \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/Repositories/AddressRepository.cs b/TickAPI/TickAPI/Events/Repositories/AddressRepository.cs new file mode 100644 index 0000000..6c2725f --- /dev/null +++ b/TickAPI/TickAPI/Events/Repositories/AddressRepository.cs @@ -0,0 +1,38 @@ +using Microsoft.EntityFrameworkCore; +using TickAPI.Common.Results.Generic; +using TickAPI.Common.TickApiDbContext; +using TickAPI.Events.Abstractions; +using TickAPI.Events.DTOs.Request; +using TickAPI.Events.Models; + +namespace TickAPI.Events.Repositories; + +public class AddressRepository : IAddressRepository +{ + private readonly TickApiDbContext _tickApiDbContext; + + public AddressRepository(TickApiDbContext tickApiDbContext) + { + _tickApiDbContext = tickApiDbContext; + } + + + public async Task> GetAddressAsync(CreateAddressDto createAddress) + { + var address = await _tickApiDbContext.Addresses.FirstOrDefaultAsync(x => + x.Street == createAddress.Street && + x.City == createAddress.City && + x.Country == createAddress.Country && + x.HouseNumber == createAddress.HouseNumber && + x.PostalCode == createAddress.PostalCode && + x.FlatNumber == createAddress.FlatNumber + ); + + if (address == null) + { + return Result
.Failure(StatusCodes.Status404NotFound,"Address not found"); + } + + return Result
.Success(address); + } +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/Services/AddressService.cs b/TickAPI/TickAPI/Events/Services/AddressService.cs new file mode 100644 index 0000000..93179c8 --- /dev/null +++ b/TickAPI/TickAPI/Events/Services/AddressService.cs @@ -0,0 +1,42 @@ +using TickAPI.Common.Results; +using TickAPI.Common.Results.Generic; +using TickAPI.Events.Abstractions; +using TickAPI.Events.DTOs.Request; +using TickAPI.Events.Models; + +namespace TickAPI.Events.Services; + +public class AddressService : IAddressService +{ + + private readonly IAddressRepository _addressRepository; + + public AddressService(IAddressRepository addressRepository) + { + _addressRepository = addressRepository; + } + public async Task> GetAddressAsync(CreateAddressDto createAddress) + { + var result = await _addressRepository.GetAddressAsync(createAddress); + if (result.IsSuccess) + { + return Result
.Success(result.Value!); + } + + return Result
.Success(FromDto(createAddress)); + } + + private static Address FromDto(CreateAddressDto dto) + { + return new Address + { + City = dto.City, + HouseNumber = dto.HouseNumber, + FlatNumber = dto.FlatNumber, + PostalCode = dto.PostalCode, + Street = dto.Street, + Country = dto.Country + }; + } + +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/Services/EventService.cs b/TickAPI/TickAPI/Events/Services/EventService.cs index 01bd109..9e3b9ff 100644 --- a/TickAPI/TickAPI/Events/Services/EventService.cs +++ b/TickAPI/TickAPI/Events/Services/EventService.cs @@ -11,19 +11,20 @@ public class EventService : IEventService { private readonly IOrganizerService _organizerService; private readonly IEventRepository _eventRepository; + private readonly IAddressService _addressService; - public EventService(IEventRepository eventRepository, IOrganizerService organizerService) + public EventService(IEventRepository eventRepository, IOrganizerService organizerService, IAddressService addressService) { _eventRepository = eventRepository; _organizerService = organizerService; + _addressService = addressService; } - public async Task> CreateNewEventAsync(string name, string description, string startDate, string endDate, uint? minimumAge, AddressDto address, EventStatus eventStatus, string organizerEmail) + public async Task> CreateNewEventAsync(string name, string description, string startDate, string endDate, uint? minimumAge, CreateAddressDto createAddress, EventStatus eventStatus, string organizerEmail) { var organizerResult = await _organizerService.GetOrganizerByEmailAsync(organizerEmail); if (!organizerResult.IsSuccess) - return Result.Failure(StatusCodes.Status400BadRequest, - $"organizer with email '{organizerEmail}' doesn't exist"); + return Result.PropagateError(organizerResult); if (!DateTime.TryParse(startDate, new System.Globalization.CultureInfo("fr-FR"), System.Globalization.DateTimeStyles.None, out DateTime startDateParsed)) @@ -36,6 +37,8 @@ public async Task> CreateNewEventAsync(string name, string descri if (endDateParsed < startDateParsed) return Result.Failure(StatusCodes.Status400BadRequest, "End date should be after start date"); + var address = await _addressService.GetAddressAsync(createAddress); + var @event = new Event { Name = name, @@ -43,7 +46,7 @@ public async Task> CreateNewEventAsync(string name, string descri StartDate = startDateParsed, EndDate = endDateParsed, MinimumAge = minimumAge, - Address = Models.Address.FromDto(address), + Address = address.Value!, Organizer = organizerResult.Value!, EventStatus = eventStatus }; diff --git a/TickAPI/TickAPI/Program.cs b/TickAPI/TickAPI/Program.cs index f6f3d13..c5eb0f3 100644 --- a/TickAPI/TickAPI/Program.cs +++ b/TickAPI/TickAPI/Program.cs @@ -84,6 +84,10 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); +// Add address services. +builder.Services.AddScoped(); +builder.Services.AddScoped(); + // Add organizer services. builder.Services.AddScoped(); builder.Services.AddScoped(); From 198927099a6f98add98c6a08f23f352b2f1a2caa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Fri, 28 Mar 2025 13:56:51 +0100 Subject: [PATCH 087/128] Change StartDate and endDate in addressDTo from string to DateTime --- .../Controllers/EventControllerTests.cs | 4 ++-- .../Events/Services/EventServicesTests.cs | 4 ++-- .../Events/Abstractions/IEventService.cs | 4 ++-- .../Events/DTOs/Request/CreateEventDto.cs | 4 ++-- .../TickAPI/Events/Services/EventService.cs | 21 +++++++++---------- 5 files changed, 18 insertions(+), 19 deletions(-) diff --git a/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs b/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs index 943d5a2..d3d9d11 100644 --- a/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs +++ b/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs @@ -23,8 +23,8 @@ public async Task CreateEvent_WheneDataIsValid_ShouldReturnSuccess() string name = "Concert"; string description = "Description of a concert"; - string startDate = "01.05.2025"; - string endDate = "01.06.2025"; + DateTime startDate = new DateTime(2025, 5, 1); + DateTime endDate = new DateTime(2025, 6, 1); uint? minimumAge = 18; string email = "123@mail.com"; EventStatus eventStatus = EventStatus.TicketsAvailable; diff --git a/TickAPI/TickAPI.Tests/Events/Services/EventServicesTests.cs b/TickAPI/TickAPI.Tests/Events/Services/EventServicesTests.cs index 24fdec6..e39523e 100644 --- a/TickAPI/TickAPI.Tests/Events/Services/EventServicesTests.cs +++ b/TickAPI/TickAPI.Tests/Events/Services/EventServicesTests.cs @@ -19,8 +19,8 @@ public async Task CreateNewEventAsync_WhenEventDataIsValid_ShouldReturnNewEvent( // arrange string name = "Concert"; string description = "Description of a concert"; - string startDate = "01.05.2025"; - string endDate = "01.06.2025"; + DateTime startDate = new DateTime(2025, 5, 1); + DateTime endDate = new DateTime(2025, 6, 1); uint? minimumAge = 18; string organizerEmail = "123@mail.com"; EventStatus eventStatus = EventStatus.TicketsAvailable; diff --git a/TickAPI/TickAPI/Events/Abstractions/IEventService.cs b/TickAPI/TickAPI/Events/Abstractions/IEventService.cs index ccf8a91..1a57e78 100644 --- a/TickAPI/TickAPI/Events/Abstractions/IEventService.cs +++ b/TickAPI/TickAPI/Events/Abstractions/IEventService.cs @@ -6,6 +6,6 @@ namespace TickAPI.Events.Abstractions; public interface IEventService { - public Task> CreateNewEventAsync(string name, string description, string startDate, - string endDate, uint? minimumAge, CreateAddressDto createAddress, EventStatus eventStatus, string organizerEmail); + public Task> CreateNewEventAsync(string name, string description, DateTime startDate, + DateTime endDate, uint? minimumAge, CreateAddressDto createAddress, EventStatus eventStatus, string organizerEmail); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/DTOs/Request/CreateEventDto.cs b/TickAPI/TickAPI/Events/DTOs/Request/CreateEventDto.cs index 16d8044..2b98c19 100644 --- a/TickAPI/TickAPI/Events/DTOs/Request/CreateEventDto.cs +++ b/TickAPI/TickAPI/Events/DTOs/Request/CreateEventDto.cs @@ -6,8 +6,8 @@ public record CreateEventDto ( string Name, string Description, - string StartDate, - string EndDate, + DateTime StartDate, + DateTime EndDate, uint? MinimumAge, EventStatus EventStatus, CreateAddressDto CreateAddress diff --git a/TickAPI/TickAPI/Events/Services/EventService.cs b/TickAPI/TickAPI/Events/Services/EventService.cs index 9e3b9ff..d9e6243 100644 --- a/TickAPI/TickAPI/Events/Services/EventService.cs +++ b/TickAPI/TickAPI/Events/Services/EventService.cs @@ -20,31 +20,30 @@ public EventService(IEventRepository eventRepository, IOrganizerService organize _addressService = addressService; } - public async Task> CreateNewEventAsync(string name, string description, string startDate, string endDate, uint? minimumAge, CreateAddressDto createAddress, EventStatus eventStatus, string organizerEmail) + public async Task> CreateNewEventAsync(string name, string description, DateTime startDate, DateTime endDate, uint? minimumAge, CreateAddressDto createAddress, EventStatus eventStatus, string organizerEmail) { var organizerResult = await _organizerService.GetOrganizerByEmailAsync(organizerEmail); if (!organizerResult.IsSuccess) return Result.PropagateError(organizerResult); - - if (!DateTime.TryParse(startDate, new System.Globalization.CultureInfo("fr-FR"), - System.Globalization.DateTimeStyles.None, out DateTime startDateParsed)) - return Result.Failure(StatusCodes.Status400BadRequest, "Invalid start date format"); - if (!DateTime.TryParse(endDate, new System.Globalization.CultureInfo("fr-FR"), - System.Globalization.DateTimeStyles.None, out DateTime endDateParsed)) - return Result.Failure(StatusCodes.Status400BadRequest, "Invalid end date format"); - if (endDateParsed < startDateParsed) + if (endDate < startDate) return Result.Failure(StatusCodes.Status400BadRequest, "End date should be after start date"); + if (startDate < DateTime.Now) + return Result.Failure(StatusCodes.Status400BadRequest, "Start date is in the past"); + + if (endDate < DateTime.Now) + return Result.Failure(StatusCodes.Status400BadRequest, "End date is in the past"); + var address = await _addressService.GetAddressAsync(createAddress); var @event = new Event { Name = name, Description = description, - StartDate = startDateParsed, - EndDate = endDateParsed, + StartDate = startDate, + EndDate = endDate, MinimumAge = minimumAge, Address = address.Value!, Organizer = organizerResult.Value!, From e336e3e36e49eb2681818b8d2f7470ae633b93b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Fri, 28 Mar 2025 14:51:17 +0100 Subject: [PATCH 088/128] Restructure folders --- .../Events/Controllers/EventControllerTests.cs | 1 + .../Events/Services/EventServicesTests.cs | 3 +++ .../Addresses/Abstractions/IAddressRepository.cs | 10 ++++++++++ .../TickAPI/Addresses/Abstractions/IAddressService.cs | 10 ++++++++++ .../DTOs/Request/CreateAddressDto.cs | 2 +- .../TickAPI/{Events => Addresses}/Models/Address.cs | 2 +- .../Repositories/AddressRepository.cs | 9 +++++---- .../{Events => Addresses}/Services/AddressService.cs | 5 ++++- .../Common/TickApiDbContext/TickApiDbContext.cs | 1 + .../TickAPI/Events/Abstractions/IAddressRepository.cs | 10 ---------- TickAPI/TickAPI/Events/Abstractions/IAddressService.cs | 10 ---------- TickAPI/TickAPI/Events/Abstractions/IEventService.cs | 3 ++- TickAPI/TickAPI/Events/DTOs/Request/CreateEventDto.cs | 1 + TickAPI/TickAPI/Events/Models/Event.cs | 3 ++- TickAPI/TickAPI/Events/Services/EventService.cs | 4 +++- TickAPI/TickAPI/Program.cs | 3 +++ TickAPI/TickAPI/TickAPI.csproj | 8 ++++++++ 17 files changed, 55 insertions(+), 30 deletions(-) create mode 100644 TickAPI/TickAPI/Addresses/Abstractions/IAddressRepository.cs create mode 100644 TickAPI/TickAPI/Addresses/Abstractions/IAddressService.cs rename TickAPI/TickAPI/{Events => Addresses}/DTOs/Request/CreateAddressDto.cs (82%) rename TickAPI/TickAPI/{Events => Addresses}/Models/Address.cs (89%) rename TickAPI/TickAPI/{Events => Addresses}/Repositories/AddressRepository.cs (87%) rename TickAPI/TickAPI/{Events => Addresses}/Services/AddressService.cs (88%) delete mode 100644 TickAPI/TickAPI/Events/Abstractions/IAddressRepository.cs delete mode 100644 TickAPI/TickAPI/Events/Abstractions/IAddressService.cs diff --git a/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs b/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs index d3d9d11..ed6081c 100644 --- a/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs +++ b/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs @@ -4,6 +4,7 @@ using Moq; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using TickAPI.Addresses.DTOs.Request; using TickAPI.Events.Controllers; using TickAPI.Events.Abstractions; using TickAPI.Common.Results; diff --git a/TickAPI/TickAPI.Tests/Events/Services/EventServicesTests.cs b/TickAPI/TickAPI.Tests/Events/Services/EventServicesTests.cs index e39523e..f512d66 100644 --- a/TickAPI/TickAPI.Tests/Events/Services/EventServicesTests.cs +++ b/TickAPI/TickAPI.Tests/Events/Services/EventServicesTests.cs @@ -2,6 +2,9 @@ using TickAPI.Events.Abstractions; using TickAPI.Events.DTOs.Request; using Moq; +using TickAPI.Addresses.Abstractions; +using TickAPI.Addresses.DTOs.Request; +using TickAPI.Addresses.Models; using TickAPI.Events.Models; using TickAPI.Organizers.Abstractions; using TickAPI.Organizers.Models; diff --git a/TickAPI/TickAPI/Addresses/Abstractions/IAddressRepository.cs b/TickAPI/TickAPI/Addresses/Abstractions/IAddressRepository.cs new file mode 100644 index 0000000..6867158 --- /dev/null +++ b/TickAPI/TickAPI/Addresses/Abstractions/IAddressRepository.cs @@ -0,0 +1,10 @@ +using TickAPI.Addresses.DTOs.Request; +using TickAPI.Common.Results.Generic; +using TickAPI.Addresses.Models; + +namespace TickAPI.Addresses.Abstractions; + +public interface IAddressRepository +{ + public Task> GetAddressAsync(CreateAddressDto createAddress); +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Addresses/Abstractions/IAddressService.cs b/TickAPI/TickAPI/Addresses/Abstractions/IAddressService.cs new file mode 100644 index 0000000..7063546 --- /dev/null +++ b/TickAPI/TickAPI/Addresses/Abstractions/IAddressService.cs @@ -0,0 +1,10 @@ +using TickAPI.Addresses.DTOs.Request; +using TickAPI.Common.Results.Generic; +using TickAPI.Addresses.Models; + +namespace TickAPI.Addresses.Abstractions; + +public interface IAddressService +{ + public Task> GetAddressAsync(CreateAddressDto createAddress); +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/DTOs/Request/CreateAddressDto.cs b/TickAPI/TickAPI/Addresses/DTOs/Request/CreateAddressDto.cs similarity index 82% rename from TickAPI/TickAPI/Events/DTOs/Request/CreateAddressDto.cs rename to TickAPI/TickAPI/Addresses/DTOs/Request/CreateAddressDto.cs index e06d179..8aa9f4c 100644 --- a/TickAPI/TickAPI/Events/DTOs/Request/CreateAddressDto.cs +++ b/TickAPI/TickAPI/Addresses/DTOs/Request/CreateAddressDto.cs @@ -1,6 +1,6 @@ using TickAPI.Events.Models; -namespace TickAPI.Events.DTOs.Request; +namespace TickAPI.Addresses.DTOs.Request; public record CreateAddressDto( diff --git a/TickAPI/TickAPI/Events/Models/Address.cs b/TickAPI/TickAPI/Addresses/Models/Address.cs similarity index 89% rename from TickAPI/TickAPI/Events/Models/Address.cs rename to TickAPI/TickAPI/Addresses/Models/Address.cs index b38931c..1aca9d9 100644 --- a/TickAPI/TickAPI/Events/Models/Address.cs +++ b/TickAPI/TickAPI/Addresses/Models/Address.cs @@ -1,4 +1,4 @@ -namespace TickAPI.Events.Models; +namespace TickAPI.Addresses.Models; using TickAPI.Events.DTOs.Request; public class Address { diff --git a/TickAPI/TickAPI/Events/Repositories/AddressRepository.cs b/TickAPI/TickAPI/Addresses/Repositories/AddressRepository.cs similarity index 87% rename from TickAPI/TickAPI/Events/Repositories/AddressRepository.cs rename to TickAPI/TickAPI/Addresses/Repositories/AddressRepository.cs index 6c2725f..e976ab6 100644 --- a/TickAPI/TickAPI/Events/Repositories/AddressRepository.cs +++ b/TickAPI/TickAPI/Addresses/Repositories/AddressRepository.cs @@ -1,11 +1,12 @@ using Microsoft.EntityFrameworkCore; using TickAPI.Common.Results.Generic; using TickAPI.Common.TickApiDbContext; -using TickAPI.Events.Abstractions; -using TickAPI.Events.DTOs.Request; -using TickAPI.Events.Models; +using TickAPI.Addresses.Abstractions; +using TickAPI.Addresses.DTOs.Request; +using TickAPI.Addresses.Models; -namespace TickAPI.Events.Repositories; + +namespace TickAPI.Addresses.Repositories; public class AddressRepository : IAddressRepository { diff --git a/TickAPI/TickAPI/Events/Services/AddressService.cs b/TickAPI/TickAPI/Addresses/Services/AddressService.cs similarity index 88% rename from TickAPI/TickAPI/Events/Services/AddressService.cs rename to TickAPI/TickAPI/Addresses/Services/AddressService.cs index 93179c8..5197703 100644 --- a/TickAPI/TickAPI/Events/Services/AddressService.cs +++ b/TickAPI/TickAPI/Addresses/Services/AddressService.cs @@ -3,8 +3,11 @@ using TickAPI.Events.Abstractions; using TickAPI.Events.DTOs.Request; using TickAPI.Events.Models; +using TickAPI.Addresses.Abstractions; +using TickAPI.Addresses.DTOs.Request; +using TickAPI.Addresses.Models; -namespace TickAPI.Events.Services; +namespace TickAPI.Addresses.Services; public class AddressService : IAddressService { diff --git a/TickAPI/TickAPI/Common/TickApiDbContext/TickApiDbContext.cs b/TickAPI/TickAPI/Common/TickApiDbContext/TickApiDbContext.cs index 51ad757..9837186 100644 --- a/TickAPI/TickAPI/Common/TickApiDbContext/TickApiDbContext.cs +++ b/TickAPI/TickAPI/Common/TickApiDbContext/TickApiDbContext.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore; +using TickAPI.Addresses.Models; using TickAPI.Admins.Models; using TickAPI.Categories.Models; using TickAPI.Customers.Models; diff --git a/TickAPI/TickAPI/Events/Abstractions/IAddressRepository.cs b/TickAPI/TickAPI/Events/Abstractions/IAddressRepository.cs deleted file mode 100644 index dba9625..0000000 --- a/TickAPI/TickAPI/Events/Abstractions/IAddressRepository.cs +++ /dev/null @@ -1,10 +0,0 @@ -using TickAPI.Common.Results.Generic; -using TickAPI.Events.DTOs.Request; -using TickAPI.Events.Models; - -namespace TickAPI.Events.Abstractions; - -public interface IAddressRepository -{ - public Task> GetAddressAsync(CreateAddressDto createAddress); -} \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/Abstractions/IAddressService.cs b/TickAPI/TickAPI/Events/Abstractions/IAddressService.cs deleted file mode 100644 index 5b2313c..0000000 --- a/TickAPI/TickAPI/Events/Abstractions/IAddressService.cs +++ /dev/null @@ -1,10 +0,0 @@ -using TickAPI.Common.Results.Generic; -using TickAPI.Events.DTOs.Request; -using TickAPI.Events.Models; - -namespace TickAPI.Events.Abstractions; - -public interface IAddressService -{ - public Task> GetAddressAsync(CreateAddressDto createAddress); -} \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/Abstractions/IEventService.cs b/TickAPI/TickAPI/Events/Abstractions/IEventService.cs index 1a57e78..1a2e531 100644 --- a/TickAPI/TickAPI/Events/Abstractions/IEventService.cs +++ b/TickAPI/TickAPI/Events/Abstractions/IEventService.cs @@ -1,4 +1,5 @@ -using TickAPI.Events.Models; +using TickAPI.Addresses.DTOs.Request; +using TickAPI.Events.Models; using TickAPI.Common.Results.Generic; using TickAPI.Events.DTOs.Request; diff --git a/TickAPI/TickAPI/Events/DTOs/Request/CreateEventDto.cs b/TickAPI/TickAPI/Events/DTOs/Request/CreateEventDto.cs index 2b98c19..1078587 100644 --- a/TickAPI/TickAPI/Events/DTOs/Request/CreateEventDto.cs +++ b/TickAPI/TickAPI/Events/DTOs/Request/CreateEventDto.cs @@ -1,4 +1,5 @@ using TickAPI.Events.Models; +using TickAPI.Addresses.DTOs.Request; namespace TickAPI.Events.DTOs.Request; diff --git a/TickAPI/TickAPI/Events/Models/Event.cs b/TickAPI/TickAPI/Events/Models/Event.cs index c16b37f..50ca434 100644 --- a/TickAPI/TickAPI/Events/Models/Event.cs +++ b/TickAPI/TickAPI/Events/Models/Event.cs @@ -1,4 +1,5 @@ -using TickAPI.Organizers.Models; +using TickAPI.Addresses.Models; +using TickAPI.Organizers.Models; using TickAPI.Categories.Models; using TickAPI.TicketTypes.Models; diff --git a/TickAPI/TickAPI/Events/Services/EventService.cs b/TickAPI/TickAPI/Events/Services/EventService.cs index d9e6243..862d907 100644 --- a/TickAPI/TickAPI/Events/Services/EventService.cs +++ b/TickAPI/TickAPI/Events/Services/EventService.cs @@ -1,4 +1,6 @@ -using TickAPI.Common.Time.Abstractions; +using TickAPI.Addresses.Abstractions; +using TickAPI.Addresses.DTOs.Request; +using TickAPI.Common.Time.Abstractions; using TickAPI.Events.Abstractions; using TickAPI.Events.Models; using TickAPI.Common.Results.Generic; diff --git a/TickAPI/TickAPI/Program.cs b/TickAPI/TickAPI/Program.cs index c5eb0f3..2b0ffca 100644 --- a/TickAPI/TickAPI/Program.cs +++ b/TickAPI/TickAPI/Program.cs @@ -27,6 +27,9 @@ using TickAPI.Tickets.Abstractions; using TickAPI.Tickets.Repositories; using TickAPI.Tickets.Services; +using TickAPI.Addresses.Abstractions; +using TickAPI.Addresses.Repositories; +using TickAPI.Addresses.Services; // Builder constants const string allowClientPolicyName = "AllowClient"; diff --git a/TickAPI/TickAPI/TickAPI.csproj b/TickAPI/TickAPI/TickAPI.csproj index d89ca81..b45f1be 100644 --- a/TickAPI/TickAPI/TickAPI.csproj +++ b/TickAPI/TickAPI/TickAPI.csproj @@ -21,4 +21,12 @@ + + + + + + + + From 64b86307582c743450d29dfce7fda850dc79d890 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Sat, 29 Mar 2025 17:06:39 +0100 Subject: [PATCH 089/128] minor fixes sggested by kubapoke --- .../Controllers/EventControllerTests.cs | 40 ++++- .../Events/Services/EventServiceTests.cs | 154 ++++++++++++++++++ .../Events/Services/EventServicesTests.cs | 72 -------- .../Addresses/Abstractions/IAddressService.cs | 2 +- .../Addresses/Services/AddressService.cs | 2 +- .../TickAPI/Events/Services/EventService.cs | 10 +- TickAPI/TickAPI/TickAPI.csproj | 9 +- 7 files changed, 201 insertions(+), 88 deletions(-) create mode 100644 TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs delete mode 100644 TickAPI/TickAPI.Tests/Events/Services/EventServicesTests.cs diff --git a/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs b/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs index ed6081c..bde0864 100644 --- a/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs +++ b/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs @@ -17,7 +17,7 @@ namespace TickAPI.Tests.Events.Controllers; public class EventControllerTests { [Fact] - public async Task CreateEvent_WheneDataIsValid_ShouldReturnSuccess() + public async Task CreateEvent_WhenDataIsValid_ShouldReturnSuccess() { //arrange @@ -64,4 +64,42 @@ public async Task CreateEvent_WheneDataIsValid_ShouldReturnSuccess() Assert.Equal("Event created succesfully", objectResult.Value); } + + [Fact] + public async Task CreateEvent_WhenMissingEmailClaims_ShouldReturnBadRequest() + { + + //arrange + string name = "Concert"; + string description = "Description of a concert"; + DateTime startDate = new DateTime(2025, 5, 1); + DateTime endDate = new DateTime(2025, 6, 1); + uint? minimumAge = 18; + string email = "123@mail.com"; + EventStatus eventStatus = EventStatus.TicketsAvailable; + CreateAddressDto createAddress = new CreateAddressDto("United States", "New York", "Main st", 20, null, "00-000"); + + var eventServiceMock = new Mock(); + + var sut = new EventController(eventServiceMock.Object); + + sut.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity()) + } + }; + + // act + var res = await sut.CreateEvent(new CreateEventDto(name, description, startDate, endDate, minimumAge, eventStatus, createAddress)); + + // assert + + var result = Assert.IsType>(res); + var objectResult = Assert.IsType(result.Result); + Assert.Equal(StatusCodes.Status400BadRequest, objectResult.StatusCode); + Assert.Equal("missing email claim", objectResult.Value); + + } } \ No newline at end of file diff --git a/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs b/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs new file mode 100644 index 0000000..939104b --- /dev/null +++ b/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs @@ -0,0 +1,154 @@ +using System.Runtime.CompilerServices; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using TickAPI.Events.Abstractions; +using TickAPI.Events.DTOs.Request; +using Moq; +using TickAPI.Addresses.Abstractions; +using TickAPI.Addresses.DTOs.Request; +using TickAPI.Addresses.Models; +using TickAPI.Events.Models; +using TickAPI.Organizers.Abstractions; +using TickAPI.Organizers.Models; +using TickAPI.Common.Results.Generic; +using TickAPI.Common.Time.Abstractions; +using TickAPI.Events.DTOs.Response; +using TickAPI.Events.Services; + +namespace TickAPI.Tests.Events.Services; + +public class EventServiceTests +{ + [Fact] + + public async Task CreateNewEventAsync_WhenEventDataIsValid_ShouldReturnNewEvent() + { + // arrange + string name = "Concert"; + string description = "Description of a concert"; + DateTime startDate = new DateTime(2025, 5, 1); + DateTime endDate = new DateTime(2025, 6, 1); + uint? minimumAge = 18; + string organizerEmail = "123@mail.com"; + EventStatus eventStatus = EventStatus.TicketsAvailable; + Guid id = Guid.NewGuid(); + CreateAddressDto createAddress = new CreateAddressDto("United States", "New York", "Main st", 20, null, "00-000"); + + var eventRepositoryMock = new Mock(); + eventRepositoryMock.Setup(e => e.AddNewEventAsync(It.IsAny())).Callback(e => e.Id = id) + .Returns(Task.CompletedTask); + + var organizerServiceMock = new Mock(); + organizerServiceMock + .Setup(m => m.GetOrganizerByEmailAsync(organizerEmail)) + .ReturnsAsync(Result.Success(new Organizer { Email = organizerEmail, IsVerified = true })); + + var addressServiceMock = new Mock(); + addressServiceMock.Setup(m => m.GetOrCreateAddressAsync(createAddress)).ReturnsAsync( + Result
.Success(new Address + { + City = createAddress.City, + Country = createAddress.Country, + FlatNumber = createAddress.FlatNumber, + HouseNumber = createAddress.HouseNumber, + PostalCode = createAddress.PostalCode, + Street = createAddress.Street, + }) + ); + + var dateTimeServiceMock = new Mock(); + dateTimeServiceMock.Setup(m => m.GetCurrentDateTime()).Returns(new DateTime(2003, 7, 11)); + + var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, dateTimeServiceMock.Object); + // act + + var result = await sut.CreateNewEventAsync(name, description, startDate, endDate, minimumAge, createAddress, eventStatus, organizerEmail); + + // assert + + + Assert.True(result.IsSuccess); + Assert.Equal(new DateTime(2025, 5, 1), result.Value!.StartDate); + Assert.Equal(new DateTime(2025, 6, 1), result.Value!.EndDate); + Assert.Equal(name, result.Value!.Name); + Assert.Equal(description, result.Value!.Description); + Assert.Equal(eventStatus, result.Value!.EventStatus); + Assert.Equal(id, result.Value!.Id); + Assert.Equal(organizerEmail, result.Value!.Organizer.Email); + } + + [Fact] + public async Task CreateNewEventAsync_WhenEndDateIsBeforeStartDate_ShouldReturnBadRequest() + { + // arrange + string name = "Concert"; + string description = "Description of a concert"; + DateTime startDate = new DateTime(2025, 8, 1); + DateTime endDate = new DateTime(2025, 6, 1); + uint? minimumAge = 18; + string organizerEmail = "123@mail.com"; + EventStatus eventStatus = EventStatus.TicketsAvailable; + Guid id = Guid.NewGuid(); + CreateAddressDto createAddress = new CreateAddressDto("United States", "New York", "Main st", 20, null, "00-000"); + + var eventRepositoryMock = new Mock(); + + var organizerServiceMock = new Mock(); + organizerServiceMock + .Setup(m => m.GetOrganizerByEmailAsync(organizerEmail)) + .ReturnsAsync(Result.Success(new Organizer { Email = organizerEmail, IsVerified = true })); + + var addressServiceMock = new Mock(); + + var dateTimeServiceMock = new Mock(); + + var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, dateTimeServiceMock.Object); + // act + + var res = await sut.CreateNewEventAsync(name, description, startDate, endDate, minimumAge, createAddress, eventStatus, organizerEmail); + + // assert + Assert.False(res.IsSuccess); + Assert.Equal(StatusCodes.Status400BadRequest, res.StatusCode); + Assert.Equal("End date should be after start date", res.ErrorMsg); + } + + [Fact] + public async Task CreateNewEventAsync_WhenStartDateIsBeforeNow_ShouldReturnBadRequest() + { + // arrange + string name = "Concert"; + string description = "Description of a concert"; + DateTime startDate = new DateTime(2025, 5, 1); + DateTime endDate = new DateTime(2025, 6, 1); + uint? minimumAge = 18; + string organizerEmail = "123@mail.com"; + EventStatus eventStatus = EventStatus.TicketsAvailable; + Guid id = Guid.NewGuid(); + CreateAddressDto createAddress = new CreateAddressDto("United States", "New York", "Main st", 20, null, "00-000"); + + var eventRepositoryMock = new Mock(); + + var organizerServiceMock = new Mock(); + organizerServiceMock + .Setup(m => m.GetOrganizerByEmailAsync(organizerEmail)) + .ReturnsAsync(Result.Success(new Organizer { Email = organizerEmail, IsVerified = true })); + + var addressServiceMock = new Mock(); + + var dateTimeServiceMock = new Mock(); + dateTimeServiceMock.Setup(m => m.GetCurrentDateTime()).Returns(new DateTime(2025, 5, 11)); + + var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, dateTimeServiceMock.Object); + // act + + var res = await sut.CreateNewEventAsync(name, description, startDate, endDate, minimumAge, createAddress, eventStatus, organizerEmail); + + // assert + Assert.False(res.IsSuccess); + Assert.Equal(StatusCodes.Status400BadRequest, res.StatusCode); + Assert.Equal("Start date is in the past", res.ErrorMsg); + } + + +} \ No newline at end of file diff --git a/TickAPI/TickAPI.Tests/Events/Services/EventServicesTests.cs b/TickAPI/TickAPI.Tests/Events/Services/EventServicesTests.cs deleted file mode 100644 index f512d66..0000000 --- a/TickAPI/TickAPI.Tests/Events/Services/EventServicesTests.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System.Runtime.CompilerServices; -using TickAPI.Events.Abstractions; -using TickAPI.Events.DTOs.Request; -using Moq; -using TickAPI.Addresses.Abstractions; -using TickAPI.Addresses.DTOs.Request; -using TickAPI.Addresses.Models; -using TickAPI.Events.Models; -using TickAPI.Organizers.Abstractions; -using TickAPI.Organizers.Models; -using TickAPI.Common.Results.Generic; -using TickAPI.Events.Services; - -namespace TickAPI.Tests.Events.Services; - -public class EventServicesTests -{ - [Fact] - - public async Task CreateNewEventAsync_WhenEventDataIsValid_ShouldReturnNewEvent() - { - // arrange - string name = "Concert"; - string description = "Description of a concert"; - DateTime startDate = new DateTime(2025, 5, 1); - DateTime endDate = new DateTime(2025, 6, 1); - uint? minimumAge = 18; - string organizerEmail = "123@mail.com"; - EventStatus eventStatus = EventStatus.TicketsAvailable; - Guid id = Guid.NewGuid(); - CreateAddressDto createAddress = new CreateAddressDto("United States", "New York", "Main st", 20, null, "00-000"); - - var eventRepositoryMock = new Mock(); - eventRepositoryMock.Setup(e => e.AddNewEventAsync(It.IsAny())).Callback(e => e.Id = id) - .Returns(Task.CompletedTask); - - var organizerServiceMock = new Mock(); - organizerServiceMock - .Setup(m => m.GetOrganizerByEmailAsync(organizerEmail)) - .ReturnsAsync(Result.Success(new Organizer { Email = organizerEmail, IsVerified = true })); - - var addressServiceMock = new Mock(); - addressServiceMock.Setup(m => m.GetAddressAsync(createAddress)).ReturnsAsync( - Result
.Success(new Address - { - City = createAddress.City, - Country = createAddress.Country, - FlatNumber = createAddress.FlatNumber, - HouseNumber = createAddress.HouseNumber, - PostalCode = createAddress.PostalCode, - Street = createAddress.Street, - }) - ); - - var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object); - // act - - var result = await sut.CreateNewEventAsync(name, description, startDate, endDate, minimumAge, createAddress, eventStatus, organizerEmail); - - // assert - - - Assert.True(result.IsSuccess); - Assert.Equal(new DateTime(2025, 5, 1), result.Value!.StartDate); - Assert.Equal(new DateTime(2025, 6, 1), result.Value!.EndDate); - Assert.Equal(name, result.Value!.Name); - Assert.Equal(description, result.Value!.Description); - Assert.Equal(eventStatus, result.Value!.EventStatus); - Assert.Equal(id, result.Value!.Id); - Assert.Equal(organizerEmail, result.Value!.Organizer.Email); - } -} \ No newline at end of file diff --git a/TickAPI/TickAPI/Addresses/Abstractions/IAddressService.cs b/TickAPI/TickAPI/Addresses/Abstractions/IAddressService.cs index 7063546..cdac615 100644 --- a/TickAPI/TickAPI/Addresses/Abstractions/IAddressService.cs +++ b/TickAPI/TickAPI/Addresses/Abstractions/IAddressService.cs @@ -6,5 +6,5 @@ namespace TickAPI.Addresses.Abstractions; public interface IAddressService { - public Task> GetAddressAsync(CreateAddressDto createAddress); + public Task> GetOrCreateAddressAsync(CreateAddressDto createAddress); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Addresses/Services/AddressService.cs b/TickAPI/TickAPI/Addresses/Services/AddressService.cs index 5197703..2858712 100644 --- a/TickAPI/TickAPI/Addresses/Services/AddressService.cs +++ b/TickAPI/TickAPI/Addresses/Services/AddressService.cs @@ -18,7 +18,7 @@ public AddressService(IAddressRepository addressRepository) { _addressRepository = addressRepository; } - public async Task> GetAddressAsync(CreateAddressDto createAddress) + public async Task> GetOrCreateAddressAsync(CreateAddressDto createAddress) { var result = await _addressRepository.GetAddressAsync(createAddress); if (result.IsSuccess) diff --git a/TickAPI/TickAPI/Events/Services/EventService.cs b/TickAPI/TickAPI/Events/Services/EventService.cs index 862d907..7f38590 100644 --- a/TickAPI/TickAPI/Events/Services/EventService.cs +++ b/TickAPI/TickAPI/Events/Services/EventService.cs @@ -14,12 +14,14 @@ public class EventService : IEventService private readonly IOrganizerService _organizerService; private readonly IEventRepository _eventRepository; private readonly IAddressService _addressService; + private readonly IDateTimeService _dateTimeService; - public EventService(IEventRepository eventRepository, IOrganizerService organizerService, IAddressService addressService) + public EventService(IEventRepository eventRepository, IOrganizerService organizerService, IAddressService addressService, IDateTimeService dateTimeService) { _eventRepository = eventRepository; _organizerService = organizerService; _addressService = addressService; + _dateTimeService = dateTimeService; } public async Task> CreateNewEventAsync(string name, string description, DateTime startDate, DateTime endDate, uint? minimumAge, CreateAddressDto createAddress, EventStatus eventStatus, string organizerEmail) @@ -32,13 +34,11 @@ public async Task> CreateNewEventAsync(string name, string descri if (endDate < startDate) return Result.Failure(StatusCodes.Status400BadRequest, "End date should be after start date"); - if (startDate < DateTime.Now) + if (startDate < _dateTimeService.GetCurrentDateTime()) return Result.Failure(StatusCodes.Status400BadRequest, "Start date is in the past"); - if (endDate < DateTime.Now) - return Result.Failure(StatusCodes.Status400BadRequest, "End date is in the past"); - var address = await _addressService.GetAddressAsync(createAddress); + var address = await _addressService.GetOrCreateAddressAsync(createAddress); var @event = new Event { diff --git a/TickAPI/TickAPI/TickAPI.csproj b/TickAPI/TickAPI/TickAPI.csproj index b45f1be..1e131f6 100644 --- a/TickAPI/TickAPI/TickAPI.csproj +++ b/TickAPI/TickAPI/TickAPI.csproj @@ -20,13 +20,6 @@ - - - - - - - - + From 466324fb47b84709f0621e98afc82c9b548f0a19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Sun, 30 Mar 2025 15:21:15 +0200 Subject: [PATCH 090/128] Add service repository and controller classes to categories folder --- .../Abstractions/ICategoryRepository.cs | 9 ++++++++ .../Abstractions/ICategoryService.cs | 9 ++++++++ .../Controllers/CategoryController.cs | 16 ++++++++++++++ .../Respositories/CategoryRepository.cs | 21 +++++++++++++++++++ .../Categories/Services/CategoryService.cs | 20 ++++++++++++++++++ 5 files changed, 75 insertions(+) create mode 100644 TickAPI/TickAPI/Categories/Abstractions/ICategoryRepository.cs create mode 100644 TickAPI/TickAPI/Categories/Abstractions/ICategoryService.cs create mode 100644 TickAPI/TickAPI/Categories/Controllers/CategoryController.cs create mode 100644 TickAPI/TickAPI/Categories/Respositories/CategoryRepository.cs create mode 100644 TickAPI/TickAPI/Categories/Services/CategoryService.cs diff --git a/TickAPI/TickAPI/Categories/Abstractions/ICategoryRepository.cs b/TickAPI/TickAPI/Categories/Abstractions/ICategoryRepository.cs new file mode 100644 index 0000000..1e5d8cb --- /dev/null +++ b/TickAPI/TickAPI/Categories/Abstractions/ICategoryRepository.cs @@ -0,0 +1,9 @@ +using TickAPI.Categories.Models; +using TickAPI.Common.Results.Generic; + +namespace TickAPI.Categories.Abstractions; + +public interface ICategoryRepository +{ + public Task>> GetCategoriesAsync(); +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Categories/Abstractions/ICategoryService.cs b/TickAPI/TickAPI/Categories/Abstractions/ICategoryService.cs new file mode 100644 index 0000000..dc74fc2 --- /dev/null +++ b/TickAPI/TickAPI/Categories/Abstractions/ICategoryService.cs @@ -0,0 +1,9 @@ +using TickAPI.Categories.Models; +using TickAPI.Common.Results.Generic; + +namespace TickAPI.Categories.Abstractions; + +public interface ICategoryService +{ + public Task>> GetCategoriesAsync(); +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Categories/Controllers/CategoryController.cs b/TickAPI/TickAPI/Categories/Controllers/CategoryController.cs new file mode 100644 index 0000000..3ffb38a --- /dev/null +++ b/TickAPI/TickAPI/Categories/Controllers/CategoryController.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Mvc; +using TickAPI.Categories.Models; + +namespace TickAPI.Categories.Controllers; + +[ApiController] +[Route("api/[controller]")] + +public class CategoryController : Controller +{ + [HttpPost("get-categories")] + public async Task> GetCategories() + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Categories/Respositories/CategoryRepository.cs b/TickAPI/TickAPI/Categories/Respositories/CategoryRepository.cs new file mode 100644 index 0000000..7bc9346 --- /dev/null +++ b/TickAPI/TickAPI/Categories/Respositories/CategoryRepository.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore; +using TickAPI.Categories.Abstractions; +using TickAPI.Categories.Models; +using TickAPI.Common.Results.Generic; +using TickAPI.Common.TickApiDbContext; +namespace TickAPI.Categories.Respositories; + +public class CategoryRepository : ICategoryRepository +{ + private readonly TickApiDbContext _tickApiDbContext; + + public CategoryRepository(TickApiDbContext tickApiDbContext) + { + _tickApiDbContext = tickApiDbContext; + } + + public async Task>> GetCategoriesAsync() + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Categories/Services/CategoryService.cs b/TickAPI/TickAPI/Categories/Services/CategoryService.cs new file mode 100644 index 0000000..3e20024 --- /dev/null +++ b/TickAPI/TickAPI/Categories/Services/CategoryService.cs @@ -0,0 +1,20 @@ +using TickAPI.Categories.Abstractions; +using TickAPI.Categories.Models; +using TickAPI.Common.Results.Generic; + +namespace TickAPI.Categories.Services; + +public class CategoryService : ICategoryRepository +{ + private readonly ICategoryRepository _categoryRepository; + + public CategoryService(ICategoryRepository categoryRepository) + { + _categoryRepository = categoryRepository; + } + + public Task>> GetCategoriesAsync() + { + throw new NotImplementedException(); + } +} \ No newline at end of file From 60544d2d6736d4ff2bf518e91141b44e6765b8c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Sun, 30 Mar 2025 15:22:40 +0200 Subject: [PATCH 091/128] Add missing files --- .../Categories/Controllers/CategoryController.cs | 13 +++++++++++++ .../TickAPI/Categories/Services/CategoryService.cs | 2 +- TickAPI/TickAPI/Program.cs | 7 +++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/TickAPI/TickAPI/Categories/Controllers/CategoryController.cs b/TickAPI/TickAPI/Categories/Controllers/CategoryController.cs index 3ffb38a..4037416 100644 --- a/TickAPI/TickAPI/Categories/Controllers/CategoryController.cs +++ b/TickAPI/TickAPI/Categories/Controllers/CategoryController.cs @@ -1,5 +1,9 @@ using Microsoft.AspNetCore.Mvc; +using TickAPI.Categories.Abstractions; using TickAPI.Categories.Models; +using TickAPI.Common.Auth.Attributes; +using TickAPI.Common.Auth.Enums; +using TickAPI.Customers.Abstractions; namespace TickAPI.Categories.Controllers; @@ -8,6 +12,15 @@ namespace TickAPI.Categories.Controllers; public class CategoryController : Controller { + + private readonly ICategoryService _categoryService; + + public CategoryController(ICategoryService categoryService) + { + _categoryService = categoryService; + } + + [AuthorizeWithPolicy(AuthPolicies.VerifiedOrganizerPolicy)] [HttpPost("get-categories")] public async Task> GetCategories() { diff --git a/TickAPI/TickAPI/Categories/Services/CategoryService.cs b/TickAPI/TickAPI/Categories/Services/CategoryService.cs index 3e20024..addb0de 100644 --- a/TickAPI/TickAPI/Categories/Services/CategoryService.cs +++ b/TickAPI/TickAPI/Categories/Services/CategoryService.cs @@ -4,7 +4,7 @@ namespace TickAPI.Categories.Services; -public class CategoryService : ICategoryRepository +public class CategoryService : ICategoryService { private readonly ICategoryRepository _categoryRepository; diff --git a/TickAPI/TickAPI/Program.cs b/TickAPI/TickAPI/Program.cs index 2b0ffca..d4110fa 100644 --- a/TickAPI/TickAPI/Program.cs +++ b/TickAPI/TickAPI/Program.cs @@ -30,6 +30,9 @@ using TickAPI.Addresses.Abstractions; using TickAPI.Addresses.Repositories; using TickAPI.Addresses.Services; +using TickAPI.Categories.Abstractions; +using TickAPI.Categories.Respositories; +using TickAPI.Categories.Services; // Builder constants const string allowClientPolicyName = "AllowClient"; @@ -99,6 +102,10 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); +// Add category services. +builder.Services.AddScoped(); +builder.Services.AddScoped(); + // Add common services. builder.Services.AddScoped(); builder.Services.AddScoped(); From a692167a1092d6ec6ef4fa2aa490eebbc5b114ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Sun, 30 Mar 2025 16:15:34 +0200 Subject: [PATCH 092/128] Get categories working without pagination --- .../Categories/Abstractions/ICategoryService.cs | 5 +++-- .../Categories/Controllers/CategoryController.cs | 14 ++++++++++++-- .../Categories/DTOs/Response/GetCategoriesDto.cs | 5 +++++ .../Categories/Respositories/CategoryRepository.cs | 4 +++- .../TickAPI/Categories/Services/CategoryService.cs | 12 ++++++++++-- TickAPI/TickAPI/TickAPI.csproj | 4 ++++ 6 files changed, 37 insertions(+), 7 deletions(-) create mode 100644 TickAPI/TickAPI/Categories/DTOs/Response/GetCategoriesDto.cs diff --git a/TickAPI/TickAPI/Categories/Abstractions/ICategoryService.cs b/TickAPI/TickAPI/Categories/Abstractions/ICategoryService.cs index dc74fc2..78318f1 100644 --- a/TickAPI/TickAPI/Categories/Abstractions/ICategoryService.cs +++ b/TickAPI/TickAPI/Categories/Abstractions/ICategoryService.cs @@ -1,9 +1,10 @@ -using TickAPI.Categories.Models; +using TickAPI.Categories.DTOs.Response; +using TickAPI.Categories.Models; using TickAPI.Common.Results.Generic; namespace TickAPI.Categories.Abstractions; public interface ICategoryService { - public Task>> GetCategoriesAsync(); + public Task>> GetCategoriesAsync(); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Categories/Controllers/CategoryController.cs b/TickAPI/TickAPI/Categories/Controllers/CategoryController.cs index 4037416..f41070a 100644 --- a/TickAPI/TickAPI/Categories/Controllers/CategoryController.cs +++ b/TickAPI/TickAPI/Categories/Controllers/CategoryController.cs @@ -1,8 +1,11 @@ using Microsoft.AspNetCore.Mvc; using TickAPI.Categories.Abstractions; +using TickAPI.Categories.DTOs.Response; using TickAPI.Categories.Models; using TickAPI.Common.Auth.Attributes; using TickAPI.Common.Auth.Enums; +using TickAPI.Common.Pagination.Abstractions; +using TickAPI.Common.Results.Generic; using TickAPI.Customers.Abstractions; namespace TickAPI.Categories.Controllers; @@ -14,6 +17,7 @@ public class CategoryController : Controller { private readonly ICategoryService _categoryService; + private readonly IPaginationService _paginationService; public CategoryController(ICategoryService categoryService) { @@ -22,8 +26,14 @@ public CategoryController(ICategoryService categoryService) [AuthorizeWithPolicy(AuthPolicies.VerifiedOrganizerPolicy)] [HttpPost("get-categories")] - public async Task> GetCategories() + public async Task>> GetCategories() { - throw new NotImplementedException(); + var res = await _categoryService.GetCategoriesAsync(); + if (!res.IsSuccess) + { + return StatusCode(StatusCodes.Status500InternalServerError, res.ErrorMsg); + } + + return Ok(res.Value); } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Categories/DTOs/Response/GetCategoriesDto.cs b/TickAPI/TickAPI/Categories/DTOs/Response/GetCategoriesDto.cs new file mode 100644 index 0000000..c7a5980 --- /dev/null +++ b/TickAPI/TickAPI/Categories/DTOs/Response/GetCategoriesDto.cs @@ -0,0 +1,5 @@ +namespace TickAPI.Categories.DTOs.Response; + +public record GetCategoriesDto( + string CategoryName +); \ No newline at end of file diff --git a/TickAPI/TickAPI/Categories/Respositories/CategoryRepository.cs b/TickAPI/TickAPI/Categories/Respositories/CategoryRepository.cs index 7bc9346..ddd9b2a 100644 --- a/TickAPI/TickAPI/Categories/Respositories/CategoryRepository.cs +++ b/TickAPI/TickAPI/Categories/Respositories/CategoryRepository.cs @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; using TickAPI.Categories.Abstractions; using TickAPI.Categories.Models; +using TickAPI.Common.Results; using TickAPI.Common.Results.Generic; using TickAPI.Common.TickApiDbContext; namespace TickAPI.Categories.Respositories; @@ -16,6 +17,7 @@ public CategoryRepository(TickApiDbContext tickApiDbContext) public async Task>> GetCategoriesAsync() { - throw new NotImplementedException(); + var list = await _tickApiDbContext.Categories.ToListAsync(); + return Result>.Success(list); } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Categories/Services/CategoryService.cs b/TickAPI/TickAPI/Categories/Services/CategoryService.cs index addb0de..2edd516 100644 --- a/TickAPI/TickAPI/Categories/Services/CategoryService.cs +++ b/TickAPI/TickAPI/Categories/Services/CategoryService.cs @@ -1,5 +1,7 @@ using TickAPI.Categories.Abstractions; +using TickAPI.Categories.DTOs.Response; using TickAPI.Categories.Models; +using TickAPI.Common.Results; using TickAPI.Common.Results.Generic; namespace TickAPI.Categories.Services; @@ -13,8 +15,14 @@ public CategoryService(ICategoryRepository categoryRepository) _categoryRepository = categoryRepository; } - public Task>> GetCategoriesAsync() + public async Task>> GetCategoriesAsync() { - throw new NotImplementedException(); + var res = await _categoryRepository.GetCategoriesAsync(); + List categories = new List(); + foreach (var category in res.Value!) + { + categories.Add(new GetCategoriesDto(category.CategoryName)); + } + return Result>.Success(categories); } } \ No newline at end of file diff --git a/TickAPI/TickAPI/TickAPI.csproj b/TickAPI/TickAPI/TickAPI.csproj index 1e131f6..39a857b 100644 --- a/TickAPI/TickAPI/TickAPI.csproj +++ b/TickAPI/TickAPI/TickAPI.csproj @@ -20,6 +20,10 @@ + + + + From a8345437741c497c61695ec3fa4c9167c1367911 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Sun, 30 Mar 2025 17:51:03 +0200 Subject: [PATCH 093/128] Get Categories with Pagination --- .../Abstractions/ICategoryRepository.cs | 2 +- .../Abstractions/ICategoryService.cs | 3 ++- .../Controllers/CategoryController.cs | 12 ++++------ .../DTOs/Response/GetCategoriesDto.cs | 4 +++- .../Respositories/CategoryRepository.cs | 5 ++--- .../Categories/Services/CategoryService.cs | 22 ++++++++++++++----- 6 files changed, 29 insertions(+), 19 deletions(-) diff --git a/TickAPI/TickAPI/Categories/Abstractions/ICategoryRepository.cs b/TickAPI/TickAPI/Categories/Abstractions/ICategoryRepository.cs index 1e5d8cb..8f998b0 100644 --- a/TickAPI/TickAPI/Categories/Abstractions/ICategoryRepository.cs +++ b/TickAPI/TickAPI/Categories/Abstractions/ICategoryRepository.cs @@ -5,5 +5,5 @@ namespace TickAPI.Categories.Abstractions; public interface ICategoryRepository { - public Task>> GetCategoriesAsync(); + public Task>> GetCategoriesAsync(); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Categories/Abstractions/ICategoryService.cs b/TickAPI/TickAPI/Categories/Abstractions/ICategoryService.cs index 78318f1..9eb60c1 100644 --- a/TickAPI/TickAPI/Categories/Abstractions/ICategoryService.cs +++ b/TickAPI/TickAPI/Categories/Abstractions/ICategoryService.cs @@ -1,10 +1,11 @@ using TickAPI.Categories.DTOs.Response; using TickAPI.Categories.Models; +using TickAPI.Common.Pagination.Responses; using TickAPI.Common.Results.Generic; namespace TickAPI.Categories.Abstractions; public interface ICategoryService { - public Task>> GetCategoriesAsync(); + public Task>> GetCategoriesAsync(int pageSize, int page); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Categories/Controllers/CategoryController.cs b/TickAPI/TickAPI/Categories/Controllers/CategoryController.cs index f41070a..88319ea 100644 --- a/TickAPI/TickAPI/Categories/Controllers/CategoryController.cs +++ b/TickAPI/TickAPI/Categories/Controllers/CategoryController.cs @@ -1,12 +1,10 @@ using Microsoft.AspNetCore.Mvc; using TickAPI.Categories.Abstractions; using TickAPI.Categories.DTOs.Response; -using TickAPI.Categories.Models; using TickAPI.Common.Auth.Attributes; using TickAPI.Common.Auth.Enums; -using TickAPI.Common.Pagination.Abstractions; -using TickAPI.Common.Results.Generic; -using TickAPI.Customers.Abstractions; +using TickAPI.Common.Pagination.Responses; + namespace TickAPI.Categories.Controllers; @@ -17,7 +15,6 @@ public class CategoryController : Controller { private readonly ICategoryService _categoryService; - private readonly IPaginationService _paginationService; public CategoryController(ICategoryService categoryService) { @@ -26,14 +23,13 @@ public CategoryController(ICategoryService categoryService) [AuthorizeWithPolicy(AuthPolicies.VerifiedOrganizerPolicy)] [HttpPost("get-categories")] - public async Task>> GetCategories() + public async Task>> GetCategories([FromQuery] int page, [FromQuery] int pageSize) { - var res = await _categoryService.GetCategoriesAsync(); + var res = await _categoryService.GetCategoriesAsync(pageSize, page); if (!res.IsSuccess) { return StatusCode(StatusCodes.Status500InternalServerError, res.ErrorMsg); } - return Ok(res.Value); } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Categories/DTOs/Response/GetCategoriesDto.cs b/TickAPI/TickAPI/Categories/DTOs/Response/GetCategoriesDto.cs index c7a5980..abbaea2 100644 --- a/TickAPI/TickAPI/Categories/DTOs/Response/GetCategoriesDto.cs +++ b/TickAPI/TickAPI/Categories/DTOs/Response/GetCategoriesDto.cs @@ -1,4 +1,6 @@ -namespace TickAPI.Categories.DTOs.Response; +using TickAPI.Common.Pagination.Responses; + +namespace TickAPI.Categories.DTOs.Response; public record GetCategoriesDto( string CategoryName diff --git a/TickAPI/TickAPI/Categories/Respositories/CategoryRepository.cs b/TickAPI/TickAPI/Categories/Respositories/CategoryRepository.cs index ddd9b2a..bcedbd2 100644 --- a/TickAPI/TickAPI/Categories/Respositories/CategoryRepository.cs +++ b/TickAPI/TickAPI/Categories/Respositories/CategoryRepository.cs @@ -1,7 +1,6 @@ using Microsoft.EntityFrameworkCore; using TickAPI.Categories.Abstractions; using TickAPI.Categories.Models; -using TickAPI.Common.Results; using TickAPI.Common.Results.Generic; using TickAPI.Common.TickApiDbContext; namespace TickAPI.Categories.Respositories; @@ -15,9 +14,9 @@ public CategoryRepository(TickApiDbContext tickApiDbContext) _tickApiDbContext = tickApiDbContext; } - public async Task>> GetCategoriesAsync() + public async Task>> GetCategoriesAsync() { var list = await _tickApiDbContext.Categories.ToListAsync(); - return Result>.Success(list); + return Result>.Success(list); } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Categories/Services/CategoryService.cs b/TickAPI/TickAPI/Categories/Services/CategoryService.cs index 2edd516..2be8597 100644 --- a/TickAPI/TickAPI/Categories/Services/CategoryService.cs +++ b/TickAPI/TickAPI/Categories/Services/CategoryService.cs @@ -1,7 +1,8 @@ using TickAPI.Categories.Abstractions; using TickAPI.Categories.DTOs.Response; using TickAPI.Categories.Models; -using TickAPI.Common.Results; +using TickAPI.Common.Pagination.Abstractions; +using TickAPI.Common.Pagination.Responses; using TickAPI.Common.Results.Generic; namespace TickAPI.Categories.Services; @@ -9,20 +10,31 @@ namespace TickAPI.Categories.Services; public class CategoryService : ICategoryService { private readonly ICategoryRepository _categoryRepository; + private readonly IPaginationService _paginationService; - public CategoryService(ICategoryRepository categoryRepository) + public CategoryService(ICategoryRepository categoryRepository, IPaginationService paginationService) { _categoryRepository = categoryRepository; + _paginationService = paginationService; } - public async Task>> GetCategoriesAsync() + public async Task>> GetCategoriesAsync(int pageSize, int page) { var res = await _categoryRepository.GetCategoriesAsync(); List categories = new List(); - foreach (var category in res.Value!) + var categoriesPaginated = _paginationService.Paginate(res.Value, pageSize, page); + if (!categoriesPaginated.IsSuccess) + { + return Result>.PropagateError(categoriesPaginated); + } + + foreach (var category in categoriesPaginated.Value.Data) { categories.Add(new GetCategoriesDto(category.CategoryName)); } - return Result>.Success(categories); + + return Result>.Success(new PaginatedData(categories, categoriesPaginated.Value.PageNumber + ,categoriesPaginated.Value.PageSize, categoriesPaginated.Value.HasNextPage, categoriesPaginated.Value.HasPreviousPage, + categoriesPaginated.Value.PaginationDetails)); } } \ No newline at end of file From 13a60f968fdcfbba802fa773782b7eaaff53a770 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Sun, 30 Mar 2025 18:25:19 +0200 Subject: [PATCH 094/128] Add tests --- .../Controllers/CategoryControllerTests.cs | 39 +++++++++++++++++ .../Services/CategoryServiceTests.cs | 42 +++++++++++++++++++ .../Controllers/CategoryController.cs | 2 +- 3 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 TickAPI/TickAPI.Tests/Categories/Controllers/CategoryControllerTests.cs create mode 100644 TickAPI/TickAPI.Tests/Categories/Services/CategoryServiceTests.cs diff --git a/TickAPI/TickAPI.Tests/Categories/Controllers/CategoryControllerTests.cs b/TickAPI/TickAPI.Tests/Categories/Controllers/CategoryControllerTests.cs new file mode 100644 index 0000000..88df29f --- /dev/null +++ b/TickAPI/TickAPI.Tests/Categories/Controllers/CategoryControllerTests.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.Mvc; +using Moq; +using TickAPI.Categories.Abstractions; +using TickAPI.Categories.Controllers; +using TickAPI.Categories.DTOs.Response; +using TickAPI.Common.Pagination.Responses; +using TickAPI.Common.Results; +using TickAPI.Common.Results.Generic; +using TickAPI.Events.DTOs.Response; + +namespace TickAPI.Tests.Categories.Controllers; + +public class CategoryControllerTests +{ + [Fact] + + public async Task GetCategories_WhenDataIsValid_ShouldReturnOk() + { + //arrange + int pageSize = 20; + int pageNumber = 0; + var categoryServiceMock = new Mock(); + categoryServiceMock.Setup(m => m.GetCategoriesAsync(pageSize, pageNumber)).ReturnsAsync( + Result>.Success(new PaginatedData(new List(), pageNumber, pageSize, true, true, + new PaginationDetails(0, 0)))); + + var sut = new CategoryController(categoryServiceMock.Object); + + //act + var res = await sut.GetCategories(pageSize, pageNumber); + + //assert + var result = Assert.IsType>>(res); + var objectResult = Assert.IsType(result.Result); + Assert.Equal(200, objectResult.StatusCode); + Assert.NotNull(objectResult.Value); + } + +} \ No newline at end of file diff --git a/TickAPI/TickAPI.Tests/Categories/Services/CategoryServiceTests.cs b/TickAPI/TickAPI.Tests/Categories/Services/CategoryServiceTests.cs new file mode 100644 index 0000000..345cdec --- /dev/null +++ b/TickAPI/TickAPI.Tests/Categories/Services/CategoryServiceTests.cs @@ -0,0 +1,42 @@ +using Microsoft.AspNetCore.Mvc; +using Moq; +using TickAPI.Categories.Abstractions; +using TickAPI.Categories.DTOs.Response; +using TickAPI.Categories.Models; +using TickAPI.Categories.Services; +using TickAPI.Common.Pagination.Abstractions; +using TickAPI.Common.Pagination.Responses; +using TickAPI.Common.Results; +using TickAPI.Common.Results.Generic; + +namespace TickAPI.Tests.Categories.Services; + +public class CategoryServiceTests +{ + [Fact] + + public async Task GetCategories_WhenDataIsValid_ShouldReturnOk() + { + //arrange + int pageSize = 10; + int page = 0; + var categoryRepositoryMock = new Mock(); + categoryRepositoryMock.Setup(repo => repo.GetCategoriesAsync()) + .ReturnsAsync(Result>.Success(new List())); + + var paginationServiceMock = new Mock(); + paginationServiceMock.Setup(p => p.Paginate(new List(), pageSize, page)).Returns( + Result>.Success(new PaginatedData(new List(), page, pageSize, + false, false, new PaginationDetails(0, 0))) + ); + + var sut = new CategoryService(categoryRepositoryMock.Object, paginationServiceMock.Object); + + //act + var res = await sut.GetCategoriesAsync(pageSize, page); + + //assert + var result = Assert.IsType>>(res); + Assert.True(result.IsSuccess); + } +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Categories/Controllers/CategoryController.cs b/TickAPI/TickAPI/Categories/Controllers/CategoryController.cs index 88319ea..2b3fb02 100644 --- a/TickAPI/TickAPI/Categories/Controllers/CategoryController.cs +++ b/TickAPI/TickAPI/Categories/Controllers/CategoryController.cs @@ -23,7 +23,7 @@ public CategoryController(ICategoryService categoryService) [AuthorizeWithPolicy(AuthPolicies.VerifiedOrganizerPolicy)] [HttpPost("get-categories")] - public async Task>> GetCategories([FromQuery] int page, [FromQuery] int pageSize) + public async Task>> GetCategories([FromQuery] int pageSize, [FromQuery] int page) { var res = await _categoryService.GetCategoriesAsync(pageSize, page); if (!res.IsSuccess) From a92023016d94c4ea378ab15d5199b0e9eb971083 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Mon, 31 Mar 2025 09:28:02 +0200 Subject: [PATCH 095/128] Create `ClaimsService` --- .../Common/Claims/Abstractions/IClaimsService.cs | 9 +++++++++ .../Common/Claims/Services/ClaimsService.cs | 16 ++++++++++++++++ TickAPI/TickAPI/Program.cs | 3 +++ 3 files changed, 28 insertions(+) create mode 100644 TickAPI/TickAPI/Common/Claims/Abstractions/IClaimsService.cs create mode 100644 TickAPI/TickAPI/Common/Claims/Services/ClaimsService.cs diff --git a/TickAPI/TickAPI/Common/Claims/Abstractions/IClaimsService.cs b/TickAPI/TickAPI/Common/Claims/Abstractions/IClaimsService.cs new file mode 100644 index 0000000..7dad5e6 --- /dev/null +++ b/TickAPI/TickAPI/Common/Claims/Abstractions/IClaimsService.cs @@ -0,0 +1,9 @@ +using System.Security.Claims; +using TickAPI.Common.Results.Generic; + +namespace TickAPI.Common.Claims.Abstractions; + +public interface IClaimsService +{ + Result GetEmailFromClaims(IEnumerable claims); +} diff --git a/TickAPI/TickAPI/Common/Claims/Services/ClaimsService.cs b/TickAPI/TickAPI/Common/Claims/Services/ClaimsService.cs new file mode 100644 index 0000000..83da865 --- /dev/null +++ b/TickAPI/TickAPI/Common/Claims/Services/ClaimsService.cs @@ -0,0 +1,16 @@ +using System.Security.Claims; +using TickAPI.Common.Claims.Abstractions; +using TickAPI.Common.Results.Generic; + +namespace TickAPI.Common.Claims.Services; + +public class ClaimsService : IClaimsService +{ + public Result GetEmailFromClaims(IEnumerable claims) + { + var email = claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value; + if (email == null) + return Result.Failure(StatusCodes.Status400BadRequest, "missing email claim"); + return Result.Success(email); + } +} diff --git a/TickAPI/TickAPI/Program.cs b/TickAPI/TickAPI/Program.cs index 2b0ffca..0c56ab6 100644 --- a/TickAPI/TickAPI/Program.cs +++ b/TickAPI/TickAPI/Program.cs @@ -30,6 +30,8 @@ using TickAPI.Addresses.Abstractions; using TickAPI.Addresses.Repositories; using TickAPI.Addresses.Services; +using TickAPI.Common.Claims.Abstractions; +using TickAPI.Common.Claims.Services; // Builder constants const string allowClientPolicyName = "AllowClient"; @@ -105,6 +107,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); From b5658df6156bc6b4a083337e19793225c90aa7e6 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Mon, 31 Mar 2025 09:38:08 +0200 Subject: [PATCH 096/128] Add tests for `ClaimsService` --- .../Claims/Services/ClaimsServiceTests.cs | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 TickAPI/TickAPI.Tests/Common/Claims/Services/ClaimsServiceTests.cs diff --git a/TickAPI/TickAPI.Tests/Common/Claims/Services/ClaimsServiceTests.cs b/TickAPI/TickAPI.Tests/Common/Claims/Services/ClaimsServiceTests.cs new file mode 100644 index 0000000..f9a0eb3 --- /dev/null +++ b/TickAPI/TickAPI.Tests/Common/Claims/Services/ClaimsServiceTests.cs @@ -0,0 +1,49 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Http; +using TickAPI.Common.Claims.Abstractions; +using TickAPI.Common.Claims.Services; + +namespace TickAPI.Tests.Common.Claims.Services; + +public class ClaimsServiceTests +{ + private readonly IClaimsService _claimsService; + + public ClaimsServiceTests() + { + _claimsService = new ClaimsService(); + } + + [Fact] + public void GetEmailFromClaims_WhenEmailInClaims_ShouldReturnEmail() + { + // Arrange + var email = "test@gmail.com"; + var claims = new List + { + new Claim(ClaimTypes.Email, email) + }; + + // Act + var result = _claimsService.GetEmailFromClaims(claims); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(email, result.Value!); + } + + [Fact] + public void GetEmailFromClaims_WhenEmailNotInClaims_ShouldReturnFailure() + { + // Arrange + var claims = new List(); + + // Act + var result = _claimsService.GetEmailFromClaims(claims); + + // Assert + Assert.True(result.IsError); + Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); + Assert.Equal("missing email claim", result.ErrorMsg); + } +} From f1f985b0090a88c226cb76e391e1648c571684c5 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Mon, 31 Mar 2025 09:39:17 +0200 Subject: [PATCH 097/128] Add `ClaimsService` to `EventController` --- .../Controllers/EventControllerTests.cs | 25 ++++++++-------- .../Events/Controllers/EventController.cs | 29 ++++++++++++------- 2 files changed, 31 insertions(+), 23 deletions(-) diff --git a/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs b/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs index bde0864..3853bb2 100644 --- a/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs +++ b/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs @@ -5,11 +5,10 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using TickAPI.Addresses.DTOs.Request; +using TickAPI.Common.Claims.Abstractions; using TickAPI.Events.Controllers; using TickAPI.Events.Abstractions; -using TickAPI.Common.Results; using TickAPI.Common.Results.Generic; -using TickAPI.Organizers.Models; using TickAPI.Events.DTOs.Response; namespace TickAPI.Tests.Events.Controllers; @@ -19,9 +18,7 @@ public class EventControllerTests [Fact] public async Task CreateEvent_WhenDataIsValid_ShouldReturnSuccess() { - //arrange - string name = "Concert"; string description = "Description of a concert"; DateTime startDate = new DateTime(2025, 5, 1); @@ -38,37 +35,38 @@ public async Task CreateEvent_WhenDataIsValid_ShouldReturnSuccess() .Setup(m => m.CreateNewEventAsync(name, description, startDate, endDate, minimumAge, createAddress, eventStatus, email)) .ReturnsAsync(Result.Success(new Event())); - var sut = new EventController(eventServiceMock.Object); - var claims = new List { new Claim(ClaimTypes.Email, email) }; - sut.ControllerContext = new ControllerContext + var controllerContext = new ControllerContext { HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(new ClaimsIdentity(claims)) } }; + + var claimsServiceMock = new Mock(); + claimsServiceMock.Setup(m => m.GetEmailFromClaims(controllerContext.HttpContext.User.Claims)).Returns(Result.Success(email)); + + var sut = new EventController(eventServiceMock.Object, claimsServiceMock.Object); + + sut.ControllerContext = controllerContext; // act var res = await sut.CreateEvent(eventDto); // assert - - var result = Assert.IsType>(res); var objectResult = Assert.IsType(result.Result); Assert.Equal(200, objectResult.StatusCode); Assert.Equal("Event created succesfully", objectResult.Value); - } [Fact] public async Task CreateEvent_WhenMissingEmailClaims_ShouldReturnBadRequest() { - //arrange string name = "Concert"; string description = "Description of a concert"; @@ -80,8 +78,10 @@ public async Task CreateEvent_WhenMissingEmailClaims_ShouldReturnBadRequest() CreateAddressDto createAddress = new CreateAddressDto("United States", "New York", "Main st", 20, null, "00-000"); var eventServiceMock = new Mock(); + var claimsServiceMock = new Mock(); + claimsServiceMock.Setup(m => m.GetEmailFromClaims(It.IsAny>())).Returns(Result.Failure(StatusCodes.Status400BadRequest, "missing email claim")); - var sut = new EventController(eventServiceMock.Object); + var sut = new EventController(eventServiceMock.Object, claimsServiceMock.Object); sut.ControllerContext = new ControllerContext { @@ -95,7 +95,6 @@ public async Task CreateEvent_WhenMissingEmailClaims_ShouldReturnBadRequest() var res = await sut.CreateEvent(new CreateEventDto(name, description, startDate, endDate, minimumAge, eventStatus, createAddress)); // assert - var result = Assert.IsType>(res); var objectResult = Assert.IsType(result.Result); Assert.Equal(StatusCodes.Status400BadRequest, objectResult.StatusCode); diff --git a/TickAPI/TickAPI/Events/Controllers/EventController.cs b/TickAPI/TickAPI/Events/Controllers/EventController.cs index fb5f65d..f75f70d 100644 --- a/TickAPI/TickAPI/Events/Controllers/EventController.cs +++ b/TickAPI/TickAPI/Events/Controllers/EventController.cs @@ -1,13 +1,12 @@ using Microsoft.AspNetCore.Mvc; using TickAPI.Events.DTOs.Response; using TickAPI.Events.DTOs.Request; -using System.Security.Claims; using TickAPI.Common.Auth.Attributes; using TickAPI.Common.Auth.Enums; -using TickAPI.Events.Models; +using TickAPI.Common.Claims.Abstractions; +using TickAPI.Common.Pagination.Responses; using TickAPI.Events.Abstractions; - namespace TickAPI.Events.Controllers; [ApiController] @@ -17,27 +16,37 @@ namespace TickAPI.Events.Controllers; public class EventController : ControllerBase { private readonly IEventService _eventService; + private readonly IClaimsService _claimsService; - public EventController(IEventService eventService) + public EventController(IEventService eventService, IClaimsService claimsService) { _eventService = eventService; + _claimsService = claimsService; } [AuthorizeWithPolicy(AuthPolicies.VerifiedOrganizerPolicy)] [HttpPost("create-event")] public async Task> CreateEvent([FromBody] CreateEventDto request) { - var email = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value; - if (email == null) - return StatusCode(StatusCodes.Status400BadRequest, "missing email claim"); - + var emailResult = _claimsService.GetEmailFromClaims(User.Claims); + if (emailResult.IsError) + { + return StatusCode(emailResult.StatusCode, emailResult.ErrorMsg); + } + var email = emailResult.Value!; var newEventResult = await _eventService.CreateNewEventAsync(request.Name, request.Description, request.StartDate, request.EndDate, request.MinimumAge, request.CreateAddress, request.EventStatus, email); - if(newEventResult.IsError) + if (newEventResult.IsError) return StatusCode(newEventResult.StatusCode, newEventResult.ErrorMsg); - return Ok("Event created succesfully"); } + + [AuthorizeWithPolicy(AuthPolicies.VerifiedOrganizerPolicy)] + [HttpGet("get-organizer-events")] + public async Task>> GetOrganizerEvents() + { + throw new NotImplementedException(); + } } \ No newline at end of file From 4336d74ad6705350384e23d3218597f63f7f44eb Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Mon, 31 Mar 2025 09:48:36 +0200 Subject: [PATCH 098/128] Use `ClaimsService` in `CustomerController` --- .../Controllers/CustomerControllerTests.cs | 38 ++++++++++++++----- .../Controllers/CustomerController.cs | 17 ++++++--- 2 files changed, 39 insertions(+), 16 deletions(-) diff --git a/TickAPI/TickAPI.Tests/Customers/Controllers/CustomerControllerTests.cs b/TickAPI/TickAPI.Tests/Customers/Controllers/CustomerControllerTests.cs index 2548884..8e9eadd 100644 --- a/TickAPI/TickAPI.Tests/Customers/Controllers/CustomerControllerTests.cs +++ b/TickAPI/TickAPI.Tests/Customers/Controllers/CustomerControllerTests.cs @@ -5,6 +5,7 @@ using TickAPI.Common.Auth.Abstractions; using TickAPI.Common.Auth.Enums; using TickAPI.Common.Auth.Responses; +using TickAPI.Common.Claims.Abstractions; using TickAPI.Common.Results.Generic; using TickAPI.Customers.Abstractions; using TickAPI.Customers.Controllers; @@ -34,11 +35,14 @@ public async Task GoogleLogin_WhenAuthSuccessAndCustomerExists_ShouldReturnToken var jwtServiceMock = new Mock(); jwtServiceMock.Setup(m => m.GenerateJwtToken(email, UserRole.Customer)) .Returns(Result.Success(jwtToken)); + + var claimsServiceMock = new Mock(); var sut = new CustomerController( googleAuthServiceMock.Object, jwtServiceMock.Object, - customerServiceMock.Object); + customerServiceMock.Object, + claimsServiceMock.Object); // Act var actionResult = await sut.GoogleLogin(new GoogleCustomerLoginDto(accessToken)); @@ -77,10 +81,13 @@ public async Task GoogleLogin_WhenAuthSuccessAndCustomerDoesNotExist_ShouldCreat jwtServiceMock.Setup(m => m.GenerateJwtToken(email, UserRole.Customer)) .Returns(Result.Success(jwtToken)); + var claimsServiceMock = new Mock(); + var sut = new CustomerController( googleAuthServiceMock.Object, jwtServiceMock.Object, - customerServiceMock.Object); + customerServiceMock.Object, + claimsServiceMock.Object); // Act var result = await sut.GoogleLogin(new GoogleCustomerLoginDto( accessToken )); @@ -113,19 +120,13 @@ public async Task AboutMe_WithValidEmailClaim_ShouldReturnCustomerDetails() var googleAuthServiceMock = new Mock(); var jwtServiceMock = new Mock(); - var sut = new CustomerController( - googleAuthServiceMock.Object, - jwtServiceMock.Object, - customerServiceMock.Object); - var claims = new List { new Claim(ClaimTypes.Email, email) }; var identity = new ClaimsIdentity(claims); var claimsPrincipal = new ClaimsPrincipal(identity); - - sut.ControllerContext = new ControllerContext + var controllerContext = new ControllerContext { HttpContext = new DefaultHttpContext { @@ -133,6 +134,18 @@ public async Task AboutMe_WithValidEmailClaim_ShouldReturnCustomerDetails() } }; + var claimsServiceMock = new Mock(); + claimsServiceMock.Setup(m => m.GetEmailFromClaims(controllerContext.HttpContext.User.Claims)).Returns(Result.Success(email)); + + var sut = new CustomerController( + googleAuthServiceMock.Object, + jwtServiceMock.Object, + customerServiceMock.Object, + claimsServiceMock.Object); + + + sut.ControllerContext = controllerContext; + // Act var result = await sut.AboutMe(); @@ -151,10 +164,15 @@ public async Task AboutMe_WithMissingEmailClaim_ShouldReturnBadRequest() var googleAuthServiceMock = new Mock(); var jwtServiceMock = new Mock(); + var claimsServiceMock = new Mock(); + claimsServiceMock.Setup(m => m.GetEmailFromClaims(It.IsAny>())).Returns(Result.Failure(StatusCodes.Status400BadRequest, "missing email claim")); + + var sut = new CustomerController( googleAuthServiceMock.Object, jwtServiceMock.Object, - customerServiceMock.Object); + customerServiceMock.Object, + claimsServiceMock.Object); var claims = new List(); var identity = new ClaimsIdentity(claims); diff --git a/TickAPI/TickAPI/Customers/Controllers/CustomerController.cs b/TickAPI/TickAPI/Customers/Controllers/CustomerController.cs index 5302175..6b5caaf 100644 --- a/TickAPI/TickAPI/Customers/Controllers/CustomerController.cs +++ b/TickAPI/TickAPI/Customers/Controllers/CustomerController.cs @@ -1,8 +1,8 @@ -using System.Security.Claims; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using TickAPI.Common.Auth.Abstractions; using TickAPI.Common.Auth.Attributes; using TickAPI.Common.Auth.Enums; +using TickAPI.Common.Claims.Abstractions; using TickAPI.Customers.Abstractions; using TickAPI.Customers.DTOs.Request; using TickAPI.Customers.DTOs.Response; @@ -16,12 +16,14 @@ public class CustomerController : ControllerBase private readonly IGoogleAuthService _googleAuthService; private readonly IJwtService _jwtService; private readonly ICustomerService _customerService; + private readonly IClaimsService _claimsService; - public CustomerController(IGoogleAuthService googleAuthService, IJwtService jwtService, ICustomerService customerService) + public CustomerController(IGoogleAuthService googleAuthService, IJwtService jwtService, ICustomerService customerService, IClaimsService claimsService) { _googleAuthService = googleAuthService; _jwtService = jwtService; _customerService = customerService; + _claimsService = claimsService; } [HttpPost("google-login")] @@ -52,9 +54,12 @@ public async Task> GoogleLogin([Fro [HttpGet("about-me")] public async Task> AboutMe() { - var email = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value; - if (email == null) - return StatusCode(StatusCodes.Status400BadRequest, "missing email claim"); + var emailResult = _claimsService.GetEmailFromClaims(User.Claims); + if (emailResult.IsError) + { + return StatusCode(emailResult.StatusCode, emailResult.ErrorMsg); + } + var email = emailResult.Value!; var customerResult = await _customerService.GetCustomerByEmailAsync(email); if (customerResult.IsError) From 03a41f7b8822663c01d750050de4c83060cb2fc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Mon, 31 Mar 2025 09:55:26 +0200 Subject: [PATCH 099/128] Minor fixes suggested by trzcinskiiK --- .../Categories/Controllers/CategoryControllerTests.cs | 6 +++--- .../Categories/Services/CategoryServiceTests.cs | 4 ++-- .../Categories/Abstractions/ICategoryService.cs | 2 +- .../Categories/Controllers/CategoryController.cs | 6 +++--- .../Categories/DTOs/Response/GetCategoriesDto.cs | 2 +- TickAPI/TickAPI/Categories/Services/CategoryService.cs | 10 +++++----- TickAPI/TickAPI/Common/Auth/Enums/AuthPolicies.cs | 1 + TickAPI/TickAPI/Program.cs | 1 + TickAPI/TickAPI/TickAPI.csproj | 4 ---- 9 files changed, 17 insertions(+), 19 deletions(-) diff --git a/TickAPI/TickAPI.Tests/Categories/Controllers/CategoryControllerTests.cs b/TickAPI/TickAPI.Tests/Categories/Controllers/CategoryControllerTests.cs index 88df29f..fdce5df 100644 --- a/TickAPI/TickAPI.Tests/Categories/Controllers/CategoryControllerTests.cs +++ b/TickAPI/TickAPI.Tests/Categories/Controllers/CategoryControllerTests.cs @@ -20,8 +20,8 @@ public async Task GetCategories_WhenDataIsValid_ShouldReturnOk() int pageSize = 20; int pageNumber = 0; var categoryServiceMock = new Mock(); - categoryServiceMock.Setup(m => m.GetCategoriesAsync(pageSize, pageNumber)).ReturnsAsync( - Result>.Success(new PaginatedData(new List(), pageNumber, pageSize, true, true, + categoryServiceMock.Setup(m => m.GetCategoriesResponsesAsync(pageSize, pageNumber)).ReturnsAsync( + Result>.Success(new PaginatedData(new List(), pageNumber, pageSize, true, true, new PaginationDetails(0, 0)))); var sut = new CategoryController(categoryServiceMock.Object); @@ -30,7 +30,7 @@ public async Task GetCategories_WhenDataIsValid_ShouldReturnOk() var res = await sut.GetCategories(pageSize, pageNumber); //assert - var result = Assert.IsType>>(res); + var result = Assert.IsType>>(res); var objectResult = Assert.IsType(result.Result); Assert.Equal(200, objectResult.StatusCode); Assert.NotNull(objectResult.Value); diff --git a/TickAPI/TickAPI.Tests/Categories/Services/CategoryServiceTests.cs b/TickAPI/TickAPI.Tests/Categories/Services/CategoryServiceTests.cs index 345cdec..0456fa0 100644 --- a/TickAPI/TickAPI.Tests/Categories/Services/CategoryServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Categories/Services/CategoryServiceTests.cs @@ -33,10 +33,10 @@ public async Task GetCategories_WhenDataIsValid_ShouldReturnOk() var sut = new CategoryService(categoryRepositoryMock.Object, paginationServiceMock.Object); //act - var res = await sut.GetCategoriesAsync(pageSize, page); + var res = await sut.GetCategoriesResponsesAsync(pageSize, page); //assert - var result = Assert.IsType>>(res); + var result = Assert.IsType>>(res); Assert.True(result.IsSuccess); } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Categories/Abstractions/ICategoryService.cs b/TickAPI/TickAPI/Categories/Abstractions/ICategoryService.cs index 9eb60c1..37e0f24 100644 --- a/TickAPI/TickAPI/Categories/Abstractions/ICategoryService.cs +++ b/TickAPI/TickAPI/Categories/Abstractions/ICategoryService.cs @@ -7,5 +7,5 @@ namespace TickAPI.Categories.Abstractions; public interface ICategoryService { - public Task>> GetCategoriesAsync(int pageSize, int page); + public Task>> GetCategoriesResponsesAsync(int pageSize, int page); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Categories/Controllers/CategoryController.cs b/TickAPI/TickAPI/Categories/Controllers/CategoryController.cs index 2b3fb02..20f4d94 100644 --- a/TickAPI/TickAPI/Categories/Controllers/CategoryController.cs +++ b/TickAPI/TickAPI/Categories/Controllers/CategoryController.cs @@ -21,11 +21,11 @@ public CategoryController(ICategoryService categoryService) _categoryService = categoryService; } - [AuthorizeWithPolicy(AuthPolicies.VerifiedOrganizerPolicy)] + [AuthorizeWithPolicy(AuthPolicies.VerifiedUserPolicy)] [HttpPost("get-categories")] - public async Task>> GetCategories([FromQuery] int pageSize, [FromQuery] int page) + public async Task>> GetCategories([FromQuery] int pageSize, [FromQuery] int page) { - var res = await _categoryService.GetCategoriesAsync(pageSize, page); + var res = await _categoryService.GetCategoriesResponsesAsync(pageSize, page); if (!res.IsSuccess) { return StatusCode(StatusCodes.Status500InternalServerError, res.ErrorMsg); diff --git a/TickAPI/TickAPI/Categories/DTOs/Response/GetCategoriesDto.cs b/TickAPI/TickAPI/Categories/DTOs/Response/GetCategoriesDto.cs index abbaea2..9ece0df 100644 --- a/TickAPI/TickAPI/Categories/DTOs/Response/GetCategoriesDto.cs +++ b/TickAPI/TickAPI/Categories/DTOs/Response/GetCategoriesDto.cs @@ -2,6 +2,6 @@ namespace TickAPI.Categories.DTOs.Response; -public record GetCategoriesDto( +public record GetCategoryResponseDto( string CategoryName ); \ No newline at end of file diff --git a/TickAPI/TickAPI/Categories/Services/CategoryService.cs b/TickAPI/TickAPI/Categories/Services/CategoryService.cs index 2be8597..f663b40 100644 --- a/TickAPI/TickAPI/Categories/Services/CategoryService.cs +++ b/TickAPI/TickAPI/Categories/Services/CategoryService.cs @@ -18,22 +18,22 @@ public CategoryService(ICategoryRepository categoryRepository, IPaginationServi _paginationService = paginationService; } - public async Task>> GetCategoriesAsync(int pageSize, int page) + public async Task>> GetCategoriesResponsesAsync(int pageSize, int page) { var res = await _categoryRepository.GetCategoriesAsync(); - List categories = new List(); + List categories = new List(); var categoriesPaginated = _paginationService.Paginate(res.Value, pageSize, page); if (!categoriesPaginated.IsSuccess) { - return Result>.PropagateError(categoriesPaginated); + return Result>.PropagateError(categoriesPaginated); } foreach (var category in categoriesPaginated.Value.Data) { - categories.Add(new GetCategoriesDto(category.CategoryName)); + categories.Add(new GetCategoryResponseDto(category.CategoryName)); } - return Result>.Success(new PaginatedData(categories, categoriesPaginated.Value.PageNumber + return Result>.Success(new PaginatedData(categories, categoriesPaginated.Value.PageNumber ,categoriesPaginated.Value.PageSize, categoriesPaginated.Value.HasNextPage, categoriesPaginated.Value.HasPreviousPage, categoriesPaginated.Value.PaginationDetails)); } diff --git a/TickAPI/TickAPI/Common/Auth/Enums/AuthPolicies.cs b/TickAPI/TickAPI/Common/Auth/Enums/AuthPolicies.cs index 33bf3d0..5c9f52a 100644 --- a/TickAPI/TickAPI/Common/Auth/Enums/AuthPolicies.cs +++ b/TickAPI/TickAPI/Common/Auth/Enums/AuthPolicies.cs @@ -7,4 +7,5 @@ public enum AuthPolicies CustomerPolicy, NewOrganizerPolicy, CreatedOrganizerPolicy, + VerifiedUserPolicy, } \ No newline at end of file diff --git a/TickAPI/TickAPI/Program.cs b/TickAPI/TickAPI/Program.cs index d4110fa..5ed6205 100644 --- a/TickAPI/TickAPI/Program.cs +++ b/TickAPI/TickAPI/Program.cs @@ -76,6 +76,7 @@ options.AddPolicy(AuthPolicies.NewOrganizerPolicy.ToString(), policy => policy.RequireRole(UserRole.NewOrganizer.ToString())); options.AddPolicy(AuthPolicies.CreatedOrganizerPolicy.ToString(), policy => policy.RequireRole(UserRole.UnverifiedOrganizer.ToString(), UserRole.Organizer.ToString())); + options.AddPolicy(AuthPolicies.VerifiedUserPolicy.ToString(), policy => policy.RequireRole(UserRole.Admin.ToString(), UserRole.Organizer.ToString(), UserRole.Customer.ToString())); }); // Add admin services. diff --git a/TickAPI/TickAPI/TickAPI.csproj b/TickAPI/TickAPI/TickAPI.csproj index 39a857b..1e131f6 100644 --- a/TickAPI/TickAPI/TickAPI.csproj +++ b/TickAPI/TickAPI/TickAPI.csproj @@ -20,10 +20,6 @@ - - - - From a0b8f99d2db5c7149a0e826200fb2e7c0fc1407c Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Mon, 31 Mar 2025 09:58:25 +0200 Subject: [PATCH 100/128] Use `ClaimsService` in `OrganizerController` --- .../Controllers/OrganizerControllerTests.cs | 89 +++++++++++++------ .../Controllers/OrganizerController.cs | 27 +++--- 2 files changed, 81 insertions(+), 35 deletions(-) diff --git a/TickAPI/TickAPI.Tests/Organizers/Controllers/OrganizerControllerTests.cs b/TickAPI/TickAPI.Tests/Organizers/Controllers/OrganizerControllerTests.cs index 9560686..5e26e85 100644 --- a/TickAPI/TickAPI.Tests/Organizers/Controllers/OrganizerControllerTests.cs +++ b/TickAPI/TickAPI.Tests/Organizers/Controllers/OrganizerControllerTests.cs @@ -6,6 +6,7 @@ using TickAPI.Common.Auth.Abstractions; using TickAPI.Common.Auth.Enums; using TickAPI.Common.Auth.Responses; +using TickAPI.Common.Claims.Abstractions; using TickAPI.Common.Results; using TickAPI.Common.Results.Generic; using TickAPI.Organizers.Abstractions; @@ -41,10 +42,13 @@ public async Task GoogleLogin_WhenAuthSuccessAndVerifiedOrganizerExists_ShouldRe .Setup(m => m.GenerateJwtToken(email, UserRole.Organizer)) .Returns(Result.Success(jwtToken)); + var claimsServiceMock = new Mock(); + var sut = new OrganizerController( googleAuthServiceMock.Object, jwtServiceMock.Object, - organizerServiceMock.Object + organizerServiceMock.Object, + claimsServiceMock.Object ); // Act @@ -79,10 +83,13 @@ public async Task GoogleLogin_WhenAuthSuccessAndUnverifiedOrganizerExists_Should .Setup(m => m.GenerateJwtToken(email, UserRole.UnverifiedOrganizer)) .Returns(Result.Success(jwtToken)); + var claimsServiceMock = new Mock(); + var sut = new OrganizerController( googleAuthServiceMock.Object, jwtServiceMock.Object, - organizerServiceMock.Object + organizerServiceMock.Object, + claimsServiceMock.Object ); // Act @@ -118,10 +125,13 @@ public async Task .Setup(m => m.GenerateJwtToken(email, UserRole.NewOrganizer)) .Returns(Result.Success(jwtToken)); + var claimsServiceMock = new Mock(); + var sut = new OrganizerController( googleAuthServiceMock.Object, jwtServiceMock.Object, - organizerServiceMock.Object + organizerServiceMock.Object, + claimsServiceMock.Object ); // Act @@ -164,16 +174,11 @@ public async Task CreateOrganizer_WhenCreatingAccountIsSuccessful_ShouldReturnTo jwtServiceMock.Setup(m => m.GenerateJwtToken(email, UserRole.UnverifiedOrganizer)) .Returns(Result.Success(jwtToken)); - var sut = new OrganizerController( - googleAuthServiceMock.Object, - jwtServiceMock.Object, - organizerServiceMock.Object); - var claims = new List { new Claim(ClaimTypes.Email, email) }; - sut.ControllerContext = new ControllerContext + var controllerContext = new ControllerContext { HttpContext = new DefaultHttpContext { @@ -181,6 +186,18 @@ public async Task CreateOrganizer_WhenCreatingAccountIsSuccessful_ShouldReturnTo } }; + var claimsServiceMock = new Mock(); + claimsServiceMock.Setup(m => m.GetEmailFromClaims(controllerContext.HttpContext.User.Claims)).Returns(Result.Success(email)); + + var sut = new OrganizerController( + googleAuthServiceMock.Object, + jwtServiceMock.Object, + organizerServiceMock.Object, + claimsServiceMock.Object); + + + sut.ControllerContext = controllerContext; + // Act var actionResult = await sut.CreateOrganizer(new CreateOrganizerDto(firstName, lastName, displayName)); @@ -198,10 +215,14 @@ public async Task CreateOrganizer_WhenMissingEmailClaim_ShouldReturnBadRequest() var jwtServiceMock = new Mock(); + var claimsServiceMock = new Mock(); + claimsServiceMock.Setup(m => m.GetEmailFromClaims(It.IsAny>())).Returns(Result.Failure(StatusCodes.Status400BadRequest, "missing email claim")); + var sut = new OrganizerController( googleAuthServiceMock.Object, jwtServiceMock.Object, - organizerServiceMock.Object); + organizerServiceMock.Object, + claimsServiceMock.Object); sut.ControllerContext = new ControllerContext { @@ -234,16 +255,18 @@ public async Task VerifyOrganizer_WhenVerificationSuccessful_ShouldReturnOk() .ReturnsAsync(Result.Success()); var jwtServiceMock = new Mock(); + + var claimsServiceMock = new Mock(); var sut = new OrganizerController( googleAuthServiceMock.Object, jwtServiceMock.Object, - organizerServiceMock.Object); + organizerServiceMock.Object, + claimsServiceMock.Object); // Act var actionResult = await sut.VerifyOrganizer(new VerifyOrganizerDto(email)); - // Assert var result = Assert.IsType(actionResult); Assert.Equal(StatusCodes.Status200OK, result.StatusCode); @@ -280,16 +303,11 @@ public async Task AboutMe_WithValidEmailClaim_ShouldReturnOrganizerDetails() var jwtServiceMock = new Mock(); - var sut = new OrganizerController( - googleAuthServiceMock.Object, - jwtServiceMock.Object, - organizerServiceMock.Object); - var claims = new List { new Claim(ClaimTypes.Email, email) }; - sut.ControllerContext = new ControllerContext + var controllerContext = new ControllerContext { HttpContext = new DefaultHttpContext { @@ -297,6 +315,17 @@ public async Task AboutMe_WithValidEmailClaim_ShouldReturnOrganizerDetails() } }; + var claimsServiceMock = new Mock(); + claimsServiceMock.Setup(m => m.GetEmailFromClaims(controllerContext.HttpContext.User.Claims)).Returns(Result.Success(email)); + + var sut = new OrganizerController( + googleAuthServiceMock.Object, + jwtServiceMock.Object, + organizerServiceMock.Object, + claimsServiceMock.Object); + + sut.ControllerContext = controllerContext; + // Act var actionResult = await sut.AboutMe(); @@ -319,10 +348,14 @@ public async Task AboutMe_WithMissingEmailClaim_ShouldReturnBadRequest() var jwtServiceMock = new Mock(); + var claimsServiceMock = new Mock(); + claimsServiceMock.Setup(m => m.GetEmailFromClaims(It.IsAny>())).Returns(Result.Failure(StatusCodes.Status400BadRequest, "missing email claim")); + var sut = new OrganizerController( googleAuthServiceMock.Object, jwtServiceMock.Object, - organizerServiceMock.Object); + organizerServiceMock.Object, + claimsServiceMock.Object); sut.ControllerContext = new ControllerContext { @@ -356,16 +389,11 @@ public async Task AboutMe_WhenOrganizerNotFound_ShouldReturnInternalServerError( var jwtServiceMock = new Mock(); - var sut = new OrganizerController( - googleAuthServiceMock.Object, - jwtServiceMock.Object, - organizerServiceMock.Object); - var claims = new List { new Claim(ClaimTypes.Email, email) }; - sut.ControllerContext = new ControllerContext + var controllerContext = new ControllerContext { HttpContext = new DefaultHttpContext { @@ -373,6 +401,17 @@ public async Task AboutMe_WhenOrganizerNotFound_ShouldReturnInternalServerError( } }; + var claimsServiceMock = new Mock(); + claimsServiceMock.Setup(m => m.GetEmailFromClaims(controllerContext.HttpContext.User.Claims)).Returns(Result.Success(email)); + + var sut = new OrganizerController( + googleAuthServiceMock.Object, + jwtServiceMock.Object, + organizerServiceMock.Object, + claimsServiceMock.Object); + + sut.ControllerContext = controllerContext; + // Act var actionResult = await sut.AboutMe(); diff --git a/TickAPI/TickAPI/Organizers/Controllers/OrganizerController.cs b/TickAPI/TickAPI/Organizers/Controllers/OrganizerController.cs index 9465d73..4643556 100644 --- a/TickAPI/TickAPI/Organizers/Controllers/OrganizerController.cs +++ b/TickAPI/TickAPI/Organizers/Controllers/OrganizerController.cs @@ -1,8 +1,8 @@ -using System.Security.Claims; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using TickAPI.Common.Auth.Abstractions; using TickAPI.Common.Auth.Attributes; using TickAPI.Common.Auth.Enums; +using TickAPI.Common.Claims.Abstractions; using TickAPI.Common.Results.Generic; using TickAPI.Organizers.Abstractions; using TickAPI.Organizers.DTOs.Request; @@ -17,13 +17,15 @@ public class OrganizerController : ControllerBase private readonly IGoogleAuthService _googleAuthService; private readonly IJwtService _jwtService; private readonly IOrganizerService _organizerService; + private readonly IClaimsService _claimsService; public OrganizerController(IGoogleAuthService googleAuthService, IJwtService jwtService, - IOrganizerService organizerService) + IOrganizerService organizerService, IClaimsService claimsService) { _googleAuthService = googleAuthService; _jwtService = jwtService; _organizerService = organizerService; + _claimsService = claimsService; } [HttpPost("google-login")] @@ -63,9 +65,12 @@ public async Task> GoogleLogin([Fr [HttpPost("create-organizer")] public async Task> CreateOrganizer([FromBody] CreateOrganizerDto request) { - var email = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value; - if (email == null) - return StatusCode(StatusCodes.Status400BadRequest, "missing email claim"); + var emailResult = _claimsService.GetEmailFromClaims(User.Claims); + if (emailResult.IsError) + { + return StatusCode(StatusCodes.Status400BadRequest, emailResult.ErrorMsg); + } + var email = emailResult.Value!; var newOrganizerResult = await _organizerService.CreateNewOrganizerAsync(email, request.FirstName, request.LastName, request.DisplayName); if(newOrganizerResult.IsError) @@ -94,10 +99,12 @@ public async Task VerifyOrganizer([FromBody] VerifyOrganizerDto re [HttpGet("about-me")] public async Task> AboutMe() { - var email = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value; - - if (email == null) - return StatusCode(StatusCodes.Status400BadRequest, "missing email claim"); + var emailResult = _claimsService.GetEmailFromClaims(User.Claims); + if (emailResult.IsError) + { + return StatusCode(StatusCodes.Status400BadRequest, emailResult.ErrorMsg); + } + var email = emailResult.Value!; var organizerResult = await _organizerService.GetOrganizerByEmailAsync(email); if (organizerResult.IsError) From caa4ca59918f57dcef44acae143ac94bd8c88b45 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Wed, 2 Apr 2025 00:52:42 +0200 Subject: [PATCH 101/128] Fixed the inability to create a customer/organizer account from a Google account with no last name --- TickAPI/TickAPI/Customers/Models/Customer.cs | 2 +- ...250401224843_NullableLastNames.Designer.cs | 357 ++++++++++++++++++ .../20250401224843_NullableLastNames.cs | 54 +++ .../TickApiDbContextModelSnapshot.cs | 68 ++-- .../TickAPI/Organizers/Models/Organizer.cs | 2 +- 5 files changed, 446 insertions(+), 37 deletions(-) create mode 100644 TickAPI/TickAPI/Migrations/20250401224843_NullableLastNames.Designer.cs create mode 100644 TickAPI/TickAPI/Migrations/20250401224843_NullableLastNames.cs diff --git a/TickAPI/TickAPI/Customers/Models/Customer.cs b/TickAPI/TickAPI/Customers/Models/Customer.cs index 8a78532..5a3c194 100644 --- a/TickAPI/TickAPI/Customers/Models/Customer.cs +++ b/TickAPI/TickAPI/Customers/Models/Customer.cs @@ -7,7 +7,7 @@ public class Customer public Guid Id { get; set; } public string Email { get; set; } public string FirstName { get; set; } - public string LastName { get; set; } + public string? LastName { get; set; } public DateTime CreationDate { get; set; } public ICollection Tickets { get; set; } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Migrations/20250401224843_NullableLastNames.Designer.cs b/TickAPI/TickAPI/Migrations/20250401224843_NullableLastNames.Designer.cs new file mode 100644 index 0000000..f5bf758 --- /dev/null +++ b/TickAPI/TickAPI/Migrations/20250401224843_NullableLastNames.Designer.cs @@ -0,0 +1,357 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using TickAPI.Common.TickApiDbContext; + +#nullable disable + +namespace TickAPI.Migrations +{ + [DbContext(typeof(TickApiDbContext))] + [Migration("20250401224843_NullableLastNames")] + partial class NullableLastNames + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("CategoryEvent", b => + { + b.Property("CategoriesId") + .HasColumnType("uniqueidentifier"); + + b.Property("EventsId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("CategoriesId", "EventsId"); + + b.HasIndex("EventsId"); + + b.ToTable("CategoryEvent"); + }); + + modelBuilder.Entity("TickAPI.Addresses.Models.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("City") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Country") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FlatNumber") + .HasColumnType("bigint"); + + b.Property("HouseNumber") + .HasColumnType("bigint"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Street") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Addresses"); + }); + + modelBuilder.Entity("TickAPI.Admins.Models.Admin", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Login") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Admins"); + }); + + modelBuilder.Entity("TickAPI.Categories.Models.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CategoryName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("TickAPI.Customers.Models.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AddressId") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("EventStatus") + .HasColumnType("int"); + + b.Property("MinimumAge") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OrganizerId") + .HasColumnType("uniqueidentifier"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("AddressId"); + + b.HasIndex("OrganizerId"); + + b.ToTable("Events"); + }); + + modelBuilder.Entity("TickAPI.Organizers.Models.Organizer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsVerified") + .HasColumnType("bit"); + + b.Property("LastName") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Organizers"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AvailableFrom") + .HasColumnType("datetime2"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EventId") + .HasColumnType("uniqueidentifier"); + + b.Property("MaxCount") + .HasColumnType("bigint"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("EventId"); + + b.ToTable("TicketTypes"); + }); + + modelBuilder.Entity("TickAPI.Tickets.Models.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ForResell") + .HasColumnType("bit"); + + b.Property("NameOnTicket") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("uniqueidentifier"); + + b.Property("Seats") + .HasColumnType("nvarchar(max)"); + + b.Property("TypeId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("TypeId"); + + b.ToTable("Tickets"); + }); + + modelBuilder.Entity("CategoryEvent", b => + { + b.HasOne("TickAPI.Categories.Models.Category", null) + .WithMany() + .HasForeignKey("CategoriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TickAPI.Events.Models.Event", null) + .WithMany() + .HasForeignKey("EventsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.HasOne("TickAPI.Addresses.Models.Address", "Address") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TickAPI.Organizers.Models.Organizer", "Organizer") + .WithMany("Events") + .HasForeignKey("OrganizerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Address"); + + b.Navigation("Organizer"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.HasOne("TickAPI.Events.Models.Event", "Event") + .WithMany("TicketTypes") + .HasForeignKey("EventId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Event"); + }); + + modelBuilder.Entity("TickAPI.Tickets.Models.Ticket", b => + { + b.HasOne("TickAPI.Customers.Models.Customer", "Owner") + .WithMany("Tickets") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TickAPI.TicketTypes.Models.TicketType", "Type") + .WithMany("Tickets") + .HasForeignKey("TypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("Type"); + }); + + modelBuilder.Entity("TickAPI.Customers.Models.Customer", b => + { + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.Navigation("TicketTypes"); + }); + + modelBuilder.Entity("TickAPI.Organizers.Models.Organizer", b => + { + b.Navigation("Events"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.Navigation("Tickets"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TickAPI/TickAPI/Migrations/20250401224843_NullableLastNames.cs b/TickAPI/TickAPI/Migrations/20250401224843_NullableLastNames.cs new file mode 100644 index 0000000..e7741a4 --- /dev/null +++ b/TickAPI/TickAPI/Migrations/20250401224843_NullableLastNames.cs @@ -0,0 +1,54 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TickAPI.Migrations +{ + /// + public partial class NullableLastNames : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "LastName", + table: "Organizers", + type: "nvarchar(max)", + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(max)"); + + migrationBuilder.AlterColumn( + name: "LastName", + table: "Customers", + type: "nvarchar(max)", + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(max)"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "LastName", + table: "Organizers", + type: "nvarchar(max)", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "nvarchar(max)", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "LastName", + table: "Customers", + type: "nvarchar(max)", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "nvarchar(max)", + oldNullable: true); + } + } +} diff --git a/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs b/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs index c622432..ca59a48 100644 --- a/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs +++ b/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs @@ -37,6 +37,38 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("CategoryEvent"); }); + modelBuilder.Entity("TickAPI.Addresses.Models.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("City") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Country") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FlatNumber") + .HasColumnType("bigint"); + + b.Property("HouseNumber") + .HasColumnType("bigint"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Street") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Addresses"); + }); + modelBuilder.Entity("TickAPI.Admins.Models.Admin", b => { b.Property("Id") @@ -89,7 +121,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("nvarchar(max)"); b.Property("LastName") - .IsRequired() .HasColumnType("nvarchar(max)"); b.HasKey("Id"); @@ -97,38 +128,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Customers"); }); - modelBuilder.Entity("TickAPI.Events.Models.Address", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier"); - - b.Property("City") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("Country") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("FlatNumber") - .HasColumnType("bigint"); - - b.Property("HouseNumber") - .HasColumnType("bigint"); - - b.Property("PostalCode") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("Street") - .HasColumnType("nvarchar(max)"); - - b.HasKey("Id"); - - b.ToTable("Addresses"); - }); - modelBuilder.Entity("TickAPI.Events.Models.Event", b => { b.Property("Id") @@ -195,7 +194,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("bit"); b.Property("LastName") - .IsRequired() .HasColumnType("nvarchar(max)"); b.HasKey("Id"); @@ -284,7 +282,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("TickAPI.Events.Models.Event", b => { - b.HasOne("TickAPI.Events.Models.Address", "Address") + b.HasOne("TickAPI.Addresses.Models.Address", "Address") .WithMany() .HasForeignKey("AddressId") .OnDelete(DeleteBehavior.Cascade) diff --git a/TickAPI/TickAPI/Organizers/Models/Organizer.cs b/TickAPI/TickAPI/Organizers/Models/Organizer.cs index b7ed81e..f55cc05 100644 --- a/TickAPI/TickAPI/Organizers/Models/Organizer.cs +++ b/TickAPI/TickAPI/Organizers/Models/Organizer.cs @@ -8,7 +8,7 @@ public class Organizer public Guid Id { get; set; } public string Email { get; set; } public string FirstName { get; set; } - public string LastName { get; set; } + public string? LastName { get; set; } public DateTime CreationDate { get; set; } public string DisplayName { get; set; } public bool IsVerified { get; set; } From 79227e90ca41bcfac6551a75e2fa05160808f728 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Wed, 2 Apr 2025 19:54:32 +0200 Subject: [PATCH 102/128] added tests to ensure that nullable last name works correctly --- .../Services/CustomerServiceTests.cs | 28 ++++++++++++ .../Services/OrganizerServiceTests.cs | 44 +++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/TickAPI/TickAPI.Tests/Customers/Services/CustomerServiceTests.cs b/TickAPI/TickAPI.Tests/Customers/Services/CustomerServiceTests.cs index 9a3eb79..07acc53 100644 --- a/TickAPI/TickAPI.Tests/Customers/Services/CustomerServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Customers/Services/CustomerServiceTests.cs @@ -71,6 +71,34 @@ public async Task CreateNewCustomerAsync_WhenCustomerWithUniqueEmail_ShouldRetur Assert.Equal(createdAt, result.Value!.CreationDate); Assert.Equal(id, result.Value!.Id); } + + [Fact] + public async Task CreateNewCustomerAsync_WhenLastNameIsNull_ShouldReturnNewCustomer() + { + const string email = "new@customer.com"; + const string firstName = "First"; + const string lastName = null; + Guid id = Guid.NewGuid(); + DateTime createdAt = new DateTime(1970, 1, 1, 8, 0, 0, DateTimeKind.Utc); + var dateTimeServiceMock = new Mock(); + dateTimeServiceMock.Setup(m => m.GetCurrentDateTime()).Returns(createdAt); + var customerRepositoryMock = new Mock(); + customerRepositoryMock.Setup(m => m.GetCustomerByEmailAsync(email)).ReturnsAsync(Result.Failure(StatusCodes.Status404NotFound, $"customer with email '{email}' not found")); + customerRepositoryMock + .Setup(m => m.AddNewCustomerAsync(It.IsAny())) + .Callback(c => c.Id = id) + .Returns(Task.CompletedTask); + var sut = new CustomerService(customerRepositoryMock.Object, dateTimeServiceMock.Object); + + var result = await sut.CreateNewCustomerAsync(email, firstName, lastName); + + Assert.True(result.IsSuccess); + Assert.Equal(email, result.Value!.Email); + Assert.Equal(firstName, result.Value!.FirstName); + Assert.Equal(lastName, result.Value!.LastName); + Assert.Equal(createdAt, result.Value!.CreationDate); + Assert.Equal(id, result.Value!.Id); + } [Fact] public async Task CreateNewCustomerAsync_WhenCustomerWithNotUniqueEmail_ShouldReturnFailure() diff --git a/TickAPI/TickAPI.Tests/Organizers/Services/OrganizerServiceTests.cs b/TickAPI/TickAPI.Tests/Organizers/Services/OrganizerServiceTests.cs index 769ee7f..1e91aa9 100644 --- a/TickAPI/TickAPI.Tests/Organizers/Services/OrganizerServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Organizers/Services/OrganizerServiceTests.cs @@ -112,6 +112,50 @@ public async Task CreateNewOrganizerAsync_WhenOrganizerDataIsValid_ShouldReturnN Assert.False(result.Value!.IsVerified); Assert.Equal(currentDate, result.Value!.CreationDate); } + + [Fact] + public async Task CreateNewOrganizerAsync_WhenLastNameIsNull_ShouldReturnNewOrganizer() + { + // Arrange + Guid id = Guid.NewGuid(); + const string email = "example@test.com"; + const string firstName = "First"; + const string lastName = null; + const string displayName = "Display"; + DateTime currentDate = new DateTime(1970, 1, 1, 8, 0, 0, DateTimeKind.Utc); + + var organizerRepositoryMock = new Mock(); + organizerRepositoryMock + .Setup(m => m.GetOrganizerByEmailAsync(email)) + .ReturnsAsync(Result.Failure(StatusCodes.Status404NotFound, $"organizer with email '{email}' not found")); + organizerRepositoryMock + .Setup(m => m.AddNewOrganizerAsync(It.IsAny())) + .Callback(o => o.Id = id) + .Returns(Task.CompletedTask); + + var dateTimeServiceMock = new Mock(); + dateTimeServiceMock + .Setup(m => m.GetCurrentDateTime()) + .Returns(currentDate); + + var sut = new OrganizerService( + organizerRepositoryMock.Object, + dateTimeServiceMock.Object + ); + + // Act + var result = await sut.CreateNewOrganizerAsync(email, firstName, lastName, displayName); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(id, result.Value!.Id); + Assert.Equal(email, result.Value!.Email); + Assert.Equal(firstName, result.Value!.FirstName); + Assert.Equal(lastName, result.Value!.LastName); + Assert.Equal(displayName, result.Value!.DisplayName); + Assert.False(result.Value!.IsVerified); + Assert.Equal(currentDate, result.Value!.CreationDate); + } [Fact] public async Task CreateNewOrganizerAsync_WhenWithNotUniqueEmail_ShouldReturnFailure() From 03e40b8a35c267427facf71ea20ad4a90cbb0850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Thu, 3 Apr 2025 14:55:58 +0200 Subject: [PATCH 103/128] Delete result return from category repo --- .../TickAPI.Tests/Categories/Services/CategoryServiceTests.cs | 2 +- .../TickAPI/Categories/Abstractions/ICategoryRepository.cs | 2 +- .../{GetCategoriesDto.cs => GetCategoryResponseDto.cs} | 0 .../TickAPI/Categories/Respositories/CategoryRepository.cs | 4 ++-- TickAPI/TickAPI/Categories/Services/CategoryService.cs | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) rename TickAPI/TickAPI/Categories/DTOs/Response/{GetCategoriesDto.cs => GetCategoryResponseDto.cs} (100%) diff --git a/TickAPI/TickAPI.Tests/Categories/Services/CategoryServiceTests.cs b/TickAPI/TickAPI.Tests/Categories/Services/CategoryServiceTests.cs index 0456fa0..0b43f6d 100644 --- a/TickAPI/TickAPI.Tests/Categories/Services/CategoryServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Categories/Services/CategoryServiceTests.cs @@ -22,7 +22,7 @@ public async Task GetCategories_WhenDataIsValid_ShouldReturnOk() int page = 0; var categoryRepositoryMock = new Mock(); categoryRepositoryMock.Setup(repo => repo.GetCategoriesAsync()) - .ReturnsAsync(Result>.Success(new List())); + .ReturnsAsync(new List()); var paginationServiceMock = new Mock(); paginationServiceMock.Setup(p => p.Paginate(new List(), pageSize, page)).Returns( diff --git a/TickAPI/TickAPI/Categories/Abstractions/ICategoryRepository.cs b/TickAPI/TickAPI/Categories/Abstractions/ICategoryRepository.cs index 8f998b0..5b6254f 100644 --- a/TickAPI/TickAPI/Categories/Abstractions/ICategoryRepository.cs +++ b/TickAPI/TickAPI/Categories/Abstractions/ICategoryRepository.cs @@ -5,5 +5,5 @@ namespace TickAPI.Categories.Abstractions; public interface ICategoryRepository { - public Task>> GetCategoriesAsync(); + public Task> GetCategoriesAsync(); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Categories/DTOs/Response/GetCategoriesDto.cs b/TickAPI/TickAPI/Categories/DTOs/Response/GetCategoryResponseDto.cs similarity index 100% rename from TickAPI/TickAPI/Categories/DTOs/Response/GetCategoriesDto.cs rename to TickAPI/TickAPI/Categories/DTOs/Response/GetCategoryResponseDto.cs diff --git a/TickAPI/TickAPI/Categories/Respositories/CategoryRepository.cs b/TickAPI/TickAPI/Categories/Respositories/CategoryRepository.cs index bcedbd2..9cba1d3 100644 --- a/TickAPI/TickAPI/Categories/Respositories/CategoryRepository.cs +++ b/TickAPI/TickAPI/Categories/Respositories/CategoryRepository.cs @@ -14,9 +14,9 @@ public CategoryRepository(TickApiDbContext tickApiDbContext) _tickApiDbContext = tickApiDbContext; } - public async Task>> GetCategoriesAsync() + public async Task> GetCategoriesAsync() { var list = await _tickApiDbContext.Categories.ToListAsync(); - return Result>.Success(list); + return list; } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Categories/Services/CategoryService.cs b/TickAPI/TickAPI/Categories/Services/CategoryService.cs index f663b40..dbbfcd8 100644 --- a/TickAPI/TickAPI/Categories/Services/CategoryService.cs +++ b/TickAPI/TickAPI/Categories/Services/CategoryService.cs @@ -20,9 +20,9 @@ public CategoryService(ICategoryRepository categoryRepository, IPaginationServi public async Task>> GetCategoriesResponsesAsync(int pageSize, int page) { - var res = await _categoryRepository.GetCategoriesAsync(); + var categoriesAllResponse = await _categoryRepository.GetCategoriesAsync(); List categories = new List(); - var categoriesPaginated = _paginationService.Paginate(res.Value, pageSize, page); + var categoriesPaginated = _paginationService.Paginate(categoriesAllResponse, pageSize, page); if (!categoriesPaginated.IsSuccess) { return Result>.PropagateError(categoriesPaginated); From 89e8ffcabac29f8a191d87b0bb5ad4dad0ead59d Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Thu, 3 Apr 2025 17:17:45 +0200 Subject: [PATCH 104/128] Add option to map data in `PaginatedData` --- .../Common/Pagination/Abstractions/IPaginationService.cs | 1 + .../TickAPI/Common/Pagination/Services/PaginationService.cs | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/TickAPI/TickAPI/Common/Pagination/Abstractions/IPaginationService.cs b/TickAPI/TickAPI/Common/Pagination/Abstractions/IPaginationService.cs index 4aec7fc..260effb 100644 --- a/TickAPI/TickAPI/Common/Pagination/Abstractions/IPaginationService.cs +++ b/TickAPI/TickAPI/Common/Pagination/Abstractions/IPaginationService.cs @@ -7,4 +7,5 @@ public interface IPaginationService { public Result GetPaginationDetails(ICollection collection, int pageSize); public Result> Paginate(ICollection collection, int pageSize, int page); + public PaginatedData MapData(PaginatedData source, Func mapFunction); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Pagination/Services/PaginationService.cs b/TickAPI/TickAPI/Common/Pagination/Services/PaginationService.cs index c7670ac..88e978b 100644 --- a/TickAPI/TickAPI/Common/Pagination/Services/PaginationService.cs +++ b/TickAPI/TickAPI/Common/Pagination/Services/PaginationService.cs @@ -55,4 +55,10 @@ public Result> Paginate(ICollection collection, int pageS return Result>.Success(paginatedData); } + + public PaginatedData MapData(PaginatedData source, Func mapFunction) + { + var newData = source.Data.Select(mapFunction).ToList(); + return new PaginatedData(newData, source.PageNumber, source.PageSize, source.HasNextPage, source.HasPreviousPage, source.PaginationDetails); + } } \ No newline at end of file From cfc08f74e14aa80a45d7b32086b785cff59a15b7 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Thu, 3 Apr 2025 17:32:06 +0200 Subject: [PATCH 105/128] Add test for `PaginationService.MapData` --- .../Services/PaginationServiceTests.cs | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/TickAPI/TickAPI.Tests/Common/Pagination/Services/PaginationServiceTests.cs b/TickAPI/TickAPI.Tests/Common/Pagination/Services/PaginationServiceTests.cs index e234b47..906f28c 100644 --- a/TickAPI/TickAPI.Tests/Common/Pagination/Services/PaginationServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Common/Pagination/Services/PaginationServiceTests.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Http; +using TickAPI.Common.Pagination.Responses; using TickAPI.Common.Pagination.Services; namespace TickAPI.Tests.Common.Pagination.Services; @@ -10,8 +11,10 @@ public class PaginationServiceTests [Fact] public void Paginate_WhenPageSizeNegative_ShouldReturnFailure() { + // Act var result = _paginationService.Paginate(new List(), -5, 0); + // Assert Assert.True(result.IsError); Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); Assert.Equal("'pageSize' param must be > 0, got: -5", result.ErrorMsg); @@ -20,8 +23,10 @@ public void Paginate_WhenPageSizeNegative_ShouldReturnFailure() [Fact] public void Paginate_WhenPageSizeZero_ShouldReturnFailure() { + // Act var result = _paginationService.Paginate(new List(), 0, 0); + // Assert Assert.True(result.IsError); Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); Assert.Equal("'pageSize' param must be > 0, got: 0", result.ErrorMsg); @@ -30,8 +35,10 @@ public void Paginate_WhenPageSizeZero_ShouldReturnFailure() [Fact] public void Paginate_WhenPageNegative_ShouldReturnFailure() { + // Act var result = _paginationService.Paginate(new List(), 1, -12); + // Assert Assert.True(result.IsError); Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); Assert.Equal("'page' param must be >= 0, got: -12", result.ErrorMsg); @@ -40,12 +47,15 @@ public void Paginate_WhenPageNegative_ShouldReturnFailure() [Fact] public void Paginate_WhenCollectionLengthSmallerThanPageSize_ShouldReturnAllElements() { + // Arrange var data = new List { 1, 2, 3, 4, 5 }; int pageSize = data.Count + 1; const int pageNumber = 0; + // Act var result = _paginationService.Paginate(data, pageSize, pageNumber); + // Assert Assert.True(result.IsSuccess); Assert.Equal(data, result.Value?.Data); Assert.Equal(pageNumber, result.Value?.PageNumber); @@ -59,12 +69,15 @@ public void Paginate_WhenCollectionLengthSmallerThanPageSize_ShouldReturnAllElem [Fact] public void Paginate_WhenCollectionLengthBiggerThanPageSize_ShouldReturnPartOfCollection() { + // Arrange var data = new List { 1, 2, 3, 4, 5 }; const int pageSize = 2; const int pageNumber = 0; + // Act var result = _paginationService.Paginate(data, pageSize, pageNumber); + // Assert Assert.True(result.IsSuccess); Assert.Equal(new List {1, 2}, result.Value?.Data); Assert.Equal(pageNumber, result.Value?.PageNumber); @@ -78,12 +91,15 @@ public void Paginate_WhenCollectionLengthBiggerThanPageSize_ShouldReturnPartOfCo [Fact] public void Paginate_WhenTakingElementsFromTheMiddle_ShouldReturnPaginatedDataWithBothBooleansTrue() { + // Arrange var data = new List { 1, 2, 3, 4, 5 }; const int pageSize = 2; const int pageNumber = 1; + // Act var result = _paginationService.Paginate(data, pageSize, pageNumber); + // Assert Assert.True(result.IsSuccess); Assert.Equal(new List {3, 4}, result.Value?.Data); Assert.Equal(pageNumber, result.Value?.PageNumber); @@ -97,12 +113,15 @@ public void Paginate_WhenTakingElementsFromTheMiddle_ShouldReturnPaginatedDataWi [Fact] public void Paginate_WhenExceededMaxPageNumber_ShouldReturnFailure() { + // Arrange var data = new List { 1, 2, 3, 4, 5 }; const int pageSize = 2; const int pageNumber = 3; + // Act var result = _paginationService.Paginate(data, pageSize, pageNumber); + // Assert Assert.True(result.IsError); Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); Assert.Equal("'page' param must be <= 2, got: 3", result.ErrorMsg); @@ -111,12 +130,15 @@ public void Paginate_WhenExceededMaxPageNumber_ShouldReturnFailure() [Fact] public void Paginate_WhenOnLastPage_ShouldReturnHasNextPageSetToFalse() { + // Arrange var data = new List { 1, 2, 3, 4, 5 }; const int pageSize = 2; const int pageNumber = 2; + // Act var result = _paginationService.Paginate(data, pageSize, pageNumber); + // Assert Assert.True(result.IsSuccess); Assert.Equal(new List() { 5 }, result.Value?.Data); Assert.Equal(pageNumber, result.Value?.PageNumber); @@ -130,12 +152,15 @@ public void Paginate_WhenOnLastPage_ShouldReturnHasNextPageSetToFalse() [Fact] public void Paginate_WhenCollectionEmptyAndFirstPageIsRequested_ShouldReturnSuccess() { + // Arrange var data = new List(); const int pageSize = 2; const int pageNumber = 0; + // Act var result = _paginationService.Paginate(data, pageSize, pageNumber); + // Assert Assert.True(result.IsSuccess); Assert.Equal(new List(), result.Value?.Data); Assert.Equal(pageNumber, result.Value?.PageNumber); @@ -145,4 +170,25 @@ public void Paginate_WhenCollectionEmptyAndFirstPageIsRequested_ShouldReturnSucc Assert.Equal(data.Count, result.Value?.PaginationDetails.AllElementsCount); Assert.Equal(0, result.Value?.PaginationDetails.MaxPageNumber); } + + [Fact] + public void MapData_ShouldApplyLambdaToEachObject() + { + // Arrange + var data = new List() {1,2,3,4,5}; + var paginatedData = new PaginatedData(data, 0, 5, true, false, new PaginationDetails(1, 10)); + Func lambda = i => i * 2; + var expectedData = new List() { 2, 4, 6, 8, 10 }; + + // Act + var result = _paginationService.MapData(paginatedData, lambda); + + // Assert + Assert.Equal(expectedData, result.Data); + Assert.Equal(paginatedData.PageNumber, result.PageNumber); + Assert.Equal(paginatedData.PageSize, result.PageSize); + Assert.Equal(paginatedData.HasPreviousPage, result.HasPreviousPage); + Assert.Equal(paginatedData.HasNextPage, result.HasNextPage); + Assert.Equal(paginatedData.PaginationDetails, result.PaginationDetails); + } } \ No newline at end of file From a4c7df0518a150a0938f4bfb901efdc2f2a90f4b Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Thu, 3 Apr 2025 17:32:48 +0200 Subject: [PATCH 106/128] Create DTOs for getting events as organizer --- .../DTOs/Response/GetEventResponseAddressDto.cs | 10 ++++++++++ .../DTOs/Response/GetEventResponseCategoryDto.cs | 5 +++++ .../Events/DTOs/Response/GetEventResponseDto.cs | 14 ++++++++++++++ 3 files changed, 29 insertions(+) create mode 100644 TickAPI/TickAPI/Events/DTOs/Response/GetEventResponseAddressDto.cs create mode 100644 TickAPI/TickAPI/Events/DTOs/Response/GetEventResponseCategoryDto.cs create mode 100644 TickAPI/TickAPI/Events/DTOs/Response/GetEventResponseDto.cs diff --git a/TickAPI/TickAPI/Events/DTOs/Response/GetEventResponseAddressDto.cs b/TickAPI/TickAPI/Events/DTOs/Response/GetEventResponseAddressDto.cs new file mode 100644 index 0000000..0f6f14f --- /dev/null +++ b/TickAPI/TickAPI/Events/DTOs/Response/GetEventResponseAddressDto.cs @@ -0,0 +1,10 @@ +namespace TickAPI.Events.DTOs.Response; + +public record GetEventResponseAddressDto( + string Country, + string City, + string PostalCode, + string? Stree, + uint? HouseNumber, + uint? FlatNumber +); diff --git a/TickAPI/TickAPI/Events/DTOs/Response/GetEventResponseCategoryDto.cs b/TickAPI/TickAPI/Events/DTOs/Response/GetEventResponseCategoryDto.cs new file mode 100644 index 0000000..4ba5d7a --- /dev/null +++ b/TickAPI/TickAPI/Events/DTOs/Response/GetEventResponseCategoryDto.cs @@ -0,0 +1,5 @@ +namespace TickAPI.Events.DTOs.Response; + +public record GetEventResponseCategoryDto( + string Name +); diff --git a/TickAPI/TickAPI/Events/DTOs/Response/GetEventResponseDto.cs b/TickAPI/TickAPI/Events/DTOs/Response/GetEventResponseDto.cs new file mode 100644 index 0000000..cafb0cc --- /dev/null +++ b/TickAPI/TickAPI/Events/DTOs/Response/GetEventResponseDto.cs @@ -0,0 +1,14 @@ +using TickAPI.Events.Models; + +namespace TickAPI.Events.DTOs.Response; + +public record GetEventResponseDto( + string Name, + string Description, + DateTime StartDate, + DateTime EndDate, + uint? Minimumage, + List Categories, + EventStatus Status, + GetEventResponseAddressDto Addres +); From f0117ac9ddc5536a41301eeb676b3b57ff9aed7d Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Thu, 3 Apr 2025 17:34:10 +0200 Subject: [PATCH 107/128] Add function for getting organizer events --- .../Events/Services/EventServiceTests.cs | 21 ++++++++------- .../Events/Abstractions/IEventService.cs | 5 +++- .../TickAPI/Events/Services/EventService.cs | 27 +++++++++++++++++-- 3 files changed, 40 insertions(+), 13 deletions(-) diff --git a/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs b/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs index 939104b..5205ff3 100644 --- a/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs @@ -1,18 +1,15 @@ -using System.Runtime.CompilerServices; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Http; using TickAPI.Events.Abstractions; -using TickAPI.Events.DTOs.Request; using Moq; using TickAPI.Addresses.Abstractions; using TickAPI.Addresses.DTOs.Request; using TickAPI.Addresses.Models; +using TickAPI.Common.Pagination.Abstractions; using TickAPI.Events.Models; using TickAPI.Organizers.Abstractions; using TickAPI.Organizers.Models; using TickAPI.Common.Results.Generic; using TickAPI.Common.Time.Abstractions; -using TickAPI.Events.DTOs.Response; using TickAPI.Events.Services; namespace TickAPI.Tests.Events.Services; @@ -59,7 +56,9 @@ public async Task CreateNewEventAsync_WhenEventDataIsValid_ShouldReturnNewEvent( var dateTimeServiceMock = new Mock(); dateTimeServiceMock.Setup(m => m.GetCurrentDateTime()).Returns(new DateTime(2003, 7, 11)); - var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, dateTimeServiceMock.Object); + var paginationServiceMock = new Mock(); + + var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, dateTimeServiceMock.Object, paginationServiceMock.Object); // act var result = await sut.CreateNewEventAsync(name, description, startDate, endDate, minimumAge, createAddress, eventStatus, organizerEmail); @@ -102,7 +101,9 @@ public async Task CreateNewEventAsync_WhenEndDateIsBeforeStartDate_ShouldReturnB var dateTimeServiceMock = new Mock(); - var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, dateTimeServiceMock.Object); + var paginationServiceMock = new Mock(); + + var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, dateTimeServiceMock.Object, paginationServiceMock.Object); // act var res = await sut.CreateNewEventAsync(name, description, startDate, endDate, minimumAge, createAddress, eventStatus, organizerEmail); @@ -139,7 +140,9 @@ public async Task CreateNewEventAsync_WhenStartDateIsBeforeNow_ShouldReturnBadRe var dateTimeServiceMock = new Mock(); dateTimeServiceMock.Setup(m => m.GetCurrentDateTime()).Returns(new DateTime(2025, 5, 11)); - var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, dateTimeServiceMock.Object); + var paginationServiceMock = new Mock(); + + var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, dateTimeServiceMock.Object, paginationServiceMock.Object); // act var res = await sut.CreateNewEventAsync(name, description, startDate, endDate, minimumAge, createAddress, eventStatus, organizerEmail); @@ -149,6 +152,4 @@ public async Task CreateNewEventAsync_WhenStartDateIsBeforeNow_ShouldReturnBadRe Assert.Equal(StatusCodes.Status400BadRequest, res.StatusCode); Assert.Equal("Start date is in the past", res.ErrorMsg); } - - } \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/Abstractions/IEventService.cs b/TickAPI/TickAPI/Events/Abstractions/IEventService.cs index 1a2e531..8aee8db 100644 --- a/TickAPI/TickAPI/Events/Abstractions/IEventService.cs +++ b/TickAPI/TickAPI/Events/Abstractions/IEventService.cs @@ -1,7 +1,9 @@ using TickAPI.Addresses.DTOs.Request; +using TickAPI.Common.Pagination.Responses; using TickAPI.Events.Models; using TickAPI.Common.Results.Generic; -using TickAPI.Events.DTOs.Request; +using TickAPI.Events.DTOs.Response; +using TickAPI.Organizers.Models; namespace TickAPI.Events.Abstractions; @@ -9,4 +11,5 @@ public interface IEventService { public Task> CreateNewEventAsync(string name, string description, DateTime startDate, DateTime endDate, uint? minimumAge, CreateAddressDto createAddress, EventStatus eventStatus, string organizerEmail); + public Result> GetOrganizerEvents(Organizer organizer, int page, int pageSize); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/Services/EventService.cs b/TickAPI/TickAPI/Events/Services/EventService.cs index 7f38590..30dafd2 100644 --- a/TickAPI/TickAPI/Events/Services/EventService.cs +++ b/TickAPI/TickAPI/Events/Services/EventService.cs @@ -1,11 +1,14 @@ using TickAPI.Addresses.Abstractions; using TickAPI.Addresses.DTOs.Request; +using TickAPI.Common.Pagination.Abstractions; +using TickAPI.Common.Pagination.Responses; using TickAPI.Common.Time.Abstractions; using TickAPI.Events.Abstractions; using TickAPI.Events.Models; using TickAPI.Common.Results.Generic; +using TickAPI.Events.DTOs.Response; using TickAPI.Organizers.Abstractions; -using TickAPI.Events.DTOs.Request; +using TickAPI.Organizers.Models; namespace TickAPI.Events.Services; @@ -15,13 +18,15 @@ public class EventService : IEventService private readonly IEventRepository _eventRepository; private readonly IAddressService _addressService; private readonly IDateTimeService _dateTimeService; + private readonly IPaginationService _paginationService; - public EventService(IEventRepository eventRepository, IOrganizerService organizerService, IAddressService addressService, IDateTimeService dateTimeService) + public EventService(IEventRepository eventRepository, IOrganizerService organizerService, IAddressService addressService, IDateTimeService dateTimeService, IPaginationService paginationService) { _eventRepository = eventRepository; _organizerService = organizerService; _addressService = addressService; _dateTimeService = dateTimeService; + _paginationService = paginationService; } public async Task> CreateNewEventAsync(string name, string description, DateTime startDate, DateTime endDate, uint? minimumAge, CreateAddressDto createAddress, EventStatus eventStatus, string organizerEmail) @@ -54,4 +59,22 @@ public async Task> CreateNewEventAsync(string name, string descri await _eventRepository.AddNewEventAsync(@event); return Result.Success(@event); } + + public Result> GetOrganizerEvents(Organizer organizer, int page, int pageSize) + { + var paginatedEventsResult = _paginationService.Paginate(organizer.Events, pageSize, page); + if (paginatedEventsResult.IsError) + { + return Result>.PropagateError(paginatedEventsResult); + } + + var paginatedData = _paginationService.MapData(paginatedEventsResult.Value!, ev => + { + var categories = ev.Categories.Select((c) => new GetEventResponseCategoryDto(c.CategoryName)).ToList(); + var address = new GetEventResponseAddressDto(ev.Address.Country, ev.Address.City, ev.Address.PostalCode, ev.Address.Street, ev.Address.HouseNumber, ev.Address.FlatNumber); + return new GetEventResponseDto(ev.Name, ev.Description, ev.StartDate, ev.EndDate, ev.MinimumAge, categories, ev.EventStatus, address); + }); + + return Result>.Success(paginatedData); + } } \ No newline at end of file From ff401f19bcaf3032032566414e72ff94af854098 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Thu, 3 Apr 2025 17:34:35 +0200 Subject: [PATCH 108/128] Setup endpoint for getting events as organizer --- .../Controllers/EventControllerTests.cs | 9 ++++-- .../Events/Controllers/EventController.cs | 29 +++++++++++++++++-- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs b/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs index 3853bb2..17c9e7d 100644 --- a/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs +++ b/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs @@ -10,6 +10,7 @@ using TickAPI.Events.Abstractions; using TickAPI.Common.Results.Generic; using TickAPI.Events.DTOs.Response; +using TickAPI.Organizers.Abstractions; namespace TickAPI.Tests.Events.Controllers; @@ -49,8 +50,10 @@ public async Task CreateEvent_WhenDataIsValid_ShouldReturnSuccess() var claimsServiceMock = new Mock(); claimsServiceMock.Setup(m => m.GetEmailFromClaims(controllerContext.HttpContext.User.Claims)).Returns(Result.Success(email)); + + var organizerServiceMock = new Mock(); - var sut = new EventController(eventServiceMock.Object, claimsServiceMock.Object); + var sut = new EventController(eventServiceMock.Object, claimsServiceMock.Object, organizerServiceMock.Object); sut.ControllerContext = controllerContext; @@ -81,7 +84,9 @@ public async Task CreateEvent_WhenMissingEmailClaims_ShouldReturnBadRequest() var claimsServiceMock = new Mock(); claimsServiceMock.Setup(m => m.GetEmailFromClaims(It.IsAny>())).Returns(Result.Failure(StatusCodes.Status400BadRequest, "missing email claim")); - var sut = new EventController(eventServiceMock.Object, claimsServiceMock.Object); + var organizerServiceMock = new Mock(); + + var sut = new EventController(eventServiceMock.Object, claimsServiceMock.Object, organizerServiceMock.Object); sut.ControllerContext = new ControllerContext { diff --git a/TickAPI/TickAPI/Events/Controllers/EventController.cs b/TickAPI/TickAPI/Events/Controllers/EventController.cs index f75f70d..52355f2 100644 --- a/TickAPI/TickAPI/Events/Controllers/EventController.cs +++ b/TickAPI/TickAPI/Events/Controllers/EventController.cs @@ -6,6 +6,7 @@ using TickAPI.Common.Claims.Abstractions; using TickAPI.Common.Pagination.Responses; using TickAPI.Events.Abstractions; +using TickAPI.Organizers.Abstractions; namespace TickAPI.Events.Controllers; @@ -17,11 +18,13 @@ public class EventController : ControllerBase { private readonly IEventService _eventService; private readonly IClaimsService _claimsService; + private readonly IOrganizerService _organizerService; - public EventController(IEventService eventService, IClaimsService claimsService) + public EventController(IEventService eventService, IClaimsService claimsService, IOrganizerService organizerService) { _eventService = eventService; _claimsService = claimsService; + _organizerService = organizerService; } [AuthorizeWithPolicy(AuthPolicies.VerifiedOrganizerPolicy)] @@ -45,8 +48,28 @@ public async Task> CreateEvent([FromBody] C [AuthorizeWithPolicy(AuthPolicies.VerifiedOrganizerPolicy)] [HttpGet("get-organizer-events")] - public async Task>> GetOrganizerEvents() + public async Task>> GetOrganizerEvents([FromQuery] int pageSize, [FromQuery] int page) { - throw new NotImplementedException(); + var emailResult = _claimsService.GetEmailFromClaims(User.Claims); + if (emailResult.IsError) + { + return StatusCode(emailResult.StatusCode, emailResult.ErrorMsg); + } + var email = emailResult.Value!; + + var organizerResult = await _organizerService.GetOrganizerByEmailAsync(email); + if (organizerResult.IsError) + { + return StatusCode(organizerResult.StatusCode, organizerResult.ErrorMsg); + } + var organizer = organizerResult.Value!; + + var paginatedDataResult = _eventService.GetOrganizerEvents(organizer, page, pageSize); + if (paginatedDataResult.IsError) + { + return StatusCode(paginatedDataResult.StatusCode, paginatedDataResult.ErrorMsg); + } + + return Ok(paginatedDataResult.Value!); } } \ No newline at end of file From feb09d8a3adee7cf9c0e2551e7f963d073d9c84e Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Thu, 3 Apr 2025 17:41:04 +0200 Subject: [PATCH 109/128] Add endpoint for getting organizer events pagination details --- .../Events/Abstractions/IEventService.cs | 1 + .../Events/Controllers/EventController.cs | 27 +++++++++++++++++++ .../TickAPI/Events/Services/EventService.cs | 5 ++++ 3 files changed, 33 insertions(+) diff --git a/TickAPI/TickAPI/Events/Abstractions/IEventService.cs b/TickAPI/TickAPI/Events/Abstractions/IEventService.cs index 8aee8db..a7876d8 100644 --- a/TickAPI/TickAPI/Events/Abstractions/IEventService.cs +++ b/TickAPI/TickAPI/Events/Abstractions/IEventService.cs @@ -12,4 +12,5 @@ public interface IEventService public Task> CreateNewEventAsync(string name, string description, DateTime startDate, DateTime endDate, uint? minimumAge, CreateAddressDto createAddress, EventStatus eventStatus, string organizerEmail); public Result> GetOrganizerEvents(Organizer organizer, int page, int pageSize); + public Result GetOrganizerEventsPaginationDetails(Organizer organizer, int pageSize); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/Controllers/EventController.cs b/TickAPI/TickAPI/Events/Controllers/EventController.cs index 52355f2..1b2cf50 100644 --- a/TickAPI/TickAPI/Events/Controllers/EventController.cs +++ b/TickAPI/TickAPI/Events/Controllers/EventController.cs @@ -72,4 +72,31 @@ public async Task>> GetOrganizer return Ok(paginatedDataResult.Value!); } + + [AuthorizeWithPolicy(AuthPolicies.VerifiedOrganizerPolicy)] + [HttpGet("get-organizer-events-pagination-details")] + public async Task> GetOrganizerEventsPaginationDetails([FromQuery] int pageSize) + { + var emailResult = _claimsService.GetEmailFromClaims(User.Claims); + if (emailResult.IsError) + { + return StatusCode(emailResult.StatusCode, emailResult.ErrorMsg); + } + var email = emailResult.Value!; + + var organizerResult = await _organizerService.GetOrganizerByEmailAsync(email); + if (organizerResult.IsError) + { + return StatusCode(organizerResult.StatusCode, organizerResult.ErrorMsg); + } + var organizer = organizerResult.Value!; + + var paginationDetailsResult = _eventService.GetOrganizerEventsPaginationDetails(organizer, pageSize); + if (paginationDetailsResult.IsError) + { + return StatusCode(paginationDetailsResult.StatusCode, paginationDetailsResult.ErrorMsg); + } + + return Ok(paginationDetailsResult.Value!); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/Services/EventService.cs b/TickAPI/TickAPI/Events/Services/EventService.cs index 30dafd2..498ac09 100644 --- a/TickAPI/TickAPI/Events/Services/EventService.cs +++ b/TickAPI/TickAPI/Events/Services/EventService.cs @@ -77,4 +77,9 @@ public Result> GetOrganizerEvents(Organizer o return Result>.Success(paginatedData); } + + public Result GetOrganizerEventsPaginationDetails(Organizer organizer, int pageSize) + { + return _paginationService.GetPaginationDetails(organizer.Events!, pageSize); + } } \ No newline at end of file From 4cf852c9a5957ca0af406f49b7bfd416d181cca3 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Thu, 3 Apr 2025 20:39:01 +0200 Subject: [PATCH 110/128] Add tests for `GetOrganizerEvents` --- .../Events/Services/EventServiceTests.cs | 173 ++++++++++++++++-- 1 file changed, 161 insertions(+), 12 deletions(-) diff --git a/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs b/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs index 5205ff3..e46c6ed 100644 --- a/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs @@ -4,12 +4,15 @@ using TickAPI.Addresses.Abstractions; using TickAPI.Addresses.DTOs.Request; using TickAPI.Addresses.Models; +using TickAPI.Categories.Models; using TickAPI.Common.Pagination.Abstractions; +using TickAPI.Common.Pagination.Responses; using TickAPI.Events.Models; using TickAPI.Organizers.Abstractions; using TickAPI.Organizers.Models; using TickAPI.Common.Results.Generic; using TickAPI.Common.Time.Abstractions; +using TickAPI.Events.DTOs.Response; using TickAPI.Events.Services; namespace TickAPI.Tests.Events.Services; @@ -20,7 +23,7 @@ public class EventServiceTests public async Task CreateNewEventAsync_WhenEventDataIsValid_ShouldReturnNewEvent() { - // arrange + // Arrange string name = "Concert"; string description = "Description of a concert"; DateTime startDate = new DateTime(2025, 5, 1); @@ -59,13 +62,11 @@ public async Task CreateNewEventAsync_WhenEventDataIsValid_ShouldReturnNewEvent( var paginationServiceMock = new Mock(); var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, dateTimeServiceMock.Object, paginationServiceMock.Object); - // act + // Act var result = await sut.CreateNewEventAsync(name, description, startDate, endDate, minimumAge, createAddress, eventStatus, organizerEmail); - // assert - - + // Assert Assert.True(result.IsSuccess); Assert.Equal(new DateTime(2025, 5, 1), result.Value!.StartDate); Assert.Equal(new DateTime(2025, 6, 1), result.Value!.EndDate); @@ -79,7 +80,7 @@ public async Task CreateNewEventAsync_WhenEventDataIsValid_ShouldReturnNewEvent( [Fact] public async Task CreateNewEventAsync_WhenEndDateIsBeforeStartDate_ShouldReturnBadRequest() { - // arrange + // Arrange string name = "Concert"; string description = "Description of a concert"; DateTime startDate = new DateTime(2025, 8, 1); @@ -104,11 +105,11 @@ public async Task CreateNewEventAsync_WhenEndDateIsBeforeStartDate_ShouldReturnB var paginationServiceMock = new Mock(); var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, dateTimeServiceMock.Object, paginationServiceMock.Object); - // act + // Act var res = await sut.CreateNewEventAsync(name, description, startDate, endDate, minimumAge, createAddress, eventStatus, organizerEmail); - // assert + // Assert Assert.False(res.IsSuccess); Assert.Equal(StatusCodes.Status400BadRequest, res.StatusCode); Assert.Equal("End date should be after start date", res.ErrorMsg); @@ -117,7 +118,7 @@ public async Task CreateNewEventAsync_WhenEndDateIsBeforeStartDate_ShouldReturnB [Fact] public async Task CreateNewEventAsync_WhenStartDateIsBeforeNow_ShouldReturnBadRequest() { - // arrange + // Arrange string name = "Concert"; string description = "Description of a concert"; DateTime startDate = new DateTime(2025, 5, 1); @@ -125,7 +126,6 @@ public async Task CreateNewEventAsync_WhenStartDateIsBeforeNow_ShouldReturnBadRe uint? minimumAge = 18; string organizerEmail = "123@mail.com"; EventStatus eventStatus = EventStatus.TicketsAvailable; - Guid id = Guid.NewGuid(); CreateAddressDto createAddress = new CreateAddressDto("United States", "New York", "Main st", 20, null, "00-000"); var eventRepositoryMock = new Mock(); @@ -143,13 +143,162 @@ public async Task CreateNewEventAsync_WhenStartDateIsBeforeNow_ShouldReturnBadRe var paginationServiceMock = new Mock(); var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, dateTimeServiceMock.Object, paginationServiceMock.Object); - // act + // Act var res = await sut.CreateNewEventAsync(name, description, startDate, endDate, minimumAge, createAddress, eventStatus, organizerEmail); - // assert + // Assert Assert.False(res.IsSuccess); Assert.Equal(StatusCodes.Status400BadRequest, res.StatusCode); Assert.Equal("Start date is in the past", res.ErrorMsg); } + + [Fact] + public void GetOrganizerEvents_WhenPaginationSucceeds_ShouldReturnPaginatedEvents() + { + // Arrange + var organizer = new Organizer + { + Email = "organizer@example.com", + IsVerified = true, + Events = new List + { + CreateSampleEvent("Event 1"), + CreateSampleEvent("Event 2"), + CreateSampleEvent("Event 3") + } + }; + int page = 0; + int pageSize = 2; + + var eventRepositoryMock = new Mock(); + var organizerServiceMock = new Mock(); + var addressServiceMock = new Mock(); + var dateTimeServiceMock = new Mock(); + var paginationServiceMock = new Mock(); + + var paginatedEvents = new PaginatedData( + organizer.Events.Take(pageSize).ToList(), + page, + pageSize, + true, + false, + new PaginationDetails(1, 3) + ); + + paginationServiceMock + .Setup(p => p.Paginate(organizer.Events, pageSize, page)) + .Returns(Result>.Success(paginatedEvents)); + + paginationServiceMock + .Setup(p => p.MapData(paginatedEvents, It.IsAny>())) + .Returns(new PaginatedData( + new List + { + CreateSampleEventResponseDto("Event 1"), + CreateSampleEventResponseDto("Event 2") + }, + page, + pageSize, + true, + false, + new PaginationDetails(1, 3) + )); + + var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, + dateTimeServiceMock.Object, paginationServiceMock.Object); + + // Act + var result = sut.GetOrganizerEvents(organizer, page, pageSize); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(2, result.Value!.Data.Count); + Assert.Equal("Event 1", result.Value!.Data[0].Name); + Assert.Equal("Event 2", result.Value!.Data[1].Name); + Assert.Equal(0, result.Value!.PageNumber); + Assert.Equal(2, result.Value!.PageSize); + Assert.True(result.Value!.HasNextPage); + Assert.False(result.Value!.HasPreviousPage); + Assert.Equal(1, result.Value!.PaginationDetails.MaxPageNumber); + Assert.Equal(3, result.Value!.PaginationDetails.AllElementsCount); + } + + [Fact] + public void GetOrganizerEvents_WhenPaginationFails_ShouldPropagateError() + { + // Arrange + var organizer = new Organizer + { + Email = "organizer@example.com", + IsVerified = true, + Events = new List + { + CreateSampleEvent("Event 1"), + CreateSampleEvent("Event 2") + } + }; + int page = 2; // Invalid page + int pageSize = 2; + + var eventRepositoryMock = new Mock(); + var organizerServiceMock = new Mock(); + var addressServiceMock = new Mock(); + var dateTimeServiceMock = new Mock(); + var paginationServiceMock = new Mock(); + + paginationServiceMock + .Setup(p => p.Paginate(organizer.Events, pageSize, page)) + .Returns(Result>.Failure(StatusCodes.Status400BadRequest, "Invalid page number")); + + var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, + dateTimeServiceMock.Object, paginationServiceMock.Object); + + // Act + var result = sut.GetOrganizerEvents(organizer, page, pageSize); + + // Assert + Assert.False(result.IsSuccess); + Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); + Assert.Equal("Invalid page number", result.ErrorMsg); + } + + // Helper methods + private Event CreateSampleEvent(string name) + { + return new Event + { + Id = Guid.NewGuid(), + Name = name, + Description = $"Description of {name}", + StartDate = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc), + EndDate = new DateTime(1970, 1, 2, 0, 0, 0, DateTimeKind.Utc), + MinimumAge = 18, + EventStatus = EventStatus.TicketsAvailable, + Categories = new List { new Category { CategoryName = "Test" } }, + Address = new Address + { + Country = "United States", + City = "New York", + PostalCode = "10001", + Street = "Main St", + HouseNumber = 123, + FlatNumber = null + } + }; + } + + private GetEventResponseDto CreateSampleEventResponseDto(string name) + { + return new GetEventResponseDto( + name, + $"Description of {name}", + new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc), + new DateTime(1970, 1, 2, 0, 0, 0, DateTimeKind.Utc), + 18, + [new GetEventResponseCategoryDto("Test")], + EventStatus.TicketsAvailable, + new GetEventResponseAddressDto("United States", "New York", "10001", "Main St", 123, null) + ); + } } \ No newline at end of file From c1fdb6d17eb408ba37aed0ad00dce033b769c93f Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Thu, 3 Apr 2025 20:53:22 +0200 Subject: [PATCH 111/128] Move helpers to `Utils` class --- .../Events/Services/EventServiceTests.cs | 53 +++---------------- TickAPI/TickAPI.Tests/Events/Utils.cs | 47 ++++++++++++++++ 2 files changed, 54 insertions(+), 46 deletions(-) create mode 100644 TickAPI/TickAPI.Tests/Events/Utils.cs diff --git a/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs b/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs index e46c6ed..d9e0d7d 100644 --- a/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs @@ -163,9 +163,9 @@ public void GetOrganizerEvents_WhenPaginationSucceeds_ShouldReturnPaginatedEvent IsVerified = true, Events = new List { - CreateSampleEvent("Event 1"), - CreateSampleEvent("Event 2"), - CreateSampleEvent("Event 3") + Utils.CreateSampleEvent("Event 1"), + Utils.CreateSampleEvent("Event 2"), + Utils.CreateSampleEvent("Event 3") } }; int page = 0; @@ -195,8 +195,8 @@ public void GetOrganizerEvents_WhenPaginationSucceeds_ShouldReturnPaginatedEvent .Returns(new PaginatedData( new List { - CreateSampleEventResponseDto("Event 1"), - CreateSampleEventResponseDto("Event 2") + Utils.CreateSampleEventResponseDto("Event 1"), + Utils.CreateSampleEventResponseDto("Event 2") }, page, pageSize, @@ -234,8 +234,8 @@ public void GetOrganizerEvents_WhenPaginationFails_ShouldPropagateError() IsVerified = true, Events = new List { - CreateSampleEvent("Event 1"), - CreateSampleEvent("Event 2") + Utils.CreateSampleEvent("Event 1"), + Utils.CreateSampleEvent("Event 2") } }; int page = 2; // Invalid page @@ -262,43 +262,4 @@ public void GetOrganizerEvents_WhenPaginationFails_ShouldPropagateError() Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); Assert.Equal("Invalid page number", result.ErrorMsg); } - - // Helper methods - private Event CreateSampleEvent(string name) - { - return new Event - { - Id = Guid.NewGuid(), - Name = name, - Description = $"Description of {name}", - StartDate = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc), - EndDate = new DateTime(1970, 1, 2, 0, 0, 0, DateTimeKind.Utc), - MinimumAge = 18, - EventStatus = EventStatus.TicketsAvailable, - Categories = new List { new Category { CategoryName = "Test" } }, - Address = new Address - { - Country = "United States", - City = "New York", - PostalCode = "10001", - Street = "Main St", - HouseNumber = 123, - FlatNumber = null - } - }; - } - - private GetEventResponseDto CreateSampleEventResponseDto(string name) - { - return new GetEventResponseDto( - name, - $"Description of {name}", - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc), - new DateTime(1970, 1, 2, 0, 0, 0, DateTimeKind.Utc), - 18, - [new GetEventResponseCategoryDto("Test")], - EventStatus.TicketsAvailable, - new GetEventResponseAddressDto("United States", "New York", "10001", "Main St", 123, null) - ); - } } \ No newline at end of file diff --git a/TickAPI/TickAPI.Tests/Events/Utils.cs b/TickAPI/TickAPI.Tests/Events/Utils.cs new file mode 100644 index 0000000..252568a --- /dev/null +++ b/TickAPI/TickAPI.Tests/Events/Utils.cs @@ -0,0 +1,47 @@ +using TickAPI.Addresses.Models; +using TickAPI.Categories.Models; +using TickAPI.Events.DTOs.Response; +using TickAPI.Events.Models; + +namespace TickAPI.Tests.Events; + +public static class Utils +{ + public static Event CreateSampleEvent(string name) + { + return new Event + { + Id = Guid.NewGuid(), + Name = name, + Description = $"Description of {name}", + StartDate = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc), + EndDate = new DateTime(1970, 1, 2, 0, 0, 0, DateTimeKind.Utc), + MinimumAge = 18, + EventStatus = EventStatus.TicketsAvailable, + Categories = new List { new Category { CategoryName = "Test" } }, + Address = new Address + { + Country = "United States", + City = "New York", + PostalCode = "10001", + Street = "Main St", + HouseNumber = 123, + FlatNumber = null + } + }; + } + + public static GetEventResponseDto CreateSampleEventResponseDto(string name) + { + return new GetEventResponseDto( + name, + $"Description of {name}", + new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc), + new DateTime(1970, 1, 2, 0, 0, 0, DateTimeKind.Utc), + 18, + [new GetEventResponseCategoryDto("Test")], + EventStatus.TicketsAvailable, + new GetEventResponseAddressDto("United States", "New York", "10001", "Main St", 123, null) + ); + } +} From ca3ea72c67fc81c335a62c8494df14eddfc58279 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Thu, 3 Apr 2025 20:54:59 +0200 Subject: [PATCH 112/128] Add tests for `GetOrganizerEvents` --- .../Controllers/EventControllerTests.cs | 236 ++++++++++++++++-- 1 file changed, 221 insertions(+), 15 deletions(-) diff --git a/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs b/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs index 17c9e7d..1b60012 100644 --- a/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs +++ b/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs @@ -6,11 +6,13 @@ using Microsoft.AspNetCore.Mvc; using TickAPI.Addresses.DTOs.Request; using TickAPI.Common.Claims.Abstractions; +using TickAPI.Common.Pagination.Responses; using TickAPI.Events.Controllers; using TickAPI.Events.Abstractions; using TickAPI.Common.Results.Generic; using TickAPI.Events.DTOs.Response; using TickAPI.Organizers.Abstractions; +using TickAPI.Organizers.Models; namespace TickAPI.Tests.Events.Controllers; @@ -19,14 +21,14 @@ public class EventControllerTests [Fact] public async Task CreateEvent_WhenDataIsValid_ShouldReturnSuccess() { - //arrange - string name = "Concert"; - string description = "Description of a concert"; + // Arrange + const string name = "Concert"; + const string description = "Description of a concert"; DateTime startDate = new DateTime(2025, 5, 1); DateTime endDate = new DateTime(2025, 6, 1); uint? minimumAge = 18; - string email = "123@mail.com"; - EventStatus eventStatus = EventStatus.TicketsAvailable; + const string email = "123@mail.com"; + const EventStatus eventStatus = EventStatus.TicketsAvailable; Guid id = Guid.NewGuid(); CreateAddressDto createAddress = new CreateAddressDto("United States", "New York", "Main st", 20, null, "00-000"); CreateEventDto eventDto = new CreateEventDto(name, description, startDate, endDate, minimumAge, eventStatus, createAddress); @@ -57,10 +59,10 @@ public async Task CreateEvent_WhenDataIsValid_ShouldReturnSuccess() sut.ControllerContext = controllerContext; - // act + // Act var res = await sut.CreateEvent(eventDto); - // assert + // Assert var result = Assert.IsType>(res); var objectResult = Assert.IsType(result.Result); Assert.Equal(200, objectResult.StatusCode); @@ -70,14 +72,13 @@ public async Task CreateEvent_WhenDataIsValid_ShouldReturnSuccess() [Fact] public async Task CreateEvent_WhenMissingEmailClaims_ShouldReturnBadRequest() { - //arrange - string name = "Concert"; - string description = "Description of a concert"; + // Arrange + const string name = "Concert"; + const string description = "Description of a concert"; DateTime startDate = new DateTime(2025, 5, 1); DateTime endDate = new DateTime(2025, 6, 1); uint? minimumAge = 18; - string email = "123@mail.com"; - EventStatus eventStatus = EventStatus.TicketsAvailable; + const EventStatus eventStatus = EventStatus.TicketsAvailable; CreateAddressDto createAddress = new CreateAddressDto("United States", "New York", "Main st", 20, null, "00-000"); var eventServiceMock = new Mock(); @@ -96,14 +97,219 @@ public async Task CreateEvent_WhenMissingEmailClaims_ShouldReturnBadRequest() } }; - // act + // Act var res = await sut.CreateEvent(new CreateEventDto(name, description, startDate, endDate, minimumAge, eventStatus, createAddress)); - // assert + // Assert var result = Assert.IsType>(res); var objectResult = Assert.IsType(result.Result); Assert.Equal(StatusCodes.Status400BadRequest, objectResult.StatusCode); Assert.Equal("missing email claim", objectResult.Value); - + } + + [Fact] + public async Task GetOrganizerEvents_WhenAllOperationsSucceed_ShouldReturnOkWithPaginatedData() + { + // Arrange + const string email = "organizer@example.com"; + const int page = 0; + const int pageSize = 10; + + var claims = new List + { + new Claim(ClaimTypes.Email, email) + }; + + var controllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity(claims)) + } + }; + + var claimsServiceMock = new Mock(); + claimsServiceMock + .Setup(m => m.GetEmailFromClaims(controllerContext.HttpContext.User.Claims)) + .Returns(Result.Success(email)); + + var organizer = new Organizer { Email = email, IsVerified = true }; + + var organizerServiceMock = new Mock(); + organizerServiceMock + .Setup(m => m.GetOrganizerByEmailAsync(email)) + .ReturnsAsync(Result.Success(organizer)); + + var paginatedData = new PaginatedData( + new List + { + Utils.CreateSampleEventResponseDto("Event 1"), + Utils.CreateSampleEventResponseDto("Event 2") + }, + page, + pageSize, + false, + false, + new PaginationDetails(0, 2) + ); + + var eventServiceMock = new Mock(); + eventServiceMock + .Setup(m => m.GetOrganizerEvents(organizer, page, pageSize)) + .Returns(Result>.Success(paginatedData)); + + var sut = new EventController(eventServiceMock.Object, claimsServiceMock.Object, organizerServiceMock.Object); + sut.ControllerContext = controllerContext; + + // Act + var response = await sut.GetOrganizerEvents(pageSize, page); + + // Assert + var result = Assert.IsType>>(response); + var okResult = Assert.IsType(result.Result); + Assert.Equal(StatusCodes.Status200OK, okResult.StatusCode); + + var returnedPaginatedData = Assert.IsType>(okResult.Value); + Assert.Equal(2, returnedPaginatedData.Data.Count); + Assert.Equal(paginatedData.Data[0], returnedPaginatedData.Data[0]); + Assert.Equal(paginatedData.Data[1], returnedPaginatedData.Data[1]); + Assert.Equal(page, returnedPaginatedData.PageNumber); + Assert.Equal(pageSize, returnedPaginatedData.PageSize); + Assert.False(returnedPaginatedData.HasNextPage); + Assert.False(returnedPaginatedData.HasPreviousPage); + } + + [Fact] + public async Task GetOrganizerEvents_WhenEmailClaimIsMissing_ShouldReturnBadRequest() + { + // Arrange + const int page = 0; + const int pageSize = 10; + const string errorMessage = "Missing email claim"; + + var claimsServiceMock = new Mock(); + claimsServiceMock + .Setup(m => m.GetEmailFromClaims(It.IsAny>())) + .Returns(Result.Failure(StatusCodes.Status400BadRequest, errorMessage)); + + var eventServiceMock = new Mock(); + var organizerServiceMock = new Mock(); + + var sut = new EventController(eventServiceMock.Object, claimsServiceMock.Object, organizerServiceMock.Object); + sut.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity()) + } + }; + + // Act + var response = await sut.GetOrganizerEvents(pageSize, page); + + // Assert + var result = Assert.IsType>>(response); + var objectResult = Assert.IsType(result.Result); + Assert.Equal(StatusCodes.Status400BadRequest, objectResult.StatusCode); + Assert.Equal(errorMessage, objectResult.Value); + } + + [Fact] + public async Task GetOrganizerEvents_WhenOrganizerIsNotFound_ShouldReturnNotFound() + { + // Arrange + const string email = "organizer@example.com"; + const int page = 0; + const int pageSize = 10; + const string errorMessage = "Organizer not found"; + + var claims = new List + { + new Claim(ClaimTypes.Email, email) + }; + + var controllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity(claims)) + } + }; + + var claimsServiceMock = new Mock(); + claimsServiceMock + .Setup(m => m.GetEmailFromClaims(controllerContext.HttpContext.User.Claims)) + .Returns(Result.Success(email)); + + var organizerServiceMock = new Mock(); + organizerServiceMock + .Setup(m => m.GetOrganizerByEmailAsync(email)) + .ReturnsAsync(Result.Failure(StatusCodes.Status404NotFound, errorMessage)); + + var eventServiceMock = new Mock(); + + var sut = new EventController(eventServiceMock.Object, claimsServiceMock.Object, organizerServiceMock.Object); + sut.ControllerContext = controllerContext; + + // Act + var response = await sut.GetOrganizerEvents(pageSize, page); + + // Assert + var result = Assert.IsType>>(response); + var objectResult = Assert.IsType(result.Result); + Assert.Equal(StatusCodes.Status404NotFound, objectResult.StatusCode); + Assert.Equal(errorMessage, objectResult.Value); + } + + [Fact] + public async Task GetOrganizerEvents_WhenPaginationFails_ShouldReturnBadRequest() + { + // Arrange + const string email = "organizer@example.com"; + const int page = -1; // Invalid page + const int pageSize = 10; + const string errorMessage = "Invalid page number"; + + var claims = new List + { + new Claim(ClaimTypes.Email, email) + }; + + var controllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity(claims)) + } + }; + + var claimsServiceMock = new Mock(); + claimsServiceMock + .Setup(m => m.GetEmailFromClaims(controllerContext.HttpContext.User.Claims)) + .Returns(Result.Success(email)); + + var organizer = new Organizer { Email = email, IsVerified = true }; + + var organizerServiceMock = new Mock(); + organizerServiceMock + .Setup(m => m.GetOrganizerByEmailAsync(email)) + .ReturnsAsync(Result.Success(organizer)); + + var eventServiceMock = new Mock(); + eventServiceMock + .Setup(m => m.GetOrganizerEvents(organizer, page, pageSize)) + .Returns(Result>.Failure(StatusCodes.Status400BadRequest, errorMessage)); + + var sut = new EventController(eventServiceMock.Object, claimsServiceMock.Object, organizerServiceMock.Object); + sut.ControllerContext = controllerContext; + + // Act + var response = await sut.GetOrganizerEvents(pageSize, page); + + // Assert + var result = Assert.IsType>>(response); + var objectResult = Assert.IsType(result.Result); + Assert.Equal(StatusCodes.Status400BadRequest, objectResult.StatusCode); + Assert.Equal(errorMessage, objectResult.Value); } } \ No newline at end of file From 68f78870eb0eafd99696b5301303e3d3a44098e7 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Thu, 3 Apr 2025 20:57:08 +0200 Subject: [PATCH 113/128] Remove unnecessary import --- TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs b/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs index d9e0d7d..7c3272a 100644 --- a/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs @@ -4,7 +4,6 @@ using TickAPI.Addresses.Abstractions; using TickAPI.Addresses.DTOs.Request; using TickAPI.Addresses.Models; -using TickAPI.Categories.Models; using TickAPI.Common.Pagination.Abstractions; using TickAPI.Common.Pagination.Responses; using TickAPI.Events.Models; From 3b7f975ebcacce74435b8bf526632c3d6f557f10 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Thu, 3 Apr 2025 21:02:01 +0200 Subject: [PATCH 114/128] Add tests for `GetOrganizerEventsPaginationDetails` --- .../Controllers/EventControllerTests.cs | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs b/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs index 1b60012..326878c 100644 --- a/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs +++ b/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs @@ -312,4 +312,110 @@ public async Task GetOrganizerEvents_WhenPaginationFails_ShouldReturnBadRequest( Assert.Equal(StatusCodes.Status400BadRequest, objectResult.StatusCode); Assert.Equal(errorMessage, objectResult.Value); } + + [Fact] + public async Task GetOrganizerEventsPaginationDetails_WhenAllOperationsSucceed_ShouldReturnOkWithPaginationDetails() + { + // Arrange + const string email = "organizer@example.com"; + const int pageSize = 10; + + var claims = new List + { + new Claim(ClaimTypes.Email, email) + }; + + var controllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity(claims)) + } + }; + + var claimsServiceMock = new Mock(); + claimsServiceMock + .Setup(m => m.GetEmailFromClaims(controllerContext.HttpContext.User.Claims)) + .Returns(Result.Success(email)); + + var organizer = new Organizer { Email = email, IsVerified = true }; + + var organizerServiceMock = new Mock(); + organizerServiceMock + .Setup(m => m.GetOrganizerByEmailAsync(email)) + .ReturnsAsync(Result.Success(organizer)); + + var paginationDetails = new PaginationDetails(2, 25); + + var eventServiceMock = new Mock(); + eventServiceMock + .Setup(m => m.GetOrganizerEventsPaginationDetails(organizer, pageSize)) + .Returns(Result.Success(paginationDetails)); + + var sut = new EventController(eventServiceMock.Object, claimsServiceMock.Object, organizerServiceMock.Object); + sut.ControllerContext = controllerContext; + + // Act + var response = await sut.GetOrganizerEventsPaginationDetails(pageSize); + + // Assert + var result = Assert.IsType>(response); + var okResult = Assert.IsType(result.Result); + Assert.Equal(StatusCodes.Status200OK, okResult.StatusCode); + + var returnedPaginationDetails = Assert.IsType(okResult.Value); + Assert.Equal(2, returnedPaginationDetails.MaxPageNumber); + Assert.Equal(25, returnedPaginationDetails.AllElementsCount); + } + + [Fact] + public async Task GetOrganizerEventsPaginationDetails_WhenPaginationDetailsFails_ShouldReturnBadRequest() + { + // Arrange + const string email = "organizer@example.com"; + const int pageSize = -1; + const string errorMessage = "Invalid page size"; + + var claims = new List + { + new Claim(ClaimTypes.Email, email) + }; + + var controllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity(claims)) + } + }; + + var claimsServiceMock = new Mock(); + claimsServiceMock + .Setup(m => m.GetEmailFromClaims(controllerContext.HttpContext.User.Claims)) + .Returns(Result.Success(email)); + + var organizer = new Organizer { Email = email, IsVerified = true }; + + var organizerServiceMock = new Mock(); + organizerServiceMock + .Setup(m => m.GetOrganizerByEmailAsync(email)) + .ReturnsAsync(Result.Success(organizer)); + + var eventServiceMock = new Mock(); + eventServiceMock + .Setup(m => m.GetOrganizerEventsPaginationDetails(organizer, pageSize)) + .Returns(Result.Failure(StatusCodes.Status400BadRequest, errorMessage)); + + var sut = new EventController(eventServiceMock.Object, claimsServiceMock.Object, organizerServiceMock.Object); + sut.ControllerContext = controllerContext; + + // Act + var response = await sut.GetOrganizerEventsPaginationDetails(pageSize); + + // Assert + var result = Assert.IsType>(response); + var objectResult = Assert.IsType(result.Result); + Assert.Equal(StatusCodes.Status400BadRequest, objectResult.StatusCode); + Assert.Equal(errorMessage, objectResult.Value); + } } \ No newline at end of file From b32e0991543a1e17c4a64f5caae19e5cf6b2a8d6 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Sat, 5 Apr 2025 23:32:24 +0200 Subject: [PATCH 115/128] Use `MapData` instead of manually mapping paginated data --- .../TickAPI/Categories/Services/CategoryService.cs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/TickAPI/TickAPI/Categories/Services/CategoryService.cs b/TickAPI/TickAPI/Categories/Services/CategoryService.cs index dbbfcd8..ee26d68 100644 --- a/TickAPI/TickAPI/Categories/Services/CategoryService.cs +++ b/TickAPI/TickAPI/Categories/Services/CategoryService.cs @@ -20,21 +20,15 @@ public CategoryService(ICategoryRepository categoryRepository, IPaginationServi public async Task>> GetCategoriesResponsesAsync(int pageSize, int page) { - var categoriesAllResponse = await _categoryRepository.GetCategoriesAsync(); - List categories = new List(); + var categoriesAllResponse = await _categoryRepository.GetCategoriesAsync(); var categoriesPaginated = _paginationService.Paginate(categoriesAllResponse, pageSize, page); if (!categoriesPaginated.IsSuccess) { return Result>.PropagateError(categoriesPaginated); } - foreach (var category in categoriesPaginated.Value.Data) - { - categories.Add(new GetCategoryResponseDto(category.CategoryName)); - } + var categoriesResponse = _paginationService.MapData(categoriesPaginated.Value!, (c) => new GetCategoryResponseDto(c.CategoryName)); - return Result>.Success(new PaginatedData(categories, categoriesPaginated.Value.PageNumber - ,categoriesPaginated.Value.PageSize, categoriesPaginated.Value.HasNextPage, categoriesPaginated.Value.HasPreviousPage, - categoriesPaginated.Value.PaginationDetails)); + return Result>.Success(categoriesResponse); } } \ No newline at end of file From 265b487784d4677159f86dbc9d79e535c4e33827 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Sat, 5 Apr 2025 23:42:03 +0200 Subject: [PATCH 116/128] Remove redundant type specifier --- TickAPI/TickAPI/Categories/Services/CategoryService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TickAPI/TickAPI/Categories/Services/CategoryService.cs b/TickAPI/TickAPI/Categories/Services/CategoryService.cs index ee26d68..f94c78d 100644 --- a/TickAPI/TickAPI/Categories/Services/CategoryService.cs +++ b/TickAPI/TickAPI/Categories/Services/CategoryService.cs @@ -21,7 +21,7 @@ public CategoryService(ICategoryRepository categoryRepository, IPaginationServi public async Task>> GetCategoriesResponsesAsync(int pageSize, int page) { var categoriesAllResponse = await _categoryRepository.GetCategoriesAsync(); - var categoriesPaginated = _paginationService.Paginate(categoriesAllResponse, pageSize, page); + var categoriesPaginated = _paginationService.Paginate(categoriesAllResponse, pageSize, page); if (!categoriesPaginated.IsSuccess) { return Result>.PropagateError(categoriesPaginated); From 64d9faca6b801caefe530b55e3759d5bada10be7 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Sun, 6 Apr 2025 00:27:03 +0200 Subject: [PATCH 117/128] Fix typos --- .../Categories/DTOs/Response/GetCategoryResponseDto.cs | 4 +--- .../Events/DTOs/Response/GetEventResponseAddressDto.cs | 2 +- TickAPI/TickAPI/Events/DTOs/Response/GetEventResponseDto.cs | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/TickAPI/TickAPI/Categories/DTOs/Response/GetCategoryResponseDto.cs b/TickAPI/TickAPI/Categories/DTOs/Response/GetCategoryResponseDto.cs index 9ece0df..c2376db 100644 --- a/TickAPI/TickAPI/Categories/DTOs/Response/GetCategoryResponseDto.cs +++ b/TickAPI/TickAPI/Categories/DTOs/Response/GetCategoryResponseDto.cs @@ -1,6 +1,4 @@ -using TickAPI.Common.Pagination.Responses; - -namespace TickAPI.Categories.DTOs.Response; +namespace TickAPI.Categories.DTOs.Response; public record GetCategoryResponseDto( string CategoryName diff --git a/TickAPI/TickAPI/Events/DTOs/Response/GetEventResponseAddressDto.cs b/TickAPI/TickAPI/Events/DTOs/Response/GetEventResponseAddressDto.cs index 0f6f14f..8ade67d 100644 --- a/TickAPI/TickAPI/Events/DTOs/Response/GetEventResponseAddressDto.cs +++ b/TickAPI/TickAPI/Events/DTOs/Response/GetEventResponseAddressDto.cs @@ -4,7 +4,7 @@ public record GetEventResponseAddressDto( string Country, string City, string PostalCode, - string? Stree, + string? Street, uint? HouseNumber, uint? FlatNumber ); diff --git a/TickAPI/TickAPI/Events/DTOs/Response/GetEventResponseDto.cs b/TickAPI/TickAPI/Events/DTOs/Response/GetEventResponseDto.cs index cafb0cc..41d1698 100644 --- a/TickAPI/TickAPI/Events/DTOs/Response/GetEventResponseDto.cs +++ b/TickAPI/TickAPI/Events/DTOs/Response/GetEventResponseDto.cs @@ -7,7 +7,7 @@ public record GetEventResponseDto( string Description, DateTime StartDate, DateTime EndDate, - uint? Minimumage, + uint? MinimumAge, List Categories, EventStatus Status, GetEventResponseAddressDto Addres From 63ec2ab27ada63e783ddbc86b7cbb63b33f887a4 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Sun, 6 Apr 2025 00:56:35 +0200 Subject: [PATCH 118/128] added basic category creation pipeline --- TickAPI/TickAPI.Tests/Events/Utils.cs | 2 +- .../Abstractions/ICategoryRepository.cs | 2 ++ .../Abstractions/ICategoryService.cs | 2 ++ .../Controllers/CategoryController.cs | 19 +++++++++++--- .../Categories/DTOs/CreateCategoryDto.cs | 5 ++++ TickAPI/TickAPI/Categories/Models/Category.cs | 2 +- .../Respositories/CategoryRepository.cs | 18 +++++++++++++ .../Categories/Services/CategoryService.cs | 26 ++++++++++++++++++- .../TickAPI/Events/Services/EventService.cs | 2 +- 9 files changed, 70 insertions(+), 8 deletions(-) create mode 100644 TickAPI/TickAPI/Categories/DTOs/CreateCategoryDto.cs diff --git a/TickAPI/TickAPI.Tests/Events/Utils.cs b/TickAPI/TickAPI.Tests/Events/Utils.cs index 252568a..8e5c0c8 100644 --- a/TickAPI/TickAPI.Tests/Events/Utils.cs +++ b/TickAPI/TickAPI.Tests/Events/Utils.cs @@ -18,7 +18,7 @@ public static Event CreateSampleEvent(string name) EndDate = new DateTime(1970, 1, 2, 0, 0, 0, DateTimeKind.Utc), MinimumAge = 18, EventStatus = EventStatus.TicketsAvailable, - Categories = new List { new Category { CategoryName = "Test" } }, + Categories = new List { new Category { Name = "Test" } }, Address = new Address { Country = "United States", diff --git a/TickAPI/TickAPI/Categories/Abstractions/ICategoryRepository.cs b/TickAPI/TickAPI/Categories/Abstractions/ICategoryRepository.cs index 5b6254f..a4b7236 100644 --- a/TickAPI/TickAPI/Categories/Abstractions/ICategoryRepository.cs +++ b/TickAPI/TickAPI/Categories/Abstractions/ICategoryRepository.cs @@ -6,4 +6,6 @@ namespace TickAPI.Categories.Abstractions; public interface ICategoryRepository { public Task> GetCategoriesAsync(); + public Task> GetCategoryByNameAsync(string categoryName); + public Task AddNewCategoryAsync(Category category); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Categories/Abstractions/ICategoryService.cs b/TickAPI/TickAPI/Categories/Abstractions/ICategoryService.cs index 37e0f24..0e33bf0 100644 --- a/TickAPI/TickAPI/Categories/Abstractions/ICategoryService.cs +++ b/TickAPI/TickAPI/Categories/Abstractions/ICategoryService.cs @@ -7,5 +7,7 @@ namespace TickAPI.Categories.Abstractions; public interface ICategoryService { + public Task> GetCategoryByNameAsync(string categoryName); public Task>> GetCategoriesResponsesAsync(int pageSize, int page); + public Task> CreateNewCategoryAsync(string categoryName); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Categories/Controllers/CategoryController.cs b/TickAPI/TickAPI/Categories/Controllers/CategoryController.cs index 20f4d94..2cfdbf6 100644 --- a/TickAPI/TickAPI/Categories/Controllers/CategoryController.cs +++ b/TickAPI/TickAPI/Categories/Controllers/CategoryController.cs @@ -1,11 +1,11 @@ using Microsoft.AspNetCore.Mvc; using TickAPI.Categories.Abstractions; +using TickAPI.Categories.DTOs; using TickAPI.Categories.DTOs.Response; using TickAPI.Common.Auth.Attributes; using TickAPI.Common.Auth.Enums; using TickAPI.Common.Pagination.Responses; - namespace TickAPI.Categories.Controllers; [ApiController] @@ -13,7 +13,6 @@ namespace TickAPI.Categories.Controllers; public class CategoryController : Controller { - private readonly ICategoryService _categoryService; public CategoryController(ICategoryService categoryService) @@ -22,14 +21,26 @@ public CategoryController(ICategoryService categoryService) } [AuthorizeWithPolicy(AuthPolicies.VerifiedUserPolicy)] - [HttpPost("get-categories")] + [HttpGet("get-categories")] public async Task>> GetCategories([FromQuery] int pageSize, [FromQuery] int page) { var res = await _categoryService.GetCategoriesResponsesAsync(pageSize, page); - if (!res.IsSuccess) + if (res.IsError) { return StatusCode(StatusCodes.Status500InternalServerError, res.ErrorMsg); } return Ok(res.Value); } + + // TODO: Add appropriate policy verification (admin, maybe also organizer?) + [HttpPost("create-category")] + public async Task CreateCategory([FromBody] CreateCategoryDto request) + { + var newCategoryResult = await _categoryService.CreateNewCategoryAsync(request.Name); + + if(newCategoryResult.IsError) + return StatusCode(newCategoryResult.StatusCode, newCategoryResult.ErrorMsg); + + return Ok(); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Categories/DTOs/CreateCategoryDto.cs b/TickAPI/TickAPI/Categories/DTOs/CreateCategoryDto.cs new file mode 100644 index 0000000..620d49f --- /dev/null +++ b/TickAPI/TickAPI/Categories/DTOs/CreateCategoryDto.cs @@ -0,0 +1,5 @@ +namespace TickAPI.Categories.DTOs; + +public record CreateCategoryDto( + string Name +); \ No newline at end of file diff --git a/TickAPI/TickAPI/Categories/Models/Category.cs b/TickAPI/TickAPI/Categories/Models/Category.cs index 053c6e1..5c71ac2 100644 --- a/TickAPI/TickAPI/Categories/Models/Category.cs +++ b/TickAPI/TickAPI/Categories/Models/Category.cs @@ -5,6 +5,6 @@ namespace TickAPI.Categories.Models; public class Category { public Guid Id { get; set; } - public string CategoryName { get; set; } + public string Name { get; set; } public ICollection Events { get; set; } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Categories/Respositories/CategoryRepository.cs b/TickAPI/TickAPI/Categories/Respositories/CategoryRepository.cs index 9cba1d3..f7a599f 100644 --- a/TickAPI/TickAPI/Categories/Respositories/CategoryRepository.cs +++ b/TickAPI/TickAPI/Categories/Respositories/CategoryRepository.cs @@ -19,4 +19,22 @@ public async Task> GetCategoriesAsync() var list = await _tickApiDbContext.Categories.ToListAsync(); return list; } + + public async Task> GetCategoryByNameAsync(string categoryName) + { + var category = await _tickApiDbContext.Categories.FirstOrDefaultAsync(c => c.Name == categoryName); + + if (category == null) + { + return Result.Failure(StatusCodes.Status404NotFound, $"category with name '{categoryName}' not found"); + } + + return Result.Success(category); + } + + public async Task AddNewCategoryAsync(Category category) + { + _tickApiDbContext.Categories.Add(category); + await _tickApiDbContext.SaveChangesAsync(); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Categories/Services/CategoryService.cs b/TickAPI/TickAPI/Categories/Services/CategoryService.cs index f94c78d..55db6b7 100644 --- a/TickAPI/TickAPI/Categories/Services/CategoryService.cs +++ b/TickAPI/TickAPI/Categories/Services/CategoryService.cs @@ -18,6 +18,11 @@ public CategoryService(ICategoryRepository categoryRepository, IPaginationServi _paginationService = paginationService; } + public Task> GetCategoryByNameAsync(string categoryName) + { + return _categoryRepository.GetCategoryByNameAsync(categoryName); + } + public async Task>> GetCategoriesResponsesAsync(int pageSize, int page) { var categoriesAllResponse = await _categoryRepository.GetCategoriesAsync(); @@ -27,8 +32,27 @@ public async Task>> GetCategoriesRe return Result>.PropagateError(categoriesPaginated); } - var categoriesResponse = _paginationService.MapData(categoriesPaginated.Value!, (c) => new GetCategoryResponseDto(c.CategoryName)); + var categoriesResponse = _paginationService.MapData(categoriesPaginated.Value!, (c) => new GetCategoryResponseDto(c.Name)); return Result>.Success(categoriesResponse); } + + public async Task> CreateNewCategoryAsync(string categoryName) + { + var alreadyExistingResult = await _categoryRepository.GetCategoryByNameAsync(categoryName); + + if (alreadyExistingResult.IsSuccess) + { + return Result.Failure(StatusCodes.Status500InternalServerError, + $"category with name {categoryName} already exists"); + } + + var category = new Category() + { + Name = categoryName + }; + + await _categoryRepository.AddNewCategoryAsync(category); + return Result.Success(category); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/Services/EventService.cs b/TickAPI/TickAPI/Events/Services/EventService.cs index 498ac09..0043914 100644 --- a/TickAPI/TickAPI/Events/Services/EventService.cs +++ b/TickAPI/TickAPI/Events/Services/EventService.cs @@ -70,7 +70,7 @@ public Result> GetOrganizerEvents(Organizer o var paginatedData = _paginationService.MapData(paginatedEventsResult.Value!, ev => { - var categories = ev.Categories.Select((c) => new GetEventResponseCategoryDto(c.CategoryName)).ToList(); + var categories = ev.Categories.Select((c) => new GetEventResponseCategoryDto(c.Name)).ToList(); var address = new GetEventResponseAddressDto(ev.Address.Country, ev.Address.City, ev.Address.PostalCode, ev.Address.Street, ev.Address.HouseNumber, ev.Address.FlatNumber); return new GetEventResponseDto(ev.Name, ev.Description, ev.StartDate, ev.EndDate, ev.MinimumAge, categories, ev.EventStatus, address); }); From 400e39f549bce812652830021ac8bd32c97a84e6 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Sun, 6 Apr 2025 00:57:01 +0200 Subject: [PATCH 119/128] seeded some categories and added an appropriate migration --- .../TickApiDbContext/TickApiDbContext.cs | 38 ++ .../20250405225108_SeedCategories.Designer.cs | 389 ++++++++++++++++++ .../20250405225108_SeedCategories.cs | 74 ++++ .../TickApiDbContextModelSnapshot.cs | 34 +- 4 files changed, 534 insertions(+), 1 deletion(-) create mode 100644 TickAPI/TickAPI/Migrations/20250405225108_SeedCategories.Designer.cs create mode 100644 TickAPI/TickAPI/Migrations/20250405225108_SeedCategories.cs diff --git a/TickAPI/TickAPI/Common/TickApiDbContext/TickApiDbContext.cs b/TickAPI/TickAPI/Common/TickApiDbContext/TickApiDbContext.cs index 9837186..6a13d40 100644 --- a/TickAPI/TickAPI/Common/TickApiDbContext/TickApiDbContext.cs +++ b/TickAPI/TickAPI/Common/TickApiDbContext/TickApiDbContext.cs @@ -24,4 +24,42 @@ public TickApiDbContext(DbContextOptions options) : base(optio public DbSet TicketTypes { get; set; } public DbSet
Addresses { get; set; } public DbSet Categories { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity().HasData( + new Category + { + Id = Guid.Parse("ec3daf69-baa9-4fcd-a674-c09884a57272"), + Name = "Music" + }, + new Category + { + Id = Guid.Parse("de89dd76-3b29-43e1-8f4b-5278b1b8bde2"), + Name = "Sports" + }, + new Category + { + Id = Guid.Parse("ea58370b-2a17-4770-abea-66399ad69fb8"), + Name = "Conferences" + }, + new Category + { + Id = Guid.Parse("4a086d9e-59de-4fd1-a1b2-bd9b5eec797c"), + Name = "Theatre" + }, + new Category + { + Id = Guid.Parse("5f8dbe65-30be-453f-8f22-191a11b2977b"), + Name = "Comedy" + }, + new Category + { + Id = Guid.Parse("4421327a-4bc8-4706-bec0-666f78ed0c69"), + Name = "Workshops" + } + ); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Migrations/20250405225108_SeedCategories.Designer.cs b/TickAPI/TickAPI/Migrations/20250405225108_SeedCategories.Designer.cs new file mode 100644 index 0000000..476547a --- /dev/null +++ b/TickAPI/TickAPI/Migrations/20250405225108_SeedCategories.Designer.cs @@ -0,0 +1,389 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using TickAPI.Common.TickApiDbContext; + +#nullable disable + +namespace TickAPI.Migrations +{ + [DbContext(typeof(TickApiDbContext))] + [Migration("20250405225108_SeedCategories")] + partial class SeedCategories + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("CategoryEvent", b => + { + b.Property("CategoriesId") + .HasColumnType("uniqueidentifier"); + + b.Property("EventsId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("CategoriesId", "EventsId"); + + b.HasIndex("EventsId"); + + b.ToTable("CategoryEvent"); + }); + + modelBuilder.Entity("TickAPI.Addresses.Models.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("City") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Country") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FlatNumber") + .HasColumnType("bigint"); + + b.Property("HouseNumber") + .HasColumnType("bigint"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Street") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Addresses"); + }); + + modelBuilder.Entity("TickAPI.Admins.Models.Admin", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Login") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Admins"); + }); + + modelBuilder.Entity("TickAPI.Categories.Models.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + + b.HasData( + new + { + Id = new Guid("ec3daf69-baa9-4fcd-a674-c09884a57272"), + Name = "Music" + }, + new + { + Id = new Guid("de89dd76-3b29-43e1-8f4b-5278b1b8bde2"), + Name = "Sports" + }, + new + { + Id = new Guid("ea58370b-2a17-4770-abea-66399ad69fb8"), + Name = "Conferences" + }, + new + { + Id = new Guid("4a086d9e-59de-4fd1-a1b2-bd9b5eec797c"), + Name = "Theatre" + }, + new + { + Id = new Guid("5f8dbe65-30be-453f-8f22-191a11b2977b"), + Name = "Comedy" + }, + new + { + Id = new Guid("4421327a-4bc8-4706-bec0-666f78ed0c69"), + Name = "Workshops" + }); + }); + + modelBuilder.Entity("TickAPI.Customers.Models.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AddressId") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("EventStatus") + .HasColumnType("int"); + + b.Property("MinimumAge") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OrganizerId") + .HasColumnType("uniqueidentifier"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("AddressId"); + + b.HasIndex("OrganizerId"); + + b.ToTable("Events"); + }); + + modelBuilder.Entity("TickAPI.Organizers.Models.Organizer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsVerified") + .HasColumnType("bit"); + + b.Property("LastName") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Organizers"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AvailableFrom") + .HasColumnType("datetime2"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EventId") + .HasColumnType("uniqueidentifier"); + + b.Property("MaxCount") + .HasColumnType("bigint"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("EventId"); + + b.ToTable("TicketTypes"); + }); + + modelBuilder.Entity("TickAPI.Tickets.Models.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ForResell") + .HasColumnType("bit"); + + b.Property("NameOnTicket") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("uniqueidentifier"); + + b.Property("Seats") + .HasColumnType("nvarchar(max)"); + + b.Property("TypeId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("TypeId"); + + b.ToTable("Tickets"); + }); + + modelBuilder.Entity("CategoryEvent", b => + { + b.HasOne("TickAPI.Categories.Models.Category", null) + .WithMany() + .HasForeignKey("CategoriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TickAPI.Events.Models.Event", null) + .WithMany() + .HasForeignKey("EventsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.HasOne("TickAPI.Addresses.Models.Address", "Address") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TickAPI.Organizers.Models.Organizer", "Organizer") + .WithMany("Events") + .HasForeignKey("OrganizerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Address"); + + b.Navigation("Organizer"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.HasOne("TickAPI.Events.Models.Event", "Event") + .WithMany("TicketTypes") + .HasForeignKey("EventId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Event"); + }); + + modelBuilder.Entity("TickAPI.Tickets.Models.Ticket", b => + { + b.HasOne("TickAPI.Customers.Models.Customer", "Owner") + .WithMany("Tickets") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TickAPI.TicketTypes.Models.TicketType", "Type") + .WithMany("Tickets") + .HasForeignKey("TypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("Type"); + }); + + modelBuilder.Entity("TickAPI.Customers.Models.Customer", b => + { + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.Navigation("TicketTypes"); + }); + + modelBuilder.Entity("TickAPI.Organizers.Models.Organizer", b => + { + b.Navigation("Events"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.Navigation("Tickets"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TickAPI/TickAPI/Migrations/20250405225108_SeedCategories.cs b/TickAPI/TickAPI/Migrations/20250405225108_SeedCategories.cs new file mode 100644 index 0000000..7e4a868 --- /dev/null +++ b/TickAPI/TickAPI/Migrations/20250405225108_SeedCategories.cs @@ -0,0 +1,74 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace TickAPI.Migrations +{ + /// + public partial class SeedCategories : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "CategoryName", + table: "Categories", + newName: "Name"); + + migrationBuilder.InsertData( + table: "Categories", + columns: new[] { "Id", "Name" }, + values: new object[,] + { + { new Guid("4421327a-4bc8-4706-bec0-666f78ed0c69"), "Workshops" }, + { new Guid("4a086d9e-59de-4fd1-a1b2-bd9b5eec797c"), "Theatre" }, + { new Guid("5f8dbe65-30be-453f-8f22-191a11b2977b"), "Comedy" }, + { new Guid("de89dd76-3b29-43e1-8f4b-5278b1b8bde2"), "Sports" }, + { new Guid("ea58370b-2a17-4770-abea-66399ad69fb8"), "Conferences" }, + { new Guid("ec3daf69-baa9-4fcd-a674-c09884a57272"), "Music" } + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DeleteData( + table: "Categories", + keyColumn: "Id", + keyValue: new Guid("4421327a-4bc8-4706-bec0-666f78ed0c69")); + + migrationBuilder.DeleteData( + table: "Categories", + keyColumn: "Id", + keyValue: new Guid("4a086d9e-59de-4fd1-a1b2-bd9b5eec797c")); + + migrationBuilder.DeleteData( + table: "Categories", + keyColumn: "Id", + keyValue: new Guid("5f8dbe65-30be-453f-8f22-191a11b2977b")); + + migrationBuilder.DeleteData( + table: "Categories", + keyColumn: "Id", + keyValue: new Guid("de89dd76-3b29-43e1-8f4b-5278b1b8bde2")); + + migrationBuilder.DeleteData( + table: "Categories", + keyColumn: "Id", + keyValue: new Guid("ea58370b-2a17-4770-abea-66399ad69fb8")); + + migrationBuilder.DeleteData( + table: "Categories", + keyColumn: "Id", + keyValue: new Guid("ec3daf69-baa9-4fcd-a674-c09884a57272")); + + migrationBuilder.RenameColumn( + name: "Name", + table: "Categories", + newName: "CategoryName"); + } + } +} diff --git a/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs b/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs index ca59a48..8866990 100644 --- a/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs +++ b/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs @@ -94,13 +94,45 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("uniqueidentifier"); - b.Property("CategoryName") + b.Property("Name") .IsRequired() .HasColumnType("nvarchar(max)"); b.HasKey("Id"); b.ToTable("Categories"); + + b.HasData( + new + { + Id = new Guid("ec3daf69-baa9-4fcd-a674-c09884a57272"), + Name = "Music" + }, + new + { + Id = new Guid("de89dd76-3b29-43e1-8f4b-5278b1b8bde2"), + Name = "Sports" + }, + new + { + Id = new Guid("ea58370b-2a17-4770-abea-66399ad69fb8"), + Name = "Conferences" + }, + new + { + Id = new Guid("4a086d9e-59de-4fd1-a1b2-bd9b5eec797c"), + Name = "Theatre" + }, + new + { + Id = new Guid("5f8dbe65-30be-453f-8f22-191a11b2977b"), + Name = "Comedy" + }, + new + { + Id = new Guid("4421327a-4bc8-4706-bec0-666f78ed0c69"), + Name = "Workshops" + }); }); modelBuilder.Entity("TickAPI.Customers.Models.Customer", b => From 291491a0144faa335d455598cc42b20caeb4f5d7 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Sun, 6 Apr 2025 01:41:05 +0200 Subject: [PATCH 120/128] added tests for more category service and controller methods --- .../Controllers/CategoryControllerTests.cs | 33 +++++- .../Services/CategoryServiceTests.cs | 112 +++++++++++++++++- .../Controllers/CategoryController.cs | 2 +- .../Categories/Services/CategoryService.cs | 2 +- 4 files changed, 137 insertions(+), 12 deletions(-) diff --git a/TickAPI/TickAPI.Tests/Categories/Controllers/CategoryControllerTests.cs b/TickAPI/TickAPI.Tests/Categories/Controllers/CategoryControllerTests.cs index fdce5df..7a0c8f0 100644 --- a/TickAPI/TickAPI.Tests/Categories/Controllers/CategoryControllerTests.cs +++ b/TickAPI/TickAPI.Tests/Categories/Controllers/CategoryControllerTests.cs @@ -2,7 +2,9 @@ using Moq; using TickAPI.Categories.Abstractions; using TickAPI.Categories.Controllers; +using TickAPI.Categories.DTOs; using TickAPI.Categories.DTOs.Response; +using TickAPI.Categories.Models; using TickAPI.Common.Pagination.Responses; using TickAPI.Common.Results; using TickAPI.Common.Results.Generic; @@ -13,10 +15,9 @@ namespace TickAPI.Tests.Categories.Controllers; public class CategoryControllerTests { [Fact] - public async Task GetCategories_WhenDataIsValid_ShouldReturnOk() { - //arrange + // Arrange int pageSize = 20; int pageNumber = 0; var categoryServiceMock = new Mock(); @@ -26,14 +27,36 @@ public async Task GetCategories_WhenDataIsValid_ShouldReturnOk() var sut = new CategoryController(categoryServiceMock.Object); - //act + // Act var res = await sut.GetCategories(pageSize, pageNumber); - //assert + // Assert var result = Assert.IsType>>(res); var objectResult = Assert.IsType(result.Result); Assert.Equal(200, objectResult.StatusCode); Assert.NotNull(objectResult.Value); } - + + [Fact] + public async Task CreateCategory_WhenDataIsValid_ShouldReturnSuccess() + { + // Arrange + const string categoryName = "TestCategory"; + var createCategoryDto = new CreateCategoryDto(categoryName); + + var categoryServiceMock = new Mock(); + categoryServiceMock + .Setup(m => m.CreateNewCategoryAsync(categoryName)) + .ReturnsAsync(Result.Success(new Category())); + + var sut = new CategoryController(categoryServiceMock.Object); + + // Act + var res = await sut.CreateCategory(createCategoryDto); + + // Assert + var objectResult = Assert.IsType(res); + Assert.Equal(200, objectResult.StatusCode); + Assert.Equal("category created successfully", objectResult.Value); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI.Tests/Categories/Services/CategoryServiceTests.cs b/TickAPI/TickAPI.Tests/Categories/Services/CategoryServiceTests.cs index 0b43f6d..2a33d9e 100644 --- a/TickAPI/TickAPI.Tests/Categories/Services/CategoryServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Categories/Services/CategoryServiceTests.cs @@ -14,10 +14,9 @@ namespace TickAPI.Tests.Categories.Services; public class CategoryServiceTests { [Fact] - - public async Task GetCategories_WhenDataIsValid_ShouldReturnOk() + public async Task GetCategoriesResponsesAsync_WhenDataIsValid_ShouldReturnOk() { - //arrange + // Arrange int pageSize = 10; int page = 0; var categoryRepositoryMock = new Mock(); @@ -32,11 +31,114 @@ public async Task GetCategories_WhenDataIsValid_ShouldReturnOk() var sut = new CategoryService(categoryRepositoryMock.Object, paginationServiceMock.Object); - //act + // Act var res = await sut.GetCategoriesResponsesAsync(pageSize, page); - //assert + // Assert var result = Assert.IsType>>(res); Assert.True(result.IsSuccess); } + + [Fact] + public async Task GetCategoryByNameAsync_WhenCategoryWithNameIsReturnedFromRepository_ShouldReturnSuccess() + { + // Arrange + const string categoryName = "TestCategory"; + + var category = new Category() + { + Name = categoryName + }; + + var categoryRepositoryMock = new Mock(); + categoryRepositoryMock + .Setup(m => m.GetCategoryByNameAsync(categoryName)) + .ReturnsAsync(Result.Success(category)); + + var paginationServiceMock = new Mock(); + + var sut = new CategoryService(categoryRepositoryMock.Object, paginationServiceMock.Object); + + // Act + var res = await sut.GetCategoryByNameAsync(categoryName); + + // Assert + Assert.True(res.IsSuccess); + Assert.Equal(categoryName, res.Value!.Name); + } + + [Fact] + public async Task GetCategoryByNameAsync_WhenCategoryWithNameIsNotReturnedFromRepository_ShouldReturnFailure() + { + // Arrange + const string categoryName = "TestCategory"; + const string errorMsg = $"category with name '{categoryName}' not found"; + const int statusCode = 404; + + var categoryRepositoryMock = new Mock(); + categoryRepositoryMock + .Setup(m => m.GetCategoryByNameAsync(categoryName)) + .ReturnsAsync(Result.Failure(statusCode, errorMsg)); + + var paginationServiceMock = new Mock(); + + var sut = new CategoryService(categoryRepositoryMock.Object, paginationServiceMock.Object); + + // Act + var res = await sut.GetCategoryByNameAsync(categoryName); + + // Assert + Assert.True(res.IsError); + Assert.Equal(errorMsg, res.ErrorMsg); + Assert.Equal(statusCode, res.StatusCode); + } + + [Fact] + public async Task CreateNewCategoryAsync_WhenCategoryDataIsValid_ShouldReturnNewCategory() + { + // Arrange + const string categoryName = "TestCategory"; + const string errorMsg = $"category with name '{categoryName}' not found"; + const int statusCode = 404; + + var categoryRepositoryMock = new Mock(); + categoryRepositoryMock + .Setup(m => m.GetCategoryByNameAsync(categoryName)) + .ReturnsAsync(Result.Failure(statusCode, errorMsg)); + + var paginationServiceMock = new Mock(); + + var sut = new CategoryService(categoryRepositoryMock.Object, paginationServiceMock.Object); + + // Act + var res = await sut.CreateNewCategoryAsync(categoryName); + + // Assert + Assert.True(res.IsSuccess); + Assert.Equal(categoryName, res.Value!.Name); + } + + [Fact] + public async Task CreateNewCategoryAsync_WhenWithNotUniqueName_ShouldReturnFailure() + { + // Arrange + const string categoryName = "TestCategory"; + + var categoryRepositoryMock = new Mock(); + categoryRepositoryMock + .Setup(m => m.GetCategoryByNameAsync(categoryName)) + .ReturnsAsync(Result.Success(new Category())); + + var paginationServiceMock = new Mock(); + + var sut = new CategoryService(categoryRepositoryMock.Object, paginationServiceMock.Object); + + // Act + var res = await sut.CreateNewCategoryAsync(categoryName); + + // Assert + Assert.True(res.IsError); + Assert.Equal(500, res.StatusCode); + Assert.Equal($"category with name '{categoryName}' already exists", res.ErrorMsg); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Categories/Controllers/CategoryController.cs b/TickAPI/TickAPI/Categories/Controllers/CategoryController.cs index 2cfdbf6..e7d7469 100644 --- a/TickAPI/TickAPI/Categories/Controllers/CategoryController.cs +++ b/TickAPI/TickAPI/Categories/Controllers/CategoryController.cs @@ -41,6 +41,6 @@ public async Task CreateCategory([FromBody] CreateCategoryDto requ if(newCategoryResult.IsError) return StatusCode(newCategoryResult.StatusCode, newCategoryResult.ErrorMsg); - return Ok(); + return Ok("category created successfully"); } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Categories/Services/CategoryService.cs b/TickAPI/TickAPI/Categories/Services/CategoryService.cs index 55db6b7..e68e8a4 100644 --- a/TickAPI/TickAPI/Categories/Services/CategoryService.cs +++ b/TickAPI/TickAPI/Categories/Services/CategoryService.cs @@ -44,7 +44,7 @@ public async Task> CreateNewCategoryAsync(string categoryName) if (alreadyExistingResult.IsSuccess) { return Result.Failure(StatusCodes.Status500InternalServerError, - $"category with name {categoryName} already exists"); + $"category with name '{categoryName}' already exists"); } var category = new Category() From 5a727fb61935d9e3a637cd5d062d2ca707e116a5 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Sun, 6 Apr 2025 12:58:37 +0200 Subject: [PATCH 121/128] implemented fixes suggested by kTrzcinskii --- .../TickAPI.Tests/Categories/Services/CategoryServiceTests.cs | 4 ++-- TickAPI/TickAPI/Categories/Services/CategoryService.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/TickAPI/TickAPI.Tests/Categories/Services/CategoryServiceTests.cs b/TickAPI/TickAPI.Tests/Categories/Services/CategoryServiceTests.cs index 2a33d9e..7f5e84f 100644 --- a/TickAPI/TickAPI.Tests/Categories/Services/CategoryServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Categories/Services/CategoryServiceTests.cs @@ -64,7 +64,7 @@ public async Task GetCategoryByNameAsync_WhenCategoryWithNameIsReturnedFromRepos // Assert Assert.True(res.IsSuccess); - Assert.Equal(categoryName, res.Value!.Name); + Assert.Equal(category, res.Value); } [Fact] @@ -138,7 +138,7 @@ public async Task CreateNewCategoryAsync_WhenWithNotUniqueName_ShouldReturnFailu // Assert Assert.True(res.IsError); - Assert.Equal(500, res.StatusCode); + Assert.Equal(400, res.StatusCode); Assert.Equal($"category with name '{categoryName}' already exists", res.ErrorMsg); } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Categories/Services/CategoryService.cs b/TickAPI/TickAPI/Categories/Services/CategoryService.cs index e68e8a4..2048c09 100644 --- a/TickAPI/TickAPI/Categories/Services/CategoryService.cs +++ b/TickAPI/TickAPI/Categories/Services/CategoryService.cs @@ -43,7 +43,7 @@ public async Task> CreateNewCategoryAsync(string categoryName) if (alreadyExistingResult.IsSuccess) { - return Result.Failure(StatusCodes.Status500InternalServerError, + return Result.Failure(StatusCodes.Status400BadRequest, $"category with name '{categoryName}' already exists"); } From 1f2cf71ea0ce9194b0800268d7b731b0ec66f008 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Sun, 6 Apr 2025 17:03:12 +0200 Subject: [PATCH 122/128] Update `PaginationService` to use `IQueryable` instead of `ICollection` --- .../Abstractions/IPaginationService.cs | 4 ++-- .../Pagination/Services/PaginationService.cs | 19 +++++++++++-------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/TickAPI/TickAPI/Common/Pagination/Abstractions/IPaginationService.cs b/TickAPI/TickAPI/Common/Pagination/Abstractions/IPaginationService.cs index 260effb..a6fe99e 100644 --- a/TickAPI/TickAPI/Common/Pagination/Abstractions/IPaginationService.cs +++ b/TickAPI/TickAPI/Common/Pagination/Abstractions/IPaginationService.cs @@ -5,7 +5,7 @@ namespace TickAPI.Common.Pagination.Abstractions; public interface IPaginationService { - public Result GetPaginationDetails(ICollection collection, int pageSize); - public Result> Paginate(ICollection collection, int pageSize, int page); + public Task> GetPaginationDetailsAsync(IQueryable collection, int pageSize); + public Task>> PaginateAsync(IQueryable collection, int pageSize, int page); public PaginatedData MapData(PaginatedData source, Func mapFunction); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Pagination/Services/PaginationService.cs b/TickAPI/TickAPI/Common/Pagination/Services/PaginationService.cs index 88e978b..6b47acf 100644 --- a/TickAPI/TickAPI/Common/Pagination/Services/PaginationService.cs +++ b/TickAPI/TickAPI/Common/Pagination/Services/PaginationService.cs @@ -1,4 +1,6 @@ -using TickAPI.Common.Pagination.Abstractions; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Query; +using TickAPI.Common.Pagination.Abstractions; using TickAPI.Common.Pagination.Responses; using TickAPI.Common.Results.Generic; @@ -6,14 +8,14 @@ namespace TickAPI.Common.Pagination.Services; public class PaginationService : IPaginationService { - public Result GetPaginationDetails(ICollection collection, int pageSize) + public async Task> GetPaginationDetailsAsync(IQueryable collection, int pageSize) { if (pageSize <= 0) { return Result.Failure(StatusCodes.Status400BadRequest, $"'pageSize' param must be > 0, got: {pageSize}"); } - - var allElementsCount = collection.Count; + + var allElementsCount = collection.Provider is IAsyncQueryProvider ? await collection.CountAsync() : collection.Count(); var maxPageNumber = Math.Max((int)Math.Ceiling(1.0 * allElementsCount / pageSize) - 1, 0); var paginationDetails = new PaginationDetails(maxPageNumber, allElementsCount); @@ -21,7 +23,7 @@ public Result GetPaginationDetails(ICollection collecti return Result.Success(paginationDetails); } - public Result> Paginate(ICollection collection, int pageSize, int page) + public async Task>> PaginateAsync(IQueryable collection, int pageSize, int page) { if (pageSize <= 0) { @@ -33,7 +35,7 @@ public Result> Paginate(ICollection collection, int pageS return Result>.Failure(StatusCodes.Status400BadRequest, $"'page' param must be >= 0, got: {page}"); } - var paginationDetailsResult = GetPaginationDetails(collection, pageSize); + var paginationDetailsResult = await GetPaginationDetailsAsync(collection, pageSize); if (paginationDetailsResult.IsError) { return Result>.PropagateError(paginationDetailsResult); @@ -46,8 +48,9 @@ public Result> Paginate(ICollection collection, int pageS return Result>.Failure(StatusCodes.Status400BadRequest, $"'page' param must be <= {paginationDetails.MaxPageNumber}, got: {page}"); } - - var data = collection.Skip(page * pageSize).Take(pageSize).ToList(); + + var paginatedQuery = collection.Skip(page * pageSize).Take(pageSize); + var data = collection.Provider is IAsyncQueryProvider ? await paginatedQuery.ToListAsync() : paginatedQuery.ToList(); var hasPreviousPage = page > 0; var hasNextPage = page < paginationDetails.MaxPageNumber; From 49f4ecc33be34be9c092b213d9df510f32a4b4f7 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Sun, 6 Apr 2025 17:03:34 +0200 Subject: [PATCH 123/128] Update tests for `PaginationService` --- .../Services/PaginationServiceTests.cs | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/TickAPI/TickAPI.Tests/Common/Pagination/Services/PaginationServiceTests.cs b/TickAPI/TickAPI.Tests/Common/Pagination/Services/PaginationServiceTests.cs index 906f28c..2f6ede0 100644 --- a/TickAPI/TickAPI.Tests/Common/Pagination/Services/PaginationServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Common/Pagination/Services/PaginationServiceTests.cs @@ -9,10 +9,10 @@ public class PaginationServiceTests private readonly PaginationService _paginationService = new(); [Fact] - public void Paginate_WhenPageSizeNegative_ShouldReturnFailure() + public async Task Paginate_WhenPageSizeNegative_ShouldReturnFailure() { // Act - var result = _paginationService.Paginate(new List(), -5, 0); + var result = await _paginationService.PaginateAsync(new List().AsQueryable(), -5, 0); // Assert Assert.True(result.IsError); @@ -21,10 +21,10 @@ public void Paginate_WhenPageSizeNegative_ShouldReturnFailure() } [Fact] - public void Paginate_WhenPageSizeZero_ShouldReturnFailure() + public async Task Paginate_WhenPageSizeZero_ShouldReturnFailure() { // Act - var result = _paginationService.Paginate(new List(), 0, 0); + var result = await _paginationService.PaginateAsync(new List().AsQueryable(), 0, 0); // Assert Assert.True(result.IsError); @@ -33,10 +33,10 @@ public void Paginate_WhenPageSizeZero_ShouldReturnFailure() } [Fact] - public void Paginate_WhenPageNegative_ShouldReturnFailure() + public async Task Paginate_WhenPageNegative_ShouldReturnFailure() { // Act - var result = _paginationService.Paginate(new List(), 1, -12); + var result = await _paginationService.PaginateAsync(new List().AsQueryable(), 1, -12); // Assert Assert.True(result.IsError); @@ -45,15 +45,15 @@ public void Paginate_WhenPageNegative_ShouldReturnFailure() } [Fact] - public void Paginate_WhenCollectionLengthSmallerThanPageSize_ShouldReturnAllElements() + public async Task Paginate_WhenCollectionLengthSmallerThanPageSize_ShouldReturnAllElements() { // Arrange - var data = new List { 1, 2, 3, 4, 5 }; - int pageSize = data.Count + 1; + var data = new List { 1, 2, 3, 4, 5 }.AsQueryable(); + int pageSize = data.Count() + 1; const int pageNumber = 0; // Act - var result = _paginationService.Paginate(data, pageSize, pageNumber); + var result = await _paginationService.PaginateAsync(data, pageSize, pageNumber); // Assert Assert.True(result.IsSuccess); @@ -62,20 +62,20 @@ public void Paginate_WhenCollectionLengthSmallerThanPageSize_ShouldReturnAllElem Assert.Equal(pageSize, result.Value?.PageSize); Assert.False(result.Value?.HasNextPage); Assert.False(result.Value?.HasPreviousPage); - Assert.Equal(data.Count, result.Value?.PaginationDetails.AllElementsCount); + Assert.Equal(data.Count(), result.Value?.PaginationDetails.AllElementsCount); Assert.Equal(0, result.Value?.PaginationDetails.MaxPageNumber); } [Fact] - public void Paginate_WhenCollectionLengthBiggerThanPageSize_ShouldReturnPartOfCollection() + public async Task Paginate_WhenCollectionLengthBiggerThanPageSize_ShouldReturnPartOfCollection() { // Arrange - var data = new List { 1, 2, 3, 4, 5 }; + var data = new List { 1, 2, 3, 4, 5 }.AsQueryable(); const int pageSize = 2; const int pageNumber = 0; // Act - var result = _paginationService.Paginate(data, pageSize, pageNumber); + var result = await _paginationService.PaginateAsync(data, pageSize, pageNumber); // Assert Assert.True(result.IsSuccess); @@ -84,20 +84,20 @@ public void Paginate_WhenCollectionLengthBiggerThanPageSize_ShouldReturnPartOfCo Assert.Equal(pageSize, result.Value?.PageSize); Assert.True(result.Value?.HasNextPage); Assert.False(result.Value?.HasPreviousPage); - Assert.Equal(data.Count, result.Value?.PaginationDetails.AllElementsCount); + Assert.Equal(data.Count(), result.Value?.PaginationDetails.AllElementsCount); Assert.Equal(2, result.Value?.PaginationDetails.MaxPageNumber); } [Fact] - public void Paginate_WhenTakingElementsFromTheMiddle_ShouldReturnPaginatedDataWithBothBooleansTrue() + public async Task Paginate_WhenTakingElementsFromTheMiddle_ShouldReturnPaginatedDataWithBothBooleansTrue() { // Arrange - var data = new List { 1, 2, 3, 4, 5 }; + var data = new List { 1, 2, 3, 4, 5 }.AsQueryable(); const int pageSize = 2; const int pageNumber = 1; // Act - var result = _paginationService.Paginate(data, pageSize, pageNumber); + var result = await _paginationService.PaginateAsync(data, pageSize, pageNumber); // Assert Assert.True(result.IsSuccess); @@ -106,20 +106,20 @@ public void Paginate_WhenTakingElementsFromTheMiddle_ShouldReturnPaginatedDataWi Assert.Equal(pageSize, result.Value?.PageSize); Assert.True(result.Value?.HasNextPage); Assert.True(result.Value?.HasPreviousPage); - Assert.Equal(data.Count, result.Value?.PaginationDetails.AllElementsCount); + Assert.Equal(data.Count(), result.Value?.PaginationDetails.AllElementsCount); Assert.Equal(2, result.Value?.PaginationDetails.MaxPageNumber); } [Fact] - public void Paginate_WhenExceededMaxPageNumber_ShouldReturnFailure() + public async Task Paginate_WhenExceededMaxPageNumber_ShouldReturnFailure() { // Arrange - var data = new List { 1, 2, 3, 4, 5 }; + var data = new List { 1, 2, 3, 4, 5 }.AsQueryable(); const int pageSize = 2; const int pageNumber = 3; // Act - var result = _paginationService.Paginate(data, pageSize, pageNumber); + var result = await _paginationService.PaginateAsync(data, pageSize, pageNumber); // Assert Assert.True(result.IsError); @@ -128,15 +128,15 @@ public void Paginate_WhenExceededMaxPageNumber_ShouldReturnFailure() } [Fact] - public void Paginate_WhenOnLastPage_ShouldReturnHasNextPageSetToFalse() + public async Task Paginate_WhenOnLastPage_ShouldReturnHasNextPageSetToFalse() { // Arrange - var data = new List { 1, 2, 3, 4, 5 }; + var data = new List { 1, 2, 3, 4, 5 }.AsQueryable(); const int pageSize = 2; const int pageNumber = 2; // Act - var result = _paginationService.Paginate(data, pageSize, pageNumber); + var result = await _paginationService.PaginateAsync(data, pageSize, pageNumber); // Assert Assert.True(result.IsSuccess); @@ -145,20 +145,20 @@ public void Paginate_WhenOnLastPage_ShouldReturnHasNextPageSetToFalse() Assert.Equal(pageSize, result.Value?.PageSize); Assert.False(result.Value?.HasNextPage); Assert.True(result.Value?.HasPreviousPage); - Assert.Equal(data.Count, result.Value?.PaginationDetails.AllElementsCount); + Assert.Equal(data.Count(), result.Value?.PaginationDetails.AllElementsCount); Assert.Equal(2, result.Value?.PaginationDetails.MaxPageNumber); } [Fact] - public void Paginate_WhenCollectionEmptyAndFirstPageIsRequested_ShouldReturnSuccess() + public async Task Paginate_WhenCollectionEmptyAndFirstPageIsRequested_ShouldReturnSuccess() { // Arrange - var data = new List(); + var data = new List().AsQueryable(); const int pageSize = 2; const int pageNumber = 0; // Act - var result = _paginationService.Paginate(data, pageSize, pageNumber); + var result = await _paginationService.PaginateAsync(data, pageSize, pageNumber); // Assert Assert.True(result.IsSuccess); @@ -167,7 +167,7 @@ public void Paginate_WhenCollectionEmptyAndFirstPageIsRequested_ShouldReturnSucc Assert.Equal(pageSize, result.Value?.PageSize); Assert.False(result.Value?.HasNextPage); Assert.False(result.Value?.HasPreviousPage); - Assert.Equal(data.Count, result.Value?.PaginationDetails.AllElementsCount); + Assert.Equal(data.Count(), result.Value?.PaginationDetails.AllElementsCount); Assert.Equal(0, result.Value?.PaginationDetails.MaxPageNumber); } From edb1f1131521c965ce596420deaf4fe08843bcb2 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Sun, 6 Apr 2025 17:05:15 +0200 Subject: [PATCH 124/128] Update `CategoryRepository` and `CategoryService` to use new `PaginationService` --- .../Services/CategoryServiceTests.cs | 20 +++++++++++++------ .../Abstractions/ICategoryRepository.cs | 2 +- .../Respositories/CategoryRepository.cs | 5 ++--- .../Categories/Services/CategoryService.cs | 4 ++-- 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/TickAPI/TickAPI.Tests/Categories/Services/CategoryServiceTests.cs b/TickAPI/TickAPI.Tests/Categories/Services/CategoryServiceTests.cs index 7f5e84f..5f38aeb 100644 --- a/TickAPI/TickAPI.Tests/Categories/Services/CategoryServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Categories/Services/CategoryServiceTests.cs @@ -19,15 +19,23 @@ public async Task GetCategoriesResponsesAsync_WhenDataIsValid_ShouldReturnOk() // Arrange int pageSize = 10; int page = 0; + var allCategories = new List().AsQueryable(); var categoryRepositoryMock = new Mock(); - categoryRepositoryMock.Setup(repo => repo.GetCategoriesAsync()) - .ReturnsAsync(new List()); + categoryRepositoryMock.Setup(repo => repo.GetCategories()) + .Returns(allCategories); var paginationServiceMock = new Mock(); - paginationServiceMock.Setup(p => p.Paginate(new List(), pageSize, page)).Returns( - Result>.Success(new PaginatedData(new List(), page, pageSize, - false, false, new PaginationDetails(0, 0))) - ); + paginationServiceMock.Setup(p => p.PaginateAsync(allCategories, pageSize, page)) + .Returns(Task.FromResult( + Result>.Success(new PaginatedData( + new List(), + page, + pageSize, + false, + false, + new PaginationDetails(0, 0)) + ) + )); var sut = new CategoryService(categoryRepositoryMock.Object, paginationServiceMock.Object); diff --git a/TickAPI/TickAPI/Categories/Abstractions/ICategoryRepository.cs b/TickAPI/TickAPI/Categories/Abstractions/ICategoryRepository.cs index a4b7236..c7d1fc3 100644 --- a/TickAPI/TickAPI/Categories/Abstractions/ICategoryRepository.cs +++ b/TickAPI/TickAPI/Categories/Abstractions/ICategoryRepository.cs @@ -5,7 +5,7 @@ namespace TickAPI.Categories.Abstractions; public interface ICategoryRepository { - public Task> GetCategoriesAsync(); + public IQueryable GetCategories(); public Task> GetCategoryByNameAsync(string categoryName); public Task AddNewCategoryAsync(Category category); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Categories/Respositories/CategoryRepository.cs b/TickAPI/TickAPI/Categories/Respositories/CategoryRepository.cs index f7a599f..e1dcdb2 100644 --- a/TickAPI/TickAPI/Categories/Respositories/CategoryRepository.cs +++ b/TickAPI/TickAPI/Categories/Respositories/CategoryRepository.cs @@ -14,10 +14,9 @@ public CategoryRepository(TickApiDbContext tickApiDbContext) _tickApiDbContext = tickApiDbContext; } - public async Task> GetCategoriesAsync() + public IQueryable GetCategories() { - var list = await _tickApiDbContext.Categories.ToListAsync(); - return list; + return _tickApiDbContext.Categories; } public async Task> GetCategoryByNameAsync(string categoryName) diff --git a/TickAPI/TickAPI/Categories/Services/CategoryService.cs b/TickAPI/TickAPI/Categories/Services/CategoryService.cs index 2048c09..929c99e 100644 --- a/TickAPI/TickAPI/Categories/Services/CategoryService.cs +++ b/TickAPI/TickAPI/Categories/Services/CategoryService.cs @@ -25,8 +25,8 @@ public Task> GetCategoryByNameAsync(string categoryName) public async Task>> GetCategoriesResponsesAsync(int pageSize, int page) { - var categoriesAllResponse = await _categoryRepository.GetCategoriesAsync(); - var categoriesPaginated = _paginationService.Paginate(categoriesAllResponse, pageSize, page); + var categoriesAllResponse = _categoryRepository.GetCategories(); + var categoriesPaginated = await _paginationService.PaginateAsync(categoriesAllResponse, pageSize, page); if (!categoriesPaginated.IsSuccess) { return Result>.PropagateError(categoriesPaginated); From 726af317848438687264a0d9b23b820430f823d6 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Sun, 6 Apr 2025 17:06:55 +0200 Subject: [PATCH 125/128] Update `EventController`, `EventService` and `EventRepository` to use new version of `PaginationService` --- .../Controllers/EventControllerTests.cs | 16 ++++---- .../Events/Services/EventServiceTests.cs | 22 +++++++---- .../Events/Abstractions/IEventRepository.cs | 3 ++ .../Events/Abstractions/IEventService.cs | 5 ++- .../Events/Controllers/EventController.cs | 4 +- .../Events/Repositories/EventRepository.cs | 14 ++++++- .../TickAPI/Events/Services/EventService.cs | 37 +++++++++++++------ 7 files changed, 68 insertions(+), 33 deletions(-) diff --git a/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs b/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs index 326878c..639eb47 100644 --- a/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs +++ b/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs @@ -155,8 +155,8 @@ public async Task GetOrganizerEvents_WhenAllOperationsSucceed_ShouldReturnOkWith var eventServiceMock = new Mock(); eventServiceMock - .Setup(m => m.GetOrganizerEvents(organizer, page, pageSize)) - .Returns(Result>.Success(paginatedData)); + .Setup(m => m.GetOrganizerEventsAsync(organizer, page, pageSize)) + .ReturnsAsync(Result>.Success(paginatedData)); var sut = new EventController(eventServiceMock.Object, claimsServiceMock.Object, organizerServiceMock.Object); sut.ControllerContext = controllerContext; @@ -297,8 +297,8 @@ public async Task GetOrganizerEvents_WhenPaginationFails_ShouldReturnBadRequest( var eventServiceMock = new Mock(); eventServiceMock - .Setup(m => m.GetOrganizerEvents(organizer, page, pageSize)) - .Returns(Result>.Failure(StatusCodes.Status400BadRequest, errorMessage)); + .Setup(m => m.GetOrganizerEventsAsync(organizer, page, pageSize)) + .ReturnsAsync(Result>.Failure(StatusCodes.Status400BadRequest, errorMessage)); var sut = new EventController(eventServiceMock.Object, claimsServiceMock.Object, organizerServiceMock.Object); sut.ControllerContext = controllerContext; @@ -349,8 +349,8 @@ public async Task GetOrganizerEventsPaginationDetails_WhenAllOperationsSucceed_S var eventServiceMock = new Mock(); eventServiceMock - .Setup(m => m.GetOrganizerEventsPaginationDetails(organizer, pageSize)) - .Returns(Result.Success(paginationDetails)); + .Setup(m => m.GetOrganizerEventsPaginationDetailsAsync(organizer, pageSize)) + .ReturnsAsync(Result.Success(paginationDetails)); var sut = new EventController(eventServiceMock.Object, claimsServiceMock.Object, organizerServiceMock.Object); sut.ControllerContext = controllerContext; @@ -403,8 +403,8 @@ public async Task GetOrganizerEventsPaginationDetails_WhenPaginationDetailsFails var eventServiceMock = new Mock(); eventServiceMock - .Setup(m => m.GetOrganizerEventsPaginationDetails(organizer, pageSize)) - .Returns(Result.Failure(StatusCodes.Status400BadRequest, errorMessage)); + .Setup(m => m.GetOrganizerEventsPaginationDetailsAsync(organizer, pageSize)) + .ReturnsAsync(Result.Failure(StatusCodes.Status400BadRequest, errorMessage)); var sut = new EventController(eventServiceMock.Object, claimsServiceMock.Object, organizerServiceMock.Object); sut.ControllerContext = controllerContext; diff --git a/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs b/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs index 7c3272a..23bf6a3 100644 --- a/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs @@ -153,7 +153,7 @@ public async Task CreateNewEventAsync_WhenStartDateIsBeforeNow_ShouldReturnBadRe } [Fact] - public void GetOrganizerEvents_WhenPaginationSucceeds_ShouldReturnPaginatedEvents() + public async Task GetOrganizerEvents_WhenPaginationSucceeds_ShouldReturnPaginatedEvents() { // Arrange var organizer = new Organizer @@ -185,9 +185,12 @@ public void GetOrganizerEvents_WhenPaginationSucceeds_ShouldReturnPaginatedEvent new PaginationDetails(1, 3) ); + var organizerEvents = organizer.Events.AsQueryable(); + eventRepositoryMock.Setup(p => p.GetEventsByOranizer(organizer)).Returns(organizerEvents); + paginationServiceMock - .Setup(p => p.Paginate(organizer.Events, pageSize, page)) - .Returns(Result>.Success(paginatedEvents)); + .Setup(p => p.PaginateAsync(organizerEvents, pageSize, page)) + .ReturnsAsync(Result>.Success(paginatedEvents)); paginationServiceMock .Setup(p => p.MapData(paginatedEvents, It.IsAny>())) @@ -208,7 +211,7 @@ public void GetOrganizerEvents_WhenPaginationSucceeds_ShouldReturnPaginatedEvent dateTimeServiceMock.Object, paginationServiceMock.Object); // Act - var result = sut.GetOrganizerEvents(organizer, page, pageSize); + var result = await sut.GetOrganizerEventsAsync(organizer, page, pageSize); // Assert Assert.True(result.IsSuccess); @@ -224,7 +227,7 @@ public void GetOrganizerEvents_WhenPaginationSucceeds_ShouldReturnPaginatedEvent } [Fact] - public void GetOrganizerEvents_WhenPaginationFails_ShouldPropagateError() + public async Task GetOrganizerEvents_WhenPaginationFails_ShouldPropagateError() { // Arrange var organizer = new Organizer @@ -246,15 +249,18 @@ public void GetOrganizerEvents_WhenPaginationFails_ShouldPropagateError() var dateTimeServiceMock = new Mock(); var paginationServiceMock = new Mock(); + var organizerEvents = organizer.Events.AsQueryable(); + eventRepositoryMock.Setup(p => p.GetEventsByOranizer(organizer)).Returns(organizerEvents); + paginationServiceMock - .Setup(p => p.Paginate(organizer.Events, pageSize, page)) - .Returns(Result>.Failure(StatusCodes.Status400BadRequest, "Invalid page number")); + .Setup(p => p.PaginateAsync(organizerEvents, pageSize, page)) + .ReturnsAsync(Result>.Failure(StatusCodes.Status400BadRequest, "Invalid page number")); var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, dateTimeServiceMock.Object, paginationServiceMock.Object); // Act - var result = sut.GetOrganizerEvents(organizer, page, pageSize); + var result = await sut.GetOrganizerEventsAsync(organizer, page, pageSize); // Assert Assert.False(result.IsSuccess); diff --git a/TickAPI/TickAPI/Events/Abstractions/IEventRepository.cs b/TickAPI/TickAPI/Events/Abstractions/IEventRepository.cs index d7a690e..45c8e00 100644 --- a/TickAPI/TickAPI/Events/Abstractions/IEventRepository.cs +++ b/TickAPI/TickAPI/Events/Abstractions/IEventRepository.cs @@ -1,8 +1,11 @@ using TickAPI.Events.Models; +using TickAPI.Organizers.Models; namespace TickAPI.Events.Abstractions; public interface IEventRepository { public Task AddNewEventAsync(Event @event); + public IQueryable GetEvents(); + public IQueryable GetEventsByOranizer(Organizer organizer); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/Abstractions/IEventService.cs b/TickAPI/TickAPI/Events/Abstractions/IEventService.cs index a7876d8..48c7ef2 100644 --- a/TickAPI/TickAPI/Events/Abstractions/IEventService.cs +++ b/TickAPI/TickAPI/Events/Abstractions/IEventService.cs @@ -11,6 +11,7 @@ public interface IEventService { public Task> CreateNewEventAsync(string name, string description, DateTime startDate, DateTime endDate, uint? minimumAge, CreateAddressDto createAddress, EventStatus eventStatus, string organizerEmail); - public Result> GetOrganizerEvents(Organizer organizer, int page, int pageSize); - public Result GetOrganizerEventsPaginationDetails(Organizer organizer, int pageSize); + public Task>> GetOrganizerEventsAsync(Organizer organizer, int page, int pageSize); + public Task> GetOrganizerEventsPaginationDetailsAsync(Organizer organizer, int pageSize); + public Task>> GetEventsAsync(int page, int pageSize); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/Controllers/EventController.cs b/TickAPI/TickAPI/Events/Controllers/EventController.cs index 1b2cf50..5fe67f1 100644 --- a/TickAPI/TickAPI/Events/Controllers/EventController.cs +++ b/TickAPI/TickAPI/Events/Controllers/EventController.cs @@ -64,7 +64,7 @@ public async Task>> GetOrganizer } var organizer = organizerResult.Value!; - var paginatedDataResult = _eventService.GetOrganizerEvents(organizer, page, pageSize); + var paginatedDataResult = await _eventService.GetOrganizerEventsAsync(organizer, page, pageSize); if (paginatedDataResult.IsError) { return StatusCode(paginatedDataResult.StatusCode, paginatedDataResult.ErrorMsg); @@ -91,7 +91,7 @@ public async Task> GetOrganizerEventsPaginationD } var organizer = organizerResult.Value!; - var paginationDetailsResult = _eventService.GetOrganizerEventsPaginationDetails(organizer, pageSize); + var paginationDetailsResult = await _eventService.GetOrganizerEventsPaginationDetailsAsync(organizer, pageSize); if (paginationDetailsResult.IsError) { return StatusCode(paginationDetailsResult.StatusCode, paginationDetailsResult.ErrorMsg); diff --git a/TickAPI/TickAPI/Events/Repositories/EventRepository.cs b/TickAPI/TickAPI/Events/Repositories/EventRepository.cs index 0ea8f73..40c05d7 100644 --- a/TickAPI/TickAPI/Events/Repositories/EventRepository.cs +++ b/TickAPI/TickAPI/Events/Repositories/EventRepository.cs @@ -1,7 +1,7 @@ -using Microsoft.EntityFrameworkCore; -using TickAPI.Common.TickApiDbContext; +using TickAPI.Common.TickApiDbContext; using TickAPI.Events.Abstractions; using TickAPI.Events.Models; +using TickAPI.Organizers.Models; namespace TickAPI.Events.Repositories; @@ -19,4 +19,14 @@ public async Task AddNewEventAsync(Event @event) _tickApiDbContext.Events.Add(@event); await _tickApiDbContext.SaveChangesAsync(); } + + public IQueryable GetEvents() + { + return _tickApiDbContext.Events; + } + + public IQueryable GetEventsByOranizer(Organizer organizer) + { + return _tickApiDbContext.Events.Where(e => e.Organizer.Id == organizer.Id); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/Services/EventService.cs b/TickAPI/TickAPI/Events/Services/EventService.cs index 0043914..bd111fa 100644 --- a/TickAPI/TickAPI/Events/Services/EventService.cs +++ b/TickAPI/TickAPI/Events/Services/EventService.cs @@ -60,26 +60,41 @@ public async Task> CreateNewEventAsync(string name, string descri return Result.Success(@event); } - public Result> GetOrganizerEvents(Organizer organizer, int page, int pageSize) + public async Task>> GetOrganizerEventsAsync(Organizer organizer, int page, int pageSize) { - var paginatedEventsResult = _paginationService.Paginate(organizer.Events, pageSize, page); + var organizerEvents = _eventRepository.GetEventsByOranizer(organizer); + return await GetPaginatedEventsAsync(organizerEvents, page, pageSize); + } + + public async Task> GetOrganizerEventsPaginationDetailsAsync(Organizer organizer, int pageSize) + { + var organizerEvents = _eventRepository.GetEventsByOranizer(organizer); + return await _paginationService.GetPaginationDetailsAsync(organizerEvents, pageSize); + } + + public async Task>> GetEventsAsync(int page, int pageSize) + { + var events = _eventRepository.GetEvents(); + return await GetPaginatedEventsAsync(events, page, pageSize); + } + + private async Task>> GetPaginatedEventsAsync(IQueryable events, int page, int pageSize) + { + var paginatedEventsResult = await _paginationService.PaginateAsync(events, pageSize, page); if (paginatedEventsResult.IsError) { return Result>.PropagateError(paginatedEventsResult); } - var paginatedData = _paginationService.MapData(paginatedEventsResult.Value!, ev => - { - var categories = ev.Categories.Select((c) => new GetEventResponseCategoryDto(c.Name)).ToList(); - var address = new GetEventResponseAddressDto(ev.Address.Country, ev.Address.City, ev.Address.PostalCode, ev.Address.Street, ev.Address.HouseNumber, ev.Address.FlatNumber); - return new GetEventResponseDto(ev.Name, ev.Description, ev.StartDate, ev.EndDate, ev.MinimumAge, categories, ev.EventStatus, address); - }); + var paginatedData = _paginationService.MapData(paginatedEventsResult.Value!, MapEventToGetEventResponseDto); return Result>.Success(paginatedData); } - - public Result GetOrganizerEventsPaginationDetails(Organizer organizer, int pageSize) + + private static GetEventResponseDto MapEventToGetEventResponseDto(Event ev) { - return _paginationService.GetPaginationDetails(organizer.Events!, pageSize); + var categories = ev.Categories.Select((c) => new GetEventResponseCategoryDto(c.Name)).ToList(); + var address = new GetEventResponseAddressDto(ev.Address.Country, ev.Address.City, ev.Address.PostalCode, ev.Address.Street, ev.Address.HouseNumber, ev.Address.FlatNumber); + return new GetEventResponseDto(ev.Name, ev.Description, ev.StartDate, ev.EndDate, ev.MinimumAge, categories, ev.EventStatus, address); } } \ No newline at end of file From db7fe09d3e4dfc91ba89e693bd9799d279548eed Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Sun, 6 Apr 2025 17:07:10 +0200 Subject: [PATCH 126/128] Remove unnecessary import --- TickAPI/TickAPI/Organizers/Models/Organizer.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/TickAPI/TickAPI/Organizers/Models/Organizer.cs b/TickAPI/TickAPI/Organizers/Models/Organizer.cs index f55cc05..1d74909 100644 --- a/TickAPI/TickAPI/Organizers/Models/Organizer.cs +++ b/TickAPI/TickAPI/Organizers/Models/Organizer.cs @@ -1,5 +1,4 @@ -using System.Reflection.Metadata.Ecma335; -using TickAPI.Events.Models; +using TickAPI.Events.Models; namespace TickAPI.Organizers.Models; From 35233af343563fd4e8d2ad131fe2c709826f38e3 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Sun, 6 Apr 2025 17:21:33 +0200 Subject: [PATCH 127/128] Add endpoints for customer for getting events --- .../Events/Abstractions/IEventService.cs | 1 + .../Events/Controllers/EventController.cs | 24 +++++++++++++++++++ .../TickAPI/Events/Services/EventService.cs | 8 ++++++- 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/TickAPI/TickAPI/Events/Abstractions/IEventService.cs b/TickAPI/TickAPI/Events/Abstractions/IEventService.cs index 48c7ef2..8afbaad 100644 --- a/TickAPI/TickAPI/Events/Abstractions/IEventService.cs +++ b/TickAPI/TickAPI/Events/Abstractions/IEventService.cs @@ -14,4 +14,5 @@ public Task> CreateNewEventAsync(string name, string description, public Task>> GetOrganizerEventsAsync(Organizer organizer, int page, int pageSize); public Task> GetOrganizerEventsPaginationDetailsAsync(Organizer organizer, int pageSize); public Task>> GetEventsAsync(int page, int pageSize); + public Task> GetEventsPaginationDetailsAsync(int pageSize); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/Controllers/EventController.cs b/TickAPI/TickAPI/Events/Controllers/EventController.cs index 5fe67f1..5ea6f4e 100644 --- a/TickAPI/TickAPI/Events/Controllers/EventController.cs +++ b/TickAPI/TickAPI/Events/Controllers/EventController.cs @@ -99,4 +99,28 @@ public async Task> GetOrganizerEventsPaginationD return Ok(paginationDetailsResult.Value!); } + + [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] + [HttpGet("get-events")] + public async Task>> GetEvents([FromQuery] int pageSize, [FromQuery] int page) + { + var paginatedDataResult = await _eventService.GetEventsAsync(page, pageSize); + if (paginatedDataResult.IsError) + { + return StatusCode(paginatedDataResult.StatusCode, paginatedDataResult.ErrorMsg); + } + return Ok(paginatedDataResult.Value!); + } + + [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] + [HttpGet("get-events-pagination-details")] + public async Task> GetEventsPaginationDetails([FromQuery] int pageSize) + { + var paginationDetailsResult = await _eventService.GetEventsPaginationDetailsAsync(pageSize); + if (paginationDetailsResult.IsError) + { + return StatusCode(paginationDetailsResult.StatusCode, paginationDetailsResult.ErrorMsg); + } + return Ok(paginationDetailsResult.Value!); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/Services/EventService.cs b/TickAPI/TickAPI/Events/Services/EventService.cs index bd111fa..6ac130c 100644 --- a/TickAPI/TickAPI/Events/Services/EventService.cs +++ b/TickAPI/TickAPI/Events/Services/EventService.cs @@ -14,7 +14,7 @@ namespace TickAPI.Events.Services; public class EventService : IEventService { - private readonly IOrganizerService _organizerService; + private readonly IOrganizerService _organizerService; private readonly IEventRepository _eventRepository; private readonly IAddressService _addressService; private readonly IDateTimeService _dateTimeService; @@ -78,6 +78,12 @@ public async Task>> GetEventsAsync(int return await GetPaginatedEventsAsync(events, page, pageSize); } + public async Task> GetEventsPaginationDetailsAsync(int pageSize) + { + var events = _eventRepository.GetEvents(); + return await _paginationService.GetPaginationDetailsAsync(events, pageSize); + } + private async Task>> GetPaginatedEventsAsync(IQueryable events, int page, int pageSize) { var paginatedEventsResult = await _paginationService.PaginateAsync(events, pageSize, page); From aa812e9ce23569f891adf3460127f1df9da3beb6 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Sun, 6 Apr 2025 17:21:46 +0200 Subject: [PATCH 128/128] Add tests for new events logic --- .../Controllers/EventControllerTests.cs | 136 +++++++++++++ .../Events/Services/EventServiceTests.cs | 180 ++++++++++++++++++ 2 files changed, 316 insertions(+) diff --git a/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs b/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs index 639eb47..8c788b1 100644 --- a/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs +++ b/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs @@ -418,4 +418,140 @@ public async Task GetOrganizerEventsPaginationDetails_WhenPaginationDetailsFails Assert.Equal(StatusCodes.Status400BadRequest, objectResult.StatusCode); Assert.Equal(errorMessage, objectResult.Value); } + + [Fact] + public async Task GetEvents_WhenAllOperationsSucceed_ShouldReturnOkWithPaginatedData() + { + // Arrange + const int page = 0; + const int pageSize = 10; + + var eventServiceMock = new Mock(); + var claimsServiceMock = new Mock(); + var organizerServiceMock = new Mock(); + + var paginatedData = new PaginatedData( + new List + { + Utils.CreateSampleEventResponseDto("Event 1"), + Utils.CreateSampleEventResponseDto("Event 2") + }, + page, + pageSize, + false, + false, + new PaginationDetails(0, 2) + ); + + eventServiceMock + .Setup(m => m.GetEventsAsync(page, pageSize)) + .ReturnsAsync(Result>.Success(paginatedData)); + + var sut = new EventController(eventServiceMock.Object, claimsServiceMock.Object, organizerServiceMock.Object); + + // Act + var response = await sut.GetEvents(pageSize, page); + + // Assert + var result = Assert.IsType>>(response); + var okResult = Assert.IsType(result.Result); + Assert.Equal(StatusCodes.Status200OK, okResult.StatusCode); + + var returnedPaginatedData = Assert.IsType>(okResult.Value); + Assert.Equal(2, returnedPaginatedData.Data.Count); + Assert.Equal(paginatedData.Data[0], returnedPaginatedData.Data[0]); + Assert.Equal(paginatedData.Data[1], returnedPaginatedData.Data[1]); + Assert.Equal(page, returnedPaginatedData.PageNumber); + Assert.Equal(pageSize, returnedPaginatedData.PageSize); + Assert.False(returnedPaginatedData.HasNextPage); + Assert.False(returnedPaginatedData.HasPreviousPage); + } + + [Fact] + public async Task GetEvents_WhenOperationFails_ShouldReturnErrorWithCorrectStatusCode() + { + // Arrange + const int page = 0; + const int pageSize = 10; + const string errorMessage = "Failed to retrieve events"; + const int statusCode = StatusCodes.Status500InternalServerError; + + var eventServiceMock = new Mock(); + var claimsServiceMock = new Mock(); + var organizerServiceMock = new Mock(); + + eventServiceMock + .Setup(m => m.GetEventsAsync(page, pageSize)) + .ReturnsAsync(Result>.Failure(statusCode, errorMessage)); + + var sut = new EventController(eventServiceMock.Object, claimsServiceMock.Object, organizerServiceMock.Object); + + // Act + var response = await sut.GetEvents(pageSize, page); + + // Assert + var result = Assert.IsType>>(response); + var objectResult = Assert.IsType(result.Result); + Assert.Equal(statusCode, objectResult.StatusCode); + Assert.Equal(errorMessage, objectResult.Value); + } + + [Fact] + public async Task GetEventsPaginationDetails_WhenAllOperationsSucceed_ShouldReturnOkWithPaginationDetails() + { + // Arrange + const int pageSize = 10; + + var eventServiceMock = new Mock(); + var claimsServiceMock = new Mock(); + var organizerServiceMock = new Mock(); + + var paginationDetails = new PaginationDetails(0, 20); + + eventServiceMock + .Setup(m => m.GetEventsPaginationDetailsAsync(pageSize)) + .ReturnsAsync(Result.Success(paginationDetails)); + + var sut = new EventController(eventServiceMock.Object, claimsServiceMock.Object, organizerServiceMock.Object); + + // Act + var response = await sut.GetEventsPaginationDetails(pageSize); + + // Assert + var result = Assert.IsType>(response); + var okResult = Assert.IsType(result.Result); + Assert.Equal(StatusCodes.Status200OK, okResult.StatusCode); + + var returnedPaginationDetails = Assert.IsType(okResult.Value); + Assert.Equal(paginationDetails.AllElementsCount, returnedPaginationDetails.AllElementsCount); + Assert.Equal(paginationDetails.MaxPageNumber, returnedPaginationDetails.MaxPageNumber); + } + + [Fact] + public async Task GetEventsPaginationDetails_WhenOperationFails_ShouldReturnErrorWithCorrectStatusCode() + { + // Arrange + const int pageSize = 10; + const string errorMessage = "Failed to retrieve pagination details"; + const int statusCode = StatusCodes.Status500InternalServerError; + + var eventServiceMock = new Mock(); + var claimsServiceMock = new Mock(); + var organizerServiceMock = new Mock(); + + eventServiceMock + .Setup(m => m.GetEventsPaginationDetailsAsync(pageSize)) + .ReturnsAsync(Result.Failure(statusCode, errorMessage)); + + var sut = new EventController(eventServiceMock.Object, claimsServiceMock.Object, organizerServiceMock.Object); + + // Act + var response = await sut.GetEventsPaginationDetails(pageSize); + + // Assert + var result = Assert.IsType>(response); + var objectResult = Assert.IsType(result.Result); + Assert.Equal(statusCode, objectResult.StatusCode); + Assert.Equal(errorMessage, objectResult.Value); +} } \ No newline at end of file diff --git a/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs b/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs index 23bf6a3..59bdd8a 100644 --- a/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs @@ -267,4 +267,184 @@ public async Task GetOrganizerEvents_WhenPaginationFails_ShouldPropagateError() Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); Assert.Equal("Invalid page number", result.ErrorMsg); } + + [Fact] + public async Task GetEventsAsync_WhenPaginationSucceeds_ShouldReturnPaginatedEvents() + { + // Arrange + var events = new List + { + Utils.CreateSampleEvent("Event 1"), + Utils.CreateSampleEvent("Event 2"), + Utils.CreateSampleEvent("Event 3") + }; + int page = 0; + int pageSize = 2; + + var eventRepositoryMock = new Mock(); + var organizerServiceMock = new Mock(); + var addressServiceMock = new Mock(); + var dateTimeServiceMock = new Mock(); + var paginationServiceMock = new Mock(); + + var paginatedEvents = new PaginatedData( + events.Take(pageSize).ToList(), + page, + pageSize, + true, + false, + new PaginationDetails(1, 3) + ); + + var eventsQueryable = events.AsQueryable(); + eventRepositoryMock.Setup(p => p.GetEvents()).Returns(eventsQueryable); + + paginationServiceMock + .Setup(p => p.PaginateAsync(eventsQueryable, pageSize, page)) + .ReturnsAsync(Result>.Success(paginatedEvents)); + + paginationServiceMock + .Setup(p => p.MapData(paginatedEvents, It.IsAny>())) + .Returns(new PaginatedData( + new List + { + Utils.CreateSampleEventResponseDto("Event 1"), + Utils.CreateSampleEventResponseDto("Event 2") + }, + page, + pageSize, + true, + false, + new PaginationDetails(1, 3) + )); + + var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, + dateTimeServiceMock.Object, paginationServiceMock.Object); + + // Act + var result = await sut.GetEventsAsync(page, pageSize); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(2, result.Value!.Data.Count); + Assert.Equal("Event 1", result.Value!.Data[0].Name); + Assert.Equal("Event 2", result.Value!.Data[1].Name); + Assert.Equal(0, result.Value!.PageNumber); + Assert.Equal(2, result.Value!.PageSize); + Assert.True(result.Value!.HasNextPage); + Assert.False(result.Value!.HasPreviousPage); + Assert.Equal(1, result.Value!.PaginationDetails.MaxPageNumber); + Assert.Equal(3, result.Value!.PaginationDetails.AllElementsCount); + } + + [Fact] + public async Task GetEventsAsync_WhenPaginationFails_ShouldPropagateError() + { + // Arrange + var events = new List + { + Utils.CreateSampleEvent("Event 1"), + Utils.CreateSampleEvent("Event 2") + }; + int page = 2; // Invalid page + int pageSize = 2; + + var eventRepositoryMock = new Mock(); + var organizerServiceMock = new Mock(); + var addressServiceMock = new Mock(); + var dateTimeServiceMock = new Mock(); + var paginationServiceMock = new Mock(); + + var eventsQueryable = events.AsQueryable(); + eventRepositoryMock.Setup(p => p.GetEvents()).Returns(eventsQueryable); + + paginationServiceMock + .Setup(p => p.PaginateAsync(eventsQueryable, pageSize, page)) + .ReturnsAsync(Result>.Failure(StatusCodes.Status400BadRequest, "Invalid page number")); + + var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, + dateTimeServiceMock.Object, paginationServiceMock.Object); + + // Act + var result = await sut.GetEventsAsync(page, pageSize); + + // Assert + Assert.False(result.IsSuccess); + Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); + Assert.Equal("Invalid page number", result.ErrorMsg); + } + + [Fact] + public async Task GetEventsPaginationDetailsAsync_WhenSuccessful_ShouldReturnPaginationDetails() + { + // Arrange + var events = new List + { + Utils.CreateSampleEvent("Event 1"), + Utils.CreateSampleEvent("Event 2"), + Utils.CreateSampleEvent("Event 3") + }; + int pageSize = 2; + + var eventRepositoryMock = new Mock(); + var organizerServiceMock = new Mock(); + var addressServiceMock = new Mock(); + var dateTimeServiceMock = new Mock(); + var paginationServiceMock = new Mock(); + + var eventsQueryable = events.AsQueryable(); + eventRepositoryMock.Setup(p => p.GetEvents()).Returns(eventsQueryable); + + var paginationDetails = new PaginationDetails(1, 3); + paginationServiceMock + .Setup(p => p.GetPaginationDetailsAsync(eventsQueryable, pageSize)) + .ReturnsAsync(Result.Success(paginationDetails)); + + var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, + dateTimeServiceMock.Object, paginationServiceMock.Object); + + // Act + var result = await sut.GetEventsPaginationDetailsAsync(pageSize); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(1, result.Value!.MaxPageNumber); + Assert.Equal(3, result.Value!.AllElementsCount); + } + + [Fact] + public async Task GetEventsPaginationDetailsAsync_WhenFails_ShouldReturnError() + { + // Arrange + var events = new List + { + Utils.CreateSampleEvent("Event 1"), + Utils.CreateSampleEvent("Event 2") + }; + int pageSize = -1; // Invalid page size + + var eventRepositoryMock = new Mock(); + var organizerServiceMock = new Mock(); + var addressServiceMock = new Mock(); + var dateTimeServiceMock = new Mock(); + var paginationServiceMock = new Mock(); + + var eventsQueryable = events.AsQueryable(); + eventRepositoryMock.Setup(p => p.GetEvents()).Returns(eventsQueryable); + + paginationServiceMock + .Setup(p => p.GetPaginationDetailsAsync(eventsQueryable, pageSize)) + .ReturnsAsync(Result.Failure(StatusCodes.Status400BadRequest, "Invalid page size")); + + var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, + dateTimeServiceMock.Object, paginationServiceMock.Object); + + // Act + var result = await sut.GetEventsPaginationDetailsAsync(pageSize); + + // Assert + Assert.False(result.IsSuccess); + Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); + Assert.Equal("Invalid page size", result.ErrorMsg); + } } \ No newline at end of file