aboutsummaryrefslogtreecommitdiff
path: root/BackEnd/Timeline.Tests/IntegratedTests
diff options
context:
space:
mode:
authorcrupest <crupest@outlook.com>2020-10-27 19:21:35 +0800
committercrupest <crupest@outlook.com>2020-10-27 19:21:35 +0800
commit05ccb4d8f1bbe3fb64e117136b4a89bcfb0b0b33 (patch)
tree929e514de85eb82a5acb96ecffc6e6d2d95f878f /BackEnd/Timeline.Tests/IntegratedTests
parent986c6f2e3b858d6332eba0b42acc6861cd4d0227 (diff)
downloadtimeline-05ccb4d8f1bbe3fb64e117136b4a89bcfb0b0b33.tar.gz
timeline-05ccb4d8f1bbe3fb64e117136b4a89bcfb0b0b33.tar.bz2
timeline-05ccb4d8f1bbe3fb64e117136b4a89bcfb0b0b33.zip
Split front and back end.
Diffstat (limited to 'BackEnd/Timeline.Tests/IntegratedTests')
-rw-r--r--BackEnd/Timeline.Tests/IntegratedTests/AuthorizationTest.cs52
-rw-r--r--BackEnd/Timeline.Tests/IntegratedTests/FrontEndTest.cs29
-rw-r--r--BackEnd/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs164
-rw-r--r--BackEnd/Timeline.Tests/IntegratedTests/TimelineTest.cs1523
-rw-r--r--BackEnd/Timeline.Tests/IntegratedTests/TokenTest.cs165
-rw-r--r--BackEnd/Timeline.Tests/IntegratedTests/UnknownEndpointTest.cs21
-rw-r--r--BackEnd/Timeline.Tests/IntegratedTests/UserAvatarTest.cs251
-rw-r--r--BackEnd/Timeline.Tests/IntegratedTests/UserTest.cs447
8 files changed, 2652 insertions, 0 deletions
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<UserInfo> 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<User>()
+ {
+ 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<UserInfo>();
+
+ var userService = scope.ServiceProvider.GetRequiredService<IUserService>();
+ 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<UserInfo>(s, options));
+ }
+
+ UserInfos = userInfoList;
+ }
+
+ await OnInitializeAsync();
+ }
+
+ public async Task DisposeAsync()
+ {
+ await OnDisposeAsync();
+ OnDispose();
+ await TestApp.DisposeAsync();
+ }
+
+ public Task<HttpClient> 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<HttpClient> 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<CreateTokenResponse>().Which.Token;
+ client.DefaultRequestHeaders.Add("Authorization", "Bearer " + token);
+ return client;
+ }
+
+ public Task<HttpClient> 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<HttpClient> CreateClientAsAdministrator(bool setApiBase = true)
+ {
+ return CreateClientAs(0, setApiBase);
+ }
+
+ public Task<HttpClient> 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<TimelineInfo> _testTimelines;
+
+ private async Task CreateTestTimelines()
+ {
+ _testTimelines = new List<TimelineInfo>();
+ 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<TimelineInfo>().Which;
+ _testTimelines.Add(timelineInfo);
+ }
+ }
+
+ private static string CalculateUrlTail(string subpath, ICollection<KeyValuePair<string, string>> 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<KeyValuePair<string, string>> query = null)
+ {
+ return $"timelines/@{(id == 0 ? "admin" : ("user" + id))}{CalculateUrlTail(subpath, query)}";
+ }
+
+ private static string GenerateOrdinaryTimelineUrl(int id, string subpath = null, ICollection<KeyValuePair<string, string>> query = null)
+ {
+ return $"timelines/t{id}{CalculateUrlTail(subpath, query)}";
+ }
+
+ public delegate string TimelineUrlGenerator(int userId, string subpath = null, ICollection<KeyValuePair<string, string>> query = null);
+
+ public static IEnumerable<object[]> 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<object[]> TimelineUrlByNameGeneratorData()
+ {
+ yield return new[] { new Func<string, string, string>(GeneratePersonalTimelineUrlByName) };
+ yield return new[] { new Func<string, string, string>(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<TimelineInfo>().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<TimelineInfo>().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<TimelineInfo>().Which;
+ }
+
+ {
+ var testResult = new List<TimelineInfo>();
+ testResult.Add(user1Timeline);
+ testResult.AddRange(_testTimelines);
+
+ var res = await client.GetAsync("timelines");
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<List<TimelineInfo>>()
+ .Which.Should().BeEquivalentTo(testResult);
+ }
+ }
+
+ [Fact]
+ public async Task TimelineList_WithQuery()
+ {
+ var testResultRelate = new List<TimelineInfo>();
+ var testResultOwn = new List<TimelineInfo>();
+ var testResultJoin = new List<TimelineInfo>();
+ var testResultOwnPrivate = new List<TimelineInfo>();
+ var testResultRelatePublic = new List<TimelineInfo>();
+ var testResultRelateRegister = new List<TimelineInfo>();
+ var testResultJoinPrivate = new List<TimelineInfo>();
+ var testResultPublic = new List<TimelineInfo>();
+
+ {
+ 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<TimelineInfo>().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<TimelineInfo>().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<TimelineInfo>().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<TimelineInfo>().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<TimelineInfo>().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<TimelineInfo>().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<List<TimelineInfo>>()
+ .Which;
+ body.Should().BeEquivalentTo(testResultRelate);
+ }
+
+ {
+ var res = await client.GetAsync("timelines?relate=user3&relateType=own");
+ var body = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<List<TimelineInfo>>()
+ .Which;
+ body.Should().BeEquivalentTo(testResultOwn);
+ }
+
+ {
+ var res = await client.GetAsync("timelines?relate=user3&visibility=public");
+ var body = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<List<TimelineInfo>>()
+ .Which;
+ body.Should().BeEquivalentTo(testResultRelatePublic);
+ }
+
+ {
+ var res = await client.GetAsync("timelines?relate=user3&visibility=register");
+ var body = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<List<TimelineInfo>>()
+ .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<List<TimelineInfo>>()
+ .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<List<TimelineInfo>>()
+ .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<List<TimelineInfo>>()
+ .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<TimelineInfo>().Which;
+ }
+
+ {
+ var res = await client.GetAsync("timelines/aaa");
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>()
+ .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<string, string, string> 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<string, string, string> 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<TimelineInfo>()
+ .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<TimelineInfo>().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<TimelineInfo>().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<TimelineInfo>().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<UserInfo> members)
+ {
+ var res = await client.GetAsync(getUrl);
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>()
+ .Which.Members.Should().NotBeNull().And.BeEquivalentTo(members);
+ }
+
+ async Task AssertEmptyMembers()
+ {
+ var res = await client.GetAsync(getUrl);
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>()
+ .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<UserInfo> { 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<object[]> 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<long> 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<TimelinePostInfo>()
+ .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<TimelinePostInfo[]>()
+ .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<TimelinePostInfo>()
+ .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<TimelinePostInfo[]>()
+ .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<TimelinePostInfo>()
+ .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<TimelinePostInfo[]>()
+ .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<TimelinePostInfo[]>()
+ .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<long> CreatePost(DateTime time)
+ {
+ var res = await client.PostAsJsonAsync(generator(1, "posts"),
+ TimelineHelper.TextPostCreateRequest("aaa", time));
+ return res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelinePostInfo>()
+ .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<TimelinePostInfo[]>()
+ .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<TimelinePostInfo>().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<TimelinePostInfo[]>().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<TimelinePostInfo[]>()
+ .Which.Should().BeEmpty();
+ }
+
+ {
+ using var scope = TestApp.Host.Services.CreateScope();
+ var database = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
+ 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<TimelinePostInfo>()
+ .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<TimelineInfo>()
+ .Which.LastModified;
+ }
+
+ await Task.Delay(1000);
+
+ {
+ var res = await client.PatchAsJsonAsync(generator(1), new TimelinePatchRequest { Description = "123" });
+ lastModified = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>()
+ .Which.LastModified.Should().BeAfter(lastModified).And.Subject.Value;
+ }
+
+ {
+ var res = await client.GetAsync(generator(1));
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>()
+ .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<TimelineInfo>()
+ .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<string> { "a", "b", "c", "d" };
+ var posts = new List<TimelinePostInfo>();
+
+ foreach (var content in postContentList)
+ {
+ var res = await client.PostAsJsonAsync(generator(1, "posts"),
+ new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Text = content, Type = TimelinePostContentTypes.Text } });
+ var post = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelinePostInfo>().Which;
+ posts.Add(post);
+ await Task.Delay(1000);
+ }
+
+ {
+ var res = await client.DeleteAsync(generator(1, $"posts/{posts[2].Id}"));
+ res.Should().BeDelete(true);
+ }
+
+ {
+ var res = await client.GetAsync(generator(1, "posts",
+ new Dictionary<string, string> { { "modifiedSince", posts[1].LastUpdated.ToString("s", CultureInfo.InvariantCulture) } }));
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<List<TimelinePostInfo>>()
+ .Which.Should().HaveCount(2)
+ .And.Subject.Select(p => p.Content.Text).Should().Equal("b", "d");
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(TimelineUrlGeneratorData))]
+ public async Task PostList_IncludeDeleted(TimelineUrlGenerator urlGenerator)
+ {
+ using var client = await CreateClientAsUser();
+
+ var postContentList = new List<string> { "a", "b", "c", "d" };
+ var posts = new List<TimelinePostInfo>();
+
+ foreach (var content in postContentList)
+ {
+ var res = await client.PostAsJsonAsync(urlGenerator(1, "posts"),
+ new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Text = content, Type = TimelinePostContentTypes.Text } });
+ posts.Add(res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelinePostInfo>().Which);
+ }
+
+ foreach (var id in new long[] { posts[0].Id, posts[2].Id })
+ {
+ var res = await client.DeleteAsync(urlGenerator(1, $"posts/{id}"));
+ res.Should().BeDelete(true);
+ }
+
+ {
+ var res = await client.GetAsync(urlGenerator(1, "posts", new Dictionary<string, string> { ["includeDeleted"] = "true" }));
+ posts = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<List<TimelinePostInfo>>()
+ .Which;
+ posts.Should().HaveCount(4);
+ posts.Select(p => p.Deleted).Should().Equal(true, false, true, false);
+ posts.Select(p => p.Content == null).Should().Equal(true, false, true, false);
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(TimelineUrlGeneratorData))]
+ public async Task Post_ModifiedSince_And_IncludeDeleted(TimelineUrlGenerator urlGenerator)
+ {
+ using var client = await CreateClientAsUser();
+
+ var postContentList = new List<string> { "a", "b", "c", "d" };
+ var posts = new List<TimelinePostInfo>();
+
+ foreach (var (content, index) in postContentList.Select((v, i) => (v, i)))
+ {
+ var res = await client.PostAsJsonAsync(urlGenerator(1, "posts"),
+ new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Text = content, Type = TimelinePostContentTypes.Text } });
+ var post = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelinePostInfo>().Which;
+ posts.Add(post);
+ await Task.Delay(1000);
+ }
+
+ {
+ var res = await client.DeleteAsync(urlGenerator(1, $"posts/{posts[2].Id}"));
+ res.Should().BeDelete(true);
+ }
+
+ {
+
+ var res = await client.GetAsync(urlGenerator(1, "posts",
+ new Dictionary<string, string> {
+ { "modifiedSince", posts[1].LastUpdated.ToString("s", CultureInfo.InvariantCulture) },
+ { "includeDeleted", "true" }
+ }));
+ posts = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<List<TimelinePostInfo>>()
+ .Which;
+ posts.Should().HaveCount(3);
+ posts.Select(p => p.Deleted).Should().Equal(false, true, false);
+ posts.Select(p => p.Content == null).Should().Equal(false, true, false);
+ }
+ }
+
+ [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<TimelineInfo>().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<TimelineInfo>()
+ .Which.Should().BeEquivalentTo(timeline);
+ }
+
+ {
+ var res = await client.GetAsync(urlGenerator(1, null,
+ new Dictionary<string, string> { { "ifModifiedSince", lastModifiedTime.AddSeconds(1).ToString("s", CultureInfo.InvariantCulture) } }));
+ res.Should().HaveStatusCode(304);
+ }
+
+ {
+ var res = await client.GetAsync(urlGenerator(1, null,
+ new Dictionary<string, string> { { "ifModifiedSince", lastModifiedTime.AddSeconds(-1).ToString("s", CultureInfo.InvariantCulture) } }));
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>()
+ .Which.Should().BeEquivalentTo(timeline);
+ }
+
+ {
+ var res = await client.GetAsync(urlGenerator(1, null,
+ new Dictionary<string, string> { { "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<string, string> { { "ifModifiedSince", lastModifiedTime.AddSeconds(1).ToString("s", CultureInfo.InvariantCulture) },
+ {"checkUniqueId", testUniqueId } }));
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>()
+ .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<TimelineInfo>()
+ .Which;
+ timeline.Title.Should().Be(timeline.Name);
+ }
+
+ {
+ var res = await client.PatchAsJsonAsync(urlGenerator(1), new TimelinePatchRequest { Title = "atitle" });
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>()
+ .Which.Title.Should().Be("atitle");
+ }
+
+ {
+ var res = await client.GetAsync(urlGenerator(1));
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>()
+ .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<TimelineInfo>().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<TimelineInfo>().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<TimelinePostInfo>();
+ 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<CreateTokenResponse> 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<CreateTokenResponse>().Which;
+ }
+
+ public static IEnumerable<object[]> 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<object[]> 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<CreateTokenResponse>().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<IUserService>();
+ 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<IUserDeleteService>();
+ 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<VerifyTokenResponse>()
+ .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<IWebHostEnvironment>();
+ 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<UserInfo[]>()
+ .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<UserInfo[]>()
+ .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<UserInfo[]>()
+ .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<UserInfo>()
+ .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<UserInfo>()
+ .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<UserInfo>()
+ .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<UserInfo>()
+ .Which.Nickname.Should().Be("aaa");
+ }
+
+ {
+ var res = await client.GetAsync("users/user1");
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<UserInfo>()
+ .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<UserInfo>()
+ .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<UserInfo>()
+ .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<object[]> 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<UserInfo>().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<UserInfo>().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<object[]> 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<object[]> 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);
+ }
+ }
+}