diff --git a/Dockerfile b/Dockerfile index c7d777f..cee6411 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,6 +9,7 @@ ARG BUILD_CONFIGURATION=Release WORKDIR /src COPY core/api.core.csproj ./core/ COPY emails/api.emails.csproj ./emails/ +COPY files/api.files.csproj ./files/ COPY tests/api.tests.csproj ./tests/ COPY Hello.sln ./ RUN dotnet restore diff --git a/Hello.sln b/Hello.sln index 94bf7f7..612455e 100644 --- a/Hello.sln +++ b/Hello.sln @@ -7,7 +7,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "api.core", "core\api.core.c EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "api.tests", "tests\api.tests.csproj", "{9080AC9C-A5FC-4FB3-9F4E-724ECD591996}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "api.emails", "emails\api.emails.csproj", "{62B49CA5-5A75-4E7B-8467-4ECDEE02CD49}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "api.emails", "emails\api.emails.csproj", "{62B49CA5-5A75-4E7B-8467-4ECDEE02CD49}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "api.files", "files\api.files.csproj", "{F2425268-5799-42FF-9778-5A1BB7B9DE31}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -27,6 +29,10 @@ Global {62B49CA5-5A75-4E7B-8467-4ECDEE02CD49}.Debug|Any CPU.Build.0 = Debug|Any CPU {62B49CA5-5A75-4E7B-8467-4ECDEE02CD49}.Release|Any CPU.ActiveCfg = Release|Any CPU {62B49CA5-5A75-4E7B-8467-4ECDEE02CD49}.Release|Any CPU.Build.0 = Release|Any CPU + {F2425268-5799-42FF-9778-5A1BB7B9DE31}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F2425268-5799-42FF-9778-5A1BB7B9DE31}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F2425268-5799-42FF-9778-5A1BB7B9DE31}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F2425268-5799-42FF-9778-5A1BB7B9DE31}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/core/.env.template b/core/.env.template index d478903..375575b 100644 --- a/core/.env.template +++ b/core/.env.template @@ -7,6 +7,7 @@ ASPNETCORE_ENVIRONMENT=Development CONNECTION_STRING=User ID=postgres;Password=test123;Host=host.docker.internal;Port=5432;Database=ps; SUPABASE_PROJECT_ID= SUPABASE_SECRET_KEY= +SUPABASE_ANON_KEY= EMAIL_SERVER=smtp.gmail.com EMAIL_PORT=587 @@ -16,3 +17,7 @@ EMAIL_FROM= EMAIL_TO_IF_DEBUG= REDIS_CONNECTION_STRING=host.docker.internal:6379,password=eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81,abortConnect=False,resolvedns=1 + +HOST_DIR= +CONTAINER_DIR=/app/volume +CDN_URL=http://localhost:6464 diff --git a/core/Controllers/EventsController.cs b/core/Controllers/EventsController.cs index 5c7e337..f444db0 100644 --- a/core/Controllers/EventsController.cs +++ b/core/Controllers/EventsController.cs @@ -31,6 +31,7 @@ public class EventsController(ILogger logger, IEventService ev public ActionResult> GetEvents( [FromQuery] DateTime? startDate, [FromQuery] DateTime? endDate, + [FromQuery] Guid? organizerId, [FromQuery] IEnumerable? activityAreas, [FromQuery] IEnumerable? tags, [FromQuery] PaginationRequest pagination) @@ -38,7 +39,7 @@ public ActionResult> GetEvents( logger.LogInformation("Getting events"); var validFilter = new PaginationRequest(pagination.PageNumber, pagination.PageSize); - var events = eventService.GetEvents(startDate, endDate, activityAreas, tags, null, State.Published); + var events = eventService.GetEvents(startDate, endDate, activityAreas, tags, organizerId, State.Published); var totalRecords = events.Count(); var paginatedRes = events .Skip((pagination.PageNumber - 1) * pagination.PageSize) diff --git a/core/Controllers/MailTestController.cs b/core/Controllers/MailTestController.cs deleted file mode 100644 index f23a73d..0000000 --- a/core/Controllers/MailTestController.cs +++ /dev/null @@ -1,36 +0,0 @@ -#if DEBUG -using api.core.Data.Responses; -using api.emails.Models; -using api.emails.Services.Abstractions; - -using Microsoft.AspNetCore.Mvc; - -namespace api.core.controllers; - -[ApiController] -[Route("api/mail")] -public class MailTestController(IEmailService service) : ControllerBase -{ - [HttpPost] - public async Task Create() - { - var result = await service.SendEmailAsync( - "test recipient", - "test subject", - new StatusChangeModel - { - Title = "Changement de status", - Salutation = "Bonjour ApplETS,", - StatusHeaderText = "La publication « Compétition de développement mobile » a été", - StatusNameText = "refusée", - StatusRefusalHeader = "Raison du refus :", - StatusRefusalReason = "La publication ne respecte pas les règles de publication", - ButtonSeePublicationText = "Voir la publication", - ButtonLink = new Uri("https://github.com/ApplETS/Backend-Hello/compare/ftr/first-email-and-layout?expand=1"), - }, - emails.EmailsUtils.StatusChangeTemplate); - - return Ok(); - } -} -#endif diff --git a/core/Controllers/OrganizerEventsController.cs b/core/Controllers/OrganizerEventsController.cs index b95e859..677b696 100644 --- a/core/Controllers/OrganizerEventsController.cs +++ b/core/Controllers/OrganizerEventsController.cs @@ -6,10 +6,8 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using api.core.data.entities; using api.core.Data.Entities; using api.core.Data.Requests; -using api.core.Data.Exceptions; namespace api.core.Controllers; @@ -46,7 +44,7 @@ public IActionResult MyEvents( } [HttpPost] - public IActionResult AddEvent([FromBody] EventRequestDTO dto) + public IActionResult AddEvent([FromForm] EventCreationRequestDTO dto) { logger.LogInformation($"Adding new event"); @@ -69,7 +67,7 @@ public IActionResult DeleteEvent(Guid id) } [HttpPatch("{id}")] - public IActionResult UpdateEvent(Guid id, [FromBody] EventRequestDTO dto) + public IActionResult UpdateEvent(Guid id, [FromForm] EventUpdateRequestDTO dto) { var userId = JwtUtils.GetUserIdFromAuthHeader(HttpContext.Request.Headers["Authorization"]!); return eventService.UpdateEvent(userId, id, dto) ? Ok() : BadRequest(); diff --git a/core/Controllers/TestController.cs b/core/Controllers/TestController.cs new file mode 100644 index 0000000..12f6708 --- /dev/null +++ b/core/Controllers/TestController.cs @@ -0,0 +1,25 @@ +using api.core.Data.Requests; +using api.emails.Models; +using api.emails.Services.Abstractions; + +using Microsoft.AspNetCore.Mvc; + + +namespace api.core.controllers; + +[ApiController] +[Route("api/test")] +public class TestController(IEmailService service, IConfiguration configuration) : ControllerBase +{ + + [HttpPost("login")] + public async Task Login([FromBody] LoginRequestDTO req, CancellationToken ct) + { + var projectId = configuration.GetValue("SUPABASE_PROJECT_ID"); + var anonKey = configuration.GetValue("SUPABASE_ANON_KEY"); + + var client = new Supabase.Client($"https://{projectId}.supabase.co", anonKey); + var response = await client.Auth.SignInWithPassword(req.Email, req.Password); + return Ok(response); + } +} \ No newline at end of file diff --git a/core/Data/Entities/Publication.cs b/core/Data/Entities/Publication.cs index b20bc1b..bcb8d64 100644 --- a/core/Data/Entities/Publication.cs +++ b/core/Data/Entities/Publication.cs @@ -22,7 +22,7 @@ public partial class Publication public string Content { get; set; } = null!; - public string ImageUrl { get; set; } = null!; + public string? ImageUrl { get; set; } public State State { get; set; } diff --git a/core/Data/Requests/EventRequestDTO.cs b/core/Data/Requests/EventRequestDTO.cs index 27c758c..8e91583 100644 --- a/core/Data/Requests/EventRequestDTO.cs +++ b/core/Data/Requests/EventRequestDTO.cs @@ -5,16 +5,10 @@ namespace api.core.Data.requests; public class EventRequestDTO { - public Guid Id { get; set; } - public string Title { get; set; } = null!; public string Content { get; set; } = null!; - public string ImageUrl { get; set; } = null!; - - public State State { get; set; } - public DateTime PublicationDate { get; set; } public DateTime EventStartDate { get; set; } @@ -23,3 +17,15 @@ public class EventRequestDTO public virtual ICollection Tags { get; set; } = new List(); } + + +public class EventCreationRequestDTO : EventRequestDTO +{ + public IFormFile Image { get; set; } +} + +public class EventUpdateRequestDTO : EventRequestDTO +{ + public IFormFile? Image { get; set; } +} + diff --git a/core/Data/Requests/LoginRequestDTO.cs b/core/Data/Requests/LoginRequestDTO.cs new file mode 100644 index 0000000..5c3ca48 --- /dev/null +++ b/core/Data/Requests/LoginRequestDTO.cs @@ -0,0 +1,7 @@ +namespace api.core.Data.Requests; + +public class LoginRequestDTO +{ + public required string Email { get; set; } + public required string Password { get; set; } +} diff --git a/core/Dockerfile b/core/Dockerfile index 402ca46..d0241c0 100644 --- a/core/Dockerfile +++ b/core/Dockerfile @@ -9,6 +9,7 @@ ARG BUILD_CONFIGURATION=Release WORKDIR /src COPY ../core/api.core.csproj ./core/ COPY ../emails/api.emails.csproj ./emails/ +COPY ../files/api.files.csproj ./files/ COPY ../tests/api.tests.csproj ./tests/ COPY ../Hello.sln ./ RUN dotnet restore @@ -24,4 +25,5 @@ RUN dotnet publish -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false FROM base AS runtime WORKDIR /app COPY --chown=app --from=publish /app/publish . -ENTRYPOINT ["dotnet", "api.core.dll"] \ No newline at end of file +RUN mkdir -p /app/volume # create volume directory to store images +ENTRYPOINT ["dotnet", "api.core.dll"] diff --git a/core/Extensions/DependencyInjectionExtension.cs b/core/Extensions/DependencyInjectionExtension.cs index 7515cec..cbb0fe0 100644 --- a/core/Extensions/DependencyInjectionExtension.cs +++ b/core/Extensions/DependencyInjectionExtension.cs @@ -5,6 +5,8 @@ using api.core.Services; using api.emails.Services; using api.emails.Services.Abstractions; +using api.files.Services; +using api.files.Services.Abstractions; namespace api.core.Extensions; @@ -25,6 +27,7 @@ public static IServiceCollection AddDependencyInjection(this IServiceCollection services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); return services; } diff --git a/core/Migrations/20240225151815_AddThumbnailToDB.Designer.cs b/core/Migrations/20240225151815_AddThumbnailToDB.Designer.cs new file mode 100644 index 0000000..38fe322 --- /dev/null +++ b/core/Migrations/20240225151815_AddThumbnailToDB.Designer.cs @@ -0,0 +1,398 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using api.core.data; + +#nullable disable + +namespace api.core.Migrations +{ + [DbContext(typeof(EventManagementContext))] + [Migration("20240225151815_AddThumbnailToDB")] + partial class AddThumbnailToDB + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("PublicationTag", b => + { + b.Property("PublicationsId") + .HasColumnType("uuid"); + + b.Property("TagsId") + .HasColumnType("uuid"); + + b.HasKey("PublicationsId", "TagsId"); + + b.HasIndex(new[] { "TagsId" }, "IX_PublicationTag_TagsId"); + + b.ToTable("PublicationTag", (string)null); + }); + + modelBuilder.Entity("TagTag", b => + { + b.Property("ChildrenTagsId") + .HasColumnType("uuid"); + + b.Property("ParentTagsId") + .HasColumnType("uuid"); + + b.HasKey("ChildrenTagsId", "ParentTagsId"); + + b.ToTable("TagTag"); + }); + + modelBuilder.Entity("TagsHierarchy", b => + { + b.Property("ChildrenTagsId") + .HasColumnType("uuid"); + + b.Property("ParentTagsId") + .HasColumnType("uuid"); + + b.HasKey("ChildrenTagsId", "ParentTagsId"); + + b.HasIndex(new[] { "ParentTagsId" }, "IX_TagsHierarchy_ParentTagsId"); + + b.ToTable("TagsHierarchy", (string)null); + }); + + modelBuilder.Entity("api.core.data.entities.Event", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("EventEndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EventStartDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Event"); + }); + + modelBuilder.Entity("api.core.data.entities.Moderator", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("(now() AT TIME ZONE 'utc'::text)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("(now() AT TIME ZONE 'utc'::text)"); + + b.HasKey("Id"); + + b.ToTable("Moderator"); + }); + + modelBuilder.Entity("api.core.data.entities.Organizer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ActivityArea") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("(now() AT TIME ZONE 'utc'::text)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscordLink") + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("FacebookLink") + .HasColumnType("text"); + + b.Property("InstagramLink") + .HasColumnType("text"); + + b.Property("LinkedInLink") + .HasColumnType("text"); + + b.Property("Organisation") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProfileDescription") + .IsRequired() + .HasColumnType("text"); + + b.Property("RedditLink") + .HasColumnType("text"); + + b.Property("TikTokLink") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("(now() AT TIME ZONE 'utc'::text)"); + + b.Property("WebSiteLink") + .HasColumnType("text"); + + b.Property("XLink") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Organizer"); + }); + + modelBuilder.Entity("api.core.data.entities.Publication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("(now() AT TIME ZONE 'utc'::text)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ImageThumbnail") + .IsRequired() + .HasColumnType("BYTEA") + .HasColumnName("image_thumbnail"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("ModeratorId") + .HasColumnType("uuid"); + + b.Property("OrganizerId") + .HasColumnType("uuid"); + + b.Property("PublicationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("State") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("(now() AT TIME ZONE 'utc'::text)"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "ModeratorId" }, "IX_Publication_ModeratorId"); + + b.HasIndex(new[] { "OrganizerId" }, "IX_Publication_OrganizerId"); + + b.ToTable("Publication"); + }); + + modelBuilder.Entity("api.core.data.entities.Report", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("(now() AT TIME ZONE 'utc'::text)"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PublicationId") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("(now() AT TIME ZONE 'utc'::text)"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "PublicationId" }, "IX_Report_PublicationId"); + + b.ToTable("Report"); + }); + + modelBuilder.Entity("api.core.data.entities.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("(now() AT TIME ZONE 'utc'::text)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PriorityValue") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("(now() AT TIME ZONE 'utc'::text)"); + + b.HasKey("Id"); + + b.ToTable("Tag"); + }); + + modelBuilder.Entity("PublicationTag", b => + { + b.HasOne("api.core.data.entities.Publication", null) + .WithMany() + .HasForeignKey("PublicationsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("api.core.data.entities.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("TagsHierarchy", b => + { + b.HasOne("api.core.data.entities.Tag", null) + .WithMany() + .HasForeignKey("ChildrenTagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("api.core.data.entities.Tag", null) + .WithMany() + .HasForeignKey("ParentTagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("api.core.data.entities.Event", b => + { + b.HasOne("api.core.data.entities.Publication", "Publication") + .WithOne("Event") + .HasForeignKey("api.core.data.entities.Event", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Publication"); + }); + + modelBuilder.Entity("api.core.data.entities.Publication", b => + { + b.HasOne("api.core.data.entities.Moderator", "Moderator") + .WithMany("Publications") + .HasForeignKey("ModeratorId") + .HasConstraintName("Publication_ModeratorId_fkey"); + + b.HasOne("api.core.data.entities.Organizer", "Organizer") + .WithMany("Publications") + .HasForeignKey("OrganizerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("Publication_OrganizerId_fkey"); + + b.Navigation("Moderator"); + + b.Navigation("Organizer"); + }); + + modelBuilder.Entity("api.core.data.entities.Report", b => + { + b.HasOne("api.core.data.entities.Publication", "Publication") + .WithMany("Reports") + .HasForeignKey("PublicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("Report_PublicationId_fkey"); + + b.Navigation("Publication"); + }); + + modelBuilder.Entity("api.core.data.entities.Moderator", b => + { + b.Navigation("Publications"); + }); + + modelBuilder.Entity("api.core.data.entities.Organizer", b => + { + b.Navigation("Publications"); + }); + + modelBuilder.Entity("api.core.data.entities.Publication", b => + { + b.Navigation("Event"); + + b.Navigation("Reports"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/core/Migrations/20240225151815_AddThumbnailToDB.cs b/core/Migrations/20240225151815_AddThumbnailToDB.cs new file mode 100644 index 0000000..39fe69e --- /dev/null +++ b/core/Migrations/20240225151815_AddThumbnailToDB.cs @@ -0,0 +1,47 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace api.core.Migrations +{ + /// + public partial class AddThumbnailToDB : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "ImageUrl", + table: "Publication", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AddColumn( + name: "image_thumbnail", + table: "Publication", + type: "BYTEA", + nullable: false, + defaultValue: new byte[0]); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "image_thumbnail", + table: "Publication"); + + migrationBuilder.AlterColumn( + name: "ImageUrl", + table: "Publication", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + } + } +} diff --git a/core/Migrations/EventManagementContextModelSnapshot.cs b/core/Migrations/EventManagementContextModelSnapshot.cs index 8d1956c..7cdcc6e 100644 --- a/core/Migrations/EventManagementContextModelSnapshot.cs +++ b/core/Migrations/EventManagementContextModelSnapshot.cs @@ -194,8 +194,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("DeletedAt") .HasColumnType("timestamp with time zone"); - b.Property("ImageUrl") + b.Property("ImageThumbnail") .IsRequired() + .HasColumnType("BYTEA") + .HasColumnName("image_thumbnail"); + + b.Property("ImageUrl") .HasColumnType("text"); b.Property("ModeratorId") diff --git a/core/Properties/launchSettings.json b/core/Properties/launchSettings.json index 0b45b9a..dc19d8a 100644 --- a/core/Properties/launchSettings.json +++ b/core/Properties/launchSettings.json @@ -39,7 +39,7 @@ "useSSL": true, "httpPort": 8080, "sslPort": 8081, - "DockerfileRunArguments": "--rm --env-file .env" + "DockerfileRunArguments": "--rm --env-file .env -v C:\\Users\\monts\\projects\\ps\\volume:/app/volume:rw" } }, "$schema": "http://json.schemastore.org/launchsettings.json", @@ -51,4 +51,4 @@ "sslPort": 44369 } } -} +} \ No newline at end of file diff --git a/core/Services/Abstractions/IEventService.cs b/core/Services/Abstractions/IEventService.cs index 1ce5d06..39749f3 100644 --- a/core/Services/Abstractions/IEventService.cs +++ b/core/Services/Abstractions/IEventService.cs @@ -10,11 +10,11 @@ public interface IEventService public EventResponseDTO GetEvent(Guid id); - public EventResponseDTO AddEvent(Guid userId, EventRequestDTO request); + public EventResponseDTO AddEvent(Guid userId, EventCreationRequestDTO request); public bool DeleteEvent(Guid userId, Guid eventId); - public bool UpdateEvent(Guid userId, Guid eventId, EventRequestDTO request); + public bool UpdateEvent(Guid userId, Guid eventId, EventUpdateRequestDTO request); public bool UpdateEventState(Guid userId, Guid eventId, State state); } diff --git a/core/Services/EventService.cs b/core/Services/EventService.cs index df8b544..e4cfabb 100644 --- a/core/Services/EventService.cs +++ b/core/Services/EventService.cs @@ -5,17 +5,31 @@ using api.core.Data.Responses; using api.core.repositories.abstractions; using api.core.services.abstractions; +using api.files.Services.Abstractions; +using Azure.Core; + +using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Tokens; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Processing; + namespace api.core.Services; public class EventService( IEventRepository evntRepo, ITagRepository tagRepo, IOrganizerRepository orgRepo, - IModeratorRepository moderatorRepo) : IEventService + IModeratorRepository moderatorRepo, + IFileShareService fileShareService, + IConfiguration configuration) : IEventService { + private const double IMAGE_RATIO_SIZE_ACCEPTANCE = 2.0; // width/height ratio + private const double TOLERANCE_ACCEPTABILITY = 0.001; + public IEnumerable GetEvents( DateTime? startDate, DateTime? endDate, @@ -48,7 +62,7 @@ public EventResponseDTO GetEvent(Guid id) return EventResponseDTO.Map(evnt); } - public EventResponseDTO AddEvent(Guid userId, EventRequestDTO request) + public EventResponseDTO AddEvent(Guid userId, EventCreationRequestDTO request) { var organizer = orgRepo.Get(userId) ?? throw new UnauthorizedException(); @@ -56,15 +70,24 @@ public EventResponseDTO AddEvent(Guid userId, EventRequestDTO request) .Where(t => request.Tags.Contains(t.Id)) ?? Enumerable.Empty(); + var id = Guid.NewGuid(); + var cdnUrl = configuration.GetValue("CDN_URL"); + var completePath = $"{cdnUrl}/{id}/{request.Image.FileName}"; + var uri = new Uri(completePath); + + HandleImageSaving(id, request.Image); + var inserted = evntRepo.Add(new Event { + Id = id, EventStartDate = request.EventStartDate, EventEndDate = request.EventEndDate, Publication = new Publication { + Id = id, Title = request.Title, Content = request.Content, - ImageUrl = request.ImageUrl, + ImageUrl = uri.ToString(), State = State.OnHold, PublicationDate = request.PublicationDate, Tags = tags.ToList(), @@ -89,7 +112,7 @@ public bool DeleteEvent(Guid userId, Guid eventId) } - public bool UpdateEvent(Guid userId, Guid eventId, EventRequestDTO request) + public bool UpdateEvent(Guid userId, Guid eventId, EventUpdateRequestDTO request) { var organizer = orgRepo.Get(userId) ?? throw new UnauthorizedException(); var evnt = evntRepo.Get(eventId); @@ -101,18 +124,29 @@ public bool UpdateEvent(Guid userId, Guid eventId, EventRequestDTO request) .Where(t => request.Tags.Contains(t.Id)) ?? Enumerable.Empty(); + Uri uri = new Uri(evnt.Publication.ImageUrl); + + if (request.Image != null) + { + var cdnUrl = configuration.GetValue("CDN_URL"); + var completePath = $"{cdnUrl}/{evnt.Id}/{request.Image?.FileName}"; + uri = new Uri(completePath); + + HandleImageSaving(eventId, request.Image); + } + return evntRepo.Update(eventId, new Event { Id = eventId, EventStartDate = request.EventStartDate, EventEndDate = request.EventEndDate, - Publication = new Publication + Publication = new () { Id = eventId, Title = request.Title, Content = request.Content, - ImageUrl = request.ImageUrl, - State = request.State, + State = State.OnHold, + ImageUrl = uri.ToString(), PublicationDate = request.PublicationDate, Tags = tags.ToList(), Organizer = organizer, @@ -141,4 +175,29 @@ private bool CanPerformAction(Guid userId, Event evnt) return (evnt!.Publication.Moderator != null && evnt.Publication.Moderator.Id == userId) || (evnt!.Publication.Organizer != null && evnt.Publication.Organizer.Id == userId); } + + private void HandleImageSaving(Guid eventId, IFormFile imageFile) + { + byte[] imageBytes = []; + try + { + using var image = Image.Load(imageFile.OpenReadStream()); + int width = image.Size.Width; + int height = image.Size.Height; + + if (Math.Abs((width / height) - IMAGE_RATIO_SIZE_ACCEPTANCE) > TOLERANCE_ACCEPTABILITY) + throw new BadParameterException(nameof(image), "Invalid image aspect ratio"); + + image.Mutate(c => c.Resize(400, 200)); + using var outputStream = new MemoryStream(); + image.SaveAsWebp(outputStream); + outputStream.Position = 0; + + fileShareService.FileUpload(eventId.ToString(), imageFile.FileName, outputStream); + } + catch (Exception e) + { + throw new BadParameterException(nameof(imageFile), $"Invalid image metadata: {e.Message}"); + } + } } diff --git a/core/api.core.csproj b/core/api.core.csproj index b8a2c7d..35e1ee2 100644 --- a/core/api.core.csproj +++ b/core/api.core.csproj @@ -25,12 +25,14 @@ + + diff --git a/files/Models/FileUploadModel.cs b/files/Models/FileUploadModel.cs new file mode 100644 index 0000000..b0cc708 --- /dev/null +++ b/files/Models/FileUploadModel.cs @@ -0,0 +1,7 @@ +namespace api.files.Models; + +public class FileUploadModel +{ + public string FileName { get; set; } + public Stream FileStream { get; set; } +} diff --git a/files/Services/Abstractions/IFileShareService.cs b/files/Services/Abstractions/IFileShareService.cs new file mode 100644 index 0000000..bfd1ad5 --- /dev/null +++ b/files/Services/Abstractions/IFileShareService.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.Http; + +namespace api.files.Services.Abstractions; + +public interface IFileShareService +{ + void FileUpload(string subPath, string fileName, Stream streamFile); +} diff --git a/files/Services/FileShareService.cs b/files/Services/FileShareService.cs new file mode 100644 index 0000000..c96b330 --- /dev/null +++ b/files/Services/FileShareService.cs @@ -0,0 +1,29 @@ +using api.files.Services.Abstractions; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; + +namespace api.files.Services; + +public class FileShareService : IFileShareService +{ + private readonly IConfiguration _config; + + public FileShareService(IConfiguration config) + { + _config = config; + } + + public void FileUpload(string subPath, string fileName, Stream streamFile) + { + // Get the configurations and create share object + var dir = Path.Combine(_config.GetValue("CONTAINER_DIR"), subPath); + + Directory.CreateDirectory(dir); + if (streamFile.Length > 0) + { + string filePath = Path.Combine(dir, fileName); + using Stream fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write); + streamFile.CopyTo(fileStream); + } + } +} diff --git a/files/api.files.csproj b/files/api.files.csproj new file mode 100644 index 0000000..8023c22 --- /dev/null +++ b/files/api.files.csproj @@ -0,0 +1,16 @@ + + + + net8.0 + enable + enable + + + + + + + + + + diff --git a/tests/Tests/Services/EventServiceTests.cs b/tests/Tests/Services/EventServiceTests.cs index 39de938..54b567c 100644 --- a/tests/Tests/Services/EventServiceTests.cs +++ b/tests/Tests/Services/EventServiceTests.cs @@ -10,9 +10,13 @@ using api.core.Data.requests; using api.core.repositories.abstractions; using api.core.Services; +using api.files.Services.Abstractions; using FluentAssertions; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; + using Moq; namespace api.tests.Tests.Services; @@ -32,7 +36,7 @@ public class EventServiceTests { Title = "EVENT IN 5 DAYS", Content = "Test", - ImageUrl = "Test", + ImageUrl = "http://example.com", State = State.Published, PublicationDate = DateTime.Now, Tags = new List @@ -65,7 +69,7 @@ public class EventServiceTests { Title = "EVENT TOMORROW, DIFFERENT ACTIVITY AREA", Content = "Test", - ImageUrl = "Test", + ImageUrl = "http://example.com", State = State.Published, PublicationDate = DateTime.Now, Tags = new List @@ -94,7 +98,7 @@ public class EventServiceTests { Title = "EVENT TOMORROW, WITHOUT TAGS", Content = "Test", - ImageUrl = "Test", + ImageUrl = "http://example.com", State = State.Published, PublicationDate = DateTime.Now, Tags = new List(), @@ -116,7 +120,7 @@ public class EventServiceTests { Title = "DELETED EVENT", Content = "Test", - ImageUrl = "Test", + ImageUrl = "http://example.com", State = State.Deleted, PublicationDate = DateTime.Now, Tags = new List @@ -147,8 +151,16 @@ public void GetEvents_ShouldReturnAllEventsExceptDeletedOnes() var mockTagRepository = new Mock(); var mockOrganizerRepository = new Mock(); var mockModeratorRepository = new Mock(); - - var eventService = new EventService(mockEventRepository.Object, mockTagRepository.Object, mockOrganizerRepository.Object, mockModeratorRepository.Object); + var mockFileShareService = new Mock(); + var mockConfig = new Mock(); + + var eventService = new EventService( + mockEventRepository.Object, + mockTagRepository.Object, + mockOrganizerRepository.Object, + mockModeratorRepository.Object, + mockFileShareService.Object, + mockConfig.Object); mockEventRepository.Setup(repo => repo.GetAll()).Returns(_events); @@ -169,8 +181,16 @@ public void GetEvents_ShouldReturnOnlyEventsAfterStartDate() var mockTagRepository = new Mock(); var mockOrganizerRepository = new Mock(); var mockModeratorRepository = new Mock(); + var mockFileShareService = new Mock(); + var mockConfig = new Mock(); - var eventService = new EventService(mockEventRepository.Object, mockTagRepository.Object, mockOrganizerRepository.Object, mockModeratorRepository.Object); + var eventService = new EventService( + mockEventRepository.Object, + mockTagRepository.Object, + mockOrganizerRepository.Object, + mockModeratorRepository.Object, + mockFileShareService.Object, + mockConfig.Object); mockEventRepository.Setup(repo => repo.GetAll()).Returns(_events); @@ -191,8 +211,16 @@ public void GetEvents_ShouldReturnOnlyEventsBeforeEndDate() var mockTagRepository = new Mock(); var mockOrganizerRepository = new Mock(); var mockModeratorRepository = new Mock(); + var mockFileShareService = new Mock(); + var mockConfig = new Mock(); - var eventService = new EventService(mockEventRepository.Object, mockTagRepository.Object, mockOrganizerRepository.Object, mockModeratorRepository.Object); + var eventService = new EventService( + mockEventRepository.Object, + mockTagRepository.Object, + mockOrganizerRepository.Object, + mockModeratorRepository.Object, + mockFileShareService.Object, + mockConfig.Object); mockEventRepository.Setup(repo => repo.GetAll()).Returns(_events); @@ -213,8 +241,16 @@ public void GetEvents_ShouldReturnOnlyEventsOnlyActivityAreaClub() var mockTagRepository = new Mock(); var mockOrganizerRepository = new Mock(); var mockModeratorRepository = new Mock(); + var mockFileShareService = new Mock(); + var mockConfig = new Mock(); - var eventService = new EventService(mockEventRepository.Object, mockTagRepository.Object, mockOrganizerRepository.Object, mockModeratorRepository.Object); + var eventService = new EventService( + mockEventRepository.Object, + mockTagRepository.Object, + mockOrganizerRepository.Object, + mockModeratorRepository.Object, + mockFileShareService.Object, + mockConfig.Object); mockEventRepository.Setup(repo => repo.GetAll()).Returns(_events); @@ -238,8 +274,16 @@ public void GetEvents_ShouldReturnOnlyEventsOnlyTagTest() var mockTagRepository = new Mock(); var mockOrganizerRepository = new Mock(); var mockModeratorRepository = new Mock(); + var mockFileShareService = new Mock(); + var mockConfig = new Mock(); - var eventService = new EventService(mockEventRepository.Object, mockTagRepository.Object, mockOrganizerRepository.Object, mockModeratorRepository.Object); + var eventService = new EventService( + mockEventRepository.Object, + mockTagRepository.Object, + mockOrganizerRepository.Object, + mockModeratorRepository.Object, + mockFileShareService.Object, + mockConfig.Object); mockEventRepository.Setup(repo => repo.GetAll()).Returns(_events); @@ -264,8 +308,16 @@ public void GetEvent_ShouldThrowAnException() var mockTagRepository = new Mock(); var mockOrganizerRepository = new Mock(); var mockModeratorRepository = new Mock(); + var mockFileShareService = new Mock(); + var mockConfig = new Mock(); - var eventService = new EventService(mockEventRepository.Object, mockTagRepository.Object, mockOrganizerRepository.Object, mockModeratorRepository.Object); + var eventService = new EventService( + mockEventRepository.Object, + mockTagRepository.Object, + mockOrganizerRepository.Object, + mockModeratorRepository.Object, + mockFileShareService.Object, + mockConfig.Object); mockEventRepository.Setup(repo => repo.Get(_events.First().Id)).Returns((Event?)null); @@ -284,9 +336,16 @@ public void GetEvent_ShouldReturnEvent() var mockTagRepository = new Mock(); var mockOrganizerRepository = new Mock(); var mockModeratorRepository = new Mock(); + var mockFileShareService = new Mock(); + var mockConfig = new Mock(); - var eventService = new EventService(mockEventRepository.Object, mockTagRepository.Object, mockOrganizerRepository.Object, mockModeratorRepository.Object); - + var eventService = new EventService( + mockEventRepository.Object, + mockTagRepository.Object, + mockOrganizerRepository.Object, + mockModeratorRepository.Object, + mockFileShareService.Object, + mockConfig.Object); mockEventRepository.Setup(repo => repo.Get(_events.First().Id)).Returns(_events.First()); @@ -308,13 +367,22 @@ public void AddEvents_ShouldThrowAnExceptionWhenOrganizerIsUnknown() var mockOrganizerRepository = new Mock(); var mockModeratorRepository = new Mock(); - mockOrganizerRepository.Setup(repo => repo.Get(It.IsAny())).Returns((Organizer?)null); + var mockFileShareService = new Mock(); + var mockConfig = new Mock(); - var eventService = new EventService(mockEventRepository.Object, mockTagRepository.Object, mockOrganizerRepository.Object, mockModeratorRepository.Object); + var eventService = new EventService( + mockEventRepository.Object, + mockTagRepository.Object, + mockOrganizerRepository.Object, + mockModeratorRepository.Object, + mockFileShareService.Object, + mockConfig.Object); + + mockOrganizerRepository.Setup(repo => repo.Get(It.IsAny())).Returns((Organizer?)null); // Act eventService.Invoking(s => - s.AddEvent(Guid.Empty, new EventRequestDTO())) + s.AddEvent(Guid.Empty, new EventCreationRequestDTO())) .Should().Throw(); } @@ -323,13 +391,25 @@ public void DeleteEvent_ShouldReturnTrue_WhenEventIsDeletedSuccessfully() { // Arrange var mockEventRepository = new Mock(); + var mockTagRepository = new Mock(); + var mockOrganizerRepository = new Mock(); + var mockModeratorRepository = new Mock(); var userId = _events.First().Publication.Organizer.Id; var eventId = _events.First().Id; mockEventRepository.Setup(repo => repo.Get(eventId)).Returns(_events.First()); mockEventRepository.Setup(repo => repo.Delete(It.IsAny())).Returns(true); - var eventService = new EventService(mockEventRepository.Object, new Mock().Object, new Mock().Object, new Mock().Object); + var mockFileShareService = new Mock(); + var mockConfig = new Mock(); + + var eventService = new EventService( + mockEventRepository.Object, + mockTagRepository.Object, + mockOrganizerRepository.Object, + mockModeratorRepository.Object, + mockFileShareService.Object, + mockConfig.Object); // Act var result = eventService.DeleteEvent(userId, eventId); @@ -344,13 +424,23 @@ public void DeleteEvent_ShouldThrowNotFoundException_WhenEventDoesNotExist() { // Arrange var mockEventRepository = new Mock(); + var mockTagRepository = new Mock(); + var mockOrganizerRepository = new Mock(); + var mockModeratorRepository = new Mock(); + var mockFileShareService = new Mock(); + var mockConfig = new Mock(); var userId = Guid.NewGuid(); var eventId = Guid.NewGuid(); mockEventRepository.Setup(repo => repo.Get(eventId)).Returns((Event?)null); - var eventService = new EventService(mockEventRepository.Object, new Mock().Object, new Mock().Object, new Mock().Object); - + var eventService = new EventService( + mockEventRepository.Object, + mockTagRepository.Object, + mockOrganizerRepository.Object, + mockModeratorRepository.Object, + mockFileShareService.Object, + mockConfig.Object); // Act Action act = () => eventService.DeleteEvent(userId, eventId); @@ -364,12 +454,23 @@ public void DeleteEvent_ShouldThrowUnauthorizedException_WhenUserIsNotAuthorized { // Arrange var mockEventRepository = new Mock(); + var mockTagRepository = new Mock(); + var mockOrganizerRepository = new Mock(); + var mockModeratorRepository = new Mock(); + var mockFileShareService = new Mock(); + var mockConfig = new Mock(); var unauthorizedUserId = Guid.NewGuid(); var eventId = _events.First().Id; mockEventRepository.Setup(repo => repo.Get(eventId)).Returns(_events.First()); - var eventService = new EventService(mockEventRepository.Object, new Mock().Object, new Mock().Object, new Mock().Object); + var eventService = new EventService( + mockEventRepository.Object, + mockTagRepository.Object, + mockOrganizerRepository.Object, + mockModeratorRepository.Object, + mockFileShareService.Object, + mockConfig.Object); // Act Action act = () => eventService.DeleteEvent(unauthorizedUserId, eventId); @@ -385,17 +486,26 @@ public void UpdateEvent_ShouldReturnTrue_WhenEventIsUpdatedSuccessfully() { // Arrange var mockEventRepository = new Mock(); + var mockTagRepository = new Mock(); var mockOrganizerRepository = new Mock(); + var mockModeratorRepository = new Mock(); + var mockFileShareService = new Mock(); + + var inMemorySettings = new Dictionary { + {"CDN_URL", "http://example.com"}, + }; + IConfiguration configuration = new ConfigurationBuilder() + .AddInMemoryCollection(inMemorySettings) + .Build(); + var userId = _events.First().Publication.Organizer.Id; var eventId = _events.First().Id; - var request = new EventRequestDTO + var request = new EventUpdateRequestDTO { - Id = Guid.NewGuid(), Title = "Sample Event Title", Content = "This is a detailed description of the event.", - ImageUrl = "https://example.com/image.jpg", - State = State.Approved, + Image = null, PublicationDate = DateTime.UtcNow, EventStartDate = DateTime.UtcNow.AddDays(10), EventEndDate = DateTime.UtcNow.AddDays(10).AddHours(1), @@ -410,7 +520,13 @@ public void UpdateEvent_ShouldReturnTrue_WhenEventIsUpdatedSuccessfully() mockEventRepository.Setup(repo => repo.Update(eventId, It.IsAny())).Returns(true); mockOrganizerRepository.Setup(repo => repo.Get(It.IsAny())).Returns(new Organizer { Id = userId }); - var eventService = new EventService(mockEventRepository.Object, new Mock().Object, mockOrganizerRepository.Object, new Mock().Object); + var eventService = new EventService( + mockEventRepository.Object, + mockTagRepository.Object, + mockOrganizerRepository.Object, + mockModeratorRepository.Object, + mockFileShareService.Object, + configuration); // Act var result = eventService.UpdateEvent(userId, eventId, request); @@ -425,16 +541,18 @@ public void UpdateEvent_ShouldThrowUnauthorizedException_WhenUserIsNotAuthorized { // Arrange var mockEventRepository = new Mock(); + var mockTagRepository = new Mock(); + var mockOrganizerRepository = new Mock(); + var mockModeratorRepository = new Mock(); + var mockFileShareService = new Mock(); + var mockConfig = new Mock(); var unauthorizedUserId = Guid.NewGuid(); var eventId = _events.First().Id; - var request = new EventRequestDTO + var request = new EventUpdateRequestDTO { - Id = Guid.NewGuid(), Title = "Sample Event Title", Content = "This is a detailed description of the event.", - ImageUrl = "https://example.com/image.jpg", - State = State.Approved, PublicationDate = DateTime.UtcNow, EventStartDate = DateTime.UtcNow.AddDays(10), EventEndDate = DateTime.UtcNow.AddDays(10).AddHours(1), @@ -448,7 +566,13 @@ public void UpdateEvent_ShouldThrowUnauthorizedException_WhenUserIsNotAuthorized // Assuming _events.First() returns an event where the organizer ID does not match `unauthorizedUserId` mockEventRepository.Setup(repo => repo.Get(eventId)).Returns(_events.First()); - var eventService = new EventService(mockEventRepository.Object, new Mock().Object, new Mock().Object, new Mock().Object); + var eventService = new EventService( + mockEventRepository.Object, + mockTagRepository.Object, + mockOrganizerRepository.Object, + mockModeratorRepository.Object, + mockFileShareService.Object, + mockConfig.Object); // Act Action act = () => eventService.UpdateEvent(unauthorizedUserId, eventId, request); @@ -463,11 +587,15 @@ public void UpdateEventState_ShouldReturnTrue_WhenStateIsUpdatedSuccessfullyByMo { // Arrange var mockEventRepository = new Mock(); + var mockTagRepository = new Mock(); + var mockOrganizerRepository = new Mock(); var mockModeratorRepository = new Mock(); + var mockFileShareService = new Mock(); + var mockConfig = new Mock(); var userId = _events.First().Publication.Moderator.Id; var eventId = _events.First().Id; - var newState = core.Data.Entities.State.Approved; + var newState = State.Approved; var eventToUpdate = _events.First(); eventToUpdate.Publication.ModeratorId = userId; @@ -476,7 +604,13 @@ public void UpdateEventState_ShouldReturnTrue_WhenStateIsUpdatedSuccessfullyByMo mockEventRepository.Setup(repo => repo.Update(eventId, It.IsAny())).Returns(true); mockModeratorRepository.Setup(repo => repo.Get(It.IsAny())).Returns(new Moderator { Id = userId }); - var eventService = new EventService(mockEventRepository.Object, new Mock().Object, new Mock().Object, mockModeratorRepository.Object); + var eventService = new EventService( + mockEventRepository.Object, + mockTagRepository.Object, + mockOrganizerRepository.Object, + mockModeratorRepository.Object, + mockFileShareService.Object, + mockConfig.Object); // Act var result = eventService.UpdateEventState(userId, eventId, newState); @@ -491,6 +625,11 @@ public void UpdateEventState_ShouldThrowUnauthorizedException_WhenUserIsNotAutho { // Arrange var mockEventRepository = new Mock(); + var mockTagRepository = new Mock(); + var mockOrganizerRepository = new Mock(); + var mockModeratorRepository = new Mock(); + var mockFileShareService = new Mock(); + var mockConfig = new Mock(); var unauthorizedUserId = Guid.NewGuid(); var eventId = _events.First().Id; var newState = State.Approved; @@ -500,7 +639,13 @@ public void UpdateEventState_ShouldThrowUnauthorizedException_WhenUserIsNotAutho mockEventRepository.Setup(repo => repo.Get(eventId)).Returns(eventToUpdate); - var eventService = new EventService(mockEventRepository.Object, new Mock().Object, new Mock().Object, new Mock().Object); + var eventService = new EventService( + mockEventRepository.Object, + mockTagRepository.Object, + mockOrganizerRepository.Object, + mockModeratorRepository.Object, + mockFileShareService.Object, + mockConfig.Object); // Act Action act = () => eventService.UpdateEventState(unauthorizedUserId, eventId, newState);