diff options
-rw-r--r-- | Timeline.Tests/IntegratedTests/TimelineTest.cs | 93 | ||||
-rw-r--r-- | Timeline.Tests/Services/TimelineServiceTest.cs | 39 | ||||
-rw-r--r-- | Timeline/Controllers/TimelineController.cs | 8 | ||||
-rw-r--r-- | Timeline/Models/Http/Timeline.cs | 12 | ||||
-rw-r--r-- | Timeline/Models/Timeline.cs | 5 | ||||
-rw-r--r-- | Timeline/Services/TimelineService.cs | 60 |
6 files changed, 162 insertions, 55 deletions
diff --git a/Timeline.Tests/IntegratedTests/TimelineTest.cs b/Timeline.Tests/IntegratedTests/TimelineTest.cs index 4f21d8d1..ba335bd6 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;
}
{
@@ -1229,28 +1231,105 @@ namespace Timeline.Tests.IntegratedTests {
using var client = await CreateClientAsUser();
- DateTime testPoint = new DateTime();
var postContentList = new List<string> { "a", "b", "c", "d" };
+ var posts = new List<TimelinePostInfo>();
- 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<TimelinePostInfo>().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<string, string> { { "modifiedSince", testPoint.ToString("s", CultureInfo.InvariantCulture) } }));
+ new Dictionary<string, string> { { "modifiedSince", posts[1].LastUpdated.ToString("s", CultureInfo.InvariantCulture) } }));
res.Should().HaveStatusCode(200)
.And.HaveJsonBody<List<TimelinePostInfo>>()
- .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");
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(TimelineUrlGeneratorData))]
+ public async Task PostList_IncludeDeleted(TimelineUrlGenerator urlGenerator)
+ {
+ using var client = await CreateClientAsUser();
+
+ var postContentList = new List<string> { "a", "b", "c", "d" };
+ var posts = new List<TimelinePostInfo>();
+
+ 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<TimelinePostInfo>().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<string, string> { ["includeDeleted"] = "true" }));
+ posts = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<List<TimelinePostInfo>>()
+ .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);
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(TimelineUrlGeneratorData))]
+ public async Task Post_ModifiedSince_And_IncludeDeleted(TimelineUrlGenerator urlGenerator)
+ {
+ using var client = await CreateClientAsUser();
+
+ var postContentList = new List<string> { "a", "b", "c", "d" };
+ var posts = new List<TimelinePostInfo>();
+
+ 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<TimelinePostInfo>().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<string, string> {
+ { "modifiedSince", posts[1].LastUpdated.ToString("s", CultureInfo.InvariantCulture) },
+ { "includeDeleted", "true" }
+ }));
+ posts = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<List<TimelinePostInfo>>()
+ .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);
}
}
}
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/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<ActionResult<List<TimelinePostInfo>>> PostListGet([FromRoute][GeneralTimelineName] string name, [FromQuery] DateTime? modifiedSince)
+ public async Task<ActionResult<List<TimelinePostInfo>>> 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<TimelinePost> posts;
- if (modifiedSince == null)
- posts = await _service.GetPosts(name);
- else
- posts = await _service.GetPosts(name, modifiedSince.Value);
+ List<TimelinePost> posts = await _service.GetPosts(name, modifiedSince, includeDeleted ?? false);
var result = _mapper.Map<List<TimelinePostInfo>>(posts);
return result;
diff --git a/Timeline/Models/Http/Timeline.cs b/Timeline/Models/Http/Timeline.cs index 80e6e69d..5404d561 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<TimelinePost, TimelinePostInfo, TimelinePostContentInfo>
+ public class TimelinePostContentResolver : IValueResolver<TimelinePost, TimelinePostInfo, TimelinePostContentInfo?>
{
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
diff --git a/Timeline/Models/Timeline.cs b/Timeline/Models/Timeline.cs index 3701ed35..7afb1984 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,8 @@ namespace Timeline.Models }
public long Id { get; set; }
- public ITimelinePostContent Content { 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; }
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.
/// </summary>
/// <param name="timelineName">The name of the timeline.</param>
- /// <returns>A list of all posts.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
- /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
- /// <exception cref="TimelineNotExistException">
- /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
- /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
- /// </exception>
- Task<List<TimelinePost>> GetPosts(string timelineName);
-
- /// <summary>
- /// Get the posts that have been modified since a given time in the timeline.
- /// </summary>
- /// <param name="timelineName">The name of the timeline.</param>
/// <param name="modifiedSince">The time that posts have been modified since.</param>
+ /// <param name="includeDeleted">Whether include deleted posts.</param>
/// <returns>A list of all posts.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
/// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
@@ -117,7 +105,7 @@ namespace Timeline.Services /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
/// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
/// </exception>
- Task<List<TimelinePost>> GetPosts(string timelineName, DateTime modifiedSince);
+ Task<List<TimelinePost>> GetPosts(string timelineName, DateTime? modifiedSince = null, bool includeDeleted = false);
/// <summary>
/// Get the etag of data of a post.
@@ -399,21 +387,23 @@ namespace Timeline.Services private async Task<TimelinePost> 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<List<TimelinePost>> GetPosts(string timelineName)
+ public async Task<List<TimelinePost>> 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<TimelinePost>();
- 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<List<TimelinePost>> 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<TimelinePost>();
foreach (var entity in postEntities)
|