From bac1c733f276ad0f449b4c60e5662d0413cd2121 Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 10 Jul 2020 15:36:51 +0800 Subject: Add deleted field. --- Timeline/Models/Http/Timeline.cs | 15 ++++++--- Timeline/Models/Timeline.cs | 4 +-- Timeline/Services/TimelineService.cs | 60 ++++++++++++++---------------------- 3 files changed, 36 insertions(+), 43 deletions(-) diff --git a/Timeline/Models/Http/Timeline.cs b/Timeline/Models/Http/Timeline.cs index 80e6e69d..aad9aa7b 100644 --- a/Timeline/Models/Http/Timeline.cs +++ b/Timeline/Models/Http/Timeline.cs @@ -18,7 +18,8 @@ namespace Timeline.Models.Http public class TimelinePostInfo { public long Id { get; set; } - public TimelinePostContentInfo Content { get; set; } = default!; + public TimelinePostContentInfo? Content { get; set; } + public bool Deleted { get; set; } public DateTime Time { get; set; } public UserInfo Author { get; set; } = default!; public DateTime LastUpdated { get; set; } = default!; @@ -73,7 +74,7 @@ namespace Timeline.Models.Http } } - public class TimelinePostContentResolver : IValueResolver + public class TimelinePostContentResolver : IValueResolver { private readonly IActionContextAccessor _actionContextAccessor; private readonly IUrlHelperFactory _urlHelperFactory; @@ -84,13 +85,18 @@ namespace Timeline.Models.Http _urlHelperFactory = urlHelperFactory; } - public TimelinePostContentInfo Resolve(TimelinePost source, TimelinePostInfo destination, TimelinePostContentInfo destMember, ResolutionContext context) + public TimelinePostContentInfo? Resolve(TimelinePost source, TimelinePostInfo destination, TimelinePostContentInfo? destMember, ResolutionContext context) { var actionContext = _actionContextAccessor.AssertActionContextForUrlFill(); var urlHelper = _urlHelperFactory.GetUrlHelper(actionContext); var sourceContent = source.Content; + if (sourceContent == null) + { + return null; + } + if (sourceContent is TextTimelinePostContent textContent) { return new TimelinePostContentInfo @@ -122,7 +128,8 @@ namespace Timeline.Models.Http public TimelineInfoAutoMapperProfile() { CreateMap().ForMember(u => u._links, opt => opt.MapFrom()); - CreateMap().ForMember(p => p.Content, opt => opt.MapFrom()); + CreateMap().ForMember(p => p.Content, opt => opt.MapFrom()) + .ForMember(p => p.Deleted, opt => opt.MapFrom((source, dest) => { return source.Content == null; })); CreateMap(); } } diff --git a/Timeline/Models/Timeline.cs b/Timeline/Models/Timeline.cs index 3701ed35..b1772fa7 100644 --- a/Timeline/Models/Timeline.cs +++ b/Timeline/Models/Timeline.cs @@ -48,7 +48,7 @@ namespace Timeline.Models public class TimelinePost { - public TimelinePost(long id, ITimelinePostContent content, DateTime time, User author, DateTime lastUpdated, string timelineName) + public TimelinePost(long id, ITimelinePostContent? content, DateTime time, User author, DateTime lastUpdated, string timelineName) { Id = id; Content = content; @@ -59,7 +59,7 @@ namespace Timeline.Models } public long Id { get; set; } - public ITimelinePostContent Content { get; set; } + public ITimelinePostContent? Content { get; set; } public DateTime Time { get; set; } public User Author { get; set; } public DateTime LastUpdated { get; set; } diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs index 73f6c8ef..a0d72ad3 100644 --- a/Timeline/Services/TimelineService.cs +++ b/Timeline/Services/TimelineService.cs @@ -96,20 +96,8 @@ namespace Timeline.Services /// Get all the posts in the timeline. /// /// The name of the timeline. - /// A list of all posts. - /// Thrown when is null. - /// Throw when is of bad format. - /// - /// Thrown when timeline with name does not exist. - /// If it is a personal timeline, then inner exception is . - /// - Task> GetPosts(string timelineName); - - /// - /// Get the posts that have been modified since a given time in the timeline. - /// - /// The name of the timeline. /// The time that posts have been modified since. + /// Whether include deleted posts. /// A list of all posts. /// Thrown when is null. /// Throw when is of bad format. @@ -117,7 +105,7 @@ namespace Timeline.Services /// Thrown when timeline with name does not exist. /// If it is a personal timeline, then inner exception is . /// - Task> GetPosts(string timelineName, DateTime modifiedSince); + Task> GetPosts(string timelineName, DateTime? modifiedSince = null, bool includeDeleted = false); /// /// Get the etag of data of a post. @@ -399,21 +387,23 @@ namespace Timeline.Services private async Task MapTimelinePostFromEntity(TimelinePostEntity entity, string timelineName) { - if (entity.Content == null) - { - throw new ArgumentException(ExceptionPostDeleted, nameof(entity)); - } + var author = await _userService.GetUserById(entity.AuthorId); - var type = entity.ContentType; + ITimelinePostContent? content = null; - ITimelinePostContent content = type switch + if (entity.Content != null) { - TimelinePostContentTypes.Text => new TextTimelinePostContent(entity.Content), - TimelinePostContentTypes.Image => new ImageTimelinePostContent(entity.Content), - _ => throw new DatabaseCorruptedException(string.Format(CultureInfo.InvariantCulture, ExceptionDatabaseUnknownContentType, type)) - }; + var type = entity.ContentType; + + content = type switch + { + TimelinePostContentTypes.Text => new TextTimelinePostContent(entity.Content), + TimelinePostContentTypes.Image => new ImageTimelinePostContent(entity.Content), + _ => throw new DatabaseCorruptedException(string.Format(CultureInfo.InvariantCulture, ExceptionDatabaseUnknownContentType, type)) + }; + } return new TimelinePost( id: entity.LocalId, @@ -519,29 +509,25 @@ namespace Timeline.Services return await MapTimelineFromEntity(timelineEntity); } - public async Task> GetPosts(string timelineName) + public async Task> GetPosts(string timelineName, DateTime? modifiedSince = null, bool includeDeleted = false) { if (timelineName == null) throw new ArgumentNullException(nameof(timelineName)); var timelineId = await FindTimelineId(timelineName); - var postEntities = await _database.TimelinePosts.OrderBy(p => p.Time).Where(p => p.TimelineId == timelineId && p.Content != null).ToListAsync(); + var query = _database.TimelinePosts.OrderBy(p => p.Time).Where(p => p.TimelineId == timelineId); - var posts = new List(); - foreach (var entity in postEntities) + if (!includeDeleted) { - posts.Add(await MapTimelinePostFromEntity(entity, timelineName)); + query = query.Where(p => p.Content != null); } - return posts; - } - public async Task> GetPosts(string timelineName, DateTime modifiedSince) - { - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); + if (modifiedSince.HasValue) + { + query = query.Where(p => p.LastUpdated >= modifiedSince); + } - var timelineId = await FindTimelineId(timelineName); - var postEntities = await _database.TimelinePosts.OrderBy(p => p.Time).Where(p => p.TimelineId == timelineId && p.Content != null && p.LastUpdated >= modifiedSince).ToListAsync(); + var postEntities = await query.ToListAsync(); var posts = new List(); foreach (var entity in postEntities) -- cgit v1.2.3 From 29b3da2aa1f1b4a8514ffea6dc57dda8d8a98170 Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 10 Jul 2020 16:10:17 +0800 Subject: Add unit tests. --- Timeline.Tests/IntegratedTests/TimelineTest.cs | 2 ++ Timeline.Tests/Services/TimelineServiceTest.cs | 39 ++++++++++++++++++++++++++ Timeline/Models/Http/Timeline.cs | 3 +- Timeline/Models/Timeline.cs | 1 + 4 files changed, 43 insertions(+), 2 deletions(-) diff --git a/Timeline.Tests/IntegratedTests/TimelineTest.cs b/Timeline.Tests/IntegratedTests/TimelineTest.cs index 4f21d8d1..64619864 100644 --- a/Timeline.Tests/IntegratedTests/TimelineTest.cs +++ b/Timeline.Tests/IntegratedTests/TimelineTest.cs @@ -942,6 +942,7 @@ namespace Timeline.Tests.IntegratedTests body.Should().NotBeNull(); body.Content.Should().BeEquivalentTo(TimelineHelper.TextPostContent(mockContent)); body.Author.Should().BeEquivalentTo(UserInfos[1]); + body.Deleted.Should().BeFalse(); createRes = body; } { @@ -963,6 +964,7 @@ namespace Timeline.Tests.IntegratedTests body.Content.Should().BeEquivalentTo(TimelineHelper.TextPostContent(mockContent2)); body.Author.Should().BeEquivalentTo(UserInfos[1]); body.Time.Should().BeCloseTo(mockTime2, 1000); + body.Deleted.Should().BeFalse(); createRes2 = body; } { diff --git a/Timeline.Tests/Services/TimelineServiceTest.cs b/Timeline.Tests/Services/TimelineServiceTest.cs index 7e7242a2..123c2b7f 100644 --- a/Timeline.Tests/Services/TimelineServiceTest.cs +++ b/Timeline.Tests/Services/TimelineServiceTest.cs @@ -118,5 +118,44 @@ namespace Timeline.Tests.Services posts.Should().HaveCount(3) .And.Subject.Select(p => (p.Content as TextTimelinePostContent).Text).Should().Equal(postContentList.Skip(1)); } + + [Theory] + [InlineData("@user")] + [InlineData("tl")] + public async Task GetPosts_IncludeDeleted(string timelineName) + { + var userId = await _userService.GetUserIdByUsername("user"); + + var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); + if (!isPersonal) + await _timelineService.CreateTimeline(timelineName, userId); + + var postContentList = new string[] { "a", "b", "c", "d" }; + + foreach (var content in postContentList) + { + await _timelineService.CreateTextPost(timelineName, userId, content, null); + } + + var posts = await _timelineService.GetPosts(timelineName); + posts.Should().HaveCount(4); + posts.Select(p => p.Deleted).Should().Equal(Enumerable.Repeat(false, posts.Count)); + posts.Select(p => ((TextTimelinePostContent)p.Content).Text).Should().Equal(postContentList); + + foreach (var id in new long[] { posts[0].Id, posts[2].Id }) + { + await _timelineService.DeletePost(timelineName, id); + } + + posts = await _timelineService.GetPosts(timelineName); + posts.Should().HaveCount(2); + posts.Select(p => p.Deleted).Should().Equal(Enumerable.Repeat(false, posts.Count)); + posts.Select(p => ((TextTimelinePostContent)p.Content).Text).Should().Equal(new string[] { "b", "d" }); + + posts = await _timelineService.GetPosts(timelineName, includeDeleted: true); + posts.Should().HaveCount(4); + posts.Select(p => p.Deleted).Should().Equal(new bool[] { true, false, true, false }); + posts.Where(p => !p.Deleted).Select(p => ((TextTimelinePostContent)p.Content).Text).Should().Equal(new string[] { "b", "d" }); + } } } diff --git a/Timeline/Models/Http/Timeline.cs b/Timeline/Models/Http/Timeline.cs index aad9aa7b..5404d561 100644 --- a/Timeline/Models/Http/Timeline.cs +++ b/Timeline/Models/Http/Timeline.cs @@ -128,8 +128,7 @@ namespace Timeline.Models.Http public TimelineInfoAutoMapperProfile() { CreateMap().ForMember(u => u._links, opt => opt.MapFrom()); - CreateMap().ForMember(p => p.Content, opt => opt.MapFrom()) - .ForMember(p => p.Deleted, opt => opt.MapFrom((source, dest) => { return source.Content == null; })); + CreateMap().ForMember(p => p.Content, opt => opt.MapFrom()); CreateMap(); } } diff --git a/Timeline/Models/Timeline.cs b/Timeline/Models/Timeline.cs index b1772fa7..7afb1984 100644 --- a/Timeline/Models/Timeline.cs +++ b/Timeline/Models/Timeline.cs @@ -60,6 +60,7 @@ namespace Timeline.Models public long Id { get; set; } public ITimelinePostContent? Content { get; set; } + public bool Deleted => Content == null; public DateTime Time { get; set; } public User Author { get; set; } public DateTime LastUpdated { get; set; } -- cgit v1.2.3 From a362066a9a8e64b8c8d58ec5952cb28caa217b35 Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 10 Jul 2020 20:18:08 +0800 Subject: Add http api and integrated tests. --- Timeline.Tests/IntegratedTests/TimelineTest.cs | 34 ++++++++++++++++++++++++++ Timeline/Controllers/TimelineController.cs | 8 ++---- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/Timeline.Tests/IntegratedTests/TimelineTest.cs b/Timeline.Tests/IntegratedTests/TimelineTest.cs index 64619864..4ba8800e 100644 --- a/Timeline.Tests/IntegratedTests/TimelineTest.cs +++ b/Timeline.Tests/IntegratedTests/TimelineTest.cs @@ -1255,5 +1255,39 @@ namespace Timeline.Tests.IntegratedTests .And.Subject.Select(p => p.Content.Text).Should().Equal(postContentList.Skip(1)); } } + + [Theory] + [MemberData(nameof(TimelineUrlGeneratorData))] + public async Task PostList_IncludeDeleted(TimelineUrlGenerator urlGenerator) + { + using var client = await CreateClientAsUser(); + + var postContentList = new List { "a", "b", "c", "d" }; + var posts = new List(); + + foreach (var content in postContentList) + { + var res = await client.PostAsJsonAsync(urlGenerator(1, "posts"), + new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Text = content, Type = TimelinePostContentTypes.Text } }); + posts.Add(res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which); + } + + foreach (var id in new long[] { posts[0].Id, posts[2].Id }) + { + var res = await client.DeleteAsync(urlGenerator(1, $"posts/{id}")); + res.Should().BeDelete(true); + } + + { + var res = await client.GetAsync(urlGenerator(1, "posts", new Dictionary { ["includeDeleted"] = "true" })); + posts = res.Should().HaveStatusCode(200) + .And.HaveJsonBody>() + .Which; + posts.Should().HaveCount(4); + posts.Select(p => p.Deleted).Should().Equal(true, false, true, false); + posts.Select(p => p.Content == null).Should().Equal(true, false, true, false); + } + } } } diff --git a/Timeline/Controllers/TimelineController.cs b/Timeline/Controllers/TimelineController.cs index b8cc608b..2330698f 100644 --- a/Timeline/Controllers/TimelineController.cs +++ b/Timeline/Controllers/TimelineController.cs @@ -102,18 +102,14 @@ namespace Timeline.Controllers } [HttpGet("timelines/{name}/posts")] - public async Task>> PostListGet([FromRoute][GeneralTimelineName] string name, [FromQuery] DateTime? modifiedSince) + public async Task>> PostListGet([FromRoute][GeneralTimelineName] string name, [FromQuery] DateTime? modifiedSince, [FromQuery] bool? includeDeleted) { if (!this.IsAdministrator() && !await _service.HasReadPermission(name, this.GetOptionalUserId())) { return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } - List posts; - if (modifiedSince == null) - posts = await _service.GetPosts(name); - else - posts = await _service.GetPosts(name, modifiedSince.Value); + List posts = await _service.GetPosts(name, modifiedSince, includeDeleted ?? false); var result = _mapper.Map>(posts); return result; -- cgit v1.2.3 From 3735e7fecd2c9eaf525cedb55912f918aad20779 Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 10 Jul 2020 20:29:02 +0800 Subject: Add another integrated test. --- Timeline.Tests/IntegratedTests/TimelineTest.cs | 57 ++++++++++++++++++++++---- 1 file changed, 50 insertions(+), 7 deletions(-) diff --git a/Timeline.Tests/IntegratedTests/TimelineTest.cs b/Timeline.Tests/IntegratedTests/TimelineTest.cs index 4ba8800e..ba335bd6 100644 --- a/Timeline.Tests/IntegratedTests/TimelineTest.cs +++ b/Timeline.Tests/IntegratedTests/TimelineTest.cs @@ -1231,28 +1231,31 @@ namespace Timeline.Tests.IntegratedTests { using var client = await CreateClientAsUser(); - DateTime testPoint = new DateTime(); var postContentList = new List { "a", "b", "c", "d" }; + var posts = new List(); - foreach (var (content, index) in postContentList.Select((v, i) => (v, i))) + foreach (var content in postContentList) { var res = await client.PostAsJsonAsync(generator(1, "posts"), new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Text = content, Type = TimelinePostContentTypes.Text } }); var post = res.Should().HaveStatusCode(200) .And.HaveJsonBody().Which; - if (index == 1) - testPoint = post.LastUpdated; + posts.Add(post); await Task.Delay(1000); } { + var res = await client.DeleteAsync(generator(1, $"posts/{posts[2].Id}")); + res.Should().BeDelete(true); + } + { var res = await client.GetAsync(generator(1, "posts", - new Dictionary { { "modifiedSince", testPoint.ToString("s", CultureInfo.InvariantCulture) } })); + new Dictionary { { "modifiedSince", posts[1].LastUpdated.ToString("s", CultureInfo.InvariantCulture) } })); res.Should().HaveStatusCode(200) .And.HaveJsonBody>() - .Which.Should().HaveCount(3) - .And.Subject.Select(p => p.Content.Text).Should().Equal(postContentList.Skip(1)); + .Which.Should().HaveCount(2) + .And.Subject.Select(p => p.Content.Text).Should().Equal("b", "d"); } } @@ -1289,5 +1292,45 @@ namespace Timeline.Tests.IntegratedTests posts.Select(p => p.Content == null).Should().Equal(true, false, true, false); } } + + [Theory] + [MemberData(nameof(TimelineUrlGeneratorData))] + public async Task Post_ModifiedSince_And_IncludeDeleted(TimelineUrlGenerator urlGenerator) + { + using var client = await CreateClientAsUser(); + + var postContentList = new List { "a", "b", "c", "d" }; + var posts = new List(); + + foreach (var (content, index) in postContentList.Select((v, i) => (v, i))) + { + var res = await client.PostAsJsonAsync(urlGenerator(1, "posts"), + new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Text = content, Type = TimelinePostContentTypes.Text } }); + var post = res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + posts.Add(post); + await Task.Delay(1000); + } + + { + var res = await client.DeleteAsync(urlGenerator(1, $"posts/{posts[2].Id}")); + res.Should().BeDelete(true); + } + + { + + var res = await client.GetAsync(urlGenerator(1, "posts", + new Dictionary { + { "modifiedSince", posts[1].LastUpdated.ToString("s", CultureInfo.InvariantCulture) }, + { "includeDeleted", "true" } + })); + posts = res.Should().HaveStatusCode(200) + .And.HaveJsonBody>() + .Which; + posts.Should().HaveCount(3); + posts.Select(p => p.Deleted).Should().Equal(false, true, false); + posts.Select(p => p.Content == null).Should().Equal(false, true, false); + } + } } } -- cgit v1.2.3