diff --git a/UsefulTime.Api/Brokers/DateTimes/DateTimeBroker.cs b/UsefulTime.Api/Brokers/DateTimes/DateTimeBroker.cs new file mode 100644 index 0000000..2141c41 --- /dev/null +++ b/UsefulTime.Api/Brokers/DateTimes/DateTimeBroker.cs @@ -0,0 +1,12 @@ +//================================================= +//Copyright (c) Coalition of Good-Hearted Engineers +//Free To Use To Find Comfort and Pease +//================================================= +namespace UsefulTime.Api.Brokers.DateTimes +{ + public class DateTimeBroker:IDateTimeBroker + { + public DateTimeOffset GetCurrentDateTimeOffset() => + DateTimeOffset.UtcNow; + } +} diff --git a/UsefulTime.Api/Brokers/DateTimes/IDateTimeBroker.cs b/UsefulTime.Api/Brokers/DateTimes/IDateTimeBroker.cs new file mode 100644 index 0000000..c57ed04 --- /dev/null +++ b/UsefulTime.Api/Brokers/DateTimes/IDateTimeBroker.cs @@ -0,0 +1,11 @@ +//================================================= +//Copyright (c) Coalition of Good-Hearted Engineers +//Free To Use To Find Comfort and Pease +//================================================= +namespace UsefulTime.Api.Brokers.DateTimes +{ + public interface IDateTimeBroker + { + DateTimeOffset GetCurrentDateTimeOffset(); + } +} diff --git a/UsefulTime.Api/Models/VideoMetadatas/Exceptions/LockedVideoMetadataException.cs b/UsefulTime.Api/Models/VideoMetadatas/Exceptions/LockedVideoMetadataException.cs new file mode 100644 index 0000000..a265b4a --- /dev/null +++ b/UsefulTime.Api/Models/VideoMetadatas/Exceptions/LockedVideoMetadataException.cs @@ -0,0 +1,15 @@ +//================================================= +//Copyright (c) Coalition of Good-Hearted Engineers +//Free To Use To Find Comfort and Pease +//================================================= +using Xeptions; + +namespace UsefulTime.Api.Models.VideoMetadatas.Exceptions +{ + public class LockedVideoMetadataException:Xeption + { + public LockedVideoMetadataException(string message, Exception exception) + :base(message,exception) + { } + } +} diff --git a/UsefulTime.Api/Program.cs b/UsefulTime.Api/Program.cs index 1f3e04c..d7f332e 100644 --- a/UsefulTime.Api/Program.cs +++ b/UsefulTime.Api/Program.cs @@ -2,6 +2,7 @@ //Copyright (c) Coalition of Good-Hearted Engineers //Free To Use To Find Comfort and Pease //================================================= +using UsefulTime.Api.Brokers.DateTimes; using UsefulTime.Api.Brokers.Loggings; using UsefulTime.Api.Brokers.Storages; using UsefulTime.Api.Services.Foundations.VideoMetadatas; @@ -18,8 +19,9 @@ private static void Main(string[] args) builder.Services.AddSwaggerGen(); builder.Services.AddTransient(); builder.Services.AddTransient(); + builder.Services.AddTransient(); builder.Services.AddTransient(); - + var app = builder.Build(); if (app.Environment.IsDevelopment()) diff --git a/UsefulTime.Api/Services/Foundations/VedioMetadatas/VideoMetadataService.Exceptions.cs b/UsefulTime.Api/Services/Foundations/VedioMetadatas/VideoMetadataService.Exceptions.cs index 843a057..ae96920 100644 --- a/UsefulTime.Api/Services/Foundations/VedioMetadatas/VideoMetadataService.Exceptions.cs +++ b/UsefulTime.Api/Services/Foundations/VedioMetadatas/VideoMetadataService.Exceptions.cs @@ -4,6 +4,7 @@ //================================================= using EFxceptions.Models.Exceptions; using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore; using UsefulTime.Api.Models.VideoMetadatas; using UsefulTime.Api.Models.VideoMetadatas.Exceptions; using Xeptions; @@ -43,6 +44,14 @@ private async ValueTask TryCatch(ReturningVideoMetadataFunction r innerException: duplicateKeyException); throw CreateAndDependencyValidationException(alreadyExistVideoMetadataException); } + catch (DbUpdateConcurrencyException dbUpdateConcurrencyException) + { + var lockedVideoMetadataException = new LockedVideoMetadataException( + "Video Metadata is locked, please try again.", + dbUpdateConcurrencyException); + + throw CreateAndLogDependencyValidationException(lockedVideoMetadataException); + } catch (Exception exception) { var failedVideoMetadataServiceException = @@ -51,6 +60,17 @@ private async ValueTask TryCatch(ReturningVideoMetadataFunction r throw CreateAndLogServiseException(failedVideoMetadataServiceException); } } + + private VideoMetadataDependencyException CreateAndLogDependencyException(Xeption exception) + { + var videoMetadataDependencyException = new VideoMetadataDependencyException( + message: "Failed video metadata error occured, cotact support", + innerException: exception); + this.loggingBroker.LogCritical(videoMetadataDependencyException); + + return videoMetadataDependencyException; + } + private VideoMetadataServiceException CreateAndLogServiseException(Xeption exception) { var videoMetadataServiceException = @@ -86,5 +106,15 @@ public VideoMetadataDependencyValidationException CreateAndDependencyValidationE this.loggingBroker.LogError(videoMetadataDependencyValidationException); return videoMetadataDependencyValidationException; } + private VideoMetadataDependencyValidationException CreateAndLogDependencyValidationException(Xeption exception) + { + var videoMetadataDependencyValidationException = new VideoMetadataDependencyValidationException( + "Video Metadata dependency error occured, Fix errors and try again.", + exception); + + this.loggingBroker.LogError(videoMetadataDependencyValidationException); + + return videoMetadataDependencyValidationException; + } } } diff --git a/UsefulTime.Api/Services/Foundations/VedioMetadatas/VideoMetadataService.Validations.cs b/UsefulTime.Api/Services/Foundations/VedioMetadatas/VideoMetadataService.Validations.cs index 8434536..f00dc84 100644 --- a/UsefulTime.Api/Services/Foundations/VedioMetadatas/VideoMetadataService.Validations.cs +++ b/UsefulTime.Api/Services/Foundations/VedioMetadatas/VideoMetadataService.Validations.cs @@ -14,11 +14,19 @@ private void ValidatevideoMetadataOnAdd(VideoMetadata videoMetadata) ValidationVideoMetadataNotNull(videoMetadata); Validate( - (Rule: IsInvalid(videoMetadata.Id), Parameter: nameof(videoMetadata.Id)), - (Rule: IsInvalid(videoMetadata.Title), Parameter: nameof(videoMetadata.Title)), - (Rule: IsInvalid(videoMetadata.BlobPath), Parameter: nameof(videoMetadata.BlobPath)), - (Rule: IsInvalid(videoMetadata.CreatedDate), Parameter: nameof(videoMetadata.CreatedDate)), - (Rule: IsInvalid(videoMetadata.UpdatedDate), Parameter: nameof(videoMetadata.UpdatedDate))); + (Rule: IsInvalid(videoMetadata.Id), Parameter: nameof(VideoMetadata.Id)), + (Rule: IsInvalid(videoMetadata.Title), Parameter: nameof(VideoMetadata.Title)), + (Rule: IsInvalid(videoMetadata.BlobPath), Parameter: nameof(VideoMetadata.BlobPath)), + (Rule: IsInvalid(videoMetadata.CreatedDate), Parameter: nameof(VideoMetadata.CreatedDate)), + (Rule: IsInvalid(videoMetadata.UpdatedDate), Parameter: nameof(VideoMetadata.UpdatedDate)), + (Rule: IsNotRecent(videoMetadata.CreatedDate), Parameter: nameof(VideoMetadata.CreatedDate)), + + (Rule: IsNotSame( + firstDate: videoMetadata.CreatedDate, + secondDate: videoMetadata.UpdatedDate, + secondDateName: nameof(VideoMetadata.UpdatedDate)), + Parameter: nameof(VideoMetadata.CreatedDate)) + ); } private void ValidationVideoMetadataNotNull(VideoMetadata videoMetadata) { @@ -27,25 +35,50 @@ private void ValidationVideoMetadataNotNull(VideoMetadata videoMetadata) throw new NullVideoMetadataException(message: "Video metadata is null"); } } - private static dynamic IsInvalid(Guid id) => new + private static dynamic IsNotSame( + DateTimeOffset firstDate, + DateTimeOffset secondDate, + string secondDateName) => new + { + Condition = firstDate != secondDate, + Message = $"Date is not same as {secondDateName}" + }; + + private dynamic IsNotRecent(DateTimeOffset date) => new { - Condition = id == Guid.Empty, - Message = "Id is required" + Condition = IsDateNotRecent(date), + Message = "Date is not recent" }; + + private bool IsDateNotRecent(DateTimeOffset date) + { + DateTimeOffset currentDateTime = this.dateTimeBroker.GetCurrentDateTimeOffset(); + TimeSpan timeDifference = currentDateTime.Subtract(date); + + return timeDifference.TotalSeconds is > 60 or < 0; + } + + private static dynamic IsInvalid(Guid Id) => new + { + Condition = Id == Guid.Empty, + Message = "Id is required." + }; + private static dynamic IsInvalid(string text) => new { Condition = string.IsNullOrWhiteSpace(text), - Message = "Text is required" + Message = "Text is required." }; + private static dynamic IsInvalid(DateTimeOffset date) => new { - Condition = date == default, - Message = "Data is required" + Condition = date == default(DateTimeOffset), + Message = "Date is required." }; + private static void Validate(params (dynamic Rule, string Parameter)[] validations) { - var invalidVideoMetadataException = - new InvalidVideoMetadataException(message: "Video metadata is invalid"); + var invalidVideoMetadataException = new InvalidVideoMetadataException(message: "Video Metadata is invalid."); foreach ((dynamic rule, string parameter) in validations) { @@ -54,6 +87,7 @@ private static void Validate(params (dynamic Rule, string Parameter)[] validatio invalidVideoMetadataException.UpsertDataList(parameter, rule.Message); } } + invalidVideoMetadataException.ThrowIfContainsErrors(); } } diff --git a/UsefulTime.Api/Services/Foundations/VedioMetadatas/VideoMetadataService.cs b/UsefulTime.Api/Services/Foundations/VedioMetadatas/VideoMetadataService.cs index 65cf6aa..e24abe4 100644 --- a/UsefulTime.Api/Services/Foundations/VedioMetadatas/VideoMetadataService.cs +++ b/UsefulTime.Api/Services/Foundations/VedioMetadatas/VideoMetadataService.cs @@ -2,6 +2,7 @@ //Copyright (c) Coalition of Good-Hearted Engineers //Free To Use To Find Comfort and Pease //================================================= +using UsefulTime.Api.Brokers.DateTimes; using UsefulTime.Api.Brokers.Loggings; using UsefulTime.Api.Brokers.Storages; using UsefulTime.Api.Models.VideoMetadatas; @@ -12,11 +13,14 @@ public partial class VideoMetadataService : IVideoMetadataService { private readonly IStorageBroker storageBroker; private readonly ILoggingBroker loggingBroker; + private readonly IDateTimeBroker dateTimeBroker; - public VideoMetadataService(IStorageBroker storageBroker, ILoggingBroker loggingBroker) + public VideoMetadataService(IStorageBroker storageBroker, ILoggingBroker loggingBroker, + IDateTimeBroker dateTimeBroker) { this.storageBroker = storageBroker; this.loggingBroker = loggingBroker; + this.dateTimeBroker = dateTimeBroker; } public ValueTask AddVideoMetadataAsync(VideoMetadata videoMetadata) => TryCatch(async () => diff --git a/UsefulTime.Unit.Tests/Services/Foundations/VideoMetadatas/VideoMetadataServiceTest.Exceptions.Add.cs b/UsefulTime.Unit.Tests/Services/Foundations/VideoMetadatas/VideoMetadataServiceTest.Exceptions.Add.cs index b9fe5b3..c9cee08 100644 --- a/UsefulTime.Unit.Tests/Services/Foundations/VideoMetadatas/VideoMetadataServiceTest.Exceptions.Add.cs +++ b/UsefulTime.Unit.Tests/Services/Foundations/VideoMetadatas/VideoMetadataServiceTest.Exceptions.Add.cs @@ -5,6 +5,7 @@ using EFxceptions.Models.Exceptions; using FluentAssertions; using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore; using Moq; using UsefulTime.Api.Models.VideoMetadatas; using UsefulTime.Api.Models.VideoMetadatas.Exceptions; @@ -103,5 +104,50 @@ public async Task ShouldThrowDependencyValidationOnAddIfDublicateKeyErrorOccursA this.storageBrokerMock.VerifyNoOtherCalls(); this.loggingBrokerMock.VerifyNoOtherCalls(); } + [Fact] + public async Task ShouldThrowDependencyValidationExceptionOnAddIfDbCurrencyErrorOccursAndLogItAsync() + { + //given + VideoMetadata someVideoMetadata = CreateRandomVideoMetadata(); + var dbUpdateConcurrencyException = new DbUpdateConcurrencyException(); + + var lockedVideoMetadataException = + new LockedVideoMetadataException("Video Metadata is locked, please try again.", + dbUpdateConcurrencyException); + + var expectedVideoMetadataDependencyValidationException = + new VideoMetadataDependencyValidationException( + "Video Metadata dependency error occured, Fix errors and try again.", + lockedVideoMetadataException); + + this.dateTimeBrokerMock.Setup(broker => + broker.GetCurrentDateTimeOffset()) + .Throws(dbUpdateConcurrencyException); + + //when + ValueTask addVideoMetadataTask = + this.videoMetadataService.AddVideoMetadataAsync(someVideoMetadata); + + var actualVideoMetadataDependencyValidationException = + await Assert.ThrowsAsync(addVideoMetadataTask.AsTask); + + //then + actualVideoMetadataDependencyValidationException.Should() + .BeEquivalentTo(expectedVideoMetadataDependencyValidationException); + + this.dateTimeBrokerMock.Verify(broker => + broker.GetCurrentDateTimeOffset(), Times.Once); + + this.loggingBrokerMock.Verify(broker => + broker.LogError(It.Is(SameExceptionAs(expectedVideoMetadataDependencyValidationException))), + Times.Once); + + this.storageBrokerMock.Verify(broker => + broker.InsertVideoMetadataAsync(someVideoMetadata), Times.Never); + + this.dateTimeBrokerMock.VerifyNoOtherCalls(); + this.storageBrokerMock.VerifyNoOtherCalls(); + this.loggingBrokerMock.VerifyNoOtherCalls(); + } } } diff --git a/UsefulTime.Unit.Tests/Services/Foundations/VideoMetadatas/VideoMetadataServiceTest.cs b/UsefulTime.Unit.Tests/Services/Foundations/VideoMetadatas/VideoMetadataServiceTest.cs index fcbc34e..4c897e5 100644 --- a/UsefulTime.Unit.Tests/Services/Foundations/VideoMetadatas/VideoMetadataServiceTest.cs +++ b/UsefulTime.Unit.Tests/Services/Foundations/VideoMetadatas/VideoMetadataServiceTest.cs @@ -8,6 +8,7 @@ using System.Linq.Expressions; using System.Runtime.Serialization; using Tynamix.ObjectFiller; +using UsefulTime.Api.Brokers.DateTimes; using UsefulTime.Api.Brokers.Loggings; using UsefulTime.Api.Brokers.Storages; using UsefulTime.Api.Models.VideoMetadatas; @@ -20,16 +21,19 @@ public partial class VideoMetadataServiceTest { private readonly Mock storageBrokerMock; private readonly Mock loggingBrokerMock; + private readonly Mock dateTimeBrokerMock; private readonly IVideoMetadataService videoMetadataService; public VideoMetadataServiceTest() { this.storageBrokerMock = new Mock(); this.loggingBrokerMock = new Mock(); + this.dateTimeBrokerMock= new Mock(); this.videoMetadataService = new VideoMetadataService (storageBroker: this.storageBrokerMock.Object, - loggingBroker: loggingBrokerMock.Object); + loggingBroker: this.loggingBrokerMock.Object, + dateTimeBroker: this.dateTimeBrokerMock.Object); } private Expression> SameExceptionAs(Xeption expectedException) =>