From 44270d115a4640ae8c468840f03e81fc236ef055 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Thu, 20 Mar 2025 13:09:13 +0100 Subject: [PATCH 1/9] 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 2/9] 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 3/9] 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 4/9] 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 5/9] 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 6/9] 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 7/9] 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 8/9] 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 9/9] 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