From 5eaacedda31da86116f25158bd07e5ad8954e7b2 Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 10 Mar 2020 19:37:58 +0800 Subject: ... --- .../IntegratedTests/IntegratedTestBase.cs | 29 ++-- .../IntegratedTests/PersonalTimelineTest.cs | 153 +++++++++++---------- Timeline.Tests/IntegratedTests/TimelineTest.cs | 72 ++++++---- Timeline.Tests/IntegratedTests/TokenTest.cs | 1 + Timeline/Controllers/TimelineController.cs | 14 ++ Timeline/Controllers/UserController.cs | 1 + Timeline/Entities/TimelineEntity.cs | 2 +- Timeline/Models/Http/Timeline.cs | 24 ++-- Timeline/Models/Http/UserInfo.cs | 2 +- Timeline/Models/Timeline.cs | 17 ++- Timeline/Services/TimelineService.cs | 79 ++++++----- Timeline/Startup.cs | 3 +- 12 files changed, 242 insertions(+), 155 deletions(-) diff --git a/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs b/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs index dfde2ea5..66904629 100644 --- a/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs +++ b/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs @@ -1,10 +1,13 @@ -using AutoMapper; -using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; using System.Net.Http; +using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading.Tasks; +using Timeline.Models; +using Timeline.Models.Converters; using Timeline.Models.Http; using Timeline.Services; using Timeline.Tests.Helpers; @@ -14,12 +17,6 @@ namespace Timeline.Tests.IntegratedTests { public abstract class IntegratedTestBase : IClassFixture>, IDisposable { - static IntegratedTestBase() - { - FluentAssertions.AssertionOptions.AssertEquivalencyUsing(options => - options.Excluding(m => m.RuntimeType == typeof(UserInfoLinks))); - } - protected TestApplication TestApp { get; } protected WebApplicationFactory Factory => TestApp.Factory; @@ -63,12 +60,22 @@ namespace Timeline.Tests.IntegratedTests var userInfoList = new List(); var userService = scope.ServiceProvider.GetRequiredService(); - var mapper = scope.ServiceProvider.GetRequiredService(); - foreach (var user in users) { userService.CreateUser(user).Wait(); - userInfoList.Add(mapper.Map(user)); + } + + using var client = CreateDefaultClient().Result; + var options = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + options.Converters.Add(new JsonStringEnumConverter()); + options.Converters.Add(new JsonDateTimeConverter()); + foreach (var user in users) + { + var s = client.GetStringAsync($"/users/{user.Username}").Result; + userInfoList.Add(JsonSerializer.Deserialize(s, options)); } UserInfos = userInfoList; diff --git a/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs b/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs index 7d0a68e8..aa37e898 100644 --- a/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs +++ b/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Threading.Tasks; +using Timeline.Models; using Timeline.Models.Http; using Timeline.Tests.Helpers; using Xunit; @@ -23,7 +24,7 @@ namespace Timeline.Tests.IntegratedTests public async Task TimelineGet_Should_Work() { using var client = await CreateDefaultClient(); - var res = await client.GetAsync("users/user1/timeline"); + var res = await client.GetAsync("timelines/@user1"); var body = res.Should().HaveStatusCode(200) .And.HaveJsonBody().Which; body.Owner.Should().BeEquivalentTo(UserInfos[1]); @@ -37,31 +38,31 @@ namespace Timeline.Tests.IntegratedTests { using var client = await CreateClientAsAdministrator(); { - var res = await client.GetAsync("users/user!!!/timeline"); + var res = await client.GetAsync("timelines/@user!!!"); res.Should().BeInvalidModel(); } { - var res = await client.PatchAsJsonAsync("users/user!!!/timeline", new TimelinePatchRequest { }); + var res = await client.PatchAsJsonAsync("timelines/@user!!!", new TimelinePatchRequest { }); res.Should().BeInvalidModel(); } { - var res = await client.PutAsync("users/user!!!/timeline/members/user1", null); + var res = await client.PutAsync("timelines/@user!!!/members/user1", null); res.Should().BeInvalidModel(); } { - var res = await client.DeleteAsync("users/user!!!/timeline/members/user1"); + var res = await client.DeleteAsync("timelines/@user!!!/members/user1"); res.Should().BeInvalidModel(); } { - var res = await client.GetAsync("users/user!!!/timeline/posts"); + var res = await client.GetAsync("timelines/@user!!!/posts"); res.Should().BeInvalidModel(); } { - var res = await client.PostAsJsonAsync("users/user!!!/timeline/posts", new TimelinePostCreateRequest { Content = "aaa" }); + var res = await client.PostAsJsonAsync("timelines/@user!!!/posts", TimelineHelper.TextPostCreateRequest("aaa")); res.Should().BeInvalidModel(); } { - var res = await client.DeleteAsync("users/user!!!/timeline/posts/123"); + var res = await client.DeleteAsync("timelines/@user!!!/posts/123"); res.Should().BeInvalidModel(); } } @@ -71,31 +72,31 @@ namespace Timeline.Tests.IntegratedTests { using var client = await CreateClientAsAdministrator(); { - var res = await client.GetAsync("users/usernotexist/timeline"); + var res = await client.GetAsync("timelines/@usernotexist"); res.Should().HaveStatusCode(404).And.HaveCommonBody(ErrorCodes.UserCommon.NotExist); } { - var res = await client.PatchAsJsonAsync("users/usernotexist/timeline", new TimelinePatchRequest { }); + var res = await client.PatchAsJsonAsync("timelines/@usernotexist", new TimelinePatchRequest { }); res.Should().HaveStatusCode(404).And.HaveCommonBody(ErrorCodes.UserCommon.NotExist); } { - var res = await client.PutAsync("users/usernotexist/timeline/members/user1", null); + var res = await client.PutAsync("timelines/@usernotexist/members/user1", null); res.Should().HaveStatusCode(404).And.HaveCommonBody(ErrorCodes.UserCommon.NotExist); } { - var res = await client.DeleteAsync("users/usernotexist/timeline/members/user1"); + var res = await client.DeleteAsync("timelines/@usernotexist/members/user1"); res.Should().HaveStatusCode(404).And.HaveCommonBody(ErrorCodes.UserCommon.NotExist); } { - var res = await client.GetAsync("users/usernotexist/timeline/posts"); + var res = await client.GetAsync("timelines/@usernotexist/posts"); res.Should().HaveStatusCode(404).And.HaveCommonBody(ErrorCodes.UserCommon.NotExist); } { - var res = await client.PostAsJsonAsync("users/usernotexist/timeline/posts", new TimelinePostCreateRequest { Content = "aaa" }); + var res = await client.PostAsJsonAsync("timelines/@usernotexist/posts", TimelineHelper.TextPostCreateRequest("aaa")); res.Should().HaveStatusCode(404).And.HaveCommonBody(ErrorCodes.UserCommon.NotExist); } { - var res = await client.DeleteAsync("users/usernotexist/timeline/posts/123"); + var res = await client.DeleteAsync("timelines/@usernotexist/posts/123"); res.Should().HaveStatusCode(404).And.HaveCommonBody(ErrorCodes.UserCommon.NotExist); } } @@ -107,7 +108,7 @@ namespace Timeline.Tests.IntegratedTests async Task AssertDescription(string description) { - var res = await client.GetAsync("users/user1/timeline"); + var res = await client.GetAsync("timelines/@user1"); var body = res.Should().HaveStatusCode(200) .And.HaveJsonBody() .Which.Description.Should().Be(description); @@ -117,21 +118,21 @@ namespace Timeline.Tests.IntegratedTests await AssertDescription(""); { - var res = await client.PatchAsJsonAsync("users/user1/timeline", + var res = await client.PatchAsJsonAsync("timelines/@user1", new TimelinePatchRequest { Description = mockDescription }); res.Should().HaveStatusCode(200) .And.HaveJsonBody().Which.Description.Should().Be(mockDescription); await AssertDescription(mockDescription); } { - var res = await client.PatchAsJsonAsync("users/user1/timeline", + var res = await client.PatchAsJsonAsync("timelines/@user1", new TimelinePatchRequest { Description = null }); res.Should().HaveStatusCode(200) .And.HaveJsonBody().Which.Description.Should().Be(mockDescription); await AssertDescription(mockDescription); } { - var res = await client.PatchAsJsonAsync("users/user1/timeline", + var res = await client.PatchAsJsonAsync("timelines/@user1", new TimelinePatchRequest { Description = "" }); res.Should().HaveStatusCode(200) .And.HaveJsonBody().Which.Description.Should().Be(""); @@ -142,7 +143,7 @@ namespace Timeline.Tests.IntegratedTests [Fact] public async Task Member_Should_Work() { - const string getUrl = "users/user1/timeline"; + const string getUrl = "timelines/@user1"; using var client = await CreateClientAsUser(); async Task AssertMembers(IList members) @@ -163,23 +164,23 @@ namespace Timeline.Tests.IntegratedTests await AssertEmptyMembers(); { - var res = await client.PutAsync("/users/user1/timeline/members/usernotexist", null); + var res = await client.PutAsync("/timelines/@user1/members/usernotexist", null); res.Should().HaveStatusCode(400) .And.HaveCommonBody(ErrorCodes.TimelineCommon.MemberPut_NotExist); } await AssertEmptyMembers(); { - var res = await client.PutAsync("/users/user1/timeline/members/user2", null); + var res = await client.PutAsync("/timelines/@user1/members/user2", null); res.Should().HaveStatusCode(200); } await AssertMembers(new List { UserInfos[2] }); { - var res = await client.DeleteAsync("/users/user1/timeline/members/user2"); + var res = await client.DeleteAsync("/timelines/@user1/members/user2"); res.Should().BeDelete(true); } await AssertEmptyMembers(); { - var res = await client.DeleteAsync("/users/user1/timeline/members/users2"); + var res = await client.DeleteAsync("/timelines/@user1/members/users2"); res.Should().BeDelete(false); } await AssertEmptyMembers(); @@ -193,37 +194,37 @@ namespace Timeline.Tests.IntegratedTests { using var client = await CreateClientAs(userNumber); { - var res = await client.GetAsync("users/user1/timeline"); + var res = await client.GetAsync("timelines/@user1"); res.Should().HaveStatusCode(get); } { - var res = await client.PatchAsJsonAsync("users/user1/timeline", new TimelinePatchRequest { Description = "hahaha" }); + var res = await client.PatchAsJsonAsync("timelines/@user1", new TimelinePatchRequest { Description = "hahaha" }); res.Should().HaveStatusCode(opPatchUser); } { - var res = await client.PatchAsJsonAsync("users/admin/timeline", new TimelinePatchRequest { Description = "hahaha" }); + var res = await client.PatchAsJsonAsync("timelines/@admin", new TimelinePatchRequest { Description = "hahaha" }); res.Should().HaveStatusCode(opPatchAdmin); } { - var res = await client.PutAsync("users/user1/timeline/members/user2", null); + var res = await client.PutAsync("timelines/@user1/members/user2", null); res.Should().HaveStatusCode(opMemberUser); } { - var res = await client.DeleteAsync("users/user1/timeline/members/user2"); + var res = await client.DeleteAsync("timelines/@user1/members/user2"); res.Should().HaveStatusCode(opMemberUser); } { - var res = await client.PutAsync("users/admin/timeline/members/user2", null); + var res = await client.PutAsync("timelines/@admin/members/user2", null); res.Should().HaveStatusCode(opMemberAdmin); } { - var res = await client.DeleteAsync("users/admin/timeline/members/user2"); + var res = await client.DeleteAsync("timelines/@admin/members/user2"); res.Should().HaveStatusCode(opMemberAdmin); } } @@ -231,13 +232,13 @@ namespace Timeline.Tests.IntegratedTests [Fact] public async Task Visibility_Test() { - const string userUrl = "users/user1/timeline/posts"; - const string adminUrl = "users/admin/timeline/posts"; + const string userUrl = "timelines/@user1/posts"; + const string adminUrl = "timelines/@admin/posts"; { using var client = await CreateClientAsUser(); using var content = new StringContent(@"{""visibility"":""abcdefg""}", System.Text.Encoding.UTF8, System.Net.Mime.MediaTypeNames.Application.Json); - var res = await client.PatchAsync("users/user1/timeline", content); + var res = await client.PatchAsync("timelines/@user1", content); res.Should().BeInvalidModel(); } { // default visibility is registered @@ -257,7 +258,7 @@ namespace Timeline.Tests.IntegratedTests { // change visibility to public { using var client = await CreateClientAsUser(); - var res = await client.PatchAsJsonAsync("users/user1/timeline", + var res = await client.PatchAsJsonAsync("timelines/@user1", new TimelinePatchRequest { Visibility = TimelineVisibility.Public }); res.Should().HaveStatusCode(200); } @@ -272,12 +273,12 @@ namespace Timeline.Tests.IntegratedTests { using var client = await CreateClientAsAdministrator(); { - var res = await client.PatchAsJsonAsync("users/user1/timeline", + var res = await client.PatchAsJsonAsync("timelines/@user1", new TimelinePatchRequest { Visibility = TimelineVisibility.Private }); res.Should().HaveStatusCode(200); } { - var res = await client.PatchAsJsonAsync("users/admin/timeline", + var res = await client.PatchAsJsonAsync("timelines/@admin", new TimelinePatchRequest { Visibility = TimelineVisibility.Private }); res.Should().HaveStatusCode(200); } @@ -299,7 +300,7 @@ namespace Timeline.Tests.IntegratedTests } { // add member using var client = await CreateClientAsAdministrator(); - var res = await client.PutAsync("/users/admin/timeline/members/user1", null); + var res = await client.PutAsync("/timelines/@admin/members/user1", null); res.Should().HaveStatusCode(200); } { // now user can read admin's @@ -316,15 +317,15 @@ namespace Timeline.Tests.IntegratedTests { using (var client = await CreateClientAsUser()) { - var res = await client.PutAsync("users/user1/timeline/members/user2", null); + var res = await client.PutAsync("timelines/@user1/members/user2", null); res.Should().HaveStatusCode(200); } using (var client = await CreateDefaultClient()) { { // no auth should get 401 - var res = await client.PostAsJsonAsync("users/user1/timeline/posts", - new TimelinePostCreateRequest { Content = "aaa" }); + var res = await client.PostAsJsonAsync("timelines/@user1/posts", + TimelineHelper.TextPostCreateRequest("aaa")); res.Should().HaveStatusCode(401); } } @@ -332,13 +333,13 @@ namespace Timeline.Tests.IntegratedTests using (var client = await CreateClientAsUser()) { { // post self's - var res = await client.PostAsJsonAsync("users/user1/timeline/posts", - new TimelinePostCreateRequest { Content = "aaa" }); + var res = await client.PostAsJsonAsync("timelines/@user1/posts", + TimelineHelper.TextPostCreateRequest("aaa")); res.Should().HaveStatusCode(200); } { // post other not as a member should get 403 - var res = await client.PostAsJsonAsync("users/admin/timeline/posts", - new TimelinePostCreateRequest { Content = "aaa" }); + var res = await client.PostAsJsonAsync("timelines/@admin/posts", + TimelineHelper.TextPostCreateRequest("aaa")); res.Should().HaveStatusCode(403); } } @@ -346,8 +347,8 @@ namespace Timeline.Tests.IntegratedTests using (var client = await CreateClientAsAdministrator()) { { // post as admin - var res = await client.PostAsJsonAsync("users/user1/timeline/posts", - new TimelinePostCreateRequest { Content = "aaa" }); + var res = await client.PostAsJsonAsync("timelines/@user1/posts", + TimelineHelper.TextPostCreateRequest("aaa")); res.Should().HaveStatusCode(200); } } @@ -355,8 +356,8 @@ namespace Timeline.Tests.IntegratedTests using (var client = await CreateClientAs(2)) { { // post as member - var res = await client.PostAsJsonAsync("users/user1/timeline/posts", - new TimelinePostCreateRequest { Content = "aaa" }); + var res = await client.PostAsJsonAsync("timelines/@user1/posts", + TimelineHelper.TextPostCreateRequest("aaa")); res.Should().HaveStatusCode(200); } } @@ -368,8 +369,8 @@ namespace Timeline.Tests.IntegratedTests async Task CreatePost(int userNumber) { using var client = await CreateClientAs(userNumber); - var res = await client.PostAsJsonAsync($"users/user1/timeline/posts", - new TimelinePostCreateRequest { Content = "aaa" }); + var res = await client.PostAsJsonAsync($"timelines/@user1/posts", + TimelineHelper.TextPostCreateRequest("aaa")); return res.Should().HaveStatusCode(200) .And.HaveJsonBody() .Which.Id; @@ -378,53 +379,53 @@ namespace Timeline.Tests.IntegratedTests using (var client = await CreateClientAsUser()) { { - var res = await client.PutAsync("users/user1/timeline/members/user2", null); + var res = await client.PutAsync("timelines/@user1/members/user2", null); res.Should().HaveStatusCode(200); } { - var res = await client.PutAsync("users/user1/timeline/members/user3", null); + var res = await client.PutAsync("timelines/@user1/members/user3", null); res.Should().HaveStatusCode(200); } } { // no auth should get 401 using var client = await CreateDefaultClient(); - var res = await client.DeleteAsync("users/user1/timeline/posts/12"); + var res = await client.DeleteAsync("timelines/@user1/posts/12"); res.Should().HaveStatusCode(401); } { // self can delete self var postId = await CreatePost(1); using var client = await CreateClientAsUser(); - var res = await client.DeleteAsync($"users/user1/timeline/posts/{postId}"); + var res = await client.DeleteAsync($"timelines/@user1/posts/{postId}"); res.Should().HaveStatusCode(200); } { // admin can delete any var postId = await CreatePost(1); using var client = await CreateClientAsAdministrator(); - var res = await client.DeleteAsync($"users/user1/timeline/posts/{postId}"); + var res = await client.DeleteAsync($"timelines/@user1/posts/{postId}"); res.Should().HaveStatusCode(200); } { // owner can delete other var postId = await CreatePost(2); using var client = await CreateClientAsUser(); - var res = await client.DeleteAsync($"users/user1/timeline/posts/{postId}"); + var res = await client.DeleteAsync($"timelines/@user1/posts/{postId}"); res.Should().HaveStatusCode(200); } { // author can delete self var postId = await CreatePost(2); using var client = await CreateClientAs(2); - var res = await client.DeleteAsync($"users/user1/timeline/posts/{postId}"); + var res = await client.DeleteAsync($"timelines/@user1/posts/{postId}"); res.Should().HaveStatusCode(200); } { // otherwise is forbidden var postId = await CreatePost(2); using var client = await CreateClientAs(3); - var res = await client.DeleteAsync($"users/user1/timeline/posts/{postId}"); + var res = await client.DeleteAsync($"timelines/@user1/posts/{postId}"); res.Should().HaveStatusCode(403); } } @@ -435,31 +436,31 @@ namespace Timeline.Tests.IntegratedTests { using var client = await CreateClientAsUser(); { - var res = await client.GetAsync("users/user1/timeline/posts"); + var res = await client.GetAsync("timelines/@user1/posts"); res.Should().HaveStatusCode(200) .And.HaveJsonBody() .Which.Should().NotBeNull().And.BeEmpty(); } { - var res = await client.PostAsJsonAsync("users/user1/timeline/posts", - new TimelinePostCreateRequest { Content = null }); + var res = await client.PostAsJsonAsync("timelines/@user1/posts", + TimelineHelper.TextPostCreateRequest(null)); res.Should().BeInvalidModel(); } const string mockContent = "aaa"; TimelinePostInfo createRes; { - var res = await client.PostAsJsonAsync("users/user1/timeline/posts", - new TimelinePostCreateRequest { Content = mockContent }); + var res = await client.PostAsJsonAsync("timelines/@user1/posts", + TimelineHelper.TextPostCreateRequest(mockContent)); var body = res.Should().HaveStatusCode(200) .And.HaveJsonBody() .Which; body.Should().NotBeNull(); - body.Content.Should().Be(mockContent); + body.Content.Should().BeEquivalentTo(TimelineHelper.TextPostContent(mockContent)); body.Author.Should().BeEquivalentTo(UserInfos[1]); createRes = body; } { - var res = await client.GetAsync("users/user1/timeline/posts"); + var res = await client.GetAsync("timelines/@user1/posts"); res.Should().HaveStatusCode(200) .And.HaveJsonBody() .Which.Should().NotBeNull().And.BeEquivalentTo(createRes); @@ -468,33 +469,33 @@ namespace Timeline.Tests.IntegratedTests var mockTime2 = DateTime.Now.AddDays(-1); TimelinePostInfo createRes2; { - var res = await client.PostAsJsonAsync("users/user1/timeline/posts", - new TimelinePostCreateRequest { Content = mockContent2, Time = mockTime2 }); + var res = await client.PostAsJsonAsync("timelines/@user1/posts", + TimelineHelper.TextPostCreateRequest(mockContent2, mockTime2)); var body = res.Should().HaveStatusCode(200) .And.HaveJsonBody() .Which; body.Should().NotBeNull(); - body.Content.Should().Be(mockContent2); + body.Content.Should().BeEquivalentTo(TimelineHelper.TextPostContent(mockContent2)); body.Author.Should().BeEquivalentTo(UserInfos[1]); body.Time.Should().BeCloseTo(mockTime2, 1000); createRes2 = body; } { - var res = await client.GetAsync("users/user1/timeline/posts"); + var res = await client.GetAsync("timelines/@user1/posts"); res.Should().HaveStatusCode(200) .And.HaveJsonBody() .Which.Should().NotBeNull().And.BeEquivalentTo(createRes, createRes2); } { - var res = await client.DeleteAsync($"users/user1/timeline/posts/{createRes.Id}"); + var res = await client.DeleteAsync($"timelines/@user1/posts/{createRes.Id}"); res.Should().BeDelete(true); } { - var res = await client.DeleteAsync("users/user1/timeline/posts/30000"); + var res = await client.DeleteAsync("timelines/@user1/posts/30000"); res.Should().BeDelete(false); } { - var res = await client.GetAsync("users/user1/timeline/posts"); + var res = await client.GetAsync("timelines/@user1/posts"); res.Should().HaveStatusCode(200) .And.HaveJsonBody() .Which.Should().NotBeNull().And.BeEquivalentTo(createRes2); @@ -509,8 +510,8 @@ namespace Timeline.Tests.IntegratedTests async Task CreatePost(DateTime time) { - var res = await client.PostAsJsonAsync("users/user1/timeline/posts", - new TimelinePostCreateRequest { Content = "aaa", Time = time }); + var res = await client.PostAsJsonAsync("timelines/@user1/posts", + TimelineHelper.TextPostCreateRequest("aaa", time)); return res.Should().HaveStatusCode(200) .And.HaveJsonBody() .Which.Id; @@ -522,7 +523,7 @@ namespace Timeline.Tests.IntegratedTests var id2 = await CreatePost(now); { - var res = await client.GetAsync("users/user1/timeline/posts"); + var res = await client.GetAsync("timelines/@user1/posts"); res.Should().HaveStatusCode(200) .And.HaveJsonBody() .Which.Select(p => p.Id).Should().Equal(id1, id2, id0); diff --git a/Timeline.Tests/IntegratedTests/TimelineTest.cs b/Timeline.Tests/IntegratedTests/TimelineTest.cs index 14a0a59e..3fceb1d5 100644 --- a/Timeline.Tests/IntegratedTests/TimelineTest.cs +++ b/Timeline.Tests/IntegratedTests/TimelineTest.cs @@ -6,12 +6,38 @@ using System.Linq; using System.Net; using System.Net.Http; using System.Threading.Tasks; +using Timeline.Models; using Timeline.Models.Http; using Timeline.Tests.Helpers; using Xunit; namespace Timeline.Tests.IntegratedTests { + public static class TimelineHelper + { + public static TimelinePostContentInfo TextPostContent(string text) + { + return new TimelinePostContentInfo + { + Type = "text", + Text = text + }; + } + + public static TimelinePostCreateRequest TextPostCreateRequest(string text, DateTime? time = null) + { + return new TimelinePostCreateRequest + { + Content = new TimelinePostCreateRequestContent + { + Type = "text", + Text = text + }, + Time = time + }; + } + } + public class TimelineTest : IntegratedTestBase { public TimelineTest(WebApplicationFactory factory) @@ -45,7 +71,7 @@ namespace Timeline.Tests.IntegratedTests var client = await CreateDefaultClient(); { - var res = await client.GetAsync("/users/user1/timeline"); + var res = await client.GetAsync("/timelines/@user1"); user1Timeline = res.Should().HaveStatusCode(200) .And.HaveJsonBody().Which; } @@ -80,7 +106,7 @@ namespace Timeline.Tests.IntegratedTests var client = await CreateClientAsUser(); { - var res = await client.PutAsync("/users/user1/timeline/members/user3", null); + var res = await client.PutAsync("/timelines/@user1/members/user3", null); res.Should().HaveStatusCode(200); } @@ -90,7 +116,7 @@ namespace Timeline.Tests.IntegratedTests } { - var res = await client.PatchAsJsonAsync("/users/user1/timeline", new TimelinePatchRequest { Visibility = TimelineVisibility.Public }); + var res = await client.PatchAsJsonAsync("/timelines/@user1", new TimelinePatchRequest { Visibility = TimelineVisibility.Public }); res.Should().HaveStatusCode(200); } @@ -100,7 +126,7 @@ namespace Timeline.Tests.IntegratedTests } { - var res = await client.GetAsync("/users/user1/timeline"); + var res = await client.GetAsync("/timelines/@user1"); var timeline = res.Should().HaveStatusCode(200) .And.HaveJsonBody().Which; testResultRelate.Add(timeline); @@ -123,7 +149,7 @@ namespace Timeline.Tests.IntegratedTests var client = await CreateClientAs(2); { - var res = await client.PutAsync("/users/user2/timeline/members/user3", null); + var res = await client.PutAsync("/timelines/@user2/members/user3", null); res.Should().HaveStatusCode(200); } @@ -133,7 +159,7 @@ namespace Timeline.Tests.IntegratedTests } { - var res = await client.PatchAsJsonAsync("/users/user2/timeline", new TimelinePatchRequest { Visibility = TimelineVisibility.Register }); + var res = await client.PatchAsJsonAsync("/timelines/@user2", new TimelinePatchRequest { Visibility = TimelineVisibility.Register }); res.Should().HaveStatusCode(200); } @@ -143,7 +169,7 @@ namespace Timeline.Tests.IntegratedTests } { - var res = await client.GetAsync("/users/user2/timeline"); + var res = await client.GetAsync("/timelines/@user2"); var timeline = res.Should().HaveStatusCode(200) .And.HaveJsonBody().Which; testResultRelate.Add(timeline); @@ -165,7 +191,7 @@ namespace Timeline.Tests.IntegratedTests var client = await CreateClientAs(3); { - var res = await client.PatchAsJsonAsync("/users/user3/timeline", new TimelinePatchRequest { Visibility = TimelineVisibility.Private }); + var res = await client.PatchAsJsonAsync("/timelines/@user3", new TimelinePatchRequest { Visibility = TimelineVisibility.Private }); res.Should().HaveStatusCode(200); } @@ -175,7 +201,7 @@ namespace Timeline.Tests.IntegratedTests } { - var res = await client.GetAsync("/users/user3/timeline"); + var res = await client.GetAsync("/timelines/@user3"); var timeline = res.Should().HaveStatusCode(200) .And.HaveJsonBody().Which; testResultRelate.Add(timeline); @@ -396,7 +422,7 @@ namespace Timeline.Tests.IntegratedTests res.Should().BeInvalidModel(); } { - var res = await client.PostAsJsonAsync("timelines/aaa!!!/posts", new TimelinePostCreateRequest { Content = "aaa" }); + var res = await client.PostAsJsonAsync("timelines/aaa!!!/posts", TimelineHelper.TextPostCreateRequest("aaa")); res.Should().BeInvalidModel(); } { @@ -430,7 +456,7 @@ namespace Timeline.Tests.IntegratedTests res.Should().HaveStatusCode(404).And.HaveCommonBody(ErrorCodes.TimelineCommon.NotExist); } { - var res = await client.PostAsJsonAsync("timelines/notexist/posts", new TimelinePostCreateRequest { Content = "aaa" }); + var res = await client.PostAsJsonAsync("timelines/notexist/posts", TimelineHelper.TextPostCreateRequest("aaa")); res.Should().HaveStatusCode(404).And.HaveCommonBody(ErrorCodes.TimelineCommon.NotExist); } { @@ -673,7 +699,7 @@ namespace Timeline.Tests.IntegratedTests { { // no auth should get 401 var res = await client.PostAsJsonAsync("timelines/t1/posts", - new TimelinePostCreateRequest { Content = "aaa" }); + TimelineHelper.TextPostCreateRequest("aaa")); res.Should().HaveStatusCode(401); } } @@ -682,12 +708,12 @@ namespace Timeline.Tests.IntegratedTests { { // post self's var res = await client.PostAsJsonAsync("timelines/t1/posts", - new TimelinePostCreateRequest { Content = "aaa" }); + TimelineHelper.TextPostCreateRequest("aaa")); res.Should().HaveStatusCode(200); } { // post other not as a member should get 403 var res = await client.PostAsJsonAsync("timelines/t0/posts", - new TimelinePostCreateRequest { Content = "aaa" }); + TimelineHelper.TextPostCreateRequest("aaa")); res.Should().HaveStatusCode(403); } } @@ -696,7 +722,7 @@ namespace Timeline.Tests.IntegratedTests { { // post as admin var res = await client.PostAsJsonAsync("timelines/t1/posts", - new TimelinePostCreateRequest { Content = "aaa" }); + TimelineHelper.TextPostCreateRequest("aaa")); res.Should().HaveStatusCode(200); } } @@ -705,7 +731,7 @@ namespace Timeline.Tests.IntegratedTests { { // post as member var res = await client.PostAsJsonAsync("timelines/t1/posts", - new TimelinePostCreateRequest { Content = "aaa" }); + TimelineHelper.TextPostCreateRequest("aaa")); res.Should().HaveStatusCode(200); } } @@ -720,7 +746,7 @@ namespace Timeline.Tests.IntegratedTests { using var client = await CreateClientAs(userNumber); var res = await client.PostAsJsonAsync($"timelines/t1/posts", - new TimelinePostCreateRequest { Content = "aaa" }); + TimelineHelper.TextPostCreateRequest("aaa")); return res.Should().HaveStatusCode(200) .And.HaveJsonBody() .Which.Id; @@ -795,19 +821,19 @@ namespace Timeline.Tests.IntegratedTests } { var res = await client.PostAsJsonAsync("timelines/t1/posts", - new TimelinePostCreateRequest { Content = null }); + TimelineHelper.TextPostCreateRequest(null)); res.Should().BeInvalidModel(); } const string mockContent = "aaa"; TimelinePostInfo createRes; { var res = await client.PostAsJsonAsync("timelines/t1/posts", - new TimelinePostCreateRequest { Content = mockContent }); + TimelineHelper.TextPostCreateRequest(mockContent)); var body = res.Should().HaveStatusCode(200) .And.HaveJsonBody() .Which; body.Should().NotBeNull(); - body.Content.Should().Be(mockContent); + body.Content.Should().BeEquivalentTo(TimelineHelper.TextPostContent(mockContent)); body.Author.Should().BeEquivalentTo(UserInfos[1]); createRes = body; } @@ -822,12 +848,12 @@ namespace Timeline.Tests.IntegratedTests TimelinePostInfo createRes2; { var res = await client.PostAsJsonAsync("timelines/t1/posts", - new TimelinePostCreateRequest { Content = mockContent2, Time = mockTime2 }); + TimelineHelper.TextPostCreateRequest(mockContent2, mockTime2)); var body = res.Should().HaveStatusCode(200) .And.HaveJsonBody() .Which; body.Should().NotBeNull(); - body.Content.Should().Be(mockContent2); + body.Content.Should().BeEquivalentTo(TimelineHelper.TextPostContent(mockContent2)); body.Author.Should().BeEquivalentTo(UserInfos[1]); body.Time.Should().BeCloseTo(mockTime2, 1000); createRes2 = body; @@ -865,7 +891,7 @@ namespace Timeline.Tests.IntegratedTests async Task CreatePost(DateTime time) { var res = await client.PostAsJsonAsync("timelines/t1/posts", - new TimelinePostCreateRequest { Content = "aaa", Time = time }); + TimelineHelper.TextPostCreateRequest("aaa", time)); return res.Should().HaveStatusCode(200) .And.HaveJsonBody() .Which.Id; diff --git a/Timeline.Tests/IntegratedTests/TokenTest.cs b/Timeline.Tests/IntegratedTests/TokenTest.cs index 928d546c..7b28746f 100644 --- a/Timeline.Tests/IntegratedTests/TokenTest.cs +++ b/Timeline.Tests/IntegratedTests/TokenTest.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.DependencyInjection; using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; +using Timeline.Models; using Timeline.Models.Http; using Timeline.Services; using Timeline.Tests.Helpers; diff --git a/Timeline/Controllers/TimelineController.cs b/Timeline/Controllers/TimelineController.cs index 38fe7475..440b0d19 100644 --- a/Timeline/Controllers/TimelineController.cs +++ b/Timeline/Controllers/TimelineController.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -113,6 +114,19 @@ namespace Timeline.Controllers return result; } + // TODO: Make cache available. + [HttpGet("timelines/{name}/posts/{id}/data")] + public async Task>> PostDataGet([FromRoute][GeneralTimelineName] string name, [FromRoute] long id) + { + if (!this.IsAdministrator() && !await _service.HasReadPermission(name, this.GetOptionalUserId())) + { + return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); + } + + var data = await _service.GetPostData(name, id); + return File(data.Data, data.Type, data.LastModified, new EntityTagHeaderValue(data.ETag)); + } + [HttpPost("timelines/{name}/posts")] [Authorize] public async Task> PostPost([FromRoute][GeneralTimelineName] string name, [FromBody] TimelinePostCreateRequest body) diff --git a/Timeline/Controllers/UserController.cs b/Timeline/Controllers/UserController.cs index a3e8d816..0bc8bcda 100644 --- a/Timeline/Controllers/UserController.cs +++ b/Timeline/Controllers/UserController.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Threading.Tasks; using Timeline.Auth; using Timeline.Helpers; +using Timeline.Models; using Timeline.Models.Http; using Timeline.Models.Validation; using Timeline.Services; diff --git a/Timeline/Entities/TimelineEntity.cs b/Timeline/Entities/TimelineEntity.cs index 56b36d4e..3149d4c2 100644 --- a/Timeline/Entities/TimelineEntity.cs +++ b/Timeline/Entities/TimelineEntity.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using Timeline.Models.Http; +using Timeline.Models; namespace Timeline.Entities { diff --git a/Timeline/Models/Http/Timeline.cs b/Timeline/Models/Http/Timeline.cs index 55c3a3bf..9e2aefd0 100644 --- a/Timeline/Models/Http/Timeline.cs +++ b/Timeline/Models/Http/Timeline.cs @@ -72,25 +72,27 @@ namespace Timeline.Models.Http } } - public class TimelinePostConverter : ITypeConverter + public class TimelinePostContentResolver : IValueResolver { private readonly IActionContextAccessor _actionContextAccessor; private readonly IUrlHelperFactory _urlHelperFactory; - public TimelinePostConverter(IActionContextAccessor actionContextAccessor, IUrlHelperFactory urlHelperFactory) + public TimelinePostContentResolver(IActionContextAccessor actionContextAccessor, IUrlHelperFactory urlHelperFactory) { _actionContextAccessor = actionContextAccessor; _urlHelperFactory = urlHelperFactory; } - public TimelinePostContentInfo Convert(ITimelinePostContent source, TimelinePostContentInfo destination, ResolutionContext context) + public TimelinePostContentInfo Resolve(TimelinePost source, TimelinePostInfo destination, TimelinePostContentInfo destMember, ResolutionContext context) { if (_actionContextAccessor.ActionContext == null) throw new InvalidOperationException("No action context, can't fill urls."); var urlHelper = _urlHelperFactory.GetUrlHelper(_actionContextAccessor.ActionContext); - if (source is TextTimelinePostContent textContent) + var sourceContent = source.Content; + + if (sourceContent is TextTimelinePostContent textContent) { return new TimelinePostContentInfo { @@ -98,14 +100,21 @@ namespace Timeline.Models.Http Text = textContent.Text }; } - else if (source is ImageTimelinePostContent imageContent) + else if (sourceContent is ImageTimelinePostContent imageContent) { return new TimelinePostContentInfo { Type = TimelinePostContentTypes.Image, - Url = urlHelper.ActionLink(action: "PostDataGet", nameof(TimelineController)[0..^nameof(Controller).Length], new { source.Name }) + Url = urlHelper.ActionLink( + action: nameof(TimelineController.PostDataGet), + controller: nameof(TimelineController)[0..^nameof(Controller).Length], + values: new { Name = source.TimelineName, Id = source.Id }) }; } + else + { + throw new InvalidOperationException("Unknown content type."); + } } } @@ -114,8 +123,7 @@ namespace Timeline.Models.Http public TimelineInfoAutoMapperProfile() { CreateMap().ForMember(u => u._links, opt => opt.MapFrom()); - CreateMap(); - CreateMap().ConvertUsing(); + CreateMap().ForMember(p => p.Content, opt => opt.MapFrom()); CreateMap(); } } diff --git a/Timeline/Models/Http/UserInfo.cs b/Timeline/Models/Http/UserInfo.cs index 4f887549..b4bf14c1 100644 --- a/Timeline/Models/Http/UserInfo.cs +++ b/Timeline/Models/Http/UserInfo.cs @@ -45,7 +45,7 @@ namespace Timeline.Models.Http { Self = urlHelper.ActionLink(nameof(UserController.Get), nameof(UserController)[0..^nameof(Controller).Length], new { destination.Username }), Avatar = urlHelper.ActionLink(nameof(UserAvatarController.Get), nameof(UserAvatarController)[0..^nameof(Controller).Length], new { destination.Username }), - Timeline = urlHelper.ActionLink(nameof(PersonalTimelineController.TimelineGet), nameof(PersonalTimelineController)[0..^nameof(Controller).Length], new { destination.Username }) + Timeline = urlHelper.ActionLink(nameof(TimelineController.TimelineGet), nameof(TimelineController)[0..^nameof(Controller).Length], new { Name = "@" + destination.Username }) }; return result; } diff --git a/Timeline/Models/Timeline.cs b/Timeline/Models/Timeline.cs index 6d4c924d..803a5c5c 100644 --- a/Timeline/Models/Timeline.cs +++ b/Timeline/Models/Timeline.cs @@ -48,11 +48,22 @@ namespace Timeline.Models public class TimelinePost { + public TimelinePost(long id, ITimelinePostContent content, DateTime time, User author, DateTime lastUpdated, string timelineName) + { + Id = id; + Content = content; + Time = time; + Author = author; + LastUpdated = lastUpdated; + TimelineName = timelineName; + } + public long Id { get; set; } - public ITimelinePostContent Content { get; set; } = default!; + public ITimelinePostContent Content { get; set; } public DateTime Time { get; set; } - public User Author { get; set; } = default!; - public DateTime LastUpdated { get; set; } = default!; + public User Author { get; set; } + public DateTime LastUpdated { get; set; } + public string TimelineName { get; set; } } #pragma warning disable CA1724 // Type names should not match namespaces diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs index 1bccb855..3a5825ae 100644 --- a/Timeline/Services/TimelineService.cs +++ b/Timeline/Services/TimelineService.cs @@ -32,12 +32,14 @@ namespace Timeline.Services public long UserId { get; set; } } - public class DataWithType + public class PostData { #pragma warning disable CA1819 // Properties should not return arrays public byte[] Data { get; set; } = default!; #pragma warning restore CA1819 // Properties should not return arrays public string Type { get; set; } = default!; + public string ETag { get; set; } = default!; + public DateTime LastModified { get; set; } = default!; } /// @@ -103,7 +105,7 @@ namespace Timeline.Services /// /// Use this method to retrieve the image of image post. /// - Task GetPostData(string name, long postId); + Task GetPostData(string name, long postId); /// /// Create a new text post in timeline. @@ -334,6 +336,8 @@ namespace Timeline.Services /// protected abstract Task FindTimelineId(string name); + protected abstract string GenerateName(string name); + public async Task GetTimeline(string name) { if (name == null) @@ -355,7 +359,7 @@ namespace Timeline.Services return new Models.Timeline { - Name = timelineEntity.Name ?? ("@" + owner.Username), + Name = GenerateName(name), Description = timelineEntity.Description ?? "", Owner = owner, Visibility = timelineEntity.Visibility, @@ -387,19 +391,19 @@ namespace Timeline.Services _ => throw new DatabaseCorruptedException(string.Format(CultureInfo.InvariantCulture, ExceptionDatabaseUnknownContentType, type)) }; - posts.Add(new TimelinePost - { - Id = entity.LocalId, - Content = content, - Author = author, - Time = entity.Time, - LastUpdated = entity.LastUpdated - }); + posts.Add(new TimelinePost( + id: entity.LocalId, + content: content, + time: entity.Time, + author: author, + lastUpdated: entity.LastUpdated, + timelineName: GenerateName(name) + )); } } return posts; } - public async Task GetPostData(string name, long postId) + public async Task GetPostData(string name, long postId) { if (name == null) throw new ArgumentNullException(nameof(name)); @@ -437,10 +441,12 @@ namespace Timeline.Services await Database.SaveChangesAsync(); } - return new DataWithType + return new PostData { Data = data, - Type = postEntity.ExtraContent + Type = postEntity.ExtraContent, + ETag = tag, + LastModified = postEntity.LastUpdated }; } @@ -474,14 +480,15 @@ namespace Timeline.Services Database.TimelinePosts.Add(postEntity); await Database.SaveChangesAsync(); - return new TimelinePost - { - Id = postEntity.LocalId, - Content = new TextTimelinePostContent(text), - Author = author, - Time = finalTime, - LastUpdated = currentTime - }; + + return new TimelinePost( + id: postEntity.LocalId, + content: new TextTimelinePostContent(text), + time: finalTime, + author: author, + lastUpdated: currentTime, + timelineName: GenerateName(name) + ); } public async Task CreateImagePost(string name, long authorId, byte[] data, DateTime? time) @@ -521,14 +528,14 @@ namespace Timeline.Services Database.TimelinePosts.Add(postEntity); await Database.SaveChangesAsync(); - return new TimelinePost - { - Id = postEntity.LocalId, - Content = new ImageTimelinePostContent(tag), - Author = author, - Time = finalTime, - LastUpdated = currentTime - }; + return new TimelinePost( + id: postEntity.LocalId, + content: new ImageTimelinePostContent(tag), + time: finalTime, + author: author, + lastUpdated: currentTime, + timelineName: GenerateName(name) + ); } public async Task DeletePost(string name, long id) @@ -767,6 +774,11 @@ namespace Timeline.Services return timelineEntity.Id; } } + + protected override string GenerateName(string name) + { + return name; + } } public class PersonalTimelineService : BaseTimelineManager, IPersonalTimelineService @@ -818,6 +830,11 @@ namespace Timeline.Services return newTimelineEntity.Id; } } + + protected override string GenerateName(string name) + { + return "@" + name; + } } public class TimelineService : ITimelineService @@ -996,7 +1013,7 @@ namespace Timeline.Services return s.GetPosts(realName); } - public Task GetPostData(string name, long postId) + public Task GetPostData(string name, long postId) { var s = BranchName(name, out var realName); return s.GetPostData(realName, postId); diff --git a/Timeline/Startup.cs b/Timeline/Startup.cs index 85822a14..d2fd22bd 100644 --- a/Timeline/Startup.cs +++ b/Timeline/Startup.cs @@ -101,7 +101,8 @@ namespace Timeline services.AddUserAvatarService(); - services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.TryAddSingleton(); -- cgit v1.2.3