From ac769e656b122ff569c3f1534701b71e00fed586 Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 27 Oct 2020 19:21:35 +0800 Subject: Split front and back end. --- .../IntegratedTests/AuthorizationTest.cs | 52 + .../Timeline.Tests/IntegratedTests/FrontEndTest.cs | 29 + .../IntegratedTests/IntegratedTestBase.cs | 164 +++ .../Timeline.Tests/IntegratedTests/TimelineTest.cs | 1523 ++++++++++++++++++++ .../Timeline.Tests/IntegratedTests/TokenTest.cs | 165 +++ .../IntegratedTests/UnknownEndpointTest.cs | 21 + .../IntegratedTests/UserAvatarTest.cs | 251 ++++ BackEnd/Timeline.Tests/IntegratedTests/UserTest.cs | 447 ++++++ 8 files changed, 2652 insertions(+) create mode 100644 BackEnd/Timeline.Tests/IntegratedTests/AuthorizationTest.cs create mode 100644 BackEnd/Timeline.Tests/IntegratedTests/FrontEndTest.cs create mode 100644 BackEnd/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs create mode 100644 BackEnd/Timeline.Tests/IntegratedTests/TimelineTest.cs create mode 100644 BackEnd/Timeline.Tests/IntegratedTests/TokenTest.cs create mode 100644 BackEnd/Timeline.Tests/IntegratedTests/UnknownEndpointTest.cs create mode 100644 BackEnd/Timeline.Tests/IntegratedTests/UserAvatarTest.cs create mode 100644 BackEnd/Timeline.Tests/IntegratedTests/UserTest.cs (limited to 'BackEnd/Timeline.Tests/IntegratedTests') diff --git a/BackEnd/Timeline.Tests/IntegratedTests/AuthorizationTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/AuthorizationTest.cs new file mode 100644 index 00000000..38071394 --- /dev/null +++ b/BackEnd/Timeline.Tests/IntegratedTests/AuthorizationTest.cs @@ -0,0 +1,52 @@ +using FluentAssertions; +using System.Net; +using System.Threading.Tasks; +using Timeline.Tests.Helpers; +using Xunit; + +namespace Timeline.Tests.IntegratedTests +{ + public class AuthorizationTest : IntegratedTestBase + { + private const string BaseUrl = "testing/auth/"; + private const string AuthorizeUrl = BaseUrl + "Authorize"; + private const string UserUrl = BaseUrl + "User"; + private const string AdminUrl = BaseUrl + "Admin"; + + [Fact] + public async Task UnauthenticationTest() + { + using var client = await CreateDefaultClient(); + var response = await client.GetAsync(AuthorizeUrl); + response.Should().HaveStatusCode(HttpStatusCode.Unauthorized); + } + + [Fact] + public async Task AuthenticationTest() + { + using var client = await CreateClientAsUser(); + var response = await client.GetAsync(AuthorizeUrl); + response.Should().HaveStatusCode(HttpStatusCode.OK); + } + + [Fact] + public async Task UserAuthorizationTest() + { + using var client = await CreateClientAsUser(); + var response1 = await client.GetAsync(UserUrl); + response1.Should().HaveStatusCode(HttpStatusCode.OK); + var response2 = await client.GetAsync(AdminUrl); + response2.Should().HaveStatusCode(HttpStatusCode.Forbidden); + } + + [Fact] + public async Task AdminAuthorizationTest() + { + using var client = await CreateClientAsAdministrator(); + var response1 = await client.GetAsync(UserUrl); + response1.Should().HaveStatusCode(HttpStatusCode.OK); + var response2 = await client.GetAsync(AdminUrl); + response2.Should().HaveStatusCode(HttpStatusCode.OK); + } + } +} diff --git a/BackEnd/Timeline.Tests/IntegratedTests/FrontEndTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/FrontEndTest.cs new file mode 100644 index 00000000..39a6e545 --- /dev/null +++ b/BackEnd/Timeline.Tests/IntegratedTests/FrontEndTest.cs @@ -0,0 +1,29 @@ +using FluentAssertions; +using System.Net.Mime; +using System.Threading.Tasks; +using Timeline.Tests.Helpers; +using Xunit; + +namespace Timeline.Tests.IntegratedTests +{ + public class FrontEndTest : IntegratedTestBase + { + [Fact] + public async Task Index() + { + using var client = await CreateDefaultClient(false); + var res = await client.GetAsync("index.html"); + res.Should().HaveStatusCode(200); + res.Content.Headers.ContentType.MediaType.Should().Be(MediaTypeNames.Text.Html); + } + + [Fact] + public async Task Fallback() + { + using var client = await CreateDefaultClient(false); + var res = await client.GetAsync("aaaaaaaaaaaaaaa"); + res.Should().HaveStatusCode(200); + res.Content.Headers.ContentType.MediaType.Should().Be(MediaTypeNames.Text.Html); + } + } +} diff --git a/BackEnd/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs b/BackEnd/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs new file mode 100644 index 00000000..7cf27297 --- /dev/null +++ b/BackEnd/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs @@ -0,0 +1,164 @@ +using FluentAssertions; +using Microsoft.AspNetCore.TestHost; +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; +using Xunit; + +namespace Timeline.Tests.IntegratedTests +{ + public abstract class IntegratedTestBase : IAsyncLifetime + { + protected TestApplication TestApp { get; } + + public IReadOnlyList UserInfos { get; private set; } + + private readonly int _userCount; + + public IntegratedTestBase() : this(1) + { + + } + + public IntegratedTestBase(int userCount) + { + if (userCount < 0) + throw new ArgumentOutOfRangeException(nameof(userCount), userCount, "User count can't be negative."); + + _userCount = userCount; + + TestApp = new TestApplication(); + } + + protected virtual Task OnInitializeAsync() + { + return Task.CompletedTask; + } + + protected virtual Task OnDisposeAsync() + { + return Task.CompletedTask; + } + + protected virtual void OnDispose() + { + + } + + public async Task InitializeAsync() + { + await TestApp.InitializeAsync(); + + using (var scope = TestApp.Host.Services.CreateScope()) + { + var users = new List() + { + new User + { + Username = "admin", + Password = "adminpw", + Administrator = true, + Nickname = "administrator" + } + }; + + for (int i = 1; i <= _userCount; i++) + { + users.Add(new User + { + Username = $"user{i}", + Password = $"user{i}pw", + Administrator = false, + Nickname = $"imuser{i}" + }); + } + + var userInfoList = new List(); + + var userService = scope.ServiceProvider.GetRequiredService(); + foreach (var user in users) + { + await userService.CreateUser(user); + } + + using var client = await CreateDefaultClient(); + var options = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + options.Converters.Add(new JsonStringEnumConverter()); + options.Converters.Add(new JsonDateTimeConverter()); + foreach (var user in users) + { + var s = await client.GetStringAsync($"users/{user.Username}"); + userInfoList.Add(JsonSerializer.Deserialize(s, options)); + } + + UserInfos = userInfoList; + } + + await OnInitializeAsync(); + } + + public async Task DisposeAsync() + { + await OnDisposeAsync(); + OnDispose(); + await TestApp.DisposeAsync(); + } + + public Task CreateDefaultClient(bool setApiBase = true) + { + var client = TestApp.Host.GetTestServer().CreateClient(); + if (setApiBase) + { + client.BaseAddress = new Uri(client.BaseAddress, "api/"); + } + return Task.FromResult(client); + } + + public async Task CreateClientWithCredential(string username, string password, bool setApiBase = true) + { + var client = TestApp.Host.GetTestServer().CreateClient(); + if (setApiBase) + { + client.BaseAddress = new Uri(client.BaseAddress, "api/"); + } + var response = await client.PostAsJsonAsync("token/create", + new CreateTokenRequest { Username = username, Password = password }); + var token = response.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which.Token; + client.DefaultRequestHeaders.Add("Authorization", "Bearer " + token); + return client; + } + + public Task CreateClientAs(int userNumber, bool setApiBase = true) + { + if (userNumber < 0) + return CreateDefaultClient(setApiBase); + if (userNumber == 0) + return CreateClientWithCredential("admin", "adminpw", setApiBase); + else + return CreateClientWithCredential($"user{userNumber}", $"user{userNumber}pw", setApiBase); + } + + public Task CreateClientAsAdministrator(bool setApiBase = true) + { + return CreateClientAs(0, setApiBase); + } + + public Task CreateClientAsUser(bool setApiBase = true) + { + return CreateClientAs(1, setApiBase); + } + } +} diff --git a/BackEnd/Timeline.Tests/IntegratedTests/TimelineTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/TimelineTest.cs new file mode 100644 index 00000000..ec46b96a --- /dev/null +++ b/BackEnd/Timeline.Tests/IntegratedTests/TimelineTest.cs @@ -0,0 +1,1523 @@ +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); + } + } + } +} diff --git a/BackEnd/Timeline.Tests/IntegratedTests/TokenTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/TokenTest.cs new file mode 100644 index 00000000..480d66cd --- /dev/null +++ b/BackEnd/Timeline.Tests/IntegratedTests/TokenTest.cs @@ -0,0 +1,165 @@ +using FluentAssertions; +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; +using Xunit; + +namespace Timeline.Tests.IntegratedTests +{ + public class TokenTest : IntegratedTestBase + { + private const string CreateTokenUrl = "token/create"; + private const string VerifyTokenUrl = "token/verify"; + + private static async Task CreateUserTokenAsync(HttpClient client, string username, string password, int? expireOffset = null) + { + var response = await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest { Username = username, Password = password, Expire = expireOffset }); + return response.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + } + + public static IEnumerable CreateToken_InvalidModel_Data() + { + yield return new[] { null, "p", null }; + yield return new[] { "u", null, null }; + yield return new object[] { "u", "p", 2000 }; + yield return new object[] { "u", "p", -1 }; + } + + [Theory] + [MemberData(nameof(CreateToken_InvalidModel_Data))] + public async Task CreateToken_InvalidModel(string username, string password, int expire) + { + using var client = await CreateDefaultClient(); + (await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest + { + Username = username, + Password = password, + Expire = expire + })).Should().BeInvalidModel(); + } + + public static IEnumerable CreateToken_UserCredential_Data() + { + yield return new[] { "usernotexist", "p" }; + yield return new[] { "user1", "???" }; + } + + [Theory] + [MemberData(nameof(CreateToken_UserCredential_Data))] + public async void CreateToken_UserCredential(string username, string password) + { + using var client = await CreateDefaultClient(); + var response = await client.PostAsJsonAsync(CreateTokenUrl, + new CreateTokenRequest { Username = username, Password = password }); + response.Should().HaveStatusCode(400) + .And.HaveCommonBody() + .Which.Code.Should().Be(ErrorCodes.TokenController.Create_BadCredential); + } + + [Fact] + public async Task CreateToken_Success() + { + using var client = await CreateDefaultClient(); + var response = await client.PostAsJsonAsync(CreateTokenUrl, + new CreateTokenRequest { Username = "user1", Password = "user1pw" }); + var body = response.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + body.Token.Should().NotBeNullOrWhiteSpace(); + body.User.Should().BeEquivalentTo(UserInfos[1]); + } + + [Fact] + public async Task VerifyToken_InvalidModel() + { + using var client = await CreateDefaultClient(); + (await client.PostAsJsonAsync(VerifyTokenUrl, + new VerifyTokenRequest { Token = null })).Should().BeInvalidModel(); + } + + [Fact] + public async Task VerifyToken_BadFormat() + { + using var client = await CreateDefaultClient(); + var response = await client.PostAsJsonAsync(VerifyTokenUrl, + new VerifyTokenRequest { Token = "bad token hahaha" }); + response.Should().HaveStatusCode(400) + .And.HaveCommonBody() + .Which.Code.Should().Be(ErrorCodes.TokenController.Verify_BadFormat); + } + + [Fact] + public async Task VerifyToken_OldVersion() + { + using var client = await CreateDefaultClient(); + var token = (await CreateUserTokenAsync(client, "user1", "user1pw")).Token; + + using (var scope = TestApp.Host.Services.CreateScope()) // UserService is scoped. + { + // create a user for test + var userService = scope.ServiceProvider.GetRequiredService(); + await userService.ModifyUser("user1", new User { Password = "user1pw" }); + } + + (await client.PostAsJsonAsync(VerifyTokenUrl, + new VerifyTokenRequest { Token = token })) + .Should().HaveStatusCode(400) + .And.HaveCommonBody() + .Which.Code.Should().Be(ErrorCodes.TokenController.Verify_OldVersion); + } + + [Fact] + public async Task VerifyToken_UserNotExist() + { + using var client = await CreateDefaultClient(); + var token = (await CreateUserTokenAsync(client, "user1", "user1pw")).Token; + + using (var scope = TestApp.Host.Services.CreateScope()) // UserDeleteService is scoped. + { + var userService = scope.ServiceProvider.GetRequiredService(); + await userService.DeleteUser("user1"); + } + + (await client.PostAsJsonAsync(VerifyTokenUrl, + new VerifyTokenRequest { Token = token })) + .Should().HaveStatusCode(400) + .And.HaveCommonBody() + .Which.Code.Should().Be(ErrorCodes.TokenController.Verify_UserNotExist); + } + + //[Fact] + //public async Task VerifyToken_Expired() + //{ + // using (var client = await CreateClientWithNoAuth()) + // { + // // I can only control the token expired time but not current time + // // because verify logic is encapsuled in other library. + // var mockClock = _factory.GetTestClock(); + // mockClock.MockCurrentTime = DateTime.Now - TimeSpan.FromDays(2); + // var token = (await client.CreateUserTokenAsync(MockUsers.UserUsername, MockUsers.UserPassword, 1)).Token; + // var response = await client.PostAsJsonAsync(VerifyTokenUrl, + // new VerifyTokenRequest { Token = token }); + // response.Should().HaveStatusCodeBadRequest() + // .And.Should().HaveBodyAsCommonResponseWithCode(TokenController.ErrorCodes.Verify_Expired); + // mockClock.MockCurrentTime = null; + // } + //} + + [Fact] + public async Task VerifyToken_Success() + { + using var client = await CreateDefaultClient(); + var createTokenResult = await CreateUserTokenAsync(client, "user1", "user1pw"); + var response = await client.PostAsJsonAsync(VerifyTokenUrl, + new VerifyTokenRequest { Token = createTokenResult.Token }); + response.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.User.Should().BeEquivalentTo(UserInfos[1]); + } + } +} diff --git a/BackEnd/Timeline.Tests/IntegratedTests/UnknownEndpointTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/UnknownEndpointTest.cs new file mode 100644 index 00000000..732232e2 --- /dev/null +++ b/BackEnd/Timeline.Tests/IntegratedTests/UnknownEndpointTest.cs @@ -0,0 +1,21 @@ +using FluentAssertions; +using System.Threading.Tasks; +using Timeline.Models.Http; +using Timeline.Tests.Helpers; +using Xunit; + +namespace Timeline.Tests.IntegratedTests +{ + public class UnknownEndpointTest : IntegratedTestBase + { + [Fact] + public async Task UnknownEndpoint() + { + using var client = await CreateDefaultClient(); + var res = await client.GetAsync("unknownEndpoint"); + res.Should().HaveStatusCode(400) + .And.HaveCommonBody() + .Which.Code.Should().Be(ErrorCodes.Common.UnknownEndpoint); + } + } +} diff --git a/BackEnd/Timeline.Tests/IntegratedTests/UserAvatarTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/UserAvatarTest.cs new file mode 100644 index 00000000..f2796005 --- /dev/null +++ b/BackEnd/Timeline.Tests/IntegratedTests/UserAvatarTest.cs @@ -0,0 +1,251 @@ +using FluentAssertions; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Png; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Mime; +using System.Threading.Tasks; +using Timeline.Models.Http; +using Timeline.Services; +using Timeline.Tests.Helpers; +using Xunit; + +namespace Timeline.Tests.IntegratedTests +{ + public class UserAvatarTest : IntegratedTestBase + { + [Fact] + public async Task Test() + { + Avatar mockAvatar = new Avatar + { + Data = ImageHelper.CreatePngWithSize(100, 100), + Type = PngFormat.Instance.DefaultMimeType + }; + + using (var client = await CreateClientAsUser()) + { + { + var res = await client.GetAsync("users/usernotexist/avatar"); + res.Should().HaveStatusCode(404) + .And.HaveCommonBody() + .Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist); + } + + var env = TestApp.Host.Services.GetRequiredService(); + var defaultAvatarData = await File.ReadAllBytesAsync(Path.Combine(env.ContentRootPath, "default-avatar.png")); + + async Task GetReturnDefault(string username = "user1") + { + var res = await client.GetAsync($"users/{username}/avatar"); + res.Should().HaveStatusCode(200); + res.Content.Headers.ContentType.MediaType.Should().Be("image/png"); + var body = await res.Content.ReadAsByteArrayAsync(); + body.Should().Equal(defaultAvatarData); + } + + { + var res = await client.GetAsync("users/user1/avatar"); + res.Should().HaveStatusCode(200); + res.Content.Headers.ContentType.MediaType.Should().Be("image/png"); + var body = await res.Content.ReadAsByteArrayAsync(); + body.Should().Equal(defaultAvatarData); + } + + await CacheTestHelper.TestCache(client, "users/user1/avatar"); + + await GetReturnDefault("admin"); + + { + using var content = new ByteArrayContent(new[] { (byte)0x00 }); + content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); + var res = await client.PutAsync("users/user1/avatar", content); + res.Should().BeInvalidModel(); + } + + { + using var content = new ByteArrayContent(new[] { (byte)0x00 }); + content.Headers.ContentLength = 1; + var res = await client.PutAsync("users/user1/avatar", content); + res.Should().BeInvalidModel(); + } + + { + using var content = new ByteArrayContent(new[] { (byte)0x00 }); + content.Headers.ContentLength = 0; + content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); + var res = await client.PutAsync("users/user1/avatar", content); + res.Should().BeInvalidModel(); + } + + { + var res = await client.PutByteArrayAsync("users/user1/avatar", new[] { (byte)0x00 }, "image/notaccept"); + res.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); + } + + { + using var content = new ByteArrayContent(new[] { (byte)0x00 }); + content.Headers.ContentLength = 1000 * 1000 * 11; + content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); + var res = await client.PutAsync("users/user1/avatar", content); + res.Should().HaveStatusCode(HttpStatusCode.BadRequest) + .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Common.Content.TooBig); + } + + { + using var content = new ByteArrayContent(new[] { (byte)0x00 }); + content.Headers.ContentLength = 2; + content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); + var res = await client.PutAsync("users/user1/avatar", content); + res.Should().BeInvalidModel(); + } + + { + using var content = new ByteArrayContent(new[] { (byte)0x00, (byte)0x01 }); + content.Headers.ContentLength = 1; + content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); + var res = await client.PutAsync("users/user1/avatar", content); + res.Should().BeInvalidModel(); + } + + { + var res = await client.PutByteArrayAsync("users/user1/avatar", new[] { (byte)0x00 }, "image/png"); + res.Should().HaveStatusCode(HttpStatusCode.BadRequest) + .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.UserAvatar.BadFormat_CantDecode); + } + + { + var res = await client.PutByteArrayAsync("users/user1/avatar", mockAvatar.Data, "image/jpeg"); + res.Should().HaveStatusCode(HttpStatusCode.BadRequest) + .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.UserAvatar.BadFormat_UnmatchedFormat); + } + + { + var res = await client.PutByteArrayAsync("users/user1/avatar", ImageHelper.CreatePngWithSize(100, 200), "image/png"); + res.Should().HaveStatusCode(HttpStatusCode.BadRequest) + .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.UserAvatar.BadFormat_BadSize); + } + + { + var res = await client.PutByteArrayAsync("users/user1/avatar", mockAvatar.Data, mockAvatar.Type); + res.Should().HaveStatusCode(HttpStatusCode.OK); + + var res2 = await client.GetAsync("users/user1/avatar"); + res2.Should().HaveStatusCode(200); + res2.Content.Headers.ContentType.MediaType.Should().Be(mockAvatar.Type); + var body = await res2.Content.ReadAsByteArrayAsync(); + body.Should().Equal(mockAvatar.Data); + } + + IEnumerable<(string, IImageFormat)> formats = new (string, IImageFormat)[] + { + ("image/jpeg", JpegFormat.Instance), + ("image/gif", GifFormat.Instance), + ("image/png", PngFormat.Instance), + }; + + foreach ((var mimeType, var format) in formats) + { + var res = await client.PutByteArrayAsync("users/user1/avatar", ImageHelper.CreateImageWithSize(100, 100, format), mimeType); + res.Should().HaveStatusCode(HttpStatusCode.OK); + } + + { + var res = await client.PutByteArrayAsync("users/admin/avatar", new[] { (byte)0x00 }, "image/png"); + res.Should().HaveStatusCode(HttpStatusCode.Forbidden) + .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Common.Forbid); + } + + { + var res = await client.DeleteAsync("users/admin/avatar"); + res.Should().HaveStatusCode(HttpStatusCode.Forbidden) + .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Common.Forbid); + } + + for (int i = 0; i < 2; i++) // double delete should work. + { + var res = await client.DeleteAsync("users/user1/avatar"); + res.Should().HaveStatusCode(200); + await GetReturnDefault(); + } + } + + // Authorization check. + using (var client = await CreateClientAsAdministrator()) + { + { + var res = await client.PutByteArrayAsync("users/user1/avatar", mockAvatar.Data, mockAvatar.Type); + res.Should().HaveStatusCode(HttpStatusCode.OK); + } + + { + var res = await client.DeleteAsync("users/user1/avatar"); + res.Should().HaveStatusCode(HttpStatusCode.OK); + } + + { + var res = await client.PutByteArrayAsync("users/usernotexist/avatar", new[] { (byte)0x00 }, "image/png"); + res.Should().HaveStatusCode(400) + .And.HaveCommonBody() + .Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist); + } + + { + var res = await client.DeleteAsync("users/usernotexist/avatar"); + res.Should().HaveStatusCode(400) + .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist); + } + } + + // bad username check + using (var client = await CreateClientAsAdministrator()) + { + { + var res = await client.GetAsync("users/u!ser/avatar"); + res.Should().BeInvalidModel(); + } + + { + var res = await client.PutByteArrayAsync("users/u!ser/avatar", ImageHelper.CreatePngWithSize(100, 100), "image/png"); + res.Should().BeInvalidModel(); + } + + { + var res = await client.DeleteAsync("users/u!ser/avatar"); + res.Should().BeInvalidModel(); + } + } + } + + [Fact] + public async Task AvatarPutReturnETag() + { + using var client = await CreateClientAsUser(); + + EntityTagHeaderValue etag; + + { + var image = ImageHelper.CreatePngWithSize(100, 100); + var res = await client.PutByteArrayAsync("users/user1/avatar", image, PngFormat.Instance.DefaultMimeType); + res.Should().HaveStatusCode(200); + etag = res.Headers.ETag; + etag.Should().NotBeNull(); + etag.Tag.Should().NotBeNullOrEmpty(); + } + + { + var res = await client.GetAsync("users/user1/avatar"); + res.Should().HaveStatusCode(200); + res.Headers.ETag.Should().Be(etag); + res.Headers.ETag.Tag.Should().Be(etag.Tag); + } + } + } +} \ No newline at end of file diff --git a/BackEnd/Timeline.Tests/IntegratedTests/UserTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/UserTest.cs new file mode 100644 index 00000000..9dfcc6a5 --- /dev/null +++ b/BackEnd/Timeline.Tests/IntegratedTests/UserTest.cs @@ -0,0 +1,447 @@ +using FluentAssertions; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Timeline.Models.Http; +using Timeline.Tests.Helpers; +using Xunit; + +namespace Timeline.Tests.IntegratedTests +{ + public class UserTest : IntegratedTestBase + { + [Fact] + public void UserListShouldHaveUniqueId() + { + foreach (var user in UserInfos) + { + user.UniqueId.Should().NotBeNullOrWhiteSpace(); + } + } + + [Fact] + public async Task GetList_NoAuth() + { + using var client = await CreateDefaultClient(); + var res = await client.GetAsync("users"); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Should().BeEquivalentTo(UserInfos); + } + + [Fact] + public async Task GetList_User() + { + using var client = await CreateClientAsUser(); + var res = await client.GetAsync("users"); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Should().BeEquivalentTo(UserInfos); + } + + [Fact] + public async Task GetList_Admin() + { + using var client = await CreateClientAsAdministrator(); + var res = await client.GetAsync("users"); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Should().BeEquivalentTo(UserInfos); + } + + [Fact] + public async Task Get_NoAuth() + { + using var client = await CreateDefaultClient(); + var res = await client.GetAsync($"users/admin"); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Should().BeEquivalentTo(UserInfos[0]); + } + + [Fact] + public async Task Get_User() + { + using var client = await CreateClientAsUser(); + var res = await client.GetAsync($"users/admin"); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Should().BeEquivalentTo(UserInfos[0]); + } + + [Fact] + public async Task Get_Admin() + { + using var client = await CreateClientAsAdministrator(); + var res = await client.GetAsync($"users/user1"); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Should().BeEquivalentTo(UserInfos[1]); + } + + [Fact] + public async Task Get_InvalidModel() + { + using var client = await CreateClientAsUser(); + var res = await client.GetAsync("users/aaa!a"); + res.Should().BeInvalidModel(); + } + + [Fact] + public async Task Get_404() + { + using var client = await CreateClientAsUser(); + var res = await client.GetAsync("users/usernotexist"); + res.Should().HaveStatusCode(404) + .And.HaveCommonBody(ErrorCodes.UserCommon.NotExist); + } + + [Fact] + public async Task Patch_User() + { + using var client = await CreateClientAsUser(); + { + var res = await client.PatchAsJsonAsync("users/user1", + new UserPatchRequest { Nickname = "aaa" }); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Nickname.Should().Be("aaa"); + } + + { + var res = await client.GetAsync("users/user1"); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Nickname.Should().Be("aaa"); + } + } + + [Fact] + public async Task Patch_Admin() + { + using var client = await CreateClientAsAdministrator(); + using var userClient = await CreateClientAsUser(); + + { + var res = await client.PatchAsJsonAsync("users/user1", + new UserPatchRequest + { + Username = "newuser", + Password = "newpw", + Administrator = true, + Nickname = "aaa" + }); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which; + body.Administrator.Should().Be(true); + body.Nickname.Should().Be("aaa"); + } + + { + var res = await client.GetAsync("users/newuser"); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which; + body.Administrator.Should().Be(true); + body.Nickname.Should().Be("aaa"); + } + + { + // Token should expire. + var res = await userClient.GetAsync("testing/auth/Authorize"); + res.Should().HaveStatusCode(HttpStatusCode.Unauthorized); + } + + { + // Check password. + (await CreateClientWithCredential("newuser", "newpw")).Dispose(); + } + } + + [Fact] + public async Task Patch_NotExist() + { + using var client = await CreateClientAsAdministrator(); + var res = await client.PatchAsJsonAsync("users/usernotexist", new UserPatchRequest { }); + res.Should().HaveStatusCode(404) + .And.HaveCommonBody() + .Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist); + } + + [Fact] + public async Task Patch_InvalidModel() + { + using var client = await CreateClientAsAdministrator(); + var res = await client.PatchAsJsonAsync("users/aaa!a", new UserPatchRequest { }); + res.Should().BeInvalidModel(); + } + + public static IEnumerable Patch_InvalidModel_Body_Data() + { + yield return new[] { new UserPatchRequest { Username = "aaa!a" } }; + yield return new[] { new UserPatchRequest { Password = "" } }; + yield return new[] { new UserPatchRequest { Nickname = new string('a', 50) } }; + } + + [Theory] + [MemberData(nameof(Patch_InvalidModel_Body_Data))] + public async Task Patch_InvalidModel_Body(UserPatchRequest body) + { + using var client = await CreateClientAsAdministrator(); + var res = await client.PatchAsJsonAsync("users/user1", body); + res.Should().BeInvalidModel(); + } + + [Fact] + public async Task Patch_UsernameConflict() + { + using var client = await CreateClientAsAdministrator(); + var res = await client.PatchAsJsonAsync("users/user1", new UserPatchRequest { Username = "admin" }); + res.Should().HaveStatusCode(400) + .And.HaveCommonBody(ErrorCodes.UserController.UsernameConflict); + } + + [Fact] + public async Task Patch_NoAuth_Unauthorized() + { + using var client = await CreateDefaultClient(); + var res = await client.PatchAsJsonAsync("users/user1", new UserPatchRequest { Nickname = "aaa" }); + res.Should().HaveStatusCode(HttpStatusCode.Unauthorized); + } + + [Fact] + public async Task Patch_User_Forbid() + { + using var client = await CreateClientAsUser(); + var res = await client.PatchAsJsonAsync("users/admin", new UserPatchRequest { Nickname = "aaa" }); + res.Should().HaveStatusCode(HttpStatusCode.Forbidden); + } + + [Fact] + public async Task Patch_Username_Forbid() + { + using var client = await CreateClientAsUser(); + var res = await client.PatchAsJsonAsync("users/user1", new UserPatchRequest { Username = "aaa" }); + res.Should().HaveStatusCode(HttpStatusCode.Forbidden); + } + + [Fact] + public async Task Patch_Password_Forbid() + { + using var client = await CreateClientAsUser(); + var res = await client.PatchAsJsonAsync("users/user1", new UserPatchRequest { Password = "aaa" }); + res.Should().HaveStatusCode(HttpStatusCode.Forbidden); + } + + [Fact] + public async Task Patch_Administrator_Forbid() + { + using var client = await CreateClientAsUser(); + var res = await client.PatchAsJsonAsync("users/user1", new UserPatchRequest { Administrator = true }); + res.Should().HaveStatusCode(HttpStatusCode.Forbidden); + } + + [Fact] + public async Task Delete_Deleted() + { + using var client = await CreateClientAsAdministrator(); + { + var res = await client.DeleteAsync("users/user1"); + res.Should().BeDelete(true); + } + + { + var res = await client.GetAsync("users/user1"); + res.Should().HaveStatusCode(404); + } + } + + [Fact] + public async Task Delete_NotExist() + { + using var client = await CreateClientAsAdministrator(); + var res = await client.DeleteAsync("users/usernotexist"); + res.Should().BeDelete(false); + } + + [Fact] + public async Task Delete_InvalidModel() + { + using var client = await CreateClientAsAdministrator(); + var res = await client.DeleteAsync("users/aaa!a"); + res.Should().BeInvalidModel(); + } + + [Fact] + public async Task Delete_NoAuth_Unauthorized() + { + using var client = await CreateDefaultClient(); + var res = await client.DeleteAsync("users/aaa!a"); + res.Should().HaveStatusCode(HttpStatusCode.Unauthorized); + } + + [Fact] + public async Task Delete_User_Forbid() + { + using var client = await CreateClientAsUser(); + var res = await client.DeleteAsync("users/aaa!a"); + res.Should().HaveStatusCode(HttpStatusCode.Forbidden); + } + + private const string createUserUrl = "userop/createuser"; + + [Fact] + public async Task Op_CreateUser() + { + using var client = await CreateClientAsAdministrator(); + { + var res = await client.PostAsJsonAsync(createUserUrl, new CreateUserRequest + { + Username = "aaa", + Password = "bbb", + Administrator = true, + Nickname = "ccc" + }); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + body.Username.Should().Be("aaa"); + body.Nickname.Should().Be("ccc"); + body.Administrator.Should().BeTrue(); + } + { + var res = await client.GetAsync("users/aaa"); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + body.Username.Should().Be("aaa"); + body.Nickname.Should().Be("ccc"); + body.Administrator.Should().BeTrue(); + } + { + // Test password. + (await CreateClientWithCredential("aaa", "bbb")).Dispose(); + } + } + + public static IEnumerable Op_CreateUser_InvalidModel_Data() + { + yield return new[] { new CreateUserRequest { Username = "aaa", Password = "bbb" } }; + yield return new[] { new CreateUserRequest { Username = "aaa", Administrator = true } }; + yield return new[] { new CreateUserRequest { Password = "bbb", Administrator = true } }; + yield return new[] { new CreateUserRequest { Username = "a!a", Password = "bbb", Administrator = true } }; + yield return new[] { new CreateUserRequest { Username = "aaa", Password = "", Administrator = true } }; + yield return new[] { new CreateUserRequest { Username = "aaa", Password = "bbb", Administrator = true, Nickname = new string('a', 40) } }; + } + + [Theory] + [MemberData(nameof(Op_CreateUser_InvalidModel_Data))] + public async Task Op_CreateUser_InvalidModel(CreateUserRequest body) + { + using var client = await CreateClientAsAdministrator(); + { + var res = await client.PostAsJsonAsync(createUserUrl, body); + res.Should().BeInvalidModel(); + } + } + + [Fact] + public async Task Op_CreateUser_UsernameConflict() + { + using var client = await CreateClientAsAdministrator(); + { + var res = await client.PostAsJsonAsync(createUserUrl, new CreateUserRequest + { + Username = "user1", + Password = "bbb", + Administrator = false + }); + res.Should().HaveStatusCode(400) + .And.HaveCommonBody(ErrorCodes.UserController.UsernameConflict); + } + } + + [Fact] + public async Task Op_CreateUser_NoAuth_Unauthorized() + { + using var client = await CreateDefaultClient(); + { + var res = await client.PostAsJsonAsync(createUserUrl, new CreateUserRequest + { + Username = "aaa", + Password = "bbb", + Administrator = false + }); + res.Should().HaveStatusCode(HttpStatusCode.Unauthorized); + } + } + + [Fact] + public async Task Op_CreateUser_User_Forbid() + { + using var client = await CreateClientAsUser(); + { + var res = await client.PostAsJsonAsync(createUserUrl, new CreateUserRequest + { + Username = "aaa", + Password = "bbb", + Administrator = false + }); + res.Should().HaveStatusCode(HttpStatusCode.Forbidden); + } + } + + private const string changePasswordUrl = "userop/changepassword"; + + [Fact] + public async Task Op_ChangePassword() + { + using var client = await CreateClientAsUser(); + { + var res = await client.PostAsJsonAsync(changePasswordUrl, + new ChangePasswordRequest { OldPassword = "user1pw", NewPassword = "newpw" }); + res.Should().HaveStatusCode(200); + } + { + var res = await client.PatchAsJsonAsync("users/user1", new UserPatchRequest { }); + res.Should().HaveStatusCode(HttpStatusCode.Unauthorized); + } + { + (await CreateClientWithCredential("user1", "newpw")).Dispose(); + } + } + + public static IEnumerable Op_ChangePassword_InvalidModel_Data() + { + yield return new[] { null, "ppp" }; + yield return new[] { "ppp", null }; + } + + [Theory] + [MemberData(nameof(Op_ChangePassword_InvalidModel_Data))] + public async Task Op_ChangePassword_InvalidModel(string oldPassword, string newPassword) + { + using var client = await CreateClientAsUser(); + var res = await client.PostAsJsonAsync(changePasswordUrl, + new ChangePasswordRequest { OldPassword = oldPassword, NewPassword = newPassword }); + res.Should().BeInvalidModel(); + } + + [Fact] + public async Task Op_ChangePassword_BadOldPassword() + { + using var client = await CreateClientAsUser(); + var res = await client.PostAsJsonAsync(changePasswordUrl, new ChangePasswordRequest { OldPassword = "???", NewPassword = "???" }); + res.Should().HaveStatusCode(400) + .And.HaveCommonBody(ErrorCodes.UserController.ChangePassword_BadOldPassword); + } + + [Fact] + public async Task Op_ChangePassword_NoAuth_Unauthorized() + { + using var client = await CreateDefaultClient(); + var res = await client.PostAsJsonAsync(changePasswordUrl, new ChangePasswordRequest { OldPassword = "???", NewPassword = "???" }); + res.Should().HaveStatusCode(HttpStatusCode.Unauthorized); + } + } +} -- cgit v1.2.3