diff --git a/Orso.Arpa.Api/Controllers/AppointmentsController.cs b/Orso.Arpa.Api/Controllers/AppointmentsController.cs index e10b14b23..d2f9d08ef 100644 --- a/Orso.Arpa.Api/Controllers/AppointmentsController.cs +++ b/Orso.Arpa.Api/Controllers/AppointmentsController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; @@ -10,301 +11,347 @@ using Orso.Arpa.Domain.AppointmentDomain.Enums; using Orso.Arpa.Domain.UserDomain.Enums; -namespace Orso.Arpa.Api.Controllers +namespace Orso.Arpa.Api.Controllers; + +public class AppointmentsController : BaseController { - public class AppointmentsController : BaseController + private readonly IAppointmentService _appointmentService; + + public AppointmentsController(IAppointmentService appointmentService) { - private readonly IAppointmentService _appointmentService; + _appointmentService = appointmentService; + } - public AppointmentsController(IAppointmentService appointmentService) - { - _appointmentService = appointmentService; - } + /// + /// Queries a list of appointments dependent on the given date and date range + /// + /// + /// + /// A list of appointments + /// + [Authorize(Roles = RoleNames.Staff)] + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task>> Get([FromQuery] DateTime? date, + [FromQuery] DateRange range) + { + return Ok(await _appointmentService.GetAsync(date, range)); + } - /// - /// Queries a list of appointments dependent on the given date and date range - /// - /// - /// - /// A list of appointments - /// - [Authorize(Roles = RoleNames.Staff)] - [HttpGet] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task>> Get([FromQuery] DateTime? date, [FromQuery] DateRange range) - { - return Ok(await _appointmentService.GetAsync(date, range)); - } + /// + /// Gets an appointment by id + /// + /// + /// Default: true. If true, request will be very slow! + /// The queried appointment + /// + /// If entity could not be found + [Authorize(Roles = RoleNames.Staff)] + [HttpGet("{id}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status404NotFound)] + public async Task> GetById([FromRoute] Guid id, + [FromQuery] bool includeParticipations = true) + { + return Ok(await _appointmentService.GetByIdAsync(id, includeParticipations)); + } - /// - /// Gets an appointment by id - /// - /// - /// Default: true. If true, request will be very slow! - /// The queried appointment - /// - /// If entity could not be found - [Authorize(Roles = RoleNames.Staff)] - [HttpGet("{id}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status404NotFound)] - public async Task> GetById([FromRoute] Guid id, [FromQuery] bool includeParticipations = true) - { - return Ok(await _appointmentService.GetByIdAsync(id, includeParticipations)); - } + /// + /// Creates a new appointment + /// + /// + /// The created appointment + /// Returns the created appointment + /// If entity could not be found + /// If validation fails + [Authorize(Roles = RoleNames.Staff)] + [HttpPost] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ValidationProblemDetails), + StatusCodes.Status422UnprocessableEntity)] + public async Task> Post( + [FromBody] AppointmentCreateDto appointmentCreateDto) + { + AppointmentDto createdAppointment = + await _appointmentService.CreateAsync(appointmentCreateDto); + return CreatedAtAction(nameof(GetById), new { id = createdAppointment.Id }, + createdAppointment); + } - /// - /// Creates a new appointment - /// - /// - /// The created appointment - /// Returns the created appointment - /// If entity could not be found - /// If validation fails - [Authorize(Roles = RoleNames.Staff)] - [HttpPost] - [ProducesResponseType(StatusCodes.Status201Created)] - [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status422UnprocessableEntity)] - public async Task> Post([FromBody] AppointmentCreateDto appointmentCreateDto) - { - AppointmentDto createdAppointment = await _appointmentService.CreateAsync(appointmentCreateDto); - return CreatedAtAction(nameof(GetById), new { id = createdAppointment.Id }, createdAppointment); - } + /// + /// Adds a room to an existing appointment + /// + /// + /// + /// If entity could not be found + /// If validation fails + [Authorize(Roles = RoleNames.Staff)] + [HttpPost("{id}/rooms/{roomId}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ValidationProblemDetails), + StatusCodes.Status422UnprocessableEntity)] + public async Task AddRoom([FromRoute] AppointmentAddRoomDto addRoomDto) + { + await _appointmentService.AddRoomAsync(addRoomDto); + return NoContent(); + } - /// - /// Adds a room to an existing appointment - /// - /// - /// - /// If entity could not be found - /// If validation fails - [Authorize(Roles = RoleNames.Staff)] - [HttpPost("{id}/rooms/{roomId}")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status422UnprocessableEntity)] - public async Task AddRoom([FromRoute] AppointmentAddRoomDto addRoomDto) - { - await _appointmentService.AddRoomAsync(addRoomDto); - return NoContent(); - } + /// + /// Adds a project to an existing appointment + /// + /// + /// + /// + /// If entity could not be found + /// If validation fails + [Authorize(Roles = RoleNames.Staff)] + [HttpPost("{id}/projects/{projectId}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ValidationProblemDetails), + StatusCodes.Status422UnprocessableEntity)] + public async Task> AddProject( + [FromRoute] AppointmentAddProjectDto addProjectDto, + [FromQuery] bool includeParticipations = true) + { + return await _appointmentService.AddProjectAsync(addProjectDto, includeParticipations); + } - /// - /// Adds a project to an existing appointment - /// - /// - /// - /// - /// If entity could not be found - /// If validation fails - [Authorize(Roles = RoleNames.Staff)] - [HttpPost("{id}/projects/{projectId}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status422UnprocessableEntity)] - public async Task> AddProject([FromRoute] AppointmentAddProjectDto addProjectDto, [FromQuery] bool includeParticipations = true) - { - return await _appointmentService.AddProjectAsync(addProjectDto, includeParticipations); - } + /// + /// Adds a section to an existing appointment + /// + /// + /// + /// + /// If domain validation fails + /// If entity could not be found + /// If validation fails + [Authorize(Roles = RoleNames.Staff)] + [HttpPost("{id}/sections/{sectionId}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ValidationProblemDetails), + StatusCodes.Status422UnprocessableEntity)] + public async Task> AddSection( + [FromRoute] AppointmentAddSectionDto addSectionDto, + [FromQuery] bool includeParticipations = true) + { + return await _appointmentService.AddSectionAsync(addSectionDto, includeParticipations); + } - /// - /// Adds a section to an existing appointment - /// - /// - /// - /// - /// If domain validation fails - /// If entity could not be found - /// If validation fails - [Authorize(Roles = RoleNames.Staff)] - [HttpPost("{id}/sections/{sectionId}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status422UnprocessableEntity)] - public async Task> AddSection([FromRoute] AppointmentAddSectionDto addSectionDto, [FromQuery] bool includeParticipations = true) - { - return await _appointmentService.AddSectionAsync(addSectionDto, includeParticipations); - } + /// + /// Sends an appointment changed notification to all participants of the appointment + /// + /// + /// + /// If entity could not be found + /// If validation fails + [Authorize(Roles = RoleNames.Staff)] + [HttpPost("{id}/notification")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ValidationProblemDetails), + StatusCodes.Status422UnprocessableEntity)] + public async Task> SendAppointmentChangedNotification( + SendAppointmentChangedNotificationDto sendAppointmentChangedNotificationDto) + { + await _appointmentService.SendAppointmentChangedNotificationAsync( + sendAppointmentChangedNotificationDto); + return NoContent(); + } - /// - /// Sends an appointment changed notification to all participants of the appointment - /// - /// - /// - /// If entity could not be found - /// If validation fails - [Authorize(Roles = RoleNames.Staff)] - [HttpPost("{id}/notification")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status422UnprocessableEntity)] - public async Task> SendAppointmentChangedNotification(SendAppointmentChangedNotificationDto sendAppointmentChangedNotificationDto) - { - await _appointmentService.SendAppointmentChangedNotificationAsync(sendAppointmentChangedNotificationDto); - return NoContent(); - } + /// + /// Sets the venue of an existing appointment + /// + /// + /// + /// If entity could not be found + /// If validation fails + [Authorize(Roles = RoleNames.Staff)] + [HttpPut("{id}/venue/set/{venueId}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ValidationProblemDetails), + StatusCodes.Status422UnprocessableEntity)] + public async Task SetVenue([FromRoute] AppointmentSetVenueDto setVenueDto) + { + await _appointmentService.SetVenueAsync(setVenueDto); + return NoContent(); + } - /// - /// Sets the venue of an existing appointment - /// - /// - /// - /// If entity could not be found - /// If validation fails - [Authorize(Roles = RoleNames.Staff)] - [HttpPut("{id}/venue/set/{venueId}")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status422UnprocessableEntity)] - public async Task SetVenue([FromRoute] AppointmentSetVenueDto setVenueDto) - { - await _appointmentService.SetVenueAsync(setVenueDto); - return NoContent(); - } + /// + /// Modifies existing appointment + /// + /// + /// + /// If entity could not be found + /// If validation fails + [Authorize(Roles = RoleNames.Staff)] + [HttpPut("{id}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ValidationProblemDetails), + StatusCodes.Status422UnprocessableEntity)] + public async Task Put(AppointmentModifyDto appointmentModifyDto) + { + await _appointmentService.ModifyAsync(appointmentModifyDto); - /// - /// Modifies existing appointment - /// - /// - /// - /// If entity could not be found - /// If validation fails - [Authorize(Roles = RoleNames.Staff)] - [HttpPut("{id}")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status422UnprocessableEntity)] - public async Task Put(AppointmentModifyDto appointmentModifyDto) - { - await _appointmentService.ModifyAsync(appointmentModifyDto); + return NoContent(); + } - return NoContent(); - } + /// + /// Removes room from existing appointment + /// + /// + /// + /// If entity could not be found + /// If validation fails + [Authorize(Roles = RoleNames.Staff)] + [HttpDelete("{id}/rooms/{roomId}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ValidationProblemDetails), + StatusCodes.Status422UnprocessableEntity)] + public async Task RemoveRoom([FromRoute] AppointmentRemoveRoomDto removeRoomDto) + { + await _appointmentService.RemoveRoomAsync(removeRoomDto); + return NoContent(); + } - /// - /// Removes room from existing appointment - /// - /// - /// - /// If entity could not be found - /// If validation fails - [Authorize(Roles = RoleNames.Staff)] - [HttpDelete("{id}/rooms/{roomId}")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status422UnprocessableEntity)] - public async Task RemoveRoom([FromRoute] AppointmentRemoveRoomDto removeRoomDto) - { - await _appointmentService.RemoveRoomAsync(removeRoomDto); - return NoContent(); - } + /// + /// Removes section from existing appointment + /// + /// + /// + /// + /// If entity could not be found + /// If validation fails + [Authorize(Roles = RoleNames.Staff)] + [HttpDelete("{id}/sections/{sectionId}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ValidationProblemDetails), + StatusCodes.Status422UnprocessableEntity)] + public async Task> RemoveSection( + [FromRoute] AppointmentRemoveSectionDto removeSectionDto, + [FromQuery] bool includeParticipations = true) + { + return await _appointmentService.RemoveSectionAsync(removeSectionDto, + includeParticipations); + } - /// - /// Removes section from existing appointment - /// - /// - /// - /// - /// If entity could not be found - /// If validation fails - [Authorize(Roles = RoleNames.Staff)] - [HttpDelete("{id}/sections/{sectionId}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status422UnprocessableEntity)] - public async Task> RemoveSection([FromRoute] AppointmentRemoveSectionDto removeSectionDto, [FromQuery] bool includeParticipations = true) - { - return await _appointmentService.RemoveSectionAsync(removeSectionDto, includeParticipations); - } + /// + /// Removes project from existing appointment + /// + /// + /// + /// + /// If entity could not be found + /// If validation fails + [Authorize(Roles = RoleNames.Staff)] + [HttpDelete("{id}/projects/{projectId}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ValidationProblemDetails), + StatusCodes.Status422UnprocessableEntity)] + public async Task> RemoveProject( + [FromRoute] AppointmentRemoveProjectDto removeProjectDto, + [FromQuery] bool includeParticipations = true) + { + return await _appointmentService.RemoveProjectAsync(removeProjectDto, + includeParticipations); + } - /// - /// Removes project from existing appointment - /// - /// - /// - /// - /// If entity could not be found - /// If validation fails - [Authorize(Roles = RoleNames.Staff)] - [HttpDelete("{id}/projects/{projectId}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status422UnprocessableEntity)] - public async Task> RemoveProject([FromRoute] AppointmentRemoveProjectDto removeProjectDto, [FromQuery] bool includeParticipations = true) - { - return await _appointmentService.RemoveProjectAsync(removeProjectDto, includeParticipations); - } + /// + /// Deletes existing appointment by id + /// + /// + /// + /// If entity could not be found + /// If validation fails + [Authorize(Roles = RoleNames.Admin)] + [HttpDelete("{id}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ValidationProblemDetails), + StatusCodes.Status422UnprocessableEntity)] + public async Task Delete([FromRoute] Guid id) + { + await _appointmentService.DeleteAsync(id); + return NoContent(); + } - /// - /// Deletes existing appointment by id - /// - /// - /// - /// If entity could not be found - /// If validation fails - [Authorize(Roles = RoleNames.Admin)] - [HttpDelete("{id}")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status422UnprocessableEntity)] - public async Task Delete([FromRoute] Guid id) - { - await _appointmentService.DeleteAsync(id); - return NoContent(); - } + /// + /// Sets start and end time of an existing appointment + /// + /// + /// + /// If entity could not be found + /// If validation fails + [Authorize(Roles = RoleNames.Staff)] + [HttpPut("{id}/dates/set")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ValidationProblemDetails), + StatusCodes.Status422UnprocessableEntity)] + public async Task> SetDates(AppointmentSetDatesDto setDatesDto) + { + return await _appointmentService.SetDatesAsync(setDatesDto); + } - /// - /// Sets start and end time of an existing appointment - /// - /// - /// - /// If entity could not be found - /// If validation fails - [Authorize(Roles = RoleNames.Staff)] - [HttpPut("{id}/dates/set")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status422UnprocessableEntity)] - public async Task> SetDates(AppointmentSetDatesDto setDatesDto) - { - return await _appointmentService.SetDatesAsync(setDatesDto); - } + /// + /// Sets the result of an appointment participation + /// + /// + /// + /// If entity could not be found + /// If validation fails + [Authorize(Roles = RoleNames.Staff)] + [HttpPut("{id}/participations/{personId}/result")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ValidationProblemDetails), + StatusCodes.Status422UnprocessableEntity)] + public async Task SetParticipationResult( + AppointmentParticipationSetResultDto setParticipationResult) + { + await _appointmentService.SetParticipationResultAsync(setParticipationResult); + return NoContent(); + } - /// - /// Sets the result of an appointment participation - /// - /// - /// - /// If entity could not be found - /// If validation fails - [Authorize(Roles = RoleNames.Staff)] - [HttpPut("{id}/participations/{personId}/result")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status422UnprocessableEntity)] - public async Task SetParticipationResult(AppointmentParticipationSetResultDto setParticipationResult) - { - await _appointmentService.SetParticipationResultAsync(setParticipationResult); - return NoContent(); - } + /// + /// Sets the prediction of an appointment participation + /// + /// + /// + /// If entity could not be found + /// If validation fails + [Authorize(Roles = RoleNames.Staff)] + [HttpPut("{id}/participations/{personId}/prediction")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ValidationProblemDetails), + StatusCodes.Status422UnprocessableEntity)] + public async Task SetParticipationPrediction( + AppointmentParticipationSetPredictionDto setParticipationPrediction) + { + await _appointmentService.SetParticipationPredictionAsync(setParticipationPrediction); + return NoContent(); + } - /// - /// Sets the prediction of an appointment participation - /// - /// - /// - /// If entity could not be found - /// If validation fails - [Authorize(Roles = RoleNames.Staff)] - [HttpPut("{id}/participations/{personId}/prediction")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status422UnprocessableEntity)] - public async Task SetParticipationPrediction(AppointmentParticipationSetPredictionDto setParticipationPrediction) - { - await _appointmentService.SetParticipationPredictionAsync(setParticipationPrediction); - return NoContent(); - } + /// + /// Exports all appointments to ics file + /// + /// + [Authorize(Roles = RoleNames.Staff)] + [HttpGet("export")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task ExportToIcs() + { + string serializedCalendar = await _appointmentService.ExportAppointmentsToIcsAsync(); + return File(Encoding.UTF8.GetBytes(serializedCalendar), "text/calendar", + "appointments.ics"); } } diff --git a/Orso.Arpa.Api/Orso.Arpa.Api.csproj b/Orso.Arpa.Api/Orso.Arpa.Api.csproj index 4cc6402ef..920a9d969 100644 --- a/Orso.Arpa.Api/Orso.Arpa.Api.csproj +++ b/Orso.Arpa.Api/Orso.Arpa.Api.csproj @@ -10,7 +10,7 @@ - + diff --git a/Orso.Arpa.Api/appsettings.Development.json b/Orso.Arpa.Api/appsettings.Development.json index 28a7faa22..0b54b6739 100644 --- a/Orso.Arpa.Api/appsettings.Development.json +++ b/Orso.Arpa.Api/appsettings.Development.json @@ -18,8 +18,12 @@ "internalLogLevel": "Info", "internalLogFile": "${basedir}/internal-nlog.txt", "extensions": [ - { "assembly": "NLog.Extensions.Logging" }, - { "assembly": "NLog.Web.AspNetCore" } + { + "assembly": "NLog.Extensions.Logging" + }, + { + "assembly": "NLog.Web.AspNetCore" + } ], "default-wrapper": { "type": "AsyncWrapper", @@ -47,7 +51,7 @@ "EmailConfiguration": { "From": "dev@arpa.orso.co", "SmtpServer": "localhost", - "Port": 25, + "Port": 8025, "Username": "", "Password": "", "DefaultSubject": "Message from ARPA" @@ -100,7 +104,10 @@ "IpRateLimiting": { "EnableEndpointRateLimiting": true, "StackBlockedRequests": true, - "EndpointWhitelist": [ "options:*", "post:/graphql" ], + "EndpointWhitelist": [ + "options:*", + "post:/graphql" + ], "HttpStatusCode": 429, "GeneralRules": [ { diff --git a/Orso.Arpa.Application/AppointmentApplication/Interfaces/IAppointmentService.cs b/Orso.Arpa.Application/AppointmentApplication/Interfaces/IAppointmentService.cs index b605ee3da..e0abf35cd 100644 --- a/Orso.Arpa.Application/AppointmentApplication/Interfaces/IAppointmentService.cs +++ b/Orso.Arpa.Application/AppointmentApplication/Interfaces/IAppointmentService.cs @@ -5,38 +5,47 @@ using Orso.Arpa.Application.AppointmentParticipationApplication.Model; using Orso.Arpa.Domain.AppointmentDomain.Enums; -namespace Orso.Arpa.Application.AppointmentApplication.Interfaces +namespace Orso.Arpa.Application.AppointmentApplication.Interfaces; + +public interface IAppointmentService { - public interface IAppointmentService - { - Task> GetAsync(DateTime? date, DateRange range); + Task> GetAsync(DateTime? date, DateRange? range); + + Task GetByIdAsync(Guid id, bool includeParticipations); + + Task CreateAsync(AppointmentCreateDto appointmentCreateDto); + + Task ModifyAsync(AppointmentModifyDto appointmentModifyDto); - Task GetByIdAsync(Guid id, bool includeParticipations); + Task RemoveRoomAsync(AppointmentRemoveRoomDto removeRoomDto); - Task CreateAsync(AppointmentCreateDto appointmentCreateDto); + Task AddRoomAsync(AppointmentAddRoomDto addRoomDto); - Task ModifyAsync(AppointmentModifyDto appointmentModifyDto); + Task AddProjectAsync(AppointmentAddProjectDto addProjectDto, + bool includeParticipations); - Task RemoveRoomAsync(AppointmentRemoveRoomDto removeRoomDto); + Task AddSectionAsync(AppointmentAddSectionDto addSectionDto, + bool includeParticipations); - Task AddRoomAsync(AppointmentAddRoomDto addRoomDto); + Task RemoveSectionAsync(AppointmentRemoveSectionDto removeSectionDto, + bool includeParticipations); - Task AddProjectAsync(AppointmentAddProjectDto addProjectDto, bool includeParticipations); + Task RemoveProjectAsync(AppointmentRemoveProjectDto removeProjectDto, + bool includeParticipations); - Task AddSectionAsync(AppointmentAddSectionDto addSectionDto, bool includeParticipations); + Task SetVenueAsync(AppointmentSetVenueDto setVenueDto); - Task RemoveSectionAsync(AppointmentRemoveSectionDto removeSectionDto, bool includeParticipations); + Task SetDatesAsync(AppointmentSetDatesDto setDatesDto); - Task RemoveProjectAsync(AppointmentRemoveProjectDto removeProjectDto, bool includeParticipations); + Task DeleteAsync(Guid id); - Task SetVenueAsync(AppointmentSetVenueDto setVenueDto); + Task SetParticipationResultAsync(AppointmentParticipationSetResultDto setParticipationResult); - Task SetDatesAsync(AppointmentSetDatesDto setDatesDto); + Task SetParticipationPredictionAsync( + AppointmentParticipationSetPredictionDto setParticipationPrediction); - Task DeleteAsync(Guid id); + Task SendAppointmentChangedNotificationAsync( + SendAppointmentChangedNotificationDto sendAppointmentChangedNotificationDto); - Task SetParticipationResultAsync(AppointmentParticipationSetResultDto setParticipationResult); - Task SetParticipationPredictionAsync(AppointmentParticipationSetPredictionDto setParticipationPrediction); - Task SendAppointmentChangedNotificationAsync(SendAppointmentChangedNotificationDto sendAppointmentChangedNotificationDto); - } + Task ExportAppointmentsToIcsAsync(); } diff --git a/Orso.Arpa.Application/AppointmentApplication/Services/AppointmentService.cs b/Orso.Arpa.Application/AppointmentApplication/Services/AppointmentService.cs index 1752c08c5..d78733545 100644 --- a/Orso.Arpa.Application/AppointmentApplication/Services/AppointmentService.cs +++ b/Orso.Arpa.Application/AppointmentApplication/Services/AppointmentService.cs @@ -11,6 +11,7 @@ using Orso.Arpa.Domain.AppointmentDomain.Commands; using Orso.Arpa.Domain.AppointmentDomain.Enums; using Orso.Arpa.Domain.AppointmentDomain.Model; +using Orso.Arpa.Domain.AppointmentDomain.Queries; using Orso.Arpa.Domain.AppointmentDomain.Util; using Orso.Arpa.Domain.General.Extensions; using Orso.Arpa.Domain.General.GenericHandlers; @@ -18,139 +19,171 @@ using Orso.Arpa.Domain.SectionDomain.Model; using Orso.Arpa.Domain.SectionDomain.Queries; -namespace Orso.Arpa.Application.AppointmentApplication.Services +namespace Orso.Arpa.Application.AppointmentApplication.Services; + +public class AppointmentService : BaseService< + AppointmentDto, + Appointment, + AppointmentCreateDto, + CreateAppointment.Command, + AppointmentModifyDto, + AppointmentModifyBodyDto, + ModifyAppointment.Command>, IAppointmentService { - public class AppointmentService : BaseService< - AppointmentDto, - Appointment, - AppointmentCreateDto, - CreateAppointment.Command, - AppointmentModifyDto, - AppointmentModifyBodyDto, - ModifyAppointment.Command>, IAppointmentService - { - public AppointmentService(IMediator mediator, IMapper mapper) : base(mediator, mapper) - { - } + public AppointmentService(IMediator mediator, IMapper mapper) : base(mediator, mapper) + { + } - public async Task AddProjectAsync(AppointmentAddProjectDto addProjectDto, bool includeParticipations) - { - AddProjectToAppointment.Command command = _mapper.Map(addProjectDto); - await _mediator.Send(command); - return await GetByIdAsync(addProjectDto.Id, includeParticipations); - } + public async Task AddProjectAsync(AppointmentAddProjectDto addProjectDto, + bool includeParticipations) + { + AddProjectToAppointment.Command command = + _mapper.Map(addProjectDto); + await _mediator.Send(command); + return await GetByIdAsync(addProjectDto.Id, includeParticipations); + } - public async Task AddSectionAsync(AppointmentAddSectionDto addSectionDto, bool includeParticipations) - { - AddSectionToAppointment.Command command = _mapper.Map(addSectionDto); - await _mediator.Send(command); - return await GetByIdAsync(addSectionDto.Id, includeParticipations); - } + public async Task AddSectionAsync(AppointmentAddSectionDto addSectionDto, + bool includeParticipations) + { + AddSectionToAppointment.Command command = + _mapper.Map(addSectionDto); + await _mediator.Send(command); + return await GetByIdAsync(addSectionDto.Id, includeParticipations); + } - public async Task AddRoomAsync(AppointmentAddRoomDto addRoomDto) - { - AddRoomToAppointment.Command command = _mapper.Map(addRoomDto); - await _mediator.Send(command); - } + public async Task AddRoomAsync(AppointmentAddRoomDto addRoomDto) + { + AddRoomToAppointment.Command + command = _mapper.Map(addRoomDto); + await _mediator.Send(command); + } - public async Task> GetAsync(DateTime? date, DateRange range) + public async Task> GetAsync(DateTime? date, DateRange? range) + { + if (range.HasValue) { date ??= DateTime.Today; + DateTime rangeStartTime = DateHelper.GetStartTime(date.Value, range.Value); + DateTime rangeEndTime = DateHelper.GetEndTime(date.Value, range.Value); - DateTime rangeStartTime = DateHelper.GetStartTime(date.Value, range); - DateTime rangeEndTime = DateHelper.GetEndTime(date.Value, range); + IQueryable entities = await _mediator.Send(new List.Query( + a => (a.EndTime <= rangeEndTime && a.EndTime >= rangeStartTime) || + (a.EndTime > rangeEndTime && a.StartTime <= rangeEndTime), + asSplitQuery: true)); + return _mapper.ProjectTo(entities); + } + else + { IQueryable entities = await _mediator.Send(new List.Query( - predicate: a => - (a.EndTime <= rangeEndTime && a.EndTime >= rangeStartTime) - || (a.EndTime > rangeEndTime && a.StartTime <= rangeEndTime), asSplitQuery: true)); - return [.. _mapper.ProjectTo(entities)]; + return _mapper.ProjectTo(entities); } + } - public async Task GetByIdAsync(Guid id, bool includeParticipations) + public async Task GetByIdAsync(Guid id, bool includeParticipations) + { + Appointment appointment = await _mediator.Send(new Details.Query(id)); + AppointmentDto dto = _mapper.Map(appointment); + if (includeParticipations) { - Appointment appointment = await _mediator.Send(new Details.Query(id)); - AppointmentDto dto = _mapper.Map(appointment); - if (includeParticipations) - { - var treeQuery = new ListFlattenedSectionTree.Query(); - IEnumerable> flattenedTree = await _mediator.Send(treeQuery); - await AddParticipationsAsync(dto, appointment, flattenedTree); - } - return dto; + var treeQuery = new ListFlattenedSectionTree.Query(); + IEnumerable> flattenedTree = await _mediator.Send(treeQuery); + await AddParticipationsAsync(dto, appointment, flattenedTree); } - private async Task AddParticipationsAsync( - AppointmentDto dto, - Appointment appointment, - IEnumerable> flattenedTree) - { - var query = new ListParticipationsForAppointment.Query - { - Appointment = appointment, - SectionTree = flattenedTree, - }; + return dto; + } - IEnumerable personGrouping = await _mediator.Send(query); + public async Task RemoveProjectAsync( + AppointmentRemoveProjectDto removeProjectDto, bool includeParticipations) + { + RemoveProjectFromAppointment.Command command = + _mapper.Map(removeProjectDto); + await _mediator.Send(command); + return await GetByIdAsync(removeProjectDto.Id, includeParticipations); + } - dto.Participations = _mapper.Map>(personGrouping); - } + public async Task RemoveSectionAsync( + AppointmentRemoveSectionDto removeSectionDto, bool includeParticipations) + { + RemoveSectionFromAppointment.Command command = + _mapper.Map(removeSectionDto); + await _mediator.Send(command); + return await GetByIdAsync(removeSectionDto.Id, includeParticipations); + } - public async Task RemoveProjectAsync(AppointmentRemoveProjectDto removeProjectDto, bool includeParticipations) - { - RemoveProjectFromAppointment.Command command = _mapper.Map(removeProjectDto); - await _mediator.Send(command); - return await GetByIdAsync(removeProjectDto.Id, includeParticipations); - } + public async Task RemoveRoomAsync(AppointmentRemoveRoomDto removeRoomDto) + { + RemoveRoomFromAppointment.Command command = + _mapper.Map(removeRoomDto); + await _mediator.Send(command); + } - public async Task RemoveSectionAsync(AppointmentRemoveSectionDto removeSectionDto, bool includeParticipations) - { - RemoveSectionFromAppointment.Command command = _mapper.Map(removeSectionDto); - await _mediator.Send(command); - return await GetByIdAsync(removeSectionDto.Id, includeParticipations); - } + public async Task SetDatesAsync(AppointmentSetDatesDto setDatesDto) + { + SetDates.Command command = _mapper.Map(setDatesDto); + Appointment appointment = await _mediator.Send(command); + AppointmentDto dto = _mapper.Map(appointment); + var treeQuery = new ListFlattenedSectionTree.Query(); + IEnumerable> flattenedTree = await _mediator.Send(treeQuery); + await AddParticipationsAsync(dto, appointment, flattenedTree); + return dto; + } - public async Task RemoveRoomAsync(AppointmentRemoveRoomDto removeRoomDto) - { - RemoveRoomFromAppointment.Command command = _mapper.Map(removeRoomDto); - await _mediator.Send(command); - } + public async Task SetVenueAsync(AppointmentSetVenueDto setVenueDto) + { + SetVenue.Command command = _mapper.Map(setVenueDto); + await _mediator.Send(command); + } - public async Task SetDatesAsync(AppointmentSetDatesDto setDatesDto) - { - SetDates.Command command = _mapper.Map(setDatesDto); - Appointment appointment = await _mediator.Send(command); - AppointmentDto dto = _mapper.Map(appointment); - var treeQuery = new ListFlattenedSectionTree.Query(); - IEnumerable> flattenedTree = await _mediator.Send(treeQuery); - await AddParticipationsAsync(dto, appointment, flattenedTree); - return dto; - } + public async Task SetParticipationResultAsync( + AppointmentParticipationSetResultDto setParticipationResult) + { + SetAppointmentParticipationResult.Command command = + _mapper.Map(setParticipationResult); + await _mediator.Send(command); + } - public async Task SetVenueAsync(AppointmentSetVenueDto setVenueDto) - { - SetVenue.Command command = _mapper.Map(setVenueDto); - await _mediator.Send(command); - } + public async Task SetParticipationPredictionAsync( + AppointmentParticipationSetPredictionDto setParticipationPrediction) + { + SetAppointmentParticipationPrediction.Command command = + _mapper.Map(setParticipationPrediction); + await _mediator.Send(command); + } - public async Task SetParticipationResultAsync(AppointmentParticipationSetResultDto setParticipationResult) - { - SetAppointmentParticipationResult.Command command = _mapper.Map(setParticipationResult); - await _mediator.Send(command); - } + public async Task SendAppointmentChangedNotificationAsync( + SendAppointmentChangedNotificationDto sendAppointmentChangedNotificationDto) + { + SendAppointmentChangedNotification.Command command = + _mapper.Map( + sendAppointmentChangedNotificationDto); + await _mediator.Send(command); + } - public async Task SetParticipationPredictionAsync(AppointmentParticipationSetPredictionDto setParticipationPrediction) + private async Task AddParticipationsAsync( + AppointmentDto dto, + Appointment appointment, + IEnumerable> flattenedTree) + { + var query = new ListParticipationsForAppointment.Query { - SetAppointmentParticipationPrediction.Command command = _mapper.Map(setParticipationPrediction); - await _mediator.Send(command); - } + Appointment = appointment, SectionTree = flattenedTree + }; - public async Task SendAppointmentChangedNotificationAsync(SendAppointmentChangedNotificationDto sendAppointmentChangedNotificationDto) - { - SendAppointmentChangedNotification.Command command = _mapper.Map(sendAppointmentChangedNotificationDto); - await _mediator.Send(command); - } + IEnumerable personGrouping = + await _mediator.Send(query); + + dto.Participations = + _mapper.Map>(personGrouping); + } + + public async Task ExportAppointmentsToIcsAsync() + { + var query = new ExportAppointmentsToIcs.Query(); + return await _mediator.Send(query); } } diff --git a/Orso.Arpa.Application/Orso.Arpa.Application.csproj b/Orso.Arpa.Application/Orso.Arpa.Application.csproj index e1a6aea97..bcb0b2c5a 100644 --- a/Orso.Arpa.Application/Orso.Arpa.Application.csproj +++ b/Orso.Arpa.Application/Orso.Arpa.Application.csproj @@ -9,4 +9,4 @@ - \ No newline at end of file + diff --git a/Orso.Arpa.Domain/AppointmentDomain/Queries/ExportAppointmentsToIcs.cs b/Orso.Arpa.Domain/AppointmentDomain/Queries/ExportAppointmentsToIcs.cs new file mode 100644 index 000000000..1e21fec0b --- /dev/null +++ b/Orso.Arpa.Domain/AppointmentDomain/Queries/ExportAppointmentsToIcs.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Ical.Net; +using Ical.Net.CalendarComponents; +using Ical.Net.DataTypes; +using Ical.Net.Serialization; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Orso.Arpa.Domain.General.Interfaces; +using Orso.Arpa.Misc.Extensions; + +namespace Orso.Arpa.Domain.AppointmentDomain.Queries; + +public class ExportAppointmentsToIcs +{ + public class Query : IRequest + { + } + + public class Handler : IRequestHandler + { + private readonly IArpaContext _arpaContext; + + public Handler(IArpaContext context) + { + _arpaContext = context; + } + + public async Task Handle(Query request, CancellationToken cancellationToken) + { + List events = await _arpaContext.Appointments.Select(a => new CalendarEvent + { + Summary = a.Name, + Description = (a.Category != null ? a.Category.SelectValue.Name : "-") + " | " + + (string.IsNullOrEmpty(a.PublicDetails) ? "-" : a.PublicDetails) + + " | " + + (string.IsNullOrEmpty(a.InternalDetails) ? "-" : a.InternalDetails), + Location = a.Venue != null ? a.Venue.Address.City : "-", + Start = new CalDateTime(DateTimeExtensions.ConvertToLocalTimeBerlin(a.StartTime)), + End = new CalDateTime(DateTimeExtensions.ConvertToLocalTimeBerlin(a.EndTime)) + }).ToListAsync(cancellationToken); + + var calendar = new Calendar(); + + string timeZoneId = "Europe/Berlin"; + var timeZone = new VTimeZone(timeZoneId); + calendar.AddTimeZone(timeZone); + + calendar.Events.AddRange(events); + + var serializer = new CalendarSerializer(); + return serializer.SerializeToString(calendar); + } + } +} diff --git a/Orso.Arpa.Domain/Orso.Arpa.Domain.csproj b/Orso.Arpa.Domain/Orso.Arpa.Domain.csproj index a68d4d8e4..4dbdee6a8 100644 --- a/Orso.Arpa.Domain/Orso.Arpa.Domain.csproj +++ b/Orso.Arpa.Domain/Orso.Arpa.Domain.csproj @@ -15,6 +15,7 @@ + diff --git a/Orso.Arpa.Domain/UserDomain/Commands/RegisterUser.cs b/Orso.Arpa.Domain/UserDomain/Commands/RegisterUser.cs index 06d8275c7..268f0e5a7 100644 --- a/Orso.Arpa.Domain/UserDomain/Commands/RegisterUser.cs +++ b/Orso.Arpa.Domain/UserDomain/Commands/RegisterUser.cs @@ -101,7 +101,7 @@ public async Task Handle(Command request, CancellationToken cancellationTo break; default: throw new ValidationException( - [new(nameof(Command.Email), "Multiple persons found with this email address. Registration aborted. Please contact your system admin.")]); + [new ValidationFailure(nameof(Command.Email), "Multiple persons found with this email address. Registration aborted. Please contact your system admin.")]); } var user = new User diff --git a/Orso.Arpa.Infrastructure/Orso.Arpa.Infrastructure.csproj b/Orso.Arpa.Infrastructure/Orso.Arpa.Infrastructure.csproj index 942a3ac17..ce74fa42f 100644 --- a/Orso.Arpa.Infrastructure/Orso.Arpa.Infrastructure.csproj +++ b/Orso.Arpa.Infrastructure/Orso.Arpa.Infrastructure.csproj @@ -22,4 +22,4 @@ - \ No newline at end of file + diff --git a/Orso.Arpa.Mail/Orso.Arpa.Mail.csproj b/Orso.Arpa.Mail/Orso.Arpa.Mail.csproj index d92e25de2..b0f21ccb1 100644 --- a/Orso.Arpa.Mail/Orso.Arpa.Mail.csproj +++ b/Orso.Arpa.Mail/Orso.Arpa.Mail.csproj @@ -37,4 +37,4 @@ PreserveNewest - \ No newline at end of file + diff --git a/Orso.Arpa.Misc/Extensions/DateTimeExtensions.cs b/Orso.Arpa.Misc/Extensions/DateTimeExtensions.cs index 3e1ff1069..5e5e13301 100644 --- a/Orso.Arpa.Misc/Extensions/DateTimeExtensions.cs +++ b/Orso.Arpa.Misc/Extensions/DateTimeExtensions.cs @@ -16,7 +16,7 @@ public static string ToGermanDateTimeString(this DateTime dateTime) return berlinDateTime.ToString("dddd, dd.MM.yyyy HH:mm", new CultureInfo("en-GB")); } - private static DateTime ConvertToLocalTimeBerlin(DateTime dateTime) + public static DateTime ConvertToLocalTimeBerlin(DateTime dateTime) { var berlinTimeZone = TimeZoneInfo.FindSystemTimeZoneById("W. Europe Standard Time"); DateTime berlinDateTime = TimeZoneInfo.ConvertTimeFromUtc(dateTime, berlinTimeZone); diff --git a/Orso.Arpa.Misc/Orso.Arpa.Misc.csproj b/Orso.Arpa.Misc/Orso.Arpa.Misc.csproj index 153982783..35ab18fbd 100644 --- a/Orso.Arpa.Misc/Orso.Arpa.Misc.csproj +++ b/Orso.Arpa.Misc/Orso.Arpa.Misc.csproj @@ -8,4 +8,4 @@ - \ No newline at end of file + diff --git a/Orso.Arpa.Persistence/Orso.Arpa.Persistence.csproj b/Orso.Arpa.Persistence/Orso.Arpa.Persistence.csproj index 531c23b02..cdfd5a3e6 100644 --- a/Orso.Arpa.Persistence/Orso.Arpa.Persistence.csproj +++ b/Orso.Arpa.Persistence/Orso.Arpa.Persistence.csproj @@ -79,4 +79,4 @@ PreserveNewest - \ No newline at end of file + diff --git a/Tests/Orso.Arpa.Api.Tests/IntegrationTests/AppointmentsControllerTests.cs b/Tests/Orso.Arpa.Api.Tests/IntegrationTests/AppointmentsControllerTests.cs index a36e14fdf..e2160b6b5 100644 --- a/Tests/Orso.Arpa.Api.Tests/IntegrationTests/AppointmentsControllerTests.cs +++ b/Tests/Orso.Arpa.Api.Tests/IntegrationTests/AppointmentsControllerTests.cs @@ -5,6 +5,8 @@ using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; +using Ical.Net; +using Ical.Net.DataTypes; using Microsoft.AspNetCore.Mvc; using netDumbster.smtp; using NUnit.Framework; @@ -12,556 +14,660 @@ using Orso.Arpa.Application.AppointmentApplication.Model; using Orso.Arpa.Application.AppointmentParticipationApplication.Model; using Orso.Arpa.Application.MusicianProfileApplication.Model; +using Orso.Arpa.Domain.AddressDomain.Model; using Orso.Arpa.Domain.AppointmentDomain.Enums; using Orso.Arpa.Domain.AppointmentDomain.Model; using Orso.Arpa.Domain.PersonDomain.Model; +using Orso.Arpa.Domain.SelectValueDomain.Model; +using Orso.Arpa.Domain.VenueDomain.Model; using Orso.Arpa.Persistence.Seed; using Orso.Arpa.Tests.Shared.DtoTestData; using Orso.Arpa.Tests.Shared.FakeData; using Orso.Arpa.Tests.Shared.TestSeedData; -namespace Orso.Arpa.Api.Tests.IntegrationTests +namespace Orso.Arpa.Api.Tests.IntegrationTests; + +[TestFixture] +public class AppointmentsControllerTests : IntegrationTestBase { - [TestFixture] - public class AppointmentsControllerTests : IntegrationTestBase + private static IEnumerable s_appointmentQueryTestData { - private static IEnumerable s_appointmentQueryTestData + get { - get - { - yield return new TestCaseData(DateRange.Day, new DateTime(2019, 12, 21), new List { + yield return new TestCaseData(DateRange.Day, new DateTime(2019, 12, 21), + new List + { AppointmentListDtoData.RockingXMasRehearsal, AppointmentListDtoData.RehearsalWeekend }); - // 16.-22.12.2019 - yield return new TestCaseData(DateRange.Week, new DateTime(2019, 12, 21), new List { + // 16.-22.12.2019 + yield return new TestCaseData(DateRange.Week, new DateTime(2019, 12, 21), + new List + { AppointmentListDtoData.RockingXMasRehearsal, AppointmentListDtoData.AppointmentWithoutProject, AppointmentListDtoData.RehearsalWeekend }); - yield return new TestCaseData(DateRange.Month, new DateTime(2020, 12, 21), new List { + yield return new TestCaseData(DateRange.Month, new DateTime(2020, 12, 21), + new List + { AppointmentListDtoData.AuditionDays, AppointmentListDtoData.PhotoSession, AppointmentListDtoData.StaffMeeting }); - } } + } - [TestCaseSource(nameof(s_appointmentQueryTestData))] - [Test, Order(1)] - public async Task Should_Get_Appointments( - DateRange dateRange, - DateTime date, - IList expectedDtos) - { - // Act - HttpResponseMessage responseMessage = await _authenticatedServer - .CreateClient() - .AuthenticateWith(_staff) - .GetAsync(ApiEndpoints.AppointmentsController.Get(date, dateRange)); - - // Assert - _ = responseMessage.StatusCode.Should().Be(HttpStatusCode.OK); - IEnumerable result = await DeserializeResponseMessageAsync>(responseMessage); - - _ = result.Should().BeEquivalentTo(expectedDtos); - } + [TestCaseSource(nameof(s_appointmentQueryTestData))] + [Test] + [Order(1)] + public async Task Should_Get_Appointments( + DateRange dateRange, + DateTime date, + IList expectedDtos) + { + // Act + HttpResponseMessage responseMessage = await _authenticatedServer + .CreateClient() + .AuthenticateWith(_staff) + .GetAsync(ApiEndpoints.AppointmentsController.Get(date, dateRange)); + + // Assert + _ = responseMessage.StatusCode.Should().Be(HttpStatusCode.OK); + IEnumerable result = + await DeserializeResponseMessageAsync>(responseMessage); + + _ = result.Should().BeEquivalentTo(expectedDtos); + } - private static IEnumerable s_appointmentByIdQueryTestData + private static IEnumerable s_appointmentByIdQueryTestData + { + get { - get - { - yield return new TestCaseData(AppointmentDtoData.RockingXMasRehearsal); - yield return new TestCaseData(AppointmentDtoData.RockingXMasConcert); - yield return new TestCaseData(AppointmentDtoData.AfterShowParty); - yield return new TestCaseData(AppointmentDtoData.StaffMeeting); - yield return new TestCaseData(AppointmentDtoData.PhotoSession); - yield return new TestCaseData(AppointmentDtoData.RehearsalWeekend); - yield return new TestCaseData(AppointmentDtoData.AuditionDays); - yield return new TestCaseData(AppointmentDtoData.AltoRehearsal); - yield return new TestCaseData(AppointmentDtoData.SopranoRehearsal); - } + yield return new TestCaseData(AppointmentDtoData.RockingXMasRehearsal); + yield return new TestCaseData(AppointmentDtoData.RockingXMasConcert); + yield return new TestCaseData(AppointmentDtoData.AfterShowParty); + yield return new TestCaseData(AppointmentDtoData.StaffMeeting); + yield return new TestCaseData(AppointmentDtoData.PhotoSession); + yield return new TestCaseData(AppointmentDtoData.RehearsalWeekend); + yield return new TestCaseData(AppointmentDtoData.AuditionDays); + yield return new TestCaseData(AppointmentDtoData.AltoRehearsal); + yield return new TestCaseData(AppointmentDtoData.SopranoRehearsal); } + } - [Test, Order(2)] - [TestCaseSource(nameof(s_appointmentByIdQueryTestData))] - public async Task Should_Get_By_Id_With_Participations(AppointmentDto expectedDto) - { - // Act - HttpResponseMessage responseMessage = await _authenticatedServer - .CreateClient() - .AuthenticateWith(FakeUsers.Staff) - .GetAsync(ApiEndpoints.AppointmentsController.Get(expectedDto.Id, true)); - - // Assert - _ = responseMessage.StatusCode.Should().Be(HttpStatusCode.OK); - AppointmentDto result = await DeserializeResponseMessageAsync(responseMessage); - _ = result.Should().BeEquivalentTo(expectedDto); - } + [Test] + [Order(2)] + [TestCaseSource(nameof(s_appointmentByIdQueryTestData))] + public async Task Should_Get_By_Id_With_Participations(AppointmentDto expectedDto) + { + // Act + HttpResponseMessage responseMessage = await _authenticatedServer + .CreateClient() + .AuthenticateWith(FakeUsers.Staff) + .GetAsync(ApiEndpoints.AppointmentsController.Get(expectedDto.Id, true)); + + // Assert + _ = responseMessage.StatusCode.Should().Be(HttpStatusCode.OK); + AppointmentDto result = + await DeserializeResponseMessageAsync(responseMessage); + _ = result.Should().BeEquivalentTo(expectedDto); + } - [Test, Order(3)] - [TestCaseSource(nameof(s_appointmentByIdQueryTestData))] - public async Task Should_Get_By_Id_Without_Participations(AppointmentDto expectedDto) - { - // Act - HttpResponseMessage responseMessage = await _authenticatedServer - .CreateClient() - .AuthenticateWith(FakeUsers.Staff) - .GetAsync(ApiEndpoints.AppointmentsController.Get(expectedDto.Id, false)); - - // Assert - _ = responseMessage.StatusCode.Should().Be(HttpStatusCode.OK); - AppointmentDto result = await DeserializeResponseMessageAsync(responseMessage); - _ = result.Should().BeEquivalentTo(expectedDto, opt => opt.Excluding(dto => dto.Participations)); - _ = result.Participations.Should().BeEmpty(); - } + [Test] + [Order(3)] + [TestCaseSource(nameof(s_appointmentByIdQueryTestData))] + public async Task Should_Get_By_Id_Without_Participations(AppointmentDto expectedDto) + { + // Act + HttpResponseMessage responseMessage = await _authenticatedServer + .CreateClient() + .AuthenticateWith(FakeUsers.Staff) + .GetAsync(ApiEndpoints.AppointmentsController.Get(expectedDto.Id, false)); + + // Assert + _ = responseMessage.StatusCode.Should().Be(HttpStatusCode.OK); + AppointmentDto result = + await DeserializeResponseMessageAsync(responseMessage); + _ = result.Should() + .BeEquivalentTo(expectedDto, opt => opt.Excluding(dto => dto.Participations)); + _ = result.Participations.Should().BeEmpty(); + } - [Test, Order(4)] - public async Task Should_Send_Appointment_Changed_Notification() { - // Arrange - _fakeSmtpServer.ClearReceivedEmail(); - IEnumerable expectedToAddresses = [ - "arpa@test.smtp" - ]; - IEnumerable expectedBccAddresses = [ - UserSeedData.Admin.Email, - UserTestSeedData.UserWithoutRole.Email, - UserTestSeedData.Staff.Email, - UserTestSeedData.Performer.Email - ]; - - // Act - HttpResponseMessage responseMessage = await _authenticatedServer - .CreateClient() - .AuthenticateWith(_staff) - .PostAsync(ApiEndpoints.AppointmentsController.SendAppointmentChangedNotification( - AppointmentSeedData.PhotoSession.Id, - true), null); - - // Assert - _ = responseMessage.StatusCode.Should().Be(HttpStatusCode.NoContent); - _fakeSmtpServer.ReceivedEmailCount.Should().Be(1); - SmtpMessage sentEmail = _fakeSmtpServer.ReceivedEmail[0]; - sentEmail.ToAddresses.Select(a => a.Address).Should().BeEquivalentTo(expectedToAddresses, opt => opt.WithoutStrictOrdering()); - sentEmail.BccAddresses.Select(a => a.Address).Should().BeEquivalentTo(expectedBccAddresses, opt => opt.WithoutStrictOrdering()); - sentEmail.Subject.Should().Be("Ein Termin in ARPA wurde aktualisiert!"); - } + [Test] + [Order(4)] + public async Task Should_Export_Appointments_To_Ics() + { + // Act + HttpResponseMessage responseMessage = await _authenticatedServer + .CreateClient() + .AuthenticateWith(_staff) + .GetAsync(ApiEndpoints.AppointmentsController.ExportToIcs()); + + // Assert + responseMessage.StatusCode.Should().Be(HttpStatusCode.OK); + responseMessage.Content.Headers.ContentType.ToString().Should().Be("text/calendar"); + + string icsContent = await responseMessage.Content.ReadAsStringAsync(); + icsContent.Should().NotBeNullOrEmpty(); + + + icsContent.Should().Contain("BEGIN:VEVENT"); + icsContent.Should().Contain("END:VEVENT"); + + icsContent.Should().Contain("SUMMARY:"); + icsContent.Should().Contain("DTSTART:"); + icsContent.Should().Contain("DTEND:"); + icsContent.Should().Contain("DESCRIPTION:"); + + icsContent.Should().Contain("BEGIN:VTIMEZONE"); + icsContent.Should().Contain("TZID:Europe/Berlin"); + + var calendar = Calendar.Load(icsContent); + calendar.Events.Should().NotBeEmpty(); + var firstEvent = calendar.Events.First(); + firstEvent.Summary.Should().NotBeNullOrEmpty(); + firstEvent.Start.Should().BeOfType(); + firstEvent.End.Should().BeOfType(); + ((CalDateTime)firstEvent.Start).Value.Should().BeAfter(DateTime.MinValue); + ((CalDateTime)firstEvent.End).Value.Should().BeAfter(((CalDateTime)firstEvent.Start).Value); + } - [Test, Order(1000)] - public async Task Should_Create() - { - // Arrange - var createDto = new AppointmentCreateDto - { - Name = "New Appointment", - InternalDetails = "Internal Details", - PublicDetails = "Public Details", - EndTime = new DateTime(2021, 3, 5, 14, 15, 20), - StartTime = new DateTime(2021, 3, 5, 9, 15, 20), - SalaryId = Guid.Parse("88da1c17-9efc-4f69-ba0f-39c76592845b"), - Status = AppointmentStatus.Scheduled - }; - - var expectedDto = new AppointmentDto - { - Name = createDto.Name, - CreatedBy = _staff.DisplayName, - CreatedAt = FakeDateTime.UtcNow, - ModifiedAt = null, - ModifiedBy = null, - InternalDetails = createDto.InternalDetails, - PublicDetails = createDto.PublicDetails, - EndTime = createDto.EndTime, - StartTime = createDto.StartTime, - Status = createDto.Status, - SalaryId = createDto.SalaryId - }; - - // Act - HttpResponseMessage responseMessage = await _authenticatedServer - .CreateClient() - .AuthenticateWith(_staff) - .PostAsync(ApiEndpoints.AppointmentsController.Post(), BuildStringContent(createDto)); - - // Assert - _ = responseMessage.StatusCode.Should().Be(HttpStatusCode.Created); - - AppointmentDto result = await DeserializeResponseMessageAsync(responseMessage); - - _ = result.Should().BeEquivalentTo(expectedDto, opt => opt.Excluding(r => r.Id)); - _ = result.Id.Should().NotBeEmpty(); - _ = responseMessage.Headers.Location.AbsolutePath.Should().Be($"/{ApiEndpoints.AppointmentsController.Get(result.Id)}"); - } + [Test] + [Order(5)] + public async Task Should_Send_Appointment_Changed_Notification() + { + // Arrange + _fakeSmtpServer.ClearReceivedEmail(); + IEnumerable expectedToAddresses = + [ + "arpa@test.smtp" + ]; + IEnumerable expectedBccAddresses = + [ + UserSeedData.Admin.Email, + UserTestSeedData.UserWithoutRole.Email, + UserTestSeedData.Staff.Email, + UserTestSeedData.Performer.Email + ]; + + // Act + HttpResponseMessage responseMessage = await _authenticatedServer + .CreateClient() + .AuthenticateWith(_staff) + .PostAsync(ApiEndpoints.AppointmentsController.SendAppointmentChangedNotification( + AppointmentSeedData.PhotoSession.Id, + true), null); + + // Assert + _ = responseMessage.StatusCode.Should().Be(HttpStatusCode.NoContent); + _fakeSmtpServer.ReceivedEmailCount.Should().Be(1); + SmtpMessage sentEmail = _fakeSmtpServer.ReceivedEmail[0]; + sentEmail.ToAddresses.Select(a => a.Address).Should() + .BeEquivalentTo(expectedToAddresses, opt => opt.WithoutStrictOrdering()); + sentEmail.BccAddresses.Select(a => a.Address).Should().BeEquivalentTo(expectedBccAddresses, + opt => opt.WithoutStrictOrdering()); + sentEmail.Subject.Should().Be("Ein Termin in ARPA wurde aktualisiert!"); + } - [Test, Order(104)] - public async Task Should_Add_Room() + [Test] + [Order(1000)] + public async Task Should_Create() + { + // Arrange + var createDto = new AppointmentCreateDto { - // Act - HttpResponseMessage responseMessage = await _authenticatedServer - .CreateClient() - .AuthenticateWith(_staff) - .PostAsync(ApiEndpoints.AppointmentsController.Room( - AppointmentSeedData.RockingXMasRehearsal.Id, - RoomSeedData.AulaWeiherhofSchule.Id), null); - - // Assert - _ = responseMessage.StatusCode.Should().Be(HttpStatusCode.NoContent); - } - - [Test, Order(100)] - public async Task Should_Add_Section() + Name = "New Appointment", + InternalDetails = "Internal Details", + PublicDetails = "Public Details", + EndTime = new DateTime(2021, 3, 5, 14, 15, 20), + StartTime = new DateTime(2021, 3, 5, 9, 15, 20), + SalaryId = Guid.Parse("88da1c17-9efc-4f69-ba0f-39c76592845b"), + Status = AppointmentStatus.Scheduled + }; + + var expectedDto = new AppointmentDto { - AppointmentDto expectedDto = AppointmentDtoData.RockingXMasRehearsal; - expectedDto.Participations.RemoveAt(1); - expectedDto.Participations.RemoveAt(1); // the second item has already been removed so the third item is on index pos. 1 now - expectedDto.Sections.Add(SectionDtoData.Alto); - - // Act - HttpResponseMessage responseMessage = await _authenticatedServer - .CreateClient() - .AuthenticateWith(_staff) - .PostAsync(ApiEndpoints.AppointmentsController.Section( - AppointmentSeedData.RockingXMasRehearsal.Id, - SectionSeedData.Alto.Id), null); + Name = createDto.Name, + CreatedBy = _staff.DisplayName, + CreatedAt = FakeDateTime.UtcNow, + ModifiedAt = null, + ModifiedBy = null, + InternalDetails = createDto.InternalDetails, + PublicDetails = createDto.PublicDetails, + EndTime = createDto.EndTime, + StartTime = createDto.StartTime, + Status = createDto.Status, + SalaryId = createDto.SalaryId + }; + + // Act + HttpResponseMessage responseMessage = await _authenticatedServer + .CreateClient() + .AuthenticateWith(_staff) + .PostAsync(ApiEndpoints.AppointmentsController.Post(), BuildStringContent(createDto)); + + // Assert + _ = responseMessage.StatusCode.Should().Be(HttpStatusCode.Created); + + AppointmentDto result = + await DeserializeResponseMessageAsync(responseMessage); + + _ = result.Should().BeEquivalentTo(expectedDto, opt => opt.Excluding(r => r.Id)); + _ = result.Id.Should().NotBeEmpty(); + _ = responseMessage.Headers.Location.AbsolutePath.Should() + .Be($"/{ApiEndpoints.AppointmentsController.Get(result.Id)}"); + } - // Assert - _ = responseMessage.StatusCode.Should().Be(HttpStatusCode.OK); - AppointmentDto result = await DeserializeResponseMessageAsync(responseMessage); - _ = result.Should().BeEquivalentTo(expectedDto); - } + [Test] + [Order(104)] + public async Task Should_Add_Room() + { + // Act + HttpResponseMessage responseMessage = await _authenticatedServer + .CreateClient() + .AuthenticateWith(_staff) + .PostAsync(ApiEndpoints.AppointmentsController.Room( + AppointmentSeedData.RockingXMasRehearsal.Id, + RoomSeedData.AulaWeiherhofSchule.Id), null); + + // Assert + _ = responseMessage.StatusCode.Should().Be(HttpStatusCode.NoContent); + } - [Test, Order(101)] - public async Task Should_Add_Project() - { - // Arrange - AppointmentDto expectedDto = AppointmentDtoData.RockingXMasConcert; - expectedDto.Participations.Clear(); - expectedDto.Participations.Add(AppointmentDtoData.PerformerParticipation); - expectedDto.Projects.Add(ProjectDtoData.HoorayForHollywood); - - // Act - HttpResponseMessage responseMessage = await _authenticatedServer - .CreateClient() - .AuthenticateWith(_staff) - .PostAsync(ApiEndpoints.AppointmentsController.Project( - AppointmentSeedData.AppointmentWithoutProject.Id, - ProjectSeedData.HoorayForHollywood.Id), null); - - // Assert - _ = responseMessage.StatusCode.Should().Be(HttpStatusCode.OK); - AppointmentDto result = await DeserializeResponseMessageAsync(responseMessage); - _ = result.Should().BeEquivalentTo(expectedDto); - } + [Test] + [Order(100)] + public async Task Should_Add_Section() + { + AppointmentDto expectedDto = AppointmentDtoData.RockingXMasRehearsal; + expectedDto.Participations.RemoveAt(1); + expectedDto.Participations + .RemoveAt( + 1); // the second item has already been removed so the third item is on index pos. 1 now + expectedDto.Sections.Add(SectionDtoData.Alto); + + // Act + HttpResponseMessage responseMessage = await _authenticatedServer + .CreateClient() + .AuthenticateWith(_staff) + .PostAsync(ApiEndpoints.AppointmentsController.Section( + AppointmentSeedData.RockingXMasRehearsal.Id, + SectionSeedData.Alto.Id), null); + + // Assert + _ = responseMessage.StatusCode.Should().Be(HttpStatusCode.OK); + AppointmentDto result = + await DeserializeResponseMessageAsync(responseMessage); + _ = result.Should().BeEquivalentTo(expectedDto); + } - private static IEnumerable PersonTestData + [Test] + [Order(101)] + public async Task Should_Add_Project() + { + // Arrange + AppointmentDto expectedDto = AppointmentDtoData.RockingXMasConcert; + expectedDto.Participations.Clear(); + expectedDto.Participations.Add(AppointmentDtoData.PerformerParticipation); + expectedDto.Projects.Add(ProjectDtoData.HoorayForHollywood); + + // Act + HttpResponseMessage responseMessage = await _authenticatedServer + .CreateClient() + .AuthenticateWith(_staff) + .PostAsync(ApiEndpoints.AppointmentsController.Project( + AppointmentSeedData.AppointmentWithoutProject.Id, + ProjectSeedData.HoorayForHollywood.Id), null); + + // Assert + _ = responseMessage.StatusCode.Should().Be(HttpStatusCode.OK); + AppointmentDto result = + await DeserializeResponseMessageAsync(responseMessage); + _ = result.Should().BeEquivalentTo(expectedDto); + } + + private static IEnumerable PersonTestData + { + get { - get - { - yield return new TestCaseData(PersonTestSeedData.Performer, HttpStatusCode.NoContent); - yield return new TestCaseData(PersonTestSeedData.LockedOutUser, HttpStatusCode.Forbidden); - } + yield return new TestCaseData(PersonTestSeedData.Performer, HttpStatusCode.NoContent); + yield return new TestCaseData(PersonTestSeedData.LockedOutUser, + HttpStatusCode.Forbidden); } + } - private static readonly string[] s_appointmentNotFoundMessage = ["Appointment could not be found."]; + private static readonly string[] s_appointmentNotFoundMessage = + ["Appointment could not be found."]; - [Test, Order(105)] - [TestCaseSource(nameof(PersonTestData))] - public async Task Should_Set_Participation_Result(Person person, HttpStatusCode expectedStatusCode) - { - // Act - HttpResponseMessage responseMessage = await _authenticatedServer - .CreateClient() - .AuthenticateWith(_staff) - .PutAsync(ApiEndpoints.AppointmentsController.SetParticipationResult( + [Test] + [Order(105)] + [TestCaseSource(nameof(PersonTestData))] + public async Task Should_Set_Participation_Result(Person person, + HttpStatusCode expectedStatusCode) + { + // Act + HttpResponseMessage responseMessage = await _authenticatedServer + .CreateClient() + .AuthenticateWith(_staff) + .PutAsync(ApiEndpoints.AppointmentsController.SetParticipationResult( AppointmentSeedData.RockingXMasRehearsal.Id, - person.Id), BuildStringContent(new AppointmentParticipationSetResultBodyDto { Result = AppointmentParticipationResult.AwaitingScan })); + person.Id), + BuildStringContent(new AppointmentParticipationSetResultBodyDto + { + Result = AppointmentParticipationResult.AwaitingScan + })); + + // Assert + _ = responseMessage.StatusCode.Should().Be(expectedStatusCode); + } - // Assert - _ = responseMessage.StatusCode.Should().Be(expectedStatusCode); - } + [Test] + [Order(106)] + public async Task Should_Set_Venue() + { + // Act + HttpResponseMessage responseMessage = await _authenticatedServer + .CreateClient() + .AuthenticateWith(_staff) + .PutAsync(ApiEndpoints.AppointmentsController.SetVenue( + AppointmentSeedData.AppointmentWithoutProject.Id, + VenueSeedData.WeiherhofSchule.Id), null); + + // Assert + _ = responseMessage.StatusCode.Should().Be(HttpStatusCode.NoContent); + } - [Test, Order(106)] - public async Task Should_Set_Venue() - { - // Act - HttpResponseMessage responseMessage = await _authenticatedServer - .CreateClient() - .AuthenticateWith(_staff) - .PutAsync(ApiEndpoints.AppointmentsController.SetVenue( - AppointmentSeedData.AppointmentWithoutProject.Id, - VenueSeedData.WeiherhofSchule.Id), null); - - // Assert - _ = responseMessage.StatusCode.Should().Be(HttpStatusCode.NoContent); - } + [Test] + [Order(107)] + public async Task Should_Modify() + { + // Arrange + Appointment appointmentToModify = AppointmentSeedData.AppointmentWithoutProject; - [Test, Order(107)] - public async Task Should_Modify() + var modifyDto = new AppointmentModifyBodyDto { - // Arrange - Appointment appointmentToModify = AppointmentSeedData.AppointmentWithoutProject; + Name = "New Appointment", + InternalDetails = "Internal Details", + PublicDetails = "Public Details", + CategoryId = SelectValueMappingSeedData.AppointmentCategoryMappings[0].Id, + SalaryId = SelectValueMappingSeedData.AppointmentSalaryMappings[0].Id, + SalaryPatternId = SelectValueMappingSeedData.AppointmentSalaryPatternMappings[0].Id, + EndTime = FakeDateTime.UtcNow.AddHours(5), + StartTime = FakeDateTime.UtcNow, + Status = AppointmentStatus.Confirmed + }; + + // Act + HttpResponseMessage responseMessage = await _authenticatedServer + .CreateClient() + .AuthenticateWith(_staff) + .PutAsync(ApiEndpoints.AppointmentsController.Put(appointmentToModify.Id), + BuildStringContent(modifyDto)); + + // Assert + _ = responseMessage.StatusCode.Should().Be(HttpStatusCode.NoContent); + } - var modifyDto = new AppointmentModifyBodyDto - { - Name = "New Appointment", - InternalDetails = "Internal Details", - PublicDetails = "Public Details", - CategoryId = SelectValueMappingSeedData.AppointmentCategoryMappings[0].Id, - SalaryId = SelectValueMappingSeedData.AppointmentSalaryMappings[0].Id, - SalaryPatternId = SelectValueMappingSeedData.AppointmentSalaryPatternMappings[0].Id, - EndTime = FakeDateTime.UtcNow.AddHours(5), - StartTime = FakeDateTime.UtcNow, - Status = AppointmentStatus.Confirmed - }; - - // Act - HttpResponseMessage responseMessage = await _authenticatedServer - .CreateClient() - .AuthenticateWith(_staff) - .PutAsync(ApiEndpoints.AppointmentsController.Put(appointmentToModify.Id), BuildStringContent(modifyDto)); - - // Assert - _ = responseMessage.StatusCode.Should().Be(HttpStatusCode.NoContent); - } + [Test] + [Order(108)] + public async Task Should_Modify_With_Only_Mandatory_Fields_Specified() + { + // Arrange + Appointment appointmentToModify = AppointmentSeedData.AppointmentWithoutProject; - [Test, Order(108)] - public async Task Should_Modify_With_Only_Mandatory_Fields_Specified() + var modifyDto = new AppointmentModifyBodyDto { - // Arrange - Appointment appointmentToModify = AppointmentSeedData.AppointmentWithoutProject; - - var modifyDto = new AppointmentModifyBodyDto - { - Name = "New Appointment", - InternalDetails = "Internal Details", - PublicDetails = "Public Details", - EndTime = FakeDateTime.UtcNow.AddHours(5), - StartTime = FakeDateTime.UtcNow, - }; - - // Act - HttpResponseMessage responseMessage = await _authenticatedServer - .CreateClient() - .AuthenticateWith(_staff) - .PutAsync(ApiEndpoints.AppointmentsController.Put(appointmentToModify.Id), BuildStringContent(modifyDto)); - - // Assert - _ = responseMessage.StatusCode.Should().Be(HttpStatusCode.NoContent); - } + Name = "New Appointment", + InternalDetails = "Internal Details", + PublicDetails = "Public Details", + EndTime = FakeDateTime.UtcNow.AddHours(5), + StartTime = FakeDateTime.UtcNow + }; + + // Act + HttpResponseMessage responseMessage = await _authenticatedServer + .CreateClient() + .AuthenticateWith(_staff) + .PutAsync(ApiEndpoints.AppointmentsController.Put(appointmentToModify.Id), + BuildStringContent(modifyDto)); + + // Assert + _ = responseMessage.StatusCode.Should().Be(HttpStatusCode.NoContent); + } - [Test, Order(109)] - public async Task Should_Not_Modify_If_Not_Existing_Id_Is_Supplied() + [Test] + [Order(109)] + public async Task Should_Not_Modify_If_Not_Existing_Id_Is_Supplied() + { + // Arrange + var modifyDto = new AppointmentModifyBodyDto { - // Arrange - var modifyDto = new AppointmentModifyBodyDto - { - Name = "New Appointment", - InternalDetails = "Internal Details", - PublicDetails = "Public Details", - EndTime = FakeDateTime.UtcNow.AddHours(5), - StartTime = FakeDateTime.UtcNow, - }; - - // Act - HttpResponseMessage responseMessage = await _authenticatedServer - .CreateClient() - .AuthenticateWith(_staff) - .PutAsync(ApiEndpoints.AppointmentsController.Put(Guid.NewGuid()), BuildStringContent(modifyDto)); - - // Assert - _ = responseMessage.StatusCode.Should().Be(HttpStatusCode.NotFound); - ValidationProblemDetails errorMessage = await DeserializeResponseMessageAsync(responseMessage); - _ = errorMessage.Title.Should().Be("Resource not found."); - _ = errorMessage.Status.Should().Be(404); - _ = errorMessage.Errors.Should().BeEquivalentTo(new Dictionary() { { "Id", s_appointmentNotFoundMessage } }); - } + Name = "New Appointment", + InternalDetails = "Internal Details", + PublicDetails = "Public Details", + EndTime = FakeDateTime.UtcNow.AddHours(5), + StartTime = FakeDateTime.UtcNow + }; + + // Act + HttpResponseMessage responseMessage = await _authenticatedServer + .CreateClient() + .AuthenticateWith(_staff) + .PutAsync(ApiEndpoints.AppointmentsController.Put(Guid.NewGuid()), + BuildStringContent(modifyDto)); + + // Assert + _ = responseMessage.StatusCode.Should().Be(HttpStatusCode.NotFound); + ValidationProblemDetails errorMessage = + await DeserializeResponseMessageAsync(responseMessage); + _ = errorMessage.Title.Should().Be("Resource not found."); + _ = errorMessage.Status.Should().Be(404); + _ = errorMessage.Errors.Should() + .BeEquivalentTo( + new Dictionary { { "Id", s_appointmentNotFoundMessage } }); + } - [Test, Order(108)] - public async Task Should_Set_Dates() + [Test] + [Order(108)] + public async Task Should_Set_Dates() + { + // Arrange + Appointment appointmentToModify = AppointmentSeedData.PhotoSession; + var setDatesDto = new AppointmentSetDatesBodyDto { - // Arrange - Appointment appointmentToModify = AppointmentSeedData.PhotoSession; - var setDatesDto = new AppointmentSetDatesBodyDto - { - StartTime = FakeDateTime.UtcNow, - EndTime = FakeDateTime.UtcNow.AddHours(5) - }; - AppointmentDto expectedDto = AppointmentDtoData.PhotoSession; - expectedDto.EndTime = setDatesDto.EndTime.Value; - expectedDto.StartTime = setDatesDto.StartTime.Value; - expectedDto.ModifiedBy = _staff.DisplayName; - expectedDto.ModifiedAt = FakeDateTime.UtcNow; - - // Act - HttpResponseMessage responseMessage = await _authenticatedServer - .CreateClient() - .AuthenticateWith(_staff) - .PutAsync(ApiEndpoints.AppointmentsController.SetDates(appointmentToModify.Id), BuildStringContent(setDatesDto)); - - // Assert - _ = responseMessage.StatusCode.Should().Be(HttpStatusCode.OK); - AppointmentDto result = await DeserializeResponseMessageAsync(responseMessage); - _ = result.Should().BeEquivalentTo(expectedDto); - } + StartTime = FakeDateTime.UtcNow, EndTime = FakeDateTime.UtcNow.AddHours(5) + }; + AppointmentDto expectedDto = AppointmentDtoData.PhotoSession; + expectedDto.EndTime = setDatesDto.EndTime.Value; + expectedDto.StartTime = setDatesDto.StartTime.Value; + expectedDto.ModifiedBy = _staff.DisplayName; + expectedDto.ModifiedAt = FakeDateTime.UtcNow; + + // Act + HttpResponseMessage responseMessage = await _authenticatedServer + .CreateClient() + .AuthenticateWith(_staff) + .PutAsync(ApiEndpoints.AppointmentsController.SetDates(appointmentToModify.Id), + BuildStringContent(setDatesDto)); + + // Assert + _ = responseMessage.StatusCode.Should().Be(HttpStatusCode.OK); + AppointmentDto result = + await DeserializeResponseMessageAsync(responseMessage); + _ = result.Should().BeEquivalentTo(expectedDto); + } - [Test, Order(109)] - public async Task Should_Remove_Room() - { - // Act - HttpResponseMessage responseMessage = await _authenticatedServer - .CreateClient() - .AuthenticateWith(_staff) - .DeleteAsync(ApiEndpoints.AppointmentsController.Room( - AppointmentSeedData.AfterShowParty.Id, - RoomSeedData.AulaWeiherhofSchule.Id)); - - // Assert - _ = responseMessage.StatusCode.Should().Be(HttpStatusCode.NoContent); - } + [Test] + [Order(109)] + public async Task Should_Remove_Room() + { + // Act + HttpResponseMessage responseMessage = await _authenticatedServer + .CreateClient() + .AuthenticateWith(_staff) + .DeleteAsync(ApiEndpoints.AppointmentsController.Room( + AppointmentSeedData.AfterShowParty.Id, + RoomSeedData.AulaWeiherhofSchule.Id)); + + // Assert + _ = responseMessage.StatusCode.Should().Be(HttpStatusCode.NoContent); + } - [Test, Order(102)] - public async Task Should_Remove_Section() + [Test] + [Order(102)] + public async Task Should_Remove_Section() + { + // Arrange + AppointmentDto expectedDto = AppointmentDtoData.AfterShowParty; + expectedDto.Sections.Clear(); + expectedDto.Participations.Add(new AppointmentParticipationListItemDto { - // Arrange - AppointmentDto expectedDto = AppointmentDtoData.AfterShowParty; - expectedDto.Sections.Clear(); - expectedDto.Participations.Add(new AppointmentParticipationListItemDto + Person = ReducedPersonDtoData.Staff, + MusicianProfiles = new List { - Person = ReducedPersonDtoData.Staff, - MusicianProfiles = new List - { - ReducedMusicianProfileDtoData.StaffProfile1, - ReducedMusicianProfileDtoData.StaffProfile2 - } - }); - expectedDto.Participations.Add(new AppointmentParticipationListItemDto + ReducedMusicianProfileDtoData.StaffProfile1, + ReducedMusicianProfileDtoData.StaffProfile2 + } + }); + expectedDto.Participations.Add(new AppointmentParticipationListItemDto + { + Person = ReducedPersonDtoData.Admin, + MusicianProfiles = new List { - Person = ReducedPersonDtoData.Admin, - MusicianProfiles = new List - { - ReducedMusicianProfileDtoData.AdminProfile1 - } - }); - - // Act - HttpResponseMessage responseMessage = await _authenticatedServer - .CreateClient() - .AuthenticateWith(_staff) - .DeleteAsync(ApiEndpoints.AppointmentsController.Section( - AppointmentSeedData.AfterShowParty.Id, - SectionSeedData.Alto.Id)); - - // Assert - _ = responseMessage.StatusCode.Should().Be(HttpStatusCode.OK); - AppointmentDto result = await DeserializeResponseMessageAsync(responseMessage); - _ = result.Should().BeEquivalentTo(expectedDto); - } + ReducedMusicianProfileDtoData.AdminProfile1 + } + }); + + // Act + HttpResponseMessage responseMessage = await _authenticatedServer + .CreateClient() + .AuthenticateWith(_staff) + .DeleteAsync(ApiEndpoints.AppointmentsController.Section( + AppointmentSeedData.AfterShowParty.Id, + SectionSeedData.Alto.Id)); + + // Assert + _ = responseMessage.StatusCode.Should().Be(HttpStatusCode.OK); + AppointmentDto result = + await DeserializeResponseMessageAsync(responseMessage); + _ = result.Should().BeEquivalentTo(expectedDto); + } - [Test, Order(103)] - public async Task Should_Remove_Project() - { - // Arrange - AppointmentDto expectedDto = AppointmentDtoData.StaffMeeting; - expectedDto.Projects.Clear(); - expectedDto.Participations.Clear(); - - AppointmentParticipationListItemDto performerParticipation = AppointmentDtoData.PerformerParticipationRockingXMasRehearsal; - performerParticipation.MusicianProfiles.Add(ReducedMusicianProfileDtoData.PerformerHornProfile); - performerParticipation.MusicianProfiles.Add(ReducedMusicianProfileDtoData.PerformerDeactivatedTubaProfile); - performerParticipation.Participation = null; - expectedDto.Participations.Add(performerParticipation); - - AppointmentParticipationListItemDto staffParticipation = AppointmentDtoData.StaffParticipation; - staffParticipation.Participation = null; - expectedDto.Participations.Add(staffParticipation); - - AppointmentParticipationListItemDto adminParticipation = AppointmentDtoData.AdminParticipation; - adminParticipation.MusicianProfiles.Add(ReducedMusicianProfileDtoData.AdminProfile2); - expectedDto.Participations.Add(adminParticipation); - - expectedDto.Participations.Add(AppointmentDtoData.WithoutRoleParticipation); - - // Act - HttpResponseMessage responseMessage = await _authenticatedServer - .CreateClient() - .AuthenticateWith(_staff) - .DeleteAsync(ApiEndpoints.AppointmentsController.Project( - AppointmentSeedData.StaffMeeting.Id, - ProjectSeedData.HoorayForHollywood.Id)); - - // Assert - _ = responseMessage.StatusCode.Should().Be(HttpStatusCode.OK); - AppointmentDto result = await DeserializeResponseMessageAsync(responseMessage); - _ = result.Should().BeEquivalentTo(expectedDto); - } + [Test] + [Order(103)] + public async Task Should_Remove_Project() + { + // Arrange + AppointmentDto expectedDto = AppointmentDtoData.StaffMeeting; + expectedDto.Projects.Clear(); + expectedDto.Participations.Clear(); + + AppointmentParticipationListItemDto performerParticipation = + AppointmentDtoData.PerformerParticipationRockingXMasRehearsal; + performerParticipation.MusicianProfiles.Add(ReducedMusicianProfileDtoData + .PerformerHornProfile); + performerParticipation.MusicianProfiles.Add(ReducedMusicianProfileDtoData + .PerformerDeactivatedTubaProfile); + performerParticipation.Participation = null; + expectedDto.Participations.Add(performerParticipation); + + AppointmentParticipationListItemDto staffParticipation = + AppointmentDtoData.StaffParticipation; + staffParticipation.Participation = null; + expectedDto.Participations.Add(staffParticipation); + + AppointmentParticipationListItemDto adminParticipation = + AppointmentDtoData.AdminParticipation; + adminParticipation.MusicianProfiles.Add(ReducedMusicianProfileDtoData.AdminProfile2); + expectedDto.Participations.Add(adminParticipation); + + expectedDto.Participations.Add(AppointmentDtoData.WithoutRoleParticipation); + + // Act + HttpResponseMessage responseMessage = await _authenticatedServer + .CreateClient() + .AuthenticateWith(_staff) + .DeleteAsync(ApiEndpoints.AppointmentsController.Project( + AppointmentSeedData.StaffMeeting.Id, + ProjectSeedData.HoorayForHollywood.Id)); + + // Assert + _ = responseMessage.StatusCode.Should().Be(HttpStatusCode.OK); + AppointmentDto result = + await DeserializeResponseMessageAsync(responseMessage); + _ = result.Should().BeEquivalentTo(expectedDto); + } - [Test, Order(118)] - public async Task Should_Set_New_Participation_Prediction() + [Test] + [Order(118)] + public async Task Should_Set_New_Participation_Prediction() + { + // Arrange + var dto = new AppointmentParticipationSetPredictionBodyDto { - // Arrange - var dto = new AppointmentParticipationSetPredictionBodyDto - { - CommentByPerformerInner = "CommentByPerformerInner", - Prediction = AppointmentParticipationPrediction.Partly - }; - - // Act - HttpResponseMessage responseMessage = await _authenticatedServer - .CreateClient() - .AuthenticateWith(_staff) - .PutAsync(ApiEndpoints.AppointmentsController.SetParticipationPrediction( + CommentByPerformerInner = "CommentByPerformerInner", + Prediction = AppointmentParticipationPrediction.Partly + }; + + // Act + HttpResponseMessage responseMessage = await _authenticatedServer + .CreateClient() + .AuthenticateWith(_staff) + .PutAsync(ApiEndpoints.AppointmentsController.SetParticipationPrediction( AppointmentSeedData.PhotoSession.Id, PersonSeedData.AdminPersonId), - BuildStringContent(dto)); + BuildStringContent(dto)); - // Assert - _ = responseMessage.StatusCode.Should().Be(HttpStatusCode.NoContent); - } + // Assert + _ = responseMessage.StatusCode.Should().Be(HttpStatusCode.NoContent); + } - [Test, Order(119)] - public async Task Should_Set_Existing_Participation_Prediction() + [Test] + [Order(119)] + public async Task Should_Set_Existing_Participation_Prediction() + { + // Arrange + var dto = new AppointmentParticipationSetPredictionBodyDto { - // Arrange - var dto = new AppointmentParticipationSetPredictionBodyDto - { - CommentByPerformerInner = "CommentByPerformerInner", - Prediction = AppointmentParticipationPrediction.Yes - }; - - // Act - HttpResponseMessage responseMessage = await _authenticatedServer - .CreateClient() - .AuthenticateWith(_staff) - .PutAsync(ApiEndpoints.AppointmentsController.SetParticipationPrediction( + CommentByPerformerInner = "CommentByPerformerInner", + Prediction = AppointmentParticipationPrediction.Yes + }; + + // Act + HttpResponseMessage responseMessage = await _authenticatedServer + .CreateClient() + .AuthenticateWith(_staff) + .PutAsync(ApiEndpoints.AppointmentsController.SetParticipationPrediction( AppointmentSeedData.RockingXMasRehearsal.Id, _performer.PersonId), - BuildStringContent(dto)); + BuildStringContent(dto)); - // Assert - _ = responseMessage.StatusCode.Should().Be(HttpStatusCode.NoContent); - } + // Assert + _ = responseMessage.StatusCode.Should().Be(HttpStatusCode.NoContent); + } - [Test, Order(10004)] - public async Task Should_Delete() - { - // Arrange - Appointment appointmentToDelete = AppointmentSeedData.StaffMeeting; - - // Act - HttpResponseMessage responseMessage = await _authenticatedServer - .CreateClient() - .AuthenticateWith(_admin) - .DeleteAsync(ApiEndpoints.AppointmentsController.Delete(appointmentToDelete.Id)); - - // Assert - _ = responseMessage.StatusCode.Should().Be(HttpStatusCode.NoContent); - - HttpResponseMessage getResponseMessage = await _authenticatedServer - .CreateClient() - .AuthenticateWith(_staff) - .GetAsync(ApiEndpoints.AppointmentsController.Get(appointmentToDelete.Id)); - _ = getResponseMessage.StatusCode.Should().Be(HttpStatusCode.NotFound); - } + + [Test] + [Order(10004)] + public async Task Should_Delete() + { + // Arrange + Appointment appointmentToDelete = AppointmentSeedData.StaffMeeting; + + // Act + HttpResponseMessage responseMessage = await _authenticatedServer + .CreateClient() + .AuthenticateWith(_admin) + .DeleteAsync(ApiEndpoints.AppointmentsController.Delete(appointmentToDelete.Id)); + + // Assert + _ = responseMessage.StatusCode.Should().Be(HttpStatusCode.NoContent); + + HttpResponseMessage getResponseMessage = await _authenticatedServer + .CreateClient() + .AuthenticateWith(_staff) + .GetAsync(ApiEndpoints.AppointmentsController.Get(appointmentToDelete.Id)); + _ = getResponseMessage.StatusCode.Should().Be(HttpStatusCode.NotFound); } } diff --git a/Tests/Orso.Arpa.Api.Tests/IntegrationTests/Shared/ApiEndpoints.cs b/Tests/Orso.Arpa.Api.Tests/IntegrationTests/Shared/ApiEndpoints.cs index f225cb8f8..75fa42eaf 100644 --- a/Tests/Orso.Arpa.Api.Tests/IntegrationTests/Shared/ApiEndpoints.cs +++ b/Tests/Orso.Arpa.Api.Tests/IntegrationTests/Shared/ApiEndpoints.cs @@ -41,7 +41,7 @@ public static class ClubController { private static string Club => $"{Base}/club"; public static string Get() => Club; } - + public static class UsersController { private static string Users => $"{Base}/users"; @@ -267,6 +267,8 @@ public static string SetParticipationPrediction(Guid id, Guid personId) => public static string SendAppointmentChangedNotification(Guid id, bool forceSending) => $"{Appointments}/{id}/notification?forceSending={forceSending}"; + + public static string ExportToIcs() => $"{Appointments}/export"; } public static class AuditLogsController diff --git a/Tests/Orso.Arpa.Api.Tests/Orso.Arpa.Api.Tests.csproj b/Tests/Orso.Arpa.Api.Tests/Orso.Arpa.Api.Tests.csproj index 18730814a..c6efbf0cb 100644 --- a/Tests/Orso.Arpa.Api.Tests/Orso.Arpa.Api.Tests.csproj +++ b/Tests/Orso.Arpa.Api.Tests/Orso.Arpa.Api.Tests.csproj @@ -28,4 +28,4 @@ ..\Orso.Arpa.Tests.Libs\netDumbster.dll - \ No newline at end of file + diff --git a/Tests/Orso.Arpa.Application.Tests/Orso.Arpa.Application.Tests.csproj b/Tests/Orso.Arpa.Application.Tests/Orso.Arpa.Application.Tests.csproj index 1cbec3ae2..6064670f3 100644 --- a/Tests/Orso.Arpa.Application.Tests/Orso.Arpa.Application.Tests.csproj +++ b/Tests/Orso.Arpa.Application.Tests/Orso.Arpa.Application.Tests.csproj @@ -17,4 +17,4 @@ - \ No newline at end of file + diff --git a/Tests/Orso.Arpa.Domain.Tests/AppointmentTests/QueryHandlerTests/ExportAppointmentsToIcsHandlerTests.cs b/Tests/Orso.Arpa.Domain.Tests/AppointmentTests/QueryHandlerTests/ExportAppointmentsToIcsHandlerTests.cs new file mode 100644 index 000000000..ee4501b00 --- /dev/null +++ b/Tests/Orso.Arpa.Domain.Tests/AppointmentTests/QueryHandlerTests/ExportAppointmentsToIcsHandlerTests.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using MockQueryable.NSubstitute; +using NSubstitute; +using NUnit.Framework; +using Orso.Arpa.Domain.AppointmentDomain.Model; +using Orso.Arpa.Domain.AppointmentDomain.Queries; +using Orso.Arpa.Domain.General.Interfaces; +using Orso.Arpa.Tests.Shared.FakeData; +using Orso.Arpa.Tests.Shared.TestSeedData; + +namespace Orso.Arpa.Domain.Tests.AppointmentTests.QueryHandlerTests; + +[TestFixture] +public class ExportAppointmentsToIcsHandlerTests +{ + private IArpaContext _arpaContext; + private ExportAppointmentsToIcs.Handler _handler; + private static readonly string[] separator = ["\r\n", "\r", "\n", Environment.NewLine]; + + [SetUp] + public void Setup() + { + _arpaContext = Substitute.For(); + _handler = new ExportAppointmentsToIcs.Handler(_arpaContext); + } + + [Test] + public async Task Should_Export_Appointments_To_Ics() + { + // Arrange + DbSet appointmentMock = + new List + { + FakeAppointments.RockingXMasRehearsal, AppointmentSeedData.RehearsalWeekend + }.AsQueryable().BuildMockDbSet(); + _arpaContext.Appointments.Returns(appointmentMock); + string[] expectedResultWithoutDynamicValues = [ + "BEGIN:VCALENDAR", + "PRODID:-//github.com/rianjs/ical.net//NONSGML ical.net 4.0//EN", + "VERSION:2.0", + "BEGIN:VTIMEZONE", + "TZID:Europe/Berlin", + "X-LIC-LOCATION:Europe/Berlin", + "END:VTIMEZONE", + "BEGIN:VEVENT", + "DESCRIPTION:Rehearsal | Let's rock | I need more coffee", + "DTEND:20191221T193000", + "DTSTART:20191221T110000", + "LOCATION:Freiburg", + "SEQUENCE:0", + "SUMMARY:Rocking X-mas Dress Rehearsal", + "END:VEVENT", + "BEGIN:VEVENT", + "DESCRIPTION:- | Accordion rehearsal weekend | -", + "DTEND:20191224T170000", + "DTSTART:20191220T160000", + "LOCATION:-", + "SEQUENCE:0", + "SUMMARY:Rehearsal weekend", + "END:VEVENT", + "END:VCALENDAR"]; + + // Act + string result = await _handler.Handle(new ExportAppointmentsToIcs.Query(), new CancellationToken()); + string[] normalizedResult = RemoveDtstampAndUid(result); + + // Assert + normalizedResult.Should().BeEquivalentTo(expectedResultWithoutDynamicValues, opt => opt.WithStrictOrdering()); + } + + private static string[] RemoveDtstampAndUid(string icsContent) + { + var lines = icsContent.Split(separator, StringSplitOptions.None); + return lines + .Where(line => + !line.StartsWith("DTSTAMP") + && !line.StartsWith("UID") + && !string.IsNullOrEmpty(line)) + .ToArray(); + } +} diff --git a/Tests/Orso.Arpa.Domain.Tests/Orso.Arpa.Domain.Tests.csproj b/Tests/Orso.Arpa.Domain.Tests/Orso.Arpa.Domain.Tests.csproj index 895538eb9..f8d9134ca 100644 --- a/Tests/Orso.Arpa.Domain.Tests/Orso.Arpa.Domain.Tests.csproj +++ b/Tests/Orso.Arpa.Domain.Tests/Orso.Arpa.Domain.Tests.csproj @@ -17,6 +17,7 @@ + @@ -40,6 +41,7 @@ + diff --git a/Tests/Orso.Arpa.Infrastructure.Tests/Orso.Arpa.Infrastructure.Tests.csproj b/Tests/Orso.Arpa.Infrastructure.Tests/Orso.Arpa.Infrastructure.Tests.csproj index b89b01671..0f581e519 100644 --- a/Tests/Orso.Arpa.Infrastructure.Tests/Orso.Arpa.Infrastructure.Tests.csproj +++ b/Tests/Orso.Arpa.Infrastructure.Tests/Orso.Arpa.Infrastructure.Tests.csproj @@ -17,4 +17,4 @@ - \ No newline at end of file + diff --git a/Tests/Orso.Arpa.Mail.Tests/Orso.Arpa.Mail.Tests.csproj b/Tests/Orso.Arpa.Mail.Tests/Orso.Arpa.Mail.Tests.csproj index f457f36a1..75e9d0886 100644 --- a/Tests/Orso.Arpa.Mail.Tests/Orso.Arpa.Mail.Tests.csproj +++ b/Tests/Orso.Arpa.Mail.Tests/Orso.Arpa.Mail.Tests.csproj @@ -20,4 +20,4 @@ ..\Orso.Arpa.Tests.Libs\netDumbster.dll - \ No newline at end of file + diff --git a/Tests/Orso.Arpa.Persistence.Tests/Orso.Arpa.Persistence.Tests.csproj b/Tests/Orso.Arpa.Persistence.Tests/Orso.Arpa.Persistence.Tests.csproj index 2476cc382..4481d5316 100644 --- a/Tests/Orso.Arpa.Persistence.Tests/Orso.Arpa.Persistence.Tests.csproj +++ b/Tests/Orso.Arpa.Persistence.Tests/Orso.Arpa.Persistence.Tests.csproj @@ -15,4 +15,4 @@ - \ No newline at end of file + diff --git a/Tests/Orso.Arpa.Tests.Shared/Orso.Arpa.Tests.Shared.csproj b/Tests/Orso.Arpa.Tests.Shared/Orso.Arpa.Tests.Shared.csproj index 6066b4878..bf4a30a83 100644 --- a/Tests/Orso.Arpa.Tests.Shared/Orso.Arpa.Tests.Shared.csproj +++ b/Tests/Orso.Arpa.Tests.Shared/Orso.Arpa.Tests.Shared.csproj @@ -21,4 +21,4 @@ PreserveNewest - \ No newline at end of file +