From 4ea535d93753826ec900879560d876cec4d58c38 Mon Sep 17 00:00:00 2001 From: crupest Date: Wed, 10 Feb 2021 02:03:06 +0800 Subject: ... --- .../Timeline/Helpers/Cache/CacheableDataDigest.cs | 16 +++++ BackEnd/Timeline/Helpers/Cache/DataCacheHelper.cs | 82 ++++++++++++++++++++++ .../Helpers/Cache/DelegateCacheableDataProvider.cs | 28 ++++++++ .../Timeline/Helpers/Cache/ICacheableDataDigest.cs | 10 +++ .../Helpers/Cache/ICacheableDataProvider.cs | 11 +++ 5 files changed, 147 insertions(+) create mode 100644 BackEnd/Timeline/Helpers/Cache/CacheableDataDigest.cs create mode 100644 BackEnd/Timeline/Helpers/Cache/DataCacheHelper.cs create mode 100644 BackEnd/Timeline/Helpers/Cache/DelegateCacheableDataProvider.cs create mode 100644 BackEnd/Timeline/Helpers/Cache/ICacheableDataDigest.cs create mode 100644 BackEnd/Timeline/Helpers/Cache/ICacheableDataProvider.cs (limited to 'BackEnd/Timeline/Helpers/Cache') diff --git a/BackEnd/Timeline/Helpers/Cache/CacheableDataDigest.cs b/BackEnd/Timeline/Helpers/Cache/CacheableDataDigest.cs new file mode 100644 index 00000000..3b5bcf52 --- /dev/null +++ b/BackEnd/Timeline/Helpers/Cache/CacheableDataDigest.cs @@ -0,0 +1,16 @@ +using System; + +namespace Timeline.Helpers.Cache +{ + public class CacheableDataDigest + { + public CacheableDataDigest(string eTag, DateTime lastModified) + { + ETag = eTag; + LastModified = lastModified; + } + + public string ETag { get; set; } + public DateTime LastModified { get; set; } + } +} diff --git a/BackEnd/Timeline/Helpers/Cache/DataCacheHelper.cs b/BackEnd/Timeline/Helpers/Cache/DataCacheHelper.cs new file mode 100644 index 00000000..c26bdddc --- /dev/null +++ b/BackEnd/Timeline/Helpers/Cache/DataCacheHelper.cs @@ -0,0 +1,82 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Net.Http.Headers; +using System; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Models; +using Timeline.Models.Http; + +namespace Timeline.Helpers.Cache +{ + public static class DataCacheHelper + { + public static async Task GenerateActionResult(Controller controller, ICacheableDataProvider provider, TimeSpan? maxAge = null) + { + const string CacheControlHeaderKey = "Cache-Control"; + const string IfNonMatchHeaderKey = "If-None-Match"; + const string IfModifiedSinceHeaderKey = "If-Modified-Since"; + const string ETagHeaderKey = "ETag"; + const string LastModifiedHeaderKey = "Last-Modified"; + + string GenerateCacheControlHeaderValue() + { + var cacheControlHeader = new CacheControlHeaderValue() + { + NoCache = true, + NoStore = false, + MaxAge = maxAge ?? TimeSpan.FromDays(14), + Private = true, + MustRevalidate = true + }; + return cacheControlHeader.ToString(); + } + + var digest = await provider.GetDigest(); + var eTagValue = '"' + digest.ETag + '"'; + var eTag = new EntityTagHeaderValue(eTagValue); + + ActionResult Generate304Result() + { + controller.Response.Headers.Add(ETagHeaderKey, eTagValue); + controller.Response.Headers.Add(LastModifiedHeaderKey, digest.LastModified.ToString("R")); + controller.Response.Headers.Add(CacheControlHeaderKey, GenerateCacheControlHeaderValue()); + return controller.StatusCode(StatusCodes.Status304NotModified, null); + } + + if (controller.Request.Headers.TryGetValue(IfNonMatchHeaderKey, out var ifNonMatchHeaderValue)) + { + if (!EntityTagHeaderValue.TryParseList(ifNonMatchHeaderValue, out var eTagList)) + { + return controller.BadRequest(ErrorResponse.Common.Header.IfNonMatch_BadFormat()); + } + + if (eTagList.FirstOrDefault(e => e.Equals(eTag)) != null) + { + return Generate304Result(); + } + } + else if (controller.Request.Headers.TryGetValue(IfModifiedSinceHeaderKey, out var ifModifiedSinceHeaderValue)) + { + if (!DateTime.TryParse(ifModifiedSinceHeaderValue, out var headerValue)) + { + return controller.BadRequest(new CommonResponse(ErrorCodes.Common.Header.IfModifiedSince_BadFormat, "Header If-Modified-Since is of bad format.")); + } + + if (headerValue > digest.LastModified) + { + return Generate304Result(); + } + } + + var data = await provider.GetData(); + controller.Response.Headers.Add(CacheControlHeaderKey, GenerateCacheControlHeaderValue()); + return controller.File(data.Data, data.ContentType, digest.LastModified, eTag); + } + + public static Task GenerateActionResult(Controller controller, Func> getDigestDelegate, Func> getDataDelegate, TimeSpan? maxAge = null) + { + return GenerateActionResult(controller, new DelegateCacheableDataProvider(getDigestDelegate, getDataDelegate), maxAge); + } + } +} diff --git a/BackEnd/Timeline/Helpers/Cache/DelegateCacheableDataProvider.cs b/BackEnd/Timeline/Helpers/Cache/DelegateCacheableDataProvider.cs new file mode 100644 index 00000000..80cb66c7 --- /dev/null +++ b/BackEnd/Timeline/Helpers/Cache/DelegateCacheableDataProvider.cs @@ -0,0 +1,28 @@ +using System; +using System.Threading.Tasks; +using Timeline.Models; + +namespace Timeline.Helpers.Cache +{ + public class DelegateCacheableDataProvider : ICacheableDataProvider + { + private readonly Func> _getDigestDelegate; + private readonly Func> _getDataDelegate; + + public DelegateCacheableDataProvider(Func> getDigestDelegate, Func> getDataDelegate) + { + _getDigestDelegate = getDigestDelegate; + _getDataDelegate = getDataDelegate; + } + + public Task GetDigest() + { + return _getDigestDelegate(); + } + + public Task GetData() + { + return _getDataDelegate(); + } + } +} diff --git a/BackEnd/Timeline/Helpers/Cache/ICacheableDataDigest.cs b/BackEnd/Timeline/Helpers/Cache/ICacheableDataDigest.cs new file mode 100644 index 00000000..32519d7e --- /dev/null +++ b/BackEnd/Timeline/Helpers/Cache/ICacheableDataDigest.cs @@ -0,0 +1,10 @@ +using System; + +namespace Timeline.Helpers.Cache +{ + public interface ICacheableDataDigest + { + string ETag { get; } + DateTime LastModified { get; } + } +} diff --git a/BackEnd/Timeline/Helpers/Cache/ICacheableDataProvider.cs b/BackEnd/Timeline/Helpers/Cache/ICacheableDataProvider.cs new file mode 100644 index 00000000..b270fb1d --- /dev/null +++ b/BackEnd/Timeline/Helpers/Cache/ICacheableDataProvider.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using Timeline.Models; + +namespace Timeline.Helpers.Cache +{ + public interface ICacheableDataProvider + { + Task GetDigest(); + Task GetData(); + } +} -- cgit v1.2.3 From e21b5f85f0d66f51e23a7c1cbf260f2981a83a49 Mon Sep 17 00:00:00 2001 From: crupest Date: Wed, 10 Feb 2021 19:17:08 +0800 Subject: ... --- .../Timeline/Helpers/Cache/CacheableDataDigest.cs | 2 +- BackEnd/Timeline/Models/ByteData.cs | 8 +- BackEnd/Timeline/Services/BasicUserService.cs | 29 +++ BackEnd/Timeline/Services/DataManager.cs | 27 ++- BackEnd/Timeline/Services/UserAvatarService.cs | 245 +++++++++++---------- 5 files changed, 189 insertions(+), 122 deletions(-) (limited to 'BackEnd/Timeline/Helpers/Cache') diff --git a/BackEnd/Timeline/Helpers/Cache/CacheableDataDigest.cs b/BackEnd/Timeline/Helpers/Cache/CacheableDataDigest.cs index 3b5bcf52..18a6c894 100644 --- a/BackEnd/Timeline/Helpers/Cache/CacheableDataDigest.cs +++ b/BackEnd/Timeline/Helpers/Cache/CacheableDataDigest.cs @@ -2,7 +2,7 @@ using System; namespace Timeline.Helpers.Cache { - public class CacheableDataDigest + public class CacheableDataDigest : ICacheableDataDigest { public CacheableDataDigest(string eTag, DateTime lastModified) { diff --git a/BackEnd/Timeline/Models/ByteData.cs b/BackEnd/Timeline/Models/ByteData.cs index 7b832eb5..a1a0c238 100644 --- a/BackEnd/Timeline/Models/ByteData.cs +++ b/BackEnd/Timeline/Models/ByteData.cs @@ -1,4 +1,5 @@ -using NSwag.Annotations; +using System; +using NSwag.Annotations; namespace Timeline.Models { @@ -14,6 +15,11 @@ namespace Timeline.Models /// The content type. public ByteData(byte[] data, string contentType) { + if (data is null) + throw new ArgumentNullException(nameof(data)); + if (contentType is null) + throw new ArgumentNullException(nameof(contentType)); + Data = data; ContentType = contentType; } diff --git a/BackEnd/Timeline/Services/BasicUserService.cs b/BackEnd/Timeline/Services/BasicUserService.cs index fbbb6677..de0829ee 100644 --- a/BackEnd/Timeline/Services/BasicUserService.cs +++ b/BackEnd/Timeline/Services/BasicUserService.cs @@ -29,6 +29,14 @@ namespace Timeline.Services /// Thrown when is of bad format. /// Thrown when the user with given username does not exist. Task GetUserIdByUsername(string username); + + /// + /// Get the username modified time of a user. + /// + /// User id. + /// The time. + /// Thrown when user does not exist. + Task GetUsernameLastModifiedTime(long userId); } public class BasicUserService : IBasicUserService @@ -62,5 +70,26 @@ namespace Timeline.Services return entity.Id; } + + public async Task GetUsernameLastModifiedTime(long userId) + { + var entity = await _database.Users.Where(u => u.Id == userId).Select(u => new { u.UsernameChangeTime }).SingleOrDefaultAsync(); + + if (entity is null) + throw new UserNotExistException(userId); + + return entity.UsernameChangeTime; + } + } + + public static class BasicUserServiceExtensions + { + public static async Task ThrowIfUserNotExist(this IBasicUserService service, long userId) + { + if (!await service.CheckUserExistence(userId)) + { + throw new UserNotExistException(userId); + } + } } } diff --git a/BackEnd/Timeline/Services/DataManager.cs b/BackEnd/Timeline/Services/DataManager.cs index d447b0d5..f24bb59b 100644 --- a/BackEnd/Timeline/Services/DataManager.cs +++ b/BackEnd/Timeline/Services/DataManager.cs @@ -38,13 +38,12 @@ namespace Timeline.Services public Task FreeEntry(string tag); /// - /// Retrieve the entry with given tag. + /// Retrieve the entry with given tag. If not exist, returns null. /// /// The tag of the entry. - /// The data of the entry. + /// The data of the entry. If not exist, returns null. /// Thrown when is null. - /// Thrown when entry with given tag does not exist. - public Task GetEntry(string tag); + public Task GetEntry(string tag); } public class DataManager : IDataManager @@ -106,17 +105,31 @@ namespace Timeline.Services } } - public async Task GetEntry(string tag) + public async Task GetEntry(string tag) { if (tag == null) throw new ArgumentNullException(nameof(tag)); var entity = await _database.Data.Where(d => d.Tag == tag).Select(d => new { d.Data }).SingleOrDefaultAsync(); - if (entity == null) - throw new InvalidOperationException(Resources.Services.DataManager.ExceptionEntryNotExist); + if (entity is null) + return null; return entity.Data; } } + + public static class DataManagerExtensions + { + /// + /// Try to get an entry and throw if not exist. + /// + public static async Task GetEntryAndCheck(this IDataManager dataManager, string tag, string notExistMessage) + { + var data = await dataManager.GetEntry(tag); + if (data is null) + throw new DatabaseCorruptedException($"Can't get data of tag {tag}. {notExistMessage}"); + return data; + } + } } diff --git a/BackEnd/Timeline/Services/UserAvatarService.cs b/BackEnd/Timeline/Services/UserAvatarService.cs index afd6cf0a..5a6d013e 100644 --- a/BackEnd/Timeline/Services/UserAvatarService.cs +++ b/BackEnd/Timeline/Services/UserAvatarService.cs @@ -2,12 +2,12 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using SixLabors.ImageSharp; using System; using System.IO; using System.Linq; using System.Threading.Tasks; using Timeline.Entities; -using Timeline.Helpers; using Timeline.Helpers.Cache; using Timeline.Models; using Timeline.Services.Exceptions; @@ -23,39 +23,53 @@ namespace Timeline.Services public interface IDefaultUserAvatarProvider { /// - /// Get the etag of default avatar. + /// Get the digest of default avatar. /// - /// - Task GetDefaultAvatarETag(); + /// The digest. + Task GetDefaultAvatarDigest(); /// /// Get the default avatar. /// + /// The avatar. Task GetDefaultAvatar(); } public interface IUserAvatarService { - Task GetAvatarDigest(long id); + /// + /// Get avatar digest of a user. + /// + /// User id. + /// The avatar digest. + /// Thrown when user does not exist. + Task GetAvatarDigest(long userId); /// - /// Get avatar of a user. If the user has no avatar set, a default one is returned. Warning: This method does not check the user existence. + /// Get avatar of a user. If the user has no avatar set, a default one is returned. /// - /// The id of the user to get avatar of. - /// The avatar info. - Task GetAvatar(long id); + /// User id. + /// The avatar. + /// Thrown when user does not exist. + Task GetAvatar(long userId); /// - /// Set avatar for a user. Warning: This method does not check the user existence. + /// Set avatar for a user. /// - /// The id of the user to set avatar for. - /// The avatar. Can be null to delete the saved avatar. - /// The etag of the avatar. - /// Thrown if any field in is null when is not null. + /// User id. + /// The new avatar data. + /// The digest of the avatar. + /// Thrown if is null. + /// Thrown when user does not exist. /// Thrown if avatar is of bad format. - Task SetAvatar(long id, ByteData avatar); + Task SetAvatar(long userId, ByteData avatar); - Task DeleteAvatar(long id); + /// + /// Remove avatar of a user. + /// + /// User id. + /// Thrown when user does not exist. + Task DeleteAvatar(long userId); } // TODO! : Make this configurable. @@ -65,9 +79,8 @@ namespace Timeline.Services private readonly string _avatarPath; - private byte[] _cacheData = default!; - private DateTime _cacheLastModified; - private string _cacheETag = default!; + private CacheableDataDigest? _cacheDigest; + private ByteData? _cacheData; public DefaultUserAvatarProvider(IWebHostEnvironment environment, IETagGenerator eTagGenerator) { @@ -78,53 +91,42 @@ namespace Timeline.Services private async Task CheckAndInit() { var path = _avatarPath; - if (_cacheData == null || File.GetLastWriteTime(path) > _cacheLastModified) + if (_cacheData == null || File.GetLastWriteTime(path) > _cacheDigest!.LastModified) { - _cacheData = await File.ReadAllBytesAsync(path); - _cacheLastModified = File.GetLastWriteTime(path); - _cacheETag = await _eTagGenerator.Generate(_cacheData); + var data = await File.ReadAllBytesAsync(path); + _cacheDigest = new CacheableDataDigest(await _eTagGenerator.Generate(data), File.GetLastWriteTime(path)); + Image.Identify(data, out var format); + _cacheData = new ByteData(data, format.DefaultMimeType); } } - public async Task GetDefaultAvatarETag() + public async Task GetDefaultAvatarDigest() { await CheckAndInit(); - return _cacheETag; + return _cacheDigest!; } - public async Task GetDefaultAvatar() + public async Task GetDefaultAvatar() { await CheckAndInit(); - return new AvatarInfo - { - Avatar = new Avatar - { - Type = "image/png", - Data = _cacheData - }, - LastModified = _cacheLastModified - }; + return _cacheData!; } } public class UserAvatarService : IUserAvatarService { - private readonly ILogger _logger; - private readonly DatabaseContext _database; - + private readonly IBasicUserService _basicUserService; private readonly IDefaultUserAvatarProvider _defaultUserAvatarProvider; - private readonly IImageValidator _imageValidator; - private readonly IDataManager _dataManager; - private readonly IClock _clock; public UserAvatarService( ILogger logger, DatabaseContext database, + IBasicUserService basicUserService, IDefaultUserAvatarProvider defaultUserAvatarProvider, IImageValidator imageValidator, IDataManager dataManager, @@ -132,106 +134,123 @@ namespace Timeline.Services { _logger = logger; _database = database; + _basicUserService = basicUserService; _defaultUserAvatarProvider = defaultUserAvatarProvider; _imageValidator = imageValidator; _dataManager = dataManager; _clock = clock; } - public async Task GetAvatarETag(long id) + public async Task GetAvatarDigest(long userId) { - var eTag = (await _database.UserAvatars.Where(a => a.UserId == id).Select(a => new { a.DataTag }).SingleOrDefaultAsync())?.DataTag; - if (eTag == null) - return await _defaultUserAvatarProvider.GetDefaultAvatarETag(); + var usernameChangeTime = await _basicUserService.GetUsernameLastModifiedTime(userId); + + var entity = await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.DataTag, a.LastModified }).SingleOrDefaultAsync(); + + if (entity is null) + { + var defaultAvatarDigest = await _defaultUserAvatarProvider.GetDefaultAvatarDigest(); + return new CacheableDataDigest(defaultAvatarDigest.ETag, new DateTime[] { usernameChangeTime, defaultAvatarDigest.LastModified }.Max()); + } + else if (entity.DataTag is null) + { + var defaultAvatarDigest = await _defaultUserAvatarProvider.GetDefaultAvatarDigest(); + return new CacheableDataDigest(defaultAvatarDigest.ETag, new DateTime[] { usernameChangeTime, defaultAvatarDigest.LastModified, entity.LastModified }.Max()); + } else - return eTag; + { + return new CacheableDataDigest(entity.DataTag, new DateTime[] { usernameChangeTime, entity.LastModified }.Max()); + } } - public async Task GetAvatar(long id) + public async Task GetAvatar(long userId) { - var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == id).Select(a => new { a.Type, a.DataTag, a.LastModified }).SingleOrDefaultAsync(); + await _basicUserService.ThrowIfUserNotExist(userId); + + var entity = await _database.UserAvatars.Where(a => a.UserId == userId).SingleOrDefaultAsync(); - if (avatarEntity != null) + if (entity is null || entity.DataTag is null) { - if (!LanguageHelper.AreSame(avatarEntity.DataTag == null, avatarEntity.Type == null)) - { - var message = Resources.Services.UserAvatarService.ExceptionDatabaseCorruptedDataAndTypeNotSame; - _logger.LogCritical(message); - throw new DatabaseCorruptedException(message); - } + return await _defaultUserAvatarProvider.GetDefaultAvatar(); + } + var data = await _dataManager.GetEntryAndCheck(entity.DataTag, $"This is required by avatar of {userId}."); - if (avatarEntity.DataTag != null) - { - var data = await _dataManager.GetEntry(avatarEntity.DataTag); - return new AvatarInfo - { - Avatar = new Avatar - { - Type = avatarEntity.Type!, - Data = data - }, - LastModified = avatarEntity.LastModified - }; - } + if (entity.Type is null) + { + Image.Identify(data, out var format); + entity.Type = format.DefaultMimeType; + await _database.SaveChangesAsync(); } - var defaultAvatar = await _defaultUserAvatarProvider.GetDefaultAvatar(); - if (avatarEntity != null) - defaultAvatar.LastModified = defaultAvatar.LastModified > avatarEntity.LastModified ? defaultAvatar.LastModified : avatarEntity.LastModified; - return defaultAvatar; + + return new ByteData(data, entity.Type); } - public async Task SetAvatar(long id, Avatar? avatar) + public async Task SetAvatar(long userId, ByteData avatar) { - if (avatar != null) - { - if (avatar.Data == null) - throw new ArgumentException(Resources.Services.UserAvatarService.ExceptionAvatarDataNull, nameof(avatar)); - if (string.IsNullOrEmpty(avatar.Type)) - throw new ArgumentException(Resources.Services.UserAvatarService.ExceptionAvatarTypeNullOrEmpty, nameof(avatar)); - } + if (avatar is null) + throw new ArgumentNullException(nameof(avatar)); + + await _imageValidator.Validate(avatar.Data, avatar.ContentType, true); + + await _basicUserService.ThrowIfUserNotExist(userId); + + var entity = await _database.UserAvatars.Where(a => a.UserId == userId).SingleOrDefaultAsync(); - var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == id).SingleOrDefaultAsync(); + await using var transaction = await _database.Database.BeginTransactionAsync(); - if (avatar == null) + var tag = await _dataManager.RetainEntry(avatar.Data); + + var now = _clock.GetCurrentTime(); + + if (entity is null) { - if (avatarEntity != null && avatarEntity.DataTag != null) + var newEntity = new UserAvatarEntity { - await _dataManager.FreeEntry(avatarEntity.DataTag); - avatarEntity.DataTag = null; - avatarEntity.Type = null; - avatarEntity.LastModified = _clock.GetCurrentTime(); - await _database.SaveChangesAsync(); - _logger.LogInformation(Resources.Services.UserAvatarService.LogUpdateEntity); - } - return await _defaultUserAvatarProvider.GetDefaultAvatarETag(); + DataTag = tag, + Type = avatar.ContentType, + LastModified = now, + UserId = userId + }; + _database.Add(newEntity); } else { - await _imageValidator.Validate(avatar.Data, avatar.Type, true); - var tag = await _dataManager.RetainEntry(avatar.Data); - var oldTag = avatarEntity?.DataTag; - var create = avatarEntity == null; - if (avatarEntity == null) - { - avatarEntity = new UserAvatarEntity(); - _database.UserAvatars.Add(avatarEntity); - } - avatarEntity.DataTag = tag; - avatarEntity.Type = avatar.Type; - avatarEntity.LastModified = _clock.GetCurrentTime(); - avatarEntity.UserId = id; - await _database.SaveChangesAsync(); - _logger.LogInformation(create ? - Resources.Services.UserAvatarService.LogCreateEntity - : Resources.Services.UserAvatarService.LogUpdateEntity); - if (oldTag != null) - { - await _dataManager.FreeEntry(oldTag); - } + if (entity.DataTag is not null) + await _dataManager.FreeEntry(entity.DataTag); - return avatarEntity.DataTag; + entity.DataTag = tag; + entity.Type = avatar.ContentType; + entity.LastModified = now; } + + await _database.SaveChangesAsync(); + + await transaction.CommitAsync(); + + return new CacheableDataDigest(tag, now); + } + + public async Task DeleteAvatar(long userId) + { + await _basicUserService.ThrowIfUserNotExist(userId); + + var entity = await _database.UserAvatars.Where(a => a.UserId == userId).SingleOrDefaultAsync(); + + if (entity is null || entity.DataTag is null) + return; + + await using var transaction = await _database.Database.BeginTransactionAsync(); + + await _dataManager.FreeEntry(entity.DataTag); + + entity.DataTag = null; + entity.Type = null; + entity.LastModified = _clock.GetCurrentTime(); + + await _database.SaveChangesAsync(); + + await transaction.CommitAsync(); } } -- cgit v1.2.3 From b5b758c41a01ab7f78f0711debe92f6add470c64 Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 12 Feb 2021 17:45:09 +0800 Subject: test: Add create post integrated tests. --- .../IntegratedTests/TimelinePostTest.cs | 70 ++++++++++++++++++++++ .../Timeline/Controllers/UserAvatarController.cs | 2 +- BackEnd/Timeline/Helpers/Cache/DataCacheHelper.cs | 2 +- BackEnd/Timeline/Models/Mapper/TimelineMapper.cs | 2 +- 4 files changed, 73 insertions(+), 3 deletions(-) (limited to 'BackEnd/Timeline/Helpers/Cache') diff --git a/BackEnd/Timeline.Tests/IntegratedTests/TimelinePostTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/TimelinePostTest.cs index 85db0908..4563db3a 100644 --- a/BackEnd/Timeline.Tests/IntegratedTests/TimelinePostTest.cs +++ b/BackEnd/Timeline.Tests/IntegratedTests/TimelinePostTest.cs @@ -10,6 +10,10 @@ using Timeline.Models.Http; using Timeline.Tests.Helpers; using Xunit; using Xunit.Abstractions; +using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Formats.Jpeg; +using System.Net; namespace Timeline.Tests.IntegratedTests { @@ -418,5 +422,71 @@ namespace Timeline.Tests.IntegratedTests } ); } + + public static IEnumerable CreatePost_ShouldWork_TestData() + { + var testByteDatas = new List() + { + new ByteData(Encoding.UTF8.GetBytes("aaa"), MimeTypes.TextPlain), + new ByteData(Encoding.UTF8.GetBytes("aaa"), MimeTypes.TextMarkdown), + new ByteData(ImageHelper.CreateImageWithSize(100, 50, PngFormat.Instance), MimeTypes.ImagePng), + new ByteData(ImageHelper.CreateImageWithSize(100, 50, JpegFormat.Instance), MimeTypes.ImageJpeg), + new ByteData(ImageHelper.CreateImageWithSize(100, 50, GifFormat.Instance), MimeTypes.ImageGif), + }; + + return TimelineNameGeneratorTestData().AppendTestData(testByteDatas); + } + + [Theory] + [MemberData(nameof(CreatePost_ShouldWork_TestData))] + public async Task CreatePost_ShouldWork(TimelineNameGenerator generator, ByteData data) + { + using var client = await CreateClientAsUser(); + + var post = await client.TestPostAsync( + $"timelines/{generator(1)}/posts", + new HttpTimelinePostCreateRequest + { + DataList = new List + { + new HttpTimelinePostCreateRequestData + { + ContentType = data.ContentType, + Data = Convert.ToBase64String(data.Data) + } + } + } + ); + + post.DataList.Should().NotBeNull().And.HaveCount(1); + var postData = post.DataList[0]; + postData.Should().NotBeNull(); + var postDataEtag = postData.ETag; + postDataEtag.Should().NotBeNullOrEmpty(); + + { + var response = await client.GetAsync($"timelines/{generator(1)}/posts/{post.Id}/data"); + response.StatusCode.Should().Be(HttpStatusCode.OK); + response.Headers.ETag.Should().NotBeNull(); + response.Headers.ETag!.Tag.Should().Be(postDataEtag); + response.Content.Headers.ContentType.Should().NotBeNull(); + response.Content.Headers.ContentType!.MediaType.Should().Be(data.ContentType); + + var body = await response.Content.ReadAsByteArrayAsync(); + body.Should().Equal(data.Data); + } + + { + var response = await client.GetAsync($"timelines/{generator(1)}/posts/{post.Id}/data/0"); + response.StatusCode.Should().Be(HttpStatusCode.OK); + response.Headers.ETag.Should().NotBeNull(); + response.Headers.ETag!.Tag.Should().Be(postDataEtag); + response.Content.Headers.ContentType.Should().NotBeNull(); + response.Content.Headers.ContentType!.MediaType.Should().Be(data.ContentType); + + var body = await response.Content.ReadAsByteArrayAsync(); + body.Should().Equal(data.Data); + } + } } } diff --git a/BackEnd/Timeline/Controllers/UserAvatarController.cs b/BackEnd/Timeline/Controllers/UserAvatarController.cs index 180d1f9b..fa13f0f6 100644 --- a/BackEnd/Timeline/Controllers/UserAvatarController.cs +++ b/BackEnd/Timeline/Controllers/UserAvatarController.cs @@ -107,7 +107,7 @@ namespace Timeline.Controllers _logger.LogInformation(Log.Format(LogPutSuccess, ("Username", username), ("Mime Type", Request.ContentType))); - Response.Headers.Append("ETag", new EntityTagHeaderValue($"\"{digest.ETag}\"").ToString()); + Response.Headers.Append("ETag", $"\"{digest.ETag}\""); return Ok(); } diff --git a/BackEnd/Timeline/Helpers/Cache/DataCacheHelper.cs b/BackEnd/Timeline/Helpers/Cache/DataCacheHelper.cs index c26bdddc..b7d86b18 100644 --- a/BackEnd/Timeline/Helpers/Cache/DataCacheHelper.cs +++ b/BackEnd/Timeline/Helpers/Cache/DataCacheHelper.cs @@ -33,7 +33,7 @@ namespace Timeline.Helpers.Cache } var digest = await provider.GetDigest(); - var eTagValue = '"' + digest.ETag + '"'; + var eTagValue = $"\"{digest.ETag}\""; var eTag = new EntityTagHeaderValue(eTagValue); ActionResult Generate304Result() diff --git a/BackEnd/Timeline/Models/Mapper/TimelineMapper.cs b/BackEnd/Timeline/Models/Mapper/TimelineMapper.cs index 1f10c123..5c46fa81 100644 --- a/BackEnd/Timeline/Models/Mapper/TimelineMapper.cs +++ b/BackEnd/Timeline/Models/Mapper/TimelineMapper.cs @@ -71,7 +71,7 @@ namespace Timeline.Models.Mapper await _database.Entry(entity).Collection(p => p.DataList).LoadAsync(); await _database.Entry(entity).Reference(e => e.Author).LoadAsync(); - List dataDigestList = entity.DataList.OrderBy(d => d.Index).Select(d => new HttpTimelinePostDataDigest(d.Kind, d.DataTag, d.LastUpdated)).ToList(); + List dataDigestList = entity.DataList.OrderBy(d => d.Index).Select(d => new HttpTimelinePostDataDigest(d.Kind, $"\"{d.DataTag}\"", d.LastUpdated)).ToList(); HttpUser? author = null; if (entity.Author is not null) -- cgit v1.2.3