using FluentAssertions; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; 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; 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() : base(3) { } protected override async Task OnInitializeAsync() { await CreateTestTimelines(); } private List _testTimelines; private async Task CreateTestTimelines() { _testTimelines = new List(); for (int i = 0; i <= 3; i++) { var client = await CreateClientAs(i); var res = await client.PostAsJsonAsync("timelines", new TimelineCreateRequest { Name = $"t{i}" }); var timelineInfo = res.Should().HaveStatusCode(200) .And.HaveJsonBody().Which; _testTimelines.Add(timelineInfo); } } private static string CalculateUrlTail(string subpath, ICollection> query) { 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 GeneratePersonalTimelineUrl(int id, string subpath = null, ICollection> query = null) { return $"timelines/@{(id == 0 ? "admin" : ("user" + id))}{CalculateUrlTail(subpath, query)}"; } 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() { yield return new[] { new TimelineUrlGenerator(GeneratePersonalTimelineUrl) }; yield return new[] { new TimelineUrlGenerator(GenerateOrdinaryTimelineUrl) }; } private static string GeneratePersonalTimelineUrlByName(string name, string subpath = null) { return $"timelines/@{name}{(subpath == null ? "" : "/" + subpath)}"; } private static string GenerateOrdinaryTimelineUrlByName(string name, string subpath = null) { return $"timelines/{name}{(subpath == null ? "" : "/" + subpath)}"; } public static IEnumerable TimelineUrlByNameGeneratorData() { yield return new[] { new Func(GeneratePersonalTimelineUrlByName) }; yield return new[] { new Func(GenerateOrdinaryTimelineUrlByName) }; } [Fact] public async Task TimelineGet_Should_Work() { using var client = await CreateDefaultClient(); { var res = await client.GetAsync("timelines/@user1"); var body = res.Should().HaveStatusCode(200) .And.HaveJsonBody().Which; body.Owner.Should().BeEquivalentTo(UserInfos[1]); body.Visibility.Should().Be(TimelineVisibility.Register); body.Description.Should().Be(""); body.Members.Should().NotBeNull().And.BeEmpty(); var links = body._links; links.Should().NotBeNull(); links.Self.Should().EndWith("timelines/@user1"); links.Posts.Should().EndWith("timelines/@user1/posts"); } { var res = await client.GetAsync("timelines/t1"); var body = res.Should().HaveStatusCode(200) .And.HaveJsonBody().Which; body.Owner.Should().BeEquivalentTo(UserInfos[1]); body.Visibility.Should().Be(TimelineVisibility.Register); body.Description.Should().Be(""); body.Members.Should().NotBeNull().And.BeEmpty(); var links = body._links; links.Should().NotBeNull(); links.Self.Should().EndWith("timelines/t1"); links.Posts.Should().EndWith("timelines/t1/posts"); } } [Fact] public async Task TimelineList() { TimelineInfo user1Timeline; var client = await CreateDefaultClient(); { var res = await client.GetAsync("timelines/@user1"); user1Timeline = res.Should().HaveStatusCode(200) .And.HaveJsonBody().Which; } { var testResult = new List(); testResult.Add(user1Timeline); testResult.AddRange(_testTimelines); var res = await client.GetAsync("timelines"); res.Should().HaveStatusCode(200) .And.HaveJsonBody>() .Which.Should().BeEquivalentTo(testResult); } } [Fact] public async Task TimelineList_WithQuery() { var testResultRelate = new List(); var testResultOwn = new List(); var testResultJoin = new List(); var testResultOwnPrivate = new List(); var testResultRelatePublic = new List(); var testResultRelateRegister = new List(); var testResultJoinPrivate = new List(); var testResultPublic = new List(); { var client = await CreateClientAsUser(); { var res = await client.PutAsync("timelines/@user1/members/user3", null); res.Should().HaveStatusCode(200); } { var res = await client.PutAsync("timelines/t1/members/user3", null); res.Should().HaveStatusCode(200); } { var res = await client.PatchAsJsonAsync("timelines/@user1", new TimelinePatchRequest { Visibility = TimelineVisibility.Public }); res.Should().HaveStatusCode(200); } { var res = await client.PatchAsJsonAsync("timelines/t1", new TimelinePatchRequest { Visibility = TimelineVisibility.Register }); res.Should().HaveStatusCode(200); } { var res = await client.GetAsync("timelines/@user1"); var timeline = res.Should().HaveStatusCode(200) .And.HaveJsonBody().Which; testResultRelate.Add(timeline); testResultJoin.Add(timeline); testResultRelatePublic.Add(timeline); testResultPublic.Add(timeline); } { var res = await client.GetAsync("timelines/t1"); var timeline = res.Should().HaveStatusCode(200) .And.HaveJsonBody().Which; testResultRelate.Add(timeline); testResultJoin.Add(timeline); testResultRelateRegister.Add(timeline); } } { var client = await CreateClientAs(2); { var res = await client.PutAsync("timelines/@user2/members/user3", null); res.Should().HaveStatusCode(200); } { var res = await client.PutAsync("timelines/t2/members/user3", null); res.Should().HaveStatusCode(200); } { var res = await client.PatchAsJsonAsync("timelines/@user2", new TimelinePatchRequest { Visibility = TimelineVisibility.Register }); res.Should().HaveStatusCode(200); } { var res = await client.PatchAsJsonAsync("timelines/t2", new TimelinePatchRequest { Visibility = TimelineVisibility.Private }); res.Should().HaveStatusCode(200); } { var res = await client.GetAsync("timelines/@user2"); var timeline = res.Should().HaveStatusCode(200) .And.HaveJsonBody().Which; testResultRelate.Add(timeline); testResultJoin.Add(timeline); testResultRelateRegister.Add(timeline); } { var res = await client.GetAsync("timelines/t2"); var timeline = res.Should().HaveStatusCode(200) .And.HaveJsonBody().Which; testResultRelate.Add(timeline); testResultJoin.Add(timeline); testResultJoinPrivate.Add(timeline); } } { var client = await CreateClientAs(3); { var res = await client.PatchAsJsonAsync("timelines/@user3", new TimelinePatchRequest { Visibility = TimelineVisibility.Private }); res.Should().HaveStatusCode(200); } { var res = await client.PatchAsJsonAsync("timelines/t3", new TimelinePatchRequest { Visibility = TimelineVisibility.Register }); res.Should().HaveStatusCode(200); } { var res = await client.GetAsync("timelines/@user3"); var timeline = res.Should().HaveStatusCode(200) .And.HaveJsonBody().Which; testResultRelate.Add(timeline); testResultOwn.Add(timeline); testResultOwnPrivate.Add(timeline); } { var res = await client.GetAsync("timelines/t3"); var timeline = res.Should().HaveStatusCode(200) .And.HaveJsonBody().Which; testResultRelate.Add(timeline); testResultOwn.Add(timeline); testResultRelateRegister.Add(timeline); } } { var client = await CreateClientAs(3); { var res = await client.GetAsync("timelines?relate=user3"); var body = res.Should().HaveStatusCode(200) .And.HaveJsonBody>() .Which; body.Should().BeEquivalentTo(testResultRelate); } { var res = await client.GetAsync("timelines?relate=user3&relateType=own"); var body = res.Should().HaveStatusCode(200) .And.HaveJsonBody>() .Which; body.Should().BeEquivalentTo(testResultOwn); } { var res = await client.GetAsync("timelines?relate=user3&visibility=public"); var body = res.Should().HaveStatusCode(200) .And.HaveJsonBody>() .Which; body.Should().BeEquivalentTo(testResultRelatePublic); } { var res = await client.GetAsync("timelines?relate=user3&visibility=register"); var body = res.Should().HaveStatusCode(200) .And.HaveJsonBody>() .Which; body.Should().BeEquivalentTo(testResultRelateRegister); } { var res = await client.GetAsync("timelines?relate=user3&relateType=join&visibility=private"); var body = res.Should().HaveStatusCode(200) .And.HaveJsonBody>() .Which; body.Should().BeEquivalentTo(testResultJoinPrivate); } { var res = await client.GetAsync("timelines?relate=user3&relateType=own&visibility=private"); var body = res.Should().HaveStatusCode(200) .And.HaveJsonBody>() .Which; body.Should().BeEquivalentTo(testResultOwnPrivate); } } { var client = await CreateDefaultClient(); { var res = await client.GetAsync("timelines?visibility=public"); var body = res.Should().HaveStatusCode(200) .And.HaveJsonBody>() .Which; body.Should().BeEquivalentTo(testResultPublic); } } } [Fact] public async Task TimelineList_InvalidModel() { var client = await CreateClientAsUser(); { var res = await client.GetAsync("timelines?relate=us!!"); res.Should().BeInvalidModel(); } { var res = await client.GetAsync("timelines?relateType=aaa"); res.Should().BeInvalidModel(); } { var res = await client.GetAsync("timelines?visibility=aaa"); res.Should().BeInvalidModel(); } } [Fact] public async Task TimelineCreate_Should_Work() { { using var client = await CreateDefaultClient(); var res = await client.PostAsJsonAsync("timelines", new TimelineCreateRequest { Name = "aaa" }); res.Should().HaveStatusCode(HttpStatusCode.Unauthorized); } using (var client = await CreateClientAsUser()) { { var res = await client.PostAsJsonAsync("timelines", new TimelineCreateRequest { Name = "!!!" }); res.Should().BeInvalidModel(); } TimelineInfo timelineInfo; { var res = await client.PostAsJsonAsync("timelines", new TimelineCreateRequest { Name = "aaa" }); timelineInfo = res.Should().HaveStatusCode(200) .And.HaveJsonBody().Which; } { var res = await client.GetAsync("timelines/aaa"); res.Should().HaveStatusCode(200) .And.HaveJsonBody() .Which.Should().BeEquivalentTo(timelineInfo); } { var res = await client.PostAsJsonAsync("timelines", new TimelineCreateRequest { Name = "aaa" }); res.Should().HaveStatusCode(400) .And.HaveCommonBody(ErrorCodes.TimelineController.NameConflict); } } } [Fact] public async Task TimelineDelete_Should_Work() { { using var client = await CreateDefaultClient(); var res = await client.DeleteAsync("timelines/t1"); res.Should().HaveStatusCode(HttpStatusCode.Unauthorized); } { using var client = await CreateClientAs(2); var res = await client.DeleteAsync("timelines/t1"); res.Should().HaveStatusCode(HttpStatusCode.Forbidden); } { using var client = await CreateClientAsAdministrator(); { var res = await client.DeleteAsync("timelines/!!!"); res.Should().BeInvalidModel(); } { var res = await client.DeleteAsync("timelines/t2"); res.Should().BeDelete(true); } { var res = await client.DeleteAsync("timelines/t2"); res.Should().BeDelete(false); } } { using var client = await CreateClientAs(1); { var res = await client.DeleteAsync("timelines/!!!"); res.Should().BeInvalidModel(); } { var res = await client.DeleteAsync("timelines/t1"); res.Should().BeDelete(true); } { var res = await client.DeleteAsync("timelines/t1"); res.Should().HaveStatusCode(400); } } } [Theory] [MemberData(nameof(TimelineUrlByNameGeneratorData))] public async Task InvalidModel_BadName(Func generator) { using var client = await CreateClientAsAdministrator(); { var res = await client.GetAsync(generator("aaa!!!", null)); res.Should().BeInvalidModel(); } { var res = await client.PatchAsJsonAsync(generator("aaa!!!", null), new TimelinePatchRequest { }); res.Should().BeInvalidModel(); } { var res = await client.PutAsync(generator("aaa!!!", "members/user1"), null); res.Should().BeInvalidModel(); } { var res = await client.DeleteAsync(generator("aaa!!!", "members/user1")); res.Should().BeInvalidModel(); } { var res = await client.GetAsync(generator("aaa!!!", "posts")); res.Should().BeInvalidModel(); } { var res = await client.PostAsJsonAsync(generator("aaa!!!", "posts"), TimelineHelper.TextPostCreateRequest("aaa")); res.Should().BeInvalidModel(); } { var res = await client.DeleteAsync(generator("aaa!!!", "posts/123")); res.Should().BeInvalidModel(); } { var res = await client.GetAsync(generator("aaa!!!", "posts/123/data")); res.Should().BeInvalidModel(); } } [Theory] [MemberData(nameof(TimelineUrlByNameGeneratorData))] public async Task Ordinary_NotFound(Func generator) { var errorCode = generator == GenerateOrdinaryTimelineUrlByName ? ErrorCodes.TimelineController.NotExist : ErrorCodes.UserCommon.NotExist; using var client = await CreateClientAsAdministrator(); { var res = await client.GetAsync(generator("notexist", null)); res.Should().HaveStatusCode(404).And.HaveCommonBody(errorCode); } { var res = await client.PatchAsJsonAsync(generator("notexist", null), new TimelinePatchRequest { }); res.Should().HaveStatusCode(400).And.HaveCommonBody(errorCode); } { var res = await client.PutAsync(generator("notexist", "members/user1"), null); res.Should().HaveStatusCode(400).And.HaveCommonBody(errorCode); } { var res = await client.DeleteAsync(generator("notexist", "members/user1")); res.Should().HaveStatusCode(400).And.HaveCommonBody(errorCode); } { var res = await client.GetAsync(generator("notexist", "posts")); res.Should().HaveStatusCode(404).And.HaveCommonBody(errorCode); } { var res = await client.PostAsJsonAsync(generator("notexist", "posts"), TimelineHelper.TextPostCreateRequest("aaa")); res.Should().HaveStatusCode(400).And.HaveCommonBody(errorCode); } { var res = await client.DeleteAsync(generator("notexist", "posts/123")); res.Should().HaveStatusCode(400).And.HaveCommonBody(errorCode); } { var res = await client.GetAsync(generator("notexist", "posts/123/data")); res.Should().HaveStatusCode(404).And.HaveCommonBody(errorCode); } } [Theory] [MemberData(nameof(TimelineUrlGeneratorData))] public async Task Description_Should_Work(TimelineUrlGenerator generator) { using var client = await CreateClientAsUser(); async Task AssertDescription(string description) { var res = await client.GetAsync(generator(1, null)); var body = res.Should().HaveStatusCode(200) .And.HaveJsonBody() .Which.Description.Should().Be(description); } const string mockDescription = "haha"; await AssertDescription(""); { var res = await client.PatchAsJsonAsync(generator(1, null), new TimelinePatchRequest { Description = mockDescription }); res.Should().HaveStatusCode(200) .And.HaveJsonBody().Which.Description.Should().Be(mockDescription); await AssertDescription(mockDescription); } { var res = await client.PatchAsJsonAsync(generator(1, null), new TimelinePatchRequest { Description = null }); res.Should().HaveStatusCode(200) .And.HaveJsonBody().Which.Description.Should().Be(mockDescription); await AssertDescription(mockDescription); } { var res = await client.PatchAsJsonAsync(generator(1, null), new TimelinePatchRequest { Description = "" }); res.Should().HaveStatusCode(200) .And.HaveJsonBody().Which.Description.Should().Be(""); await AssertDescription(""); } } [Theory] [MemberData(nameof(TimelineUrlGeneratorData))] public async Task Member_Should_Work(TimelineUrlGenerator generator) { var getUrl = generator(1, null); using var client = await CreateClientAsUser(); async Task AssertMembers(IList members) { var res = await client.GetAsync(getUrl); res.Should().HaveStatusCode(200) .And.HaveJsonBody() .Which.Members.Should().NotBeNull().And.BeEquivalentTo(members); } async Task AssertEmptyMembers() { var res = await client.GetAsync(getUrl); res.Should().HaveStatusCode(200) .And.HaveJsonBody() .Which.Members.Should().NotBeNull().And.BeEmpty(); } await AssertEmptyMembers(); { var res = await client.PutAsync(generator(1, "members/usernotexist"), null); res.Should().HaveStatusCode(400) .And.HaveCommonBody(ErrorCodes.TimelineController.MemberPut_NotExist); } await AssertEmptyMembers(); { var res = await client.PutAsync(generator(1, "members/user2"), null); res.Should().HaveStatusCode(200); } await AssertMembers(new List { UserInfos[2] }); { var res = await client.DeleteAsync(generator(1, "members/user2")); res.Should().BeDelete(true); } await AssertEmptyMembers(); { var res = await client.DeleteAsync(generator(1, "members/aaa")); res.Should().BeDelete(false); } await AssertEmptyMembers(); } public static IEnumerable Permission_Timeline_Data() { 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"); res.Should().HaveStatusCode(get); } { var res = await client.PatchAsJsonAsync(generator(1, null), new TimelinePatchRequest { Description = "hahaha" }); res.Should().HaveStatusCode(opPatchUser); } { var res = await client.PatchAsJsonAsync(generator(0, null), new TimelinePatchRequest { Description = "hahaha" }); res.Should().HaveStatusCode(opPatchAdmin); } { var res = await client.PutAsync(generator(1, "members/user2"), null); res.Should().HaveStatusCode(opMemberUser); } { var res = await client.DeleteAsync(generator(1, "members/user2")); res.Should().HaveStatusCode(opMemberUser); } { var res = await client.PutAsync(generator(0, "members/user2"), null); res.Should().HaveStatusCode(opMemberAdmin); } { var res = await client.DeleteAsync(generator(0, "members/user2")); res.Should().HaveStatusCode(opMemberAdmin); } } [Theory] [MemberData(nameof(TimelineUrlGeneratorData))] public async Task Visibility_Test(TimelineUrlGenerator generator) { var userUrl = generator(1, "posts"); var adminUrl = generator(0, "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(generator(1, null), content); res.Should().BeInvalidModel(); } { // default visibility is registered { using var client = await CreateDefaultClient(); var res = await client.GetAsync(userUrl); res.Should().HaveStatusCode(403); } { using var client = await CreateClientAsUser(); var res = await client.GetAsync(adminUrl); res.Should().HaveStatusCode(200); } } { // change visibility to public { using var client = await CreateClientAsUser(); var res = await client.PatchAsJsonAsync(generator(1, null), new TimelinePatchRequest { Visibility = TimelineVisibility.Public }); res.Should().HaveStatusCode(200); } { using var client = await CreateDefaultClient(); var res = await client.GetAsync(userUrl); res.Should().HaveStatusCode(200); } } { // change visibility to private { using var client = await CreateClientAsAdministrator(); { var res = await client.PatchAsJsonAsync(generator(1, null), new TimelinePatchRequest { Visibility = TimelineVisibility.Private }); res.Should().HaveStatusCode(200); } { var res = await client.PatchAsJsonAsync(generator(0, null), new TimelinePatchRequest { Visibility = TimelineVisibility.Private }); res.Should().HaveStatusCode(200); } } { using var client = await CreateDefaultClient(); var res = await client.GetAsync(userUrl); res.Should().HaveStatusCode(403); } { // user can't read admin's using var client = await CreateClientAsUser(); var res = await client.GetAsync(adminUrl); res.Should().HaveStatusCode(403); } { // admin can read user's using var client = await CreateClientAsAdministrator(); var res = await client.GetAsync(userUrl); res.Should().HaveStatusCode(200); } { // add member using var client = await CreateClientAsAdministrator(); var res = await client.PutAsync(generator(0, "members/user1"), null); res.Should().HaveStatusCode(200); } { // now user can read admin's using var client = await CreateClientAsUser(); var res = await client.GetAsync(adminUrl); res.Should().HaveStatusCode(200); } } } [Theory] [MemberData(nameof(TimelineUrlGeneratorData))] public async Task Permission_Post_Create(TimelineUrlGenerator generator) { using (var client = await CreateClientAsUser()) { var res = await client.PutAsync(generator(1, "members/user2"), null); res.Should().HaveStatusCode(200); } using (var client = await CreateDefaultClient()) { { // no auth should get 401 var res = await client.PostAsJsonAsync(generator(1, "posts"), TimelineHelper.TextPostCreateRequest("aaa")); res.Should().HaveStatusCode(401); } } using (var client = await CreateClientAsUser()) { { // post self's var res = await client.PostAsJsonAsync(generator(1, "posts"), TimelineHelper.TextPostCreateRequest("aaa")); res.Should().HaveStatusCode(200); } { // post other not as a member should get 403 var res = await client.PostAsJsonAsync(generator(0, "posts"), TimelineHelper.TextPostCreateRequest("aaa")); res.Should().HaveStatusCode(403); } } using (var client = await CreateClientAsAdministrator()) { { // post as admin var res = await client.PostAsJsonAsync(generator(1, "posts"), TimelineHelper.TextPostCreateRequest("aaa")); res.Should().HaveStatusCode(200); } } using (var client = await CreateClientAs(2)) { { // post as member var res = await client.PostAsJsonAsync(generator(1, "posts"), TimelineHelper.TextPostCreateRequest("aaa")); res.Should().HaveStatusCode(200); } } } [Theory] [MemberData(nameof(TimelineUrlGeneratorData))] public async Task Permission_Post_Delete(TimelineUrlGenerator generator) { async Task CreatePost(int userNumber) { using var client = await CreateClientAs(userNumber); var res = await client.PostAsJsonAsync(generator(1, "posts"), TimelineHelper.TextPostCreateRequest("aaa")); return res.Should().HaveStatusCode(200) .And.HaveJsonBody() .Which.Id; } using (var client = await CreateClientAsUser()) { { var res = await client.PutAsync(generator(1, "members/user2"), null); res.Should().HaveStatusCode(200); } { var res = await client.PutAsync(generator(1, "members/user3"), null); res.Should().HaveStatusCode(200); } } { // no auth should get 401 using var client = await CreateDefaultClient(); var res = await client.DeleteAsync(generator(1, "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(generator(1, $"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(generator(1, $"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(generator(1, $"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(generator(1, $"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(generator(1, $"posts/{postId}")); res.Should().HaveStatusCode(403); } } [Theory] [MemberData(nameof(TimelineUrlGeneratorData))] public async Task TextPost_ShouldWork(TimelineUrlGenerator generator) { { using var client = await CreateClientAsUser(); { var res = await client.GetAsync(generator(1, "posts")); res.Should().HaveStatusCode(200) .And.HaveJsonBody() .Which.Should().NotBeNull().And.BeEmpty(); } { var res = await client.PostAsJsonAsync(generator(1, "posts"), TimelineHelper.TextPostCreateRequest(null)); res.Should().BeInvalidModel(); } const string mockContent = "aaa"; TimelinePostInfo createRes; { var res = await client.PostAsJsonAsync(generator(1, "posts"), TimelineHelper.TextPostCreateRequest(mockContent)); var body = res.Should().HaveStatusCode(200) .And.HaveJsonBody() .Which; body.Should().NotBeNull(); body.Content.Should().BeEquivalentTo(TimelineHelper.TextPostContent(mockContent)); body.Author.Should().BeEquivalentTo(UserInfos[1]); body.Deleted.Should().BeFalse(); createRes = body; } { var res = await client.GetAsync(generator(1, "posts")); res.Should().HaveStatusCode(200) .And.HaveJsonBody() .Which.Should().NotBeNull().And.BeEquivalentTo(createRes); } const string mockContent2 = "bbb"; var mockTime2 = DateTime.UtcNow.AddDays(-1); TimelinePostInfo createRes2; { var res = await client.PostAsJsonAsync(generator(1, "posts"), TimelineHelper.TextPostCreateRequest(mockContent2, mockTime2)); var body = res.Should().HaveStatusCode(200) .And.HaveJsonBody() .Which; body.Should().NotBeNull(); 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; } { var res = await client.GetAsync(generator(1, "posts")); res.Should().HaveStatusCode(200) .And.HaveJsonBody() .Which.Should().NotBeNull().And.BeEquivalentTo(createRes, createRes2); } { var res = await client.DeleteAsync(generator(1, $"posts/{createRes.Id}")); res.Should().BeDelete(true); } { var res = await client.DeleteAsync(generator(1, $"posts/{createRes.Id}")); res.Should().BeDelete(false); } { var res = await client.DeleteAsync(generator(1, "posts/30000")); res.Should().BeDelete(false); } { var res = await client.GetAsync(generator(1, "posts")); res.Should().HaveStatusCode(200) .And.HaveJsonBody() .Which.Should().NotBeNull().And.BeEquivalentTo(createRes2); } } } [Theory] [MemberData(nameof(TimelineUrlGeneratorData))] public async Task GetPost_Should_Ordered(TimelineUrlGenerator generator) { using var client = await CreateClientAsUser(); async Task CreatePost(DateTime time) { var res = await client.PostAsJsonAsync(generator(1, "posts"), TimelineHelper.TextPostCreateRequest("aaa", time)); return res.Should().HaveStatusCode(200) .And.HaveJsonBody() .Which.Id; } var now = DateTime.UtcNow; var id0 = await CreatePost(now.AddDays(1)); var id1 = await CreatePost(now.AddDays(-1)); var id2 = await CreatePost(now); { var res = await client.GetAsync(generator(1, "posts")); res.Should().HaveStatusCode(200) .And.HaveJsonBody() .Which.Select(p => p.Id).Should().Equal(id1, id2, id0); } } [Theory] [MemberData(nameof(TimelineUrlGeneratorData))] public async Task CreatePost_InvalidModel(TimelineUrlGenerator generator) { using var client = await CreateClientAsUser(); { var res = await client.PostAsJsonAsync(generator(1, "posts"), new TimelinePostCreateRequest { Content = null }); res.Should().BeInvalidModel(); } { var res = await client.PostAsJsonAsync(generator(1, "posts"), new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = null } }); res.Should().BeInvalidModel(); } { var res = await client.PostAsJsonAsync(generator(1, "posts"), new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = "hahaha" } }); res.Should().BeInvalidModel(); } { var res = await client.PostAsJsonAsync(generator(1, "posts"), new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = "text", Text = null } }); res.Should().BeInvalidModel(); } { var res = await client.PostAsJsonAsync(generator(1, "posts"), new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = "image", Data = null } }); res.Should().BeInvalidModel(); } { // image not base64 var res = await client.PostAsJsonAsync(generator(1, "posts"), new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = "image", Data = "!!!" } }); res.Should().BeInvalidModel(); } { // image base64 not image var res = await client.PostAsJsonAsync(generator(1, "posts"), new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = "image", Data = Convert.ToBase64String(new byte[] { 0x01, 0x02, 0x03 }) } }); res.Should().BeInvalidModel(); } } [Theory] [MemberData(nameof(TimelineUrlGeneratorData))] public async Task ImagePost_ShouldWork(TimelineUrlGenerator generator) { var imageData = ImageHelper.CreatePngWithSize(100, 200); long postId; string postImageUrl; void AssertPostContent(TimelinePostContentInfo content) { content.Type.Should().Be(TimelinePostContentTypes.Image); content.Url.Should().EndWith(generator(1, $"posts/{postId}/data")); content.Text.Should().Be(null); } using var client = await CreateClientAsUser(); { var res = await client.PostAsJsonAsync(generator(1, "posts"), new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = TimelinePostContentTypes.Image, Data = Convert.ToBase64String(imageData) } }); var body = res.Should().HaveStatusCode(200) .And.HaveJsonBody().Which; postId = body.Id; postImageUrl = body.Content.Url; AssertPostContent(body.Content); } { var res = await client.GetAsync(generator(1, "posts")); var body = res.Should().HaveStatusCode(200) .And.HaveJsonBody().Which; body.Should().HaveCount(1); var post = body[0]; post.Id.Should().Be(postId); AssertPostContent(post.Content); } { var res = await client.GetAsync(generator(1, $"posts/{postId}/data")); res.Content.Headers.ContentType.MediaType.Should().Be("image/png"); var data = await res.Content.ReadAsByteArrayAsync(); var image = Image.Load(data, out var format); image.Width.Should().Be(100); image.Height.Should().Be(200); format.Name.Should().Be(PngFormat.Instance.Name); } { await CacheTestHelper.TestCache(client, generator(1, $"posts/{postId}/data")); } { var res = await client.DeleteAsync(generator(1, $"posts/{postId}")); res.Should().BeDelete(true); } { var res = await client.DeleteAsync(generator(1, $"posts/{postId}")); res.Should().BeDelete(false); } { var res = await client.GetAsync(generator(1, "posts")); res.Should().HaveStatusCode(200) .And.HaveJsonBody() .Which.Should().BeEmpty(); } { using var scope = TestApp.Host.Services.CreateScope(); var database = scope.ServiceProvider.GetRequiredService(); var count = await database.Data.CountAsync(); count.Should().Be(0); } } [Theory] [MemberData(nameof(TimelineUrlGeneratorData))] public async Task ImagePost_400(TimelineUrlGenerator generator) { using var client = await CreateClientAsUser(); { var res = await client.GetAsync(generator(1, "posts/11234/data")); res.Should().HaveStatusCode(404) .And.HaveCommonBody(ErrorCodes.TimelineController.PostNotExist); } long postId; { var res = await client.PostAsJsonAsync(generator(1, "posts"), TimelineHelper.TextPostCreateRequest("aaa")); var body = res.Should().HaveStatusCode(200) .And.HaveJsonBody() .Which; postId = body.Id; } { var res = await client.GetAsync(generator(1, $"posts/{postId}/data")); res.Should().HaveStatusCode(400) .And.HaveCommonBody(ErrorCodes.TimelineController.PostNoData); } } [Theory] [MemberData(nameof(TimelineUrlGeneratorData))] public async Task Timeline_LastModified(TimelineUrlGenerator generator) { using var client = await CreateClientAsUser(); DateTime lastModified; { var res = await client.GetAsync(generator(1)); lastModified = res.Should().HaveStatusCode(200) .And.HaveJsonBody() .Which.LastModified; } await Task.Delay(1000); { var res = await client.PatchAsJsonAsync(generator(1), new TimelinePatchRequest { Description = "123" }); lastModified = res.Should().HaveStatusCode(200) .And.HaveJsonBody() .Which.LastModified.Should().BeAfter(lastModified).And.Subject.Value; } { var res = await client.GetAsync(generator(1)); res.Should().HaveStatusCode(200) .And.HaveJsonBody() .Which.LastModified.Should().Be(lastModified); } await Task.Delay(1000); { var res = await client.PutAsync(generator(1, "members/user2"), null); res.Should().HaveStatusCode(200); } { var res = await client.GetAsync(generator(1)); res.Should().HaveStatusCode(200) .And.HaveJsonBody() .Which.LastModified.Should().BeAfter(lastModified); } } [Theory] [MemberData(nameof(TimelineUrlGeneratorData))] public async Task Post_ModifiedSince(TimelineUrlGenerator generator) { 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(generator(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(generator(1, $"posts/{posts[2].Id}")); res.Should().BeDelete(true); } { var res = await client.GetAsync(generator(1, "posts", new Dictionary { { "modifiedSince", posts[1].LastUpdated.ToString("s", CultureInfo.InvariantCulture) } })); res.Should().HaveStatusCode(200) .And.HaveJsonBody>() .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 { "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); } } [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); } } [Theory] [MemberData(nameof(TimelineUrlGeneratorData))] public async Task Timeline_Get_IfModifiedSince_And_CheckUniqueId(TimelineUrlGenerator urlGenerator) { using var client = await CreateClientAsUser(); DateTime lastModifiedTime; TimelineInfo timeline; string uniqueId; { var res = await client.GetAsync(urlGenerator(1)); var body = res.Should().HaveStatusCode(200) .And.HaveJsonBody().Which; timeline = body; lastModifiedTime = body.LastModified; uniqueId = body.UniqueId; } { using var req = new HttpRequestMessage { RequestUri = new Uri(client.BaseAddress, urlGenerator(1)), Method = HttpMethod.Get, }; req.Headers.IfModifiedSince = lastModifiedTime.AddSeconds(1); var res = await client.SendAsync(req); res.Should().HaveStatusCode(304); } { using var req = new HttpRequestMessage { RequestUri = new Uri(client.BaseAddress, urlGenerator(1)), Method = HttpMethod.Get, }; req.Headers.IfModifiedSince = lastModifiedTime.AddSeconds(-1); var res = await client.SendAsync(req); res.Should().HaveStatusCode(200) .And.HaveJsonBody() .Which.Should().BeEquivalentTo(timeline); } { var res = await client.GetAsync(urlGenerator(1, null, new Dictionary { { "ifModifiedSince", lastModifiedTime.AddSeconds(1).ToString("s", CultureInfo.InvariantCulture) } })); res.Should().HaveStatusCode(304); } { var res = await client.GetAsync(urlGenerator(1, null, new Dictionary { { "ifModifiedSince", lastModifiedTime.AddSeconds(-1).ToString("s", CultureInfo.InvariantCulture) } })); res.Should().HaveStatusCode(200) .And.HaveJsonBody() .Which.Should().BeEquivalentTo(timeline); } { var res = await client.GetAsync(urlGenerator(1, null, new Dictionary { { "ifModifiedSince", lastModifiedTime.AddSeconds(1).ToString("s", CultureInfo.InvariantCulture) }, {"checkUniqueId", uniqueId } })); res.Should().HaveStatusCode(304); } { var testUniqueId = (uniqueId[0] == 'a' ? "b" : "a") + uniqueId[1..]; var res = await client.GetAsync(urlGenerator(1, null, new Dictionary { { "ifModifiedSince", lastModifiedTime.AddSeconds(1).ToString("s", CultureInfo.InvariantCulture) }, {"checkUniqueId", testUniqueId } })); res.Should().HaveStatusCode(200) .And.HaveJsonBody() .Which.Should().BeEquivalentTo(timeline); } } [Theory] [MemberData(nameof(TimelineUrlGeneratorData))] public async Task Title(TimelineUrlGenerator urlGenerator) { using var client = await CreateClientAsUser(); { var res = await client.GetAsync(urlGenerator(1)); var timeline = res.Should().HaveStatusCode(200) .And.HaveJsonBody() .Which; timeline.Title.Should().Be(timeline.Name); } { var res = await client.PatchAsJsonAsync(urlGenerator(1), new TimelinePatchRequest { Title = "atitle" }); res.Should().HaveStatusCode(200) .And.HaveJsonBody() .Which.Title.Should().Be("atitle"); } { var res = await client.GetAsync(urlGenerator(1)); res.Should().HaveStatusCode(200) .And.HaveJsonBody() .Which.Title.Should().Be("atitle"); } } [Fact] public async Task ChangeName() { { using var client = await CreateDefaultClient(); var res = await client.PostAsJsonAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "t1", NewName = "tttttttt" }); res.Should().HaveStatusCode(401); } { using var client = await CreateClientAs(2); var res = await client.PostAsJsonAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "t1", NewName = "tttttttt" }); res.Should().HaveStatusCode(403); } using (var client = await CreateClientAsUser()) { { var res = await client.PostAsJsonAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "!!!", NewName = "tttttttt" }); res.Should().BeInvalidModel(); } { var res = await client.PostAsJsonAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "ttt", NewName = "!!!!" }); res.Should().BeInvalidModel(); } { var res = await client.PostAsJsonAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "ttttt", NewName = "tttttttt" }); res.Should().HaveStatusCode(400).And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.TimelineController.NotExist); } { var res = await client.PostAsJsonAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "t1", NewName = "newt" }); res.Should().HaveStatusCode(200).And.HaveJsonBody().Which.Name.Should().Be("newt"); } { var res = await client.GetAsync("timelines/t1"); res.Should().HaveStatusCode(404); } { var res = await client.GetAsync("timelines/newt"); res.Should().HaveStatusCode(200).And.HaveJsonBody().Which.Name.Should().Be("newt"); } } } [Theory] [MemberData(nameof(TimelineUrlGeneratorData))] public async Task PostDataETag(TimelineUrlGenerator urlGenerator) { using var client = await CreateClientAsUser(); long id; string etag; { var res = await client.PostAsJsonAsync(urlGenerator(1, "posts"), new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = TimelinePostContentTypes.Image, Data = Convert.ToBase64String(ImageHelper.CreatePngWithSize(100, 50)) } }); res.Should().HaveStatusCode(200); var body = await res.ReadBodyAsJsonAsync(); body.Content.ETag.Should().NotBeNullOrEmpty(); id = body.Id; etag = body.Content.ETag; } { var res = await client.GetAsync(urlGenerator(1, $"posts/{id}/data")); res.Should().HaveStatusCode(200); res.Headers.ETag.Should().NotBeNull(); res.Headers.ETag.ToString().Should().Be(etag); } } } }