From 4214c5dd5724bbe0bb8225534568dd4b0e904068 Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 18 Jun 2020 19:41:51 +0800 Subject: feat(back): Timeline service add post modified since. --- Timeline/Services/TimelineService.cs | 77 ++++++++++++++++++++++++++---------- 1 file changed, 56 insertions(+), 21 deletions(-) (limited to 'Timeline/Services/TimelineService.cs') diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs index 6c1e91c6..e0b1b51d 100644 --- a/Timeline/Services/TimelineService.cs +++ b/Timeline/Services/TimelineService.cs @@ -105,6 +105,20 @@ namespace Timeline.Services /// 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. + /// 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, DateTime modifiedSince); + /// /// Get the etag of data of a post. /// @@ -383,6 +397,34 @@ 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 = 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, + author: author, + content: content, + time: entity.Time, + lastUpdated: entity.LastUpdated, + timelineName: timelineName + ); + } + private TimelineEntity CreateNewTimelineEntity(string? name, long ownerId) { var currentTime = _clock.GetCurrentTime(); @@ -488,30 +530,23 @@ namespace Timeline.Services var posts = new List(); foreach (var entity in postEntities) { - if (entity.Content == null) - { - throw new Exception(); - } - - var author = await _userService.GetUserById(entity.AuthorId); + posts.Add(await MapTimelinePostFromEntity(entity, timelineName)); + } + return posts; + } - var type = entity.ContentType; + public async Task> GetPosts(string timelineName, DateTime modifiedSince) + { + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); - ITimelinePostContent content = type switch - { - TimelinePostContentTypes.Text => new TextTimelinePostContent(entity.Content), - TimelinePostContentTypes.Image => new ImageTimelinePostContent(entity.Content), - _ => throw new DatabaseCorruptedException(string.Format(CultureInfo.InvariantCulture, ExceptionDatabaseUnknownContentType, type)) - }; + 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(); - posts.Add(new TimelinePost( - id: entity.LocalId, - author: author, - content: content, - time: entity.Time, - lastUpdated: entity.LastUpdated, - timelineName: timelineName - )); + var posts = new List(); + foreach (var entity in postEntities) + { + posts.Add(await MapTimelinePostFromEntity(entity, timelineName)); } return posts; } -- cgit v1.2.3 From 35f4e03157fdb6643dc42db0cd764804e00944aa Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 18 Jun 2020 23:36:56 +0800 Subject: Add integrated tests. And fix a behavior. --- Timeline.Tests/IntegratedTests/TimelineTest.cs | 98 +++++++++++++++++++++----- Timeline.Tests/Services/TimelineServiceTest.cs | 4 +- Timeline/Services/TimelineService.cs | 2 +- 3 files changed, 82 insertions(+), 22 deletions(-) (limited to 'Timeline/Services/TimelineService.cs') diff --git a/Timeline.Tests/IntegratedTests/TimelineTest.cs b/Timeline.Tests/IntegratedTests/TimelineTest.cs index 6736fecd..4f21d8d1 100644 --- a/Timeline.Tests/IntegratedTests/TimelineTest.cs +++ b/Timeline.Tests/IntegratedTests/TimelineTest.cs @@ -5,9 +5,11 @@ using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Png; using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Net; using System.Net.Http; +using System.Text; using System.Threading.Tasks; using Timeline.Entities; using Timeline.Models; @@ -68,17 +70,43 @@ namespace Timeline.Tests.IntegratedTests } } - private static string GeneratePersonalTimelineUrl(int id, string subpath = null) + private static string CalculateUrlTail(string subpath, ICollection> query) { - return $"timelines/@{(id == 0 ? "admin" : ("user" + id))}/{(subpath ?? "")}"; + StringBuilder result = new StringBuilder(); + if (subpath != null) + { + if (!subpath.StartsWith("/", StringComparison.OrdinalIgnoreCase)) + result.Append("/"); + result.Append(subpath); + } + + if (query != null && query.Count != 0) + { + result.Append("?"); + foreach (var (key, value, index) in query.Select((pair, index) => (pair.Key, pair.Value, index))) + { + result.Append(WebUtility.UrlEncode(key)); + result.Append("="); + result.Append(WebUtility.UrlEncode(value)); + if (index != query.Count - 1) + result.Append("&"); + } + } + + return result.ToString(); } - private static string GenerateOrdinaryTimelineUrl(int id, string subpath = null) + private static string GeneratePersonalTimelineUrl(int id, string subpath = null, ICollection> query = null) { - return $"timelines/t{id}/{(subpath ?? "")}"; + return $"timelines/@{(id == 0 ? "admin" : ("user" + id))}{CalculateUrlTail(subpath, query)}"; } - public delegate string TimelineUrlGenerator(int userId, string subpath = null); + private static string GenerateOrdinaryTimelineUrl(int id, string subpath = null, ICollection> query = null) + { + return $"timelines/t{id}{CalculateUrlTail(subpath, query)}"; + } + + public delegate string TimelineUrlGenerator(int userId, string subpath = null, ICollection> query = null); public static IEnumerable TimelineUrlGeneratorData() { @@ -88,12 +116,12 @@ namespace Timeline.Tests.IntegratedTests private static string GeneratePersonalTimelineUrlByName(string name, string subpath = null) { - return $"timelines/@{name}/{(subpath ?? "")}"; + return $"timelines/@{name}{(subpath == null ? "" : "/" + subpath)}"; } private static string GenerateOrdinaryTimelineUrlByName(string name, string subpath = null) { - return $"timelines/{name}/{(subpath ?? "")}"; + return $"timelines/{name}{(subpath == null ? "" : "/" + subpath)}"; } public static IEnumerable TimelineUrlByNameGeneratorData() @@ -632,19 +660,20 @@ namespace Timeline.Tests.IntegratedTests await AssertEmptyMembers(); } - [Theory] - [InlineData(nameof(GenerateOrdinaryTimelineUrl), -1, 200, 401, 401, 401, 401)] - [InlineData(nameof(GenerateOrdinaryTimelineUrl), 1, 200, 200, 403, 200, 403)] - [InlineData(nameof(GenerateOrdinaryTimelineUrl), 0, 200, 200, 200, 200, 200)] - [InlineData(nameof(GeneratePersonalTimelineUrl), -1, 200, 401, 401, 401, 401)] - [InlineData(nameof(GeneratePersonalTimelineUrl), 1, 200, 200, 403, 200, 403)] - [InlineData(nameof(GeneratePersonalTimelineUrl), 0, 200, 200, 200, 200, 200)] - - public async Task Permission_Timeline(string generatorName, int userNumber, int get, int opPatchUser, int opPatchAdmin, int opMemberUser, int opMemberAdmin) + public static IEnumerable Permission_Timeline_Data() { - var method = GetType().GetMethod(generatorName, System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic); - Func generator = (int id, string subpath) => (string)method.Invoke(null, new object[] { id, subpath }); + yield return new object[] { new TimelineUrlGenerator(GenerateOrdinaryTimelineUrl), -1, 200, 401, 401, 401, 401 }; + yield return new object[] { new TimelineUrlGenerator(GenerateOrdinaryTimelineUrl), 1, 200, 200, 403, 200, 403 }; + yield return new object[] { new TimelineUrlGenerator(GenerateOrdinaryTimelineUrl), 0, 200, 200, 200, 200, 200 }; + yield return new object[] { new TimelineUrlGenerator(GeneratePersonalTimelineUrl), -1, 200, 401, 401, 401, 401 }; + yield return new object[] { new TimelineUrlGenerator(GeneratePersonalTimelineUrl), 1, 200, 200, 403, 200, 403 }; + yield return new object[] { new TimelineUrlGenerator(GeneratePersonalTimelineUrl), 0, 200, 200, 200, 200, 200 }; + } + [Theory] + [MemberData(nameof(Permission_Timeline_Data))] + public async Task Permission_Timeline(TimelineUrlGenerator generator, int userNumber, int get, int opPatchUser, int opPatchAdmin, int opMemberUser, int opMemberAdmin) + { using var client = await CreateClientAs(userNumber); { var res = await client.GetAsync("timelines/t1"); @@ -1150,7 +1179,7 @@ namespace Timeline.Tests.IntegratedTests [Theory] [MemberData(nameof(TimelineUrlGeneratorData))] - public async Task LastModified(TimelineUrlGenerator generator) + public async Task Timeline_LastModified(TimelineUrlGenerator generator) { using var client = await CreateClientAsUser(); @@ -1193,5 +1222,36 @@ namespace Timeline.Tests.IntegratedTests .Which.LastModified.Should().BeAfter(lastModified); } } + + [Theory] + [MemberData(nameof(TimelineUrlGeneratorData))] + public async Task Post_ModifiedSince(TimelineUrlGenerator generator) + { + using var client = await CreateClientAsUser(); + + DateTime testPoint = new DateTime(); + var postContentList = new List { "a", "b", "c", "d" }; + + foreach (var (content, index) in postContentList.Select((v, i) => (v, i))) + { + 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; + await Task.Delay(1000); + } + + { + + var res = await client.GetAsync(generator(1, "posts", + new Dictionary { { "modifiedSince", testPoint.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)); + } + } } } diff --git a/Timeline.Tests/Services/TimelineServiceTest.cs b/Timeline.Tests/Services/TimelineServiceTest.cs index cde827db..7e7242a2 100644 --- a/Timeline.Tests/Services/TimelineServiceTest.cs +++ b/Timeline.Tests/Services/TimelineServiceTest.cs @@ -115,8 +115,8 @@ namespace Timeline.Tests.Services } var posts = await _timelineService.GetPosts(timelineName, testPoint); - posts.Should().HaveCount(2) - .And.Subject.Select(p => (p.Content as TextTimelinePostContent).Text).Should().Equal(postContentList.Skip(2)); + posts.Should().HaveCount(3) + .And.Subject.Select(p => (p.Content as TextTimelinePostContent).Text).Should().Equal(postContentList.Skip(1)); } } } diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs index e0b1b51d..73f6c8ef 100644 --- a/Timeline/Services/TimelineService.cs +++ b/Timeline/Services/TimelineService.cs @@ -541,7 +541,7 @@ namespace Timeline.Services 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 && p.LastUpdated > modifiedSince).ToListAsync(); + var postEntities = await _database.TimelinePosts.OrderBy(p => p.Time).Where(p => p.TimelineId == timelineId && p.Content != null && p.LastUpdated >= modifiedSince).ToListAsync(); var posts = new List(); foreach (var entity in postEntities) -- cgit v1.2.3