diff options
Diffstat (limited to 'BackEnd')
11 files changed, 751 insertions, 631 deletions
diff --git a/BackEnd/Timeline.Tests/IntegratedTests/TimelineHubTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/TimelineHubTest.cs index e8b774d8..9b28a648 100644 --- a/BackEnd/Timeline.Tests/IntegratedTests/TimelineHubTest.cs +++ b/BackEnd/Timeline.Tests/IntegratedTests/TimelineHubTest.cs @@ -1,6 +1,7 @@ using FluentAssertions;
using Microsoft.AspNetCore.SignalR.Client;
using System;
+using System.Threading;
using System.Threading.Tasks;
using Timeline.SignalRHub;
using Xunit;
@@ -17,7 +18,7 @@ namespace Timeline.Tests.IntegratedTests private HubConnection CreateConnection(string? token)
{
- return new HubConnectionBuilder().WithUrl("http://localhost/api/hub/timeline",
+ return new HubConnectionBuilder().WithUrl("ws://localhost/api/hub/timeline",
options =>
{
options.HttpMessageHandlerFactory = _ => TestApp.Server.CreateHandler();
@@ -33,36 +34,42 @@ namespace Timeline.Tests.IntegratedTests await using var connection = CreateConnection(token);
+ await connection.StartAsync();
+ connection.State.Should().Be(HubConnectionState.Connected);
+
+ using SemaphoreSlim semaphore = new SemaphoreSlim(0);
+
var changed = false;
connection.On<string>(nameof(ITimelineClient.OnTimelinePostChanged), (timelineName) =>
{
timelineName.Should().Be(generator(1));
changed = true;
+ semaphore.Release();
});
- await connection.StartAsync();
- connection.State.Should().Be(HubConnectionState.Connected);
+ await Task.Run(async () =>
+ {
+ using var client = await CreateClientAsUser();
- using var client = await CreateClientAsUser();
+ await client.TestPostAsync($"timelines/{generator(1)}/posts", TimelinePostTest.CreateTextPostRequest("aaa"));
- await client.TestPostAsync($"timelines/{generator(1)}/posts", TimelinePostTest.CreateTextPostRequest("aaa"));
- await Task.Delay(1);
- changed.Should().BeFalse();
+ changed.Should().BeFalse();
- await connection.InvokeAsync(nameof(TimelineHub.SubscribeTimelinePostChange), generator(1));
+ await connection.InvokeAsync(nameof(TimelineHub.SubscribeTimelinePostChange), generator(1));
- await client.TestPostAsync($"timelines/{generator(1)}/posts", TimelinePostTest.CreateTextPostRequest("bbb"));
- await Task.Delay(1);
- changed.Should().BeTrue();
+ await client.TestPostAsync($"timelines/{generator(1)}/posts", TimelinePostTest.CreateTextPostRequest("bbb"));
+ await semaphore.WaitAsync();
+ changed.Should().BeTrue();
- changed = false;
+ changed = false;
- await connection.InvokeAsync(nameof(TimelineHub.UnsubscribeTimelinePostChange), generator(1));
+ await connection.InvokeAsync(nameof(TimelineHub.UnsubscribeTimelinePostChange), generator(1));
- await client.TestPostAsync($"timelines/{generator(1)}/posts", TimelinePostTest.CreateTextPostRequest("ccc"));
- await Task.Delay(1);
- changed.Should().BeFalse();
+ await client.TestPostAsync($"timelines/{generator(1)}/posts", TimelinePostTest.CreateTextPostRequest("ccc"));
+ changed.Should().BeFalse();
+
+ });
}
[Fact]
diff --git a/BackEnd/Timeline.Tests/IntegratedTests/TimelinePostTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/TimelinePostTest.cs index 749b2cfd..a6853c34 100644 --- a/BackEnd/Timeline.Tests/IntegratedTests/TimelinePostTest.cs +++ b/BackEnd/Timeline.Tests/IntegratedTests/TimelinePostTest.cs @@ -1,18 +1,8 @@ -using FluentAssertions;
-using SixLabors.ImageSharp.Formats.Gif;
-using SixLabors.ImageSharp.Formats.Jpeg;
-using SixLabors.ImageSharp.Formats.Png;
-using System;
+using System;
using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Net;
using System.Text;
-using System.Threading.Tasks;
using Timeline.Models;
using Timeline.Models.Http;
-using Timeline.Tests.Helpers;
-using Xunit;
using Xunit.Abstractions;
namespace Timeline.Tests.IntegratedTests
@@ -36,7 +26,7 @@ namespace Timeline.Tests.IntegratedTests };
}
- private static HttpTimelinePostCreateRequest CreateMarkdownPostRequest(string text, DateTime? time = null, string? color = null)
+ public static HttpTimelinePostCreateRequest CreateMarkdownPostRequest(string text, DateTime? time = null, string? color = null)
{
return new HttpTimelinePostCreateRequest()
{
@@ -57,598 +47,5 @@ namespace Timeline.Tests.IntegratedTests {
}
-
- [Theory]
- [MemberData(nameof(TimelineNameGeneratorTestData))]
- public async Task GetPostsAndVisibility_Should_Work(TimelineNameGenerator generator)
- {
- { // default visibility is registered
- {
- using var client = await CreateDefaultClient();
- await client.TestGetAssertForbiddenAsync($"timelines/{generator(1)}/posts");
- }
-
- {
- using var client = await CreateClientAsUser();
- await client.TestGetAsync($"timelines/{generator(0)}/posts");
- }
- }
-
- { // change visibility to public
- {
- using var client = await CreateClientAsUser();
- await client.PatchTimelineAsync(generator(1), new() { Visibility = TimelineVisibility.Public });
- }
-
- {
- using var client = await CreateDefaultClient();
- await client.TestGetAsync($"timelines/{generator(1)}/posts");
- }
- }
-
- { // change visibility to private
- {
- using var client = await CreateClientAsAdministrator();
- await client.PatchTimelineAsync(generator(1), new() { Visibility = TimelineVisibility.Private });
- await client.PatchTimelineAsync(generator(0), new() { Visibility = TimelineVisibility.Private });
- }
- {
- using var client = await CreateDefaultClient();
- await client.TestGetAssertForbiddenAsync($"timelines/{generator(1)}/posts");
- }
- { // user can't read admin's
- using var client = await CreateClientAsUser();
- await client.TestGetAssertForbiddenAsync($"timelines/{generator(0)}/posts");
- }
- { // admin can read user's
- using var client = await CreateClientAsAdministrator();
- await client.TestGetAsync($"timelines/{generator(1)}/posts");
- }
- { // add member
- using var client = await CreateClientAsAdministrator();
- await client.PutTimelineMemberAsync(generator(0), "user1");
- }
- { // now user can read admin's
- using var client = await CreateClientAsUser();
- await client.TestGetAsync($"timelines/{generator(0)}/posts");
- }
- }
- }
-
- [Theory]
- [MemberData(nameof(TimelineNameGeneratorTestData))]
- public async Task Post_ModifiedSince(TimelineNameGenerator generator)
- {
- using var client = await CreateClientAsUser();
-
- var posts = new List<HttpTimelinePost>();
-
- for (int i = 0; i < 4; i++)
- {
- var post = await client.TestPostAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts", CreateTextPostRequest("a"));
- posts.Add(post);
- await Task.Delay(TimeSpan.FromSeconds(1));
- }
-
- await client.TestDeleteAsync($"timelines/{generator(1)}/posts/{posts[2].Id}");
-
- {
- var body = await client.TestGetAsync<List<HttpTimelinePost>>($"timelines/{generator(1)}/posts?modifiedSince={posts[1].LastUpdated.ToString("s", CultureInfo.InvariantCulture) }");
- body.Should().HaveCount(2)
- .And.Subject.Select(p => p.Id).Should().Equal(posts[1].Id, posts[3].Id);
- }
- }
-
- [Theory]
- [MemberData(nameof(TimelineNameGeneratorTestData))]
- public async Task PostList_IncludeDeleted(TimelineNameGenerator generator)
- {
- using var client = await CreateClientAsUser();
-
- var posts = new List<HttpTimelinePost>();
-
- for (int i = 0; i < 4; i++)
- {
- var body = await client.TestPostAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts", CreateTextPostRequest("a"));
- posts.Add(body);
- }
-
- foreach (var id in new long[] { posts[0].Id, posts[2].Id })
- {
- await client.TestDeleteAsync($"timelines/{generator(1)}/posts/{id}");
- }
-
- {
- posts = await client.TestGetAsync<List<HttpTimelinePost>>($"timelines/{generator(1)}/posts?includeDeleted=true");
- posts.Should().HaveCount(4);
- posts.Select(p => p.Deleted).Should().Equal(true, false, true, false);
- }
- }
-
- [Theory]
- [MemberData(nameof(TimelineNameGeneratorTestData))]
- public async Task Post_ModifiedSince_And_IncludeDeleted(TimelineNameGenerator generator)
- {
- using var client = await CreateClientAsUser();
-
- var posts = new List<HttpTimelinePost>();
-
- for (int i = 0; i < 4; i++)
- {
- var post = await client.TestPostAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts", CreateTextPostRequest("a"));
- posts.Add(post);
- await Task.Delay(TimeSpan.FromSeconds(1));
- }
-
- await client.TestDeleteAsync($"timelines/{generator(1)}/posts/{posts[2].Id}");
-
- {
- posts = await client.TestGetAsync<List<HttpTimelinePost>>($"timelines/{generator(1)}/posts?modifiedSince={posts[1].LastUpdated.ToString("s", CultureInfo.InvariantCulture)}&includeDeleted=true");
- posts.Should().HaveCount(3);
- posts.Select(p => p.Deleted).Should().Equal(false, true, false);
- }
- }
-
- [Theory]
- [MemberData(nameof(TimelineNameGeneratorTestData))]
- public async Task CreatePostPermission_Should_Work(TimelineNameGenerator generator)
- {
- using (var client = await CreateClientAsUser())
- {
- await client.PutTimelineMemberAsync(generator(1), "user2");
- }
-
- using (var client = await CreateDefaultClient())
- { // no auth should get 401
- await client.TestPostAssertUnauthorizedAsync($"timelines/{generator(1)}/posts", CreateTextPostRequest("aaa"));
- }
-
- using (var client = await CreateClientAsUser())
- {
- // post self's
- await client.TestPostAsync($"timelines/{generator(1)}/posts", CreateTextPostRequest("aaa"));
- // post other not as a member should get 403
- await client.TestPostAssertForbiddenAsync($"timelines/{generator(0)}/posts", CreateTextPostRequest("aaa"));
- }
-
- using (var client = await CreateClientAsAdministrator())
- { // post as admin
- await client.TestPostAsync($"timelines/{generator(1)}/posts", CreateTextPostRequest("aaa"));
- }
-
- using (var client = await CreateClientAs(2))
- { // post as member
- await client.TestPostAsync($"timelines/{generator(1)}/posts", CreateTextPostRequest("aaa"));
- }
- }
-
- [Theory]
- [MemberData(nameof(TimelineNameGeneratorTestData))]
- public async Task DeletePostPermission_Should_Work(TimelineNameGenerator generator)
- {
- async Task<long> CreatePost(int userNumber)
- {
- using var client = await CreateClientAs(userNumber);
- var body = await client.TestPostAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts", CreateTextPostRequest("aaa"));
- return body.Id;
- }
-
- using (var client = await CreateClientAsUser())
- {
- await client.PutTimelineMemberAsync(generator(1), "user2");
- await client.PutTimelineMemberAsync(generator(1), "user3");
- }
-
- { // no auth should get 401
- using var client = await CreateDefaultClient();
- await client.TestDeleteAssertUnauthorizedAsync($"timelines/{generator(1)}/posts/12");
- }
-
- { // self can delete self
- var postId = await CreatePost(1);
- using var client = await CreateClientAsUser();
- await client.TestDeleteAsync($"timelines/{generator(1)}/posts/{postId}");
- }
-
- { // admin can delete any
- var postId = await CreatePost(1);
- using var client = await CreateClientAsAdministrator();
- await client.TestDeleteAsync($"timelines/{generator(1)}/posts/{postId}");
- }
-
- { // owner can delete other
- var postId = await CreatePost(2);
- using var client = await CreateClientAsUser();
- await client.TestDeleteAsync($"timelines/{generator(1)}/posts/{postId}");
- }
-
- { // author can delete self
- var postId = await CreatePost(2);
- using var client = await CreateClientAs(2);
- await client.TestDeleteAsync($"timelines/{generator(1)}/posts/{postId}");
- }
-
- { // otherwise is forbidden
- var postId = await CreatePost(2);
- using var client = await CreateClientAs(3);
- await client.TestDeleteAssertForbiddenAsync($"timelines/{generator(1)}/posts/{postId}");
- }
- }
-
- [Theory]
- [MemberData(nameof(TimelineNameGeneratorTestData))]
- public async Task GetPost_Should_Ordered(TimelineNameGenerator generator)
- {
- using var client = await CreateClientAsUser();
-
- async Task<long> CreatePost(DateTime time)
- {
- var body = await client.TestPostAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts", CreateTextPostRequest("aaa", time));
- return body.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 body = await client.TestGetAsync<List<HttpTimelinePost>>($"timelines/{generator(1)}/posts");
- body.Select(p => p.Id).Should().Equal(id1, id2, id0);
- }
- }
-
- [Theory]
- [MemberData(nameof(TimelineNameGeneratorTestData))]
- public async Task Color(TimelineNameGenerator generator)
- {
- using var client = await CreateClientAsUser();
-
- await client.TestPostAssertInvalidModelAsync($"timelines/{generator(1)}/posts", CreateTextPostRequest("a", color: "aa"));
-
- long id;
-
- {
- var post = await client.TestPostAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts",
- CreateTextPostRequest("a", color: "#aabbcc"));
- post.Color.Should().Be("#aabbcc");
- id = post.Id;
- }
-
- {
- var post = await client.TestGetAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts/{id}");
- post.Color.Should().Be("#aabbcc");
- }
- }
-
- [Theory]
- [MemberData(nameof(TimelineNameGeneratorTestData))]
- public async Task GetPost(TimelineNameGenerator generator)
- {
- using var client = await CreateClientAsUser();
-
-
- await client.TestGetAssertNotFoundAsync($"timelines/{generator(1)}/posts/1");
-
- var post = await client.TestPostAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts", CreateTextPostRequest("a"));
-
- var post2 = await client.TestGetAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts/{post.Id}");
- post2.Should().BeEquivalentTo(post);
-
- await client.TestDeleteAsync($"timelines/{generator(1)}/posts/{post.Id}");
-
- await client.TestGetAssertNotFoundAsync($"timelines/{generator(1)}/posts/{post.Id}");
- }
-
- [Theory]
- [MemberData(nameof(TimelineNameGeneratorTestData))]
- public async Task PatchPost(TimelineNameGenerator generator)
- {
- using var client = await CreateClientAsUser();
-
- var post = await client.TestPostAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts",
- CreateTextPostRequest("a"));
-
- var date = new DateTime(2000, 10, 1);
-
- var post2 = await client.TestPatchAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts/{post.Id}", new HttpTimelinePostPatchRequest
- {
- Time = date,
- Color = "#aabbcc"
- });
- post2.Time.Should().Be(date);
- post2.Color.Should().Be("#aabbcc");
-
- var post3 = await client.TestGetAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts/{post.Id}");
- post3.Time.Should().Be(date);
- post3.Color.Should().Be("#aabbcc");
- }
-
- public static IEnumerable<object?[]> CreatePost_InvalidModelTest_TestData()
- {
- var testDataList = new List<List<HttpTimelinePostCreateRequestData>?>()
- {
- null,
- new List<HttpTimelinePostCreateRequestData>(),
- Enumerable.Repeat<HttpTimelinePostCreateRequestData>(new HttpTimelinePostCreateRequestData
- {
- ContentType = "text/plain",
- Data = Convert.ToBase64String(Encoding.UTF8.GetBytes("a"))
- }, 200).ToList(),
- };
-
- var testData = new List<HttpTimelinePostCreateRequestData?>()
- {
- null,
- new HttpTimelinePostCreateRequestData
- {
- ContentType = null!,
- Data = Convert.ToBase64String(Encoding.UTF8.GetBytes("a"))
- },
- new HttpTimelinePostCreateRequestData
- {
- ContentType = "text/plain",
- Data = null!
- },
- new HttpTimelinePostCreateRequestData
- {
- ContentType = "text/xxxxxxx",
- Data = Convert.ToBase64String(Encoding.UTF8.GetBytes("a"))
- },
- new HttpTimelinePostCreateRequestData
- {
- ContentType = "text/plain",
- Data = "aaa"
- },
- new HttpTimelinePostCreateRequestData
- {
- ContentType = "text/plain",
- Data = Convert.ToBase64String(new byte[] {0xE4, 0x1, 0xA0})
- },
- new HttpTimelinePostCreateRequestData
- {
- ContentType = "image/jpeg",
- Data = Convert.ToBase64String(ImageHelper.CreatePngWithSize(100, 100))
- },
- new HttpTimelinePostCreateRequestData
- {
- ContentType = "image/jpeg",
- Data = Convert.ToBase64String(new byte[] { 100, 200 })
- }
-
- };
-
- testDataList.AddRange(testData.Select(d => new List<HttpTimelinePostCreateRequestData>() { d! }));
-
- return TimelineNameGeneratorTestData().AppendTestData(testDataList);
- }
-
- [Theory]
- [MemberData(nameof(CreatePost_InvalidModelTest_TestData))]
- public async Task CreatePost_InvalidModel(TimelineNameGenerator generator, List<HttpTimelinePostCreateRequestData> dataList)
- {
- using var client = await CreateClientAsUser();
-
- await client.TestPostAssertInvalidModelAsync(
- $"timelines/{generator(1)}/posts",
- new HttpTimelinePostCreateRequest
- {
- DataList = dataList
- }
- );
- }
-
- public static IEnumerable<object?[]> CreatePost_ShouldWork_TestData()
- {
- var testByteDatas = new List<ByteData>()
- {
- new ByteData(Encoding.UTF8.GetBytes("aaa"), MimeTypes.TextPlain),
- new ByteData(Encoding.UTF8.GetBytes("aaa"), MimeTypes.TextMarkdown),
- new ByteData(ImageHelper.CreateImageWithSize(100, 50, PngFormat.Instance), MimeTypes.ImagePng),
- new ByteData(ImageHelper.CreateImageWithSize(100, 50, JpegFormat.Instance), MimeTypes.ImageJpeg),
- new ByteData(ImageHelper.CreateImageWithSize(100, 50, GifFormat.Instance), MimeTypes.ImageGif),
- };
-
- return TimelineNameGeneratorTestData().AppendTestData(testByteDatas);
- }
-
- [Theory]
- [MemberData(nameof(CreatePost_ShouldWork_TestData))]
- public async Task CreatePost_ShouldWork(TimelineNameGenerator generator, ByteData data)
- {
- using var client = await CreateClientAsUser();
-
- var post = await client.TestPostAsync<HttpTimelinePost>(
- $"timelines/{generator(1)}/posts",
- new HttpTimelinePostCreateRequest
- {
- DataList = new List<HttpTimelinePostCreateRequestData>
- {
- new HttpTimelinePostCreateRequestData
- {
- ContentType = data.ContentType,
- Data = Convert.ToBase64String(data.Data)
- }
- }
- }
- );
-
- post.DataList.Should().NotBeNull().And.HaveCount(1);
- var postData = post.DataList[0];
- postData.Should().NotBeNull();
- var postDataEtag = postData.ETag;
- postDataEtag.Should().NotBeNullOrEmpty();
-
- {
- var response = await client.GetAsync($"timelines/{generator(1)}/posts/{post.Id}/data");
- response.StatusCode.Should().Be(HttpStatusCode.OK);
- response.Headers.ETag.Should().NotBeNull();
- response.Headers.ETag!.Tag.Should().Be(postDataEtag);
- response.Content.Headers.ContentType.Should().NotBeNull();
- response.Content.Headers.ContentType!.MediaType.Should().Be(data.ContentType);
-
- var body = await response.Content.ReadAsByteArrayAsync();
- body.Should().Equal(data.Data);
- }
-
- {
- var response = await client.GetAsync($"timelines/{generator(1)}/posts/{post.Id}/data/0");
- response.StatusCode.Should().Be(HttpStatusCode.OK);
- response.Headers.ETag.Should().NotBeNull();
- response.Headers.ETag!.Tag.Should().Be(postDataEtag);
- response.Content.Headers.ContentType.Should().NotBeNull();
- response.Content.Headers.ContentType!.MediaType.Should().Be(data.ContentType);
-
- var body = await response.Content.ReadAsByteArrayAsync();
- body.Should().Equal(data.Data);
- }
- }
-
- [Theory]
- [MemberData(nameof(TimelineNameGeneratorTestData))]
- public async Task CreatePost_MultipleData_ShouldWork(TimelineNameGenerator generator)
- {
- using var client = await CreateClientAsUser();
-
- var textData = Encoding.UTF8.GetBytes("aaa");
- var imageData = ImageHelper.CreatePngWithSize(100, 50);
-
- var post = await client.TestPostAsync<HttpTimelinePost>(
- $"timelines/{generator(1)}/posts",
- new HttpTimelinePostCreateRequest
- {
- DataList = new List<HttpTimelinePostCreateRequestData>
- {
- new HttpTimelinePostCreateRequestData
- {
- ContentType = MimeTypes.TextMarkdown,
- Data = Convert.ToBase64String(textData)
- },
- new HttpTimelinePostCreateRequestData
- {
- ContentType = MimeTypes.ImagePng,
- Data = Convert.ToBase64String(imageData)
- }
- }
- }
- );
-
- post.DataList.Should().NotBeNull().And.HaveCount(2);
-
- var postData0 = post.DataList[0];
- postData0.Should().NotBeNull();
- var postDataEtag0 = postData0.ETag;
- postDataEtag0.Should().NotBeNullOrEmpty();
-
- var postData1 = post.DataList[1];
- postData1.Should().NotBeNull();
- var postDataEtag1 = postData1.ETag;
- postDataEtag1.Should().NotBeNullOrEmpty();
-
- {
- var response = await client.GetAsync($"timelines/{generator(1)}/posts/{post.Id}/data");
- response.StatusCode.Should().Be(HttpStatusCode.OK);
- response.Headers.ETag.Should().NotBeNull();
- response.Headers.ETag!.Tag.Should().Be(postDataEtag0);
- response.Content.Headers.ContentType.Should().NotBeNull();
- response.Content.Headers.ContentType!.MediaType.Should().Be(MimeTypes.TextMarkdown);
-
- var body = await response.Content.ReadAsByteArrayAsync();
- body.Should().Equal(textData);
- }
-
- {
- var response = await client.GetAsync($"timelines/{generator(1)}/posts/{post.Id}/data/0");
- response.StatusCode.Should().Be(HttpStatusCode.OK);
- response.Headers.ETag.Should().NotBeNull();
- response.Headers.ETag!.Tag.Should().Be(postDataEtag0);
- response.Content.Headers.ContentType.Should().NotBeNull();
- response.Content.Headers.ContentType!.MediaType.Should().Be(MimeTypes.TextMarkdown);
-
- var body = await response.Content.ReadAsByteArrayAsync();
- body.Should().Equal(textData);
- }
-
- {
- var response = await client.GetAsync($"timelines/{generator(1)}/posts/{post.Id}/data/1");
- response.StatusCode.Should().Be(HttpStatusCode.OK);
- response.Headers.ETag.Should().NotBeNull();
- response.Headers.ETag!.Tag.Should().Be(postDataEtag1);
- response.Content.Headers.ContentType.Should().NotBeNull();
- response.Content.Headers.ContentType!.MediaType.Should().Be(MimeTypes.ImagePng);
-
- var body = await response.Content.ReadAsByteArrayAsync();
- body.Should().Equal(imageData);
- }
- }
-
- [Theory]
- [MemberData(nameof(TimelineNameGeneratorTestData))]
- public async Task Post_Editable(TimelineNameGenerator generator)
- {
- HttpTimelinePost post;
-
- {
- using var client = await CreateClientAsUser();
- post = await client.TestPostAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts", CreateTextPostRequest("a"));
-
- post.Editable.Should().BeTrue();
- }
-
- {
- using var client = await CreateClientAs(2);
- var post2 = await client.TestGetAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts/{post.Id}");
- post2.Editable.Should().BeFalse();
- }
- }
-
- [Theory]
- [MemberData(nameof(TimelineNameGeneratorTestData))]
- public async Task Post_Markdown_Url_Map(TimelineNameGenerator generator)
- {
- using var client = await CreateClientAsUser();
- var post = await client.TestPostAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts", CreateMarkdownPostRequest("[aaa](1) "));
-
- var res = await client.GetAsync($"timelines/{generator(1)}/posts/{post.Id}/data");
- var markdown = await res.Content.ReadAsStringAsync();
-
- markdown.Should().MatchRegex(@$"\[aaa\]\(https?://.*/timelines/{generator(1)}/posts/{post.Id}/data/1\)");
- markdown.Should().MatchRegex(@$"\[bbb\]\(https?://.*/timelines/{generator(1)}/posts/{post.Id}/data/2\)");
- }
-
- [Theory]
- [MemberData(nameof(TimelineNameGeneratorTestData))]
- public async Task Post_Markdown_Delete_Test(TimelineNameGenerator generator)
- {
- {
- using var client = await CreateClientAs(2);
- var post = await client.TestPostAsync<HttpTimelinePost>($"timelines/{generator(2)}/posts", CreateMarkdownPostRequest("[aaa](https://crupest.life)"));
- await client.TestDeleteAsync($"timelines/{generator(2)}/posts/{post.Id}");
- }
-
- {
- using var client = await CreateClientAsUser();
- var post = await client.TestPostAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts", CreateMarkdownPostRequest("[aaa](https://crupest.life)"));
- await client.TestDeleteAsync($"timelines/{generator(1)}/posts/{post.Id}");
- }
- }
-
- [Theory]
- [MemberData(nameof(TimelineNameGeneratorTestData))]
- public async Task Post_List_Pagination_Test(TimelineNameGenerator generator)
- {
- using var client = await CreateClientAsUser();
- var posts = new List<HttpTimelinePost>();
- for (int i = 0; i < 50; i++)
- {
- var post = await client.TestPostAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts", CreateTextPostRequest(i.ToString()));
- posts.Add(post);
- }
-
- {
- var p = await client.TestGetAsync<List<HttpTimelinePost>>($"timelines/{generator(1)}/posts?page=2&numberPerPage=10");
- p.Should().BeEquivalentTo(posts.Skip(10).Take(10));
- }
- }
}
}
diff --git a/BackEnd/Timeline.Tests/IntegratedTests/TimelinePostTest1.cs b/BackEnd/Timeline.Tests/IntegratedTests/TimelinePostTest1.cs new file mode 100644 index 00000000..58efd74a --- /dev/null +++ b/BackEnd/Timeline.Tests/IntegratedTests/TimelinePostTest1.cs @@ -0,0 +1,327 @@ +using FluentAssertions; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Models; +using Timeline.Models.Http; +using Xunit; +using Xunit.Abstractions; + +namespace Timeline.Tests.IntegratedTests +{ + public class TimelinePostTest1 : TimelinePostTest + { + public TimelinePostTest1(ITestOutputHelper testOutputHelper) : base(testOutputHelper) + { + + } + + [Theory] + [MemberData(nameof(TimelineNameGeneratorTestData))] + public async Task GetPostsAndVisibility_Should_Work(TimelineNameGenerator generator) + { + { // default visibility is registered + { + using var client = await CreateDefaultClient(); + await client.TestGetAssertForbiddenAsync($"timelines/{generator(1)}/posts"); + } + + { + using var client = await CreateClientAsUser(); + await client.TestGetAsync($"timelines/{generator(0)}/posts"); + } + } + + { // change visibility to public + { + using var client = await CreateClientAsUser(); + await client.PatchTimelineAsync(generator(1), new() { Visibility = TimelineVisibility.Public }); + } + + { + using var client = await CreateDefaultClient(); + await client.TestGetAsync($"timelines/{generator(1)}/posts"); + } + } + + { // change visibility to private + { + using var client = await CreateClientAsAdministrator(); + await client.PatchTimelineAsync(generator(1), new() { Visibility = TimelineVisibility.Private }); + await client.PatchTimelineAsync(generator(0), new() { Visibility = TimelineVisibility.Private }); + } + { + using var client = await CreateDefaultClient(); + await client.TestGetAssertForbiddenAsync($"timelines/{generator(1)}/posts"); + } + { // user can't read admin's + using var client = await CreateClientAsUser(); + await client.TestGetAssertForbiddenAsync($"timelines/{generator(0)}/posts"); + } + { // admin can read user's + using var client = await CreateClientAsAdministrator(); + await client.TestGetAsync($"timelines/{generator(1)}/posts"); + } + { // add member + using var client = await CreateClientAsAdministrator(); + await client.PutTimelineMemberAsync(generator(0), "user1"); + } + { // now user can read admin's + using var client = await CreateClientAsUser(); + await client.TestGetAsync($"timelines/{generator(0)}/posts"); + } + } + } + + [Theory] + [MemberData(nameof(TimelineNameGeneratorTestData))] + public async Task Post_ModifiedSince(TimelineNameGenerator generator) + { + using var client = await CreateClientAsUser(); + + var posts = new List<HttpTimelinePost>(); + + for (int i = 0; i < 4; i++) + { + var post = await client.TestPostAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts", CreateTextPostRequest("a")); + posts.Add(post); + await Task.Delay(TimeSpan.FromSeconds(1)); + } + + await client.TestDeleteAsync($"timelines/{generator(1)}/posts/{posts[2].Id}"); + + { + var body = await client.TestGetAsync<List<HttpTimelinePost>>($"timelines/{generator(1)}/posts?modifiedSince={posts[1].LastUpdated.ToString("s", CultureInfo.InvariantCulture) }"); + body.Should().HaveCount(2) + .And.Subject.Select(p => p.Id).Should().Equal(posts[1].Id, posts[3].Id); + } + } + + [Theory] + [MemberData(nameof(TimelineNameGeneratorTestData))] + public async Task PostList_IncludeDeleted(TimelineNameGenerator generator) + { + using var client = await CreateClientAsUser(); + + var posts = new List<HttpTimelinePost>(); + + for (int i = 0; i < 4; i++) + { + var body = await client.TestPostAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts", CreateTextPostRequest("a")); + posts.Add(body); + } + + foreach (var id in new long[] { posts[0].Id, posts[2].Id }) + { + await client.TestDeleteAsync($"timelines/{generator(1)}/posts/{id}"); + } + + { + posts = await client.TestGetAsync<List<HttpTimelinePost>>($"timelines/{generator(1)}/posts?includeDeleted=true"); + posts.Should().HaveCount(4); + posts.Select(p => p.Deleted).Should().Equal(true, false, true, false); + } + } + + [Theory] + [MemberData(nameof(TimelineNameGeneratorTestData))] + public async Task Post_ModifiedSince_And_IncludeDeleted(TimelineNameGenerator generator) + { + using var client = await CreateClientAsUser(); + + var posts = new List<HttpTimelinePost>(); + + for (int i = 0; i < 4; i++) + { + var post = await client.TestPostAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts", CreateTextPostRequest("a")); + posts.Add(post); + await Task.Delay(TimeSpan.FromSeconds(1)); + } + + await client.TestDeleteAsync($"timelines/{generator(1)}/posts/{posts[2].Id}"); + + { + posts = await client.TestGetAsync<List<HttpTimelinePost>>($"timelines/{generator(1)}/posts?modifiedSince={posts[1].LastUpdated.ToString("s", CultureInfo.InvariantCulture)}&includeDeleted=true"); + posts.Should().HaveCount(3); + posts.Select(p => p.Deleted).Should().Equal(false, true, false); + } + } + + [Theory] + [MemberData(nameof(TimelineNameGeneratorTestData))] + public async Task CreatePostPermission_Should_Work(TimelineNameGenerator generator) + { + using (var client = await CreateClientAsUser()) + { + await client.PutTimelineMemberAsync(generator(1), "user2"); + } + + using (var client = await CreateDefaultClient()) + { // no auth should get 401 + await client.TestPostAssertUnauthorizedAsync($"timelines/{generator(1)}/posts", CreateTextPostRequest("aaa")); + } + + using (var client = await CreateClientAsUser()) + { + // post self's + await client.TestPostAsync($"timelines/{generator(1)}/posts", CreateTextPostRequest("aaa")); + // post other not as a member should get 403 + await client.TestPostAssertForbiddenAsync($"timelines/{generator(0)}/posts", CreateTextPostRequest("aaa")); + } + + using (var client = await CreateClientAsAdministrator()) + { // post as admin + await client.TestPostAsync($"timelines/{generator(1)}/posts", CreateTextPostRequest("aaa")); + } + + using (var client = await CreateClientAs(2)) + { // post as member + await client.TestPostAsync($"timelines/{generator(1)}/posts", CreateTextPostRequest("aaa")); + } + } + + [Theory] + [MemberData(nameof(TimelineNameGeneratorTestData))] + public async Task DeletePostPermission_Should_Work(TimelineNameGenerator generator) + { + async Task<long> CreatePost(int userNumber) + { + using var client = await CreateClientAs(userNumber); + var body = await client.TestPostAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts", CreateTextPostRequest("aaa")); + return body.Id; + } + + using (var client = await CreateClientAsUser()) + { + await client.PutTimelineMemberAsync(generator(1), "user2"); + await client.PutTimelineMemberAsync(generator(1), "user3"); + } + + { // no auth should get 401 + using var client = await CreateDefaultClient(); + await client.TestDeleteAssertUnauthorizedAsync($"timelines/{generator(1)}/posts/12"); + } + + { // self can delete self + var postId = await CreatePost(1); + using var client = await CreateClientAsUser(); + await client.TestDeleteAsync($"timelines/{generator(1)}/posts/{postId}"); + } + + { // admin can delete any + var postId = await CreatePost(1); + using var client = await CreateClientAsAdministrator(); + await client.TestDeleteAsync($"timelines/{generator(1)}/posts/{postId}"); + } + + { // owner can delete other + var postId = await CreatePost(2); + using var client = await CreateClientAsUser(); + await client.TestDeleteAsync($"timelines/{generator(1)}/posts/{postId}"); + } + + { // author can delete self + var postId = await CreatePost(2); + using var client = await CreateClientAs(2); + await client.TestDeleteAsync($"timelines/{generator(1)}/posts/{postId}"); + } + + { // otherwise is forbidden + var postId = await CreatePost(2); + using var client = await CreateClientAs(3); + await client.TestDeleteAssertForbiddenAsync($"timelines/{generator(1)}/posts/{postId}"); + } + } + + [Theory] + [MemberData(nameof(TimelineNameGeneratorTestData))] + public async Task GetPost_Should_Ordered(TimelineNameGenerator generator) + { + using var client = await CreateClientAsUser(); + + async Task<long> CreatePost(DateTime time) + { + var body = await client.TestPostAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts", CreateTextPostRequest("aaa", time)); + return body.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 body = await client.TestGetAsync<List<HttpTimelinePost>>($"timelines/{generator(1)}/posts"); + body.Select(p => p.Id).Should().Equal(id1, id2, id0); + } + } + + [Theory] + [MemberData(nameof(TimelineNameGeneratorTestData))] + public async Task Color(TimelineNameGenerator generator) + { + using var client = await CreateClientAsUser(); + + await client.TestPostAssertInvalidModelAsync($"timelines/{generator(1)}/posts", CreateTextPostRequest("a", color: "aa")); + + long id; + + { + var post = await client.TestPostAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts", + CreateTextPostRequest("a", color: "#aabbcc")); + post.Color.Should().Be("#aabbcc"); + id = post.Id; + } + + { + var post = await client.TestGetAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts/{id}"); + post.Color.Should().Be("#aabbcc"); + } + } + + [Theory] + [MemberData(nameof(TimelineNameGeneratorTestData))] + public async Task GetPost(TimelineNameGenerator generator) + { + using var client = await CreateClientAsUser(); + + + await client.TestGetAssertNotFoundAsync($"timelines/{generator(1)}/posts/1"); + + var post = await client.TestPostAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts", CreateTextPostRequest("a")); + + var post2 = await client.TestGetAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts/{post.Id}"); + post2.Should().BeEquivalentTo(post); + + await client.TestDeleteAsync($"timelines/{generator(1)}/posts/{post.Id}"); + + await client.TestGetAssertNotFoundAsync($"timelines/{generator(1)}/posts/{post.Id}"); + } + + [Theory] + [MemberData(nameof(TimelineNameGeneratorTestData))] + public async Task PatchPost(TimelineNameGenerator generator) + { + using var client = await CreateClientAsUser(); + + var post = await client.TestPostAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts", + CreateTextPostRequest("a")); + + var date = new DateTime(2000, 10, 1); + + var post2 = await client.TestPatchAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts/{post.Id}", new HttpTimelinePostPatchRequest + { + Time = date, + Color = "#aabbcc" + }); + post2.Time.Should().Be(date); + post2.Color.Should().Be("#aabbcc"); + + var post3 = await client.TestGetAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts/{post.Id}"); + post3.Time.Should().Be(date); + post3.Color.Should().Be("#aabbcc"); + } + } +} diff --git a/BackEnd/Timeline.Tests/IntegratedTests/TimelinePostTest2.cs b/BackEnd/Timeline.Tests/IntegratedTests/TimelinePostTest2.cs new file mode 100644 index 00000000..78be068f --- /dev/null +++ b/BackEnd/Timeline.Tests/IntegratedTests/TimelinePostTest2.cs @@ -0,0 +1,326 @@ +using FluentAssertions; +using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Png; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using Timeline.Models; +using Timeline.Models.Http; +using Timeline.Tests.Helpers; +using Xunit; +using Xunit.Abstractions; + +namespace Timeline.Tests.IntegratedTests +{ + public class TimelinePostTest2 : TimelinePostTest + { + public TimelinePostTest2(ITestOutputHelper testOutputHelper) : base(testOutputHelper) + { + + } + + public static IEnumerable<object?[]> CreatePost_InvalidModelTest_TestData() + { + var testDataList = new List<List<HttpTimelinePostCreateRequestData>?>() + { + null, + new List<HttpTimelinePostCreateRequestData>(), + Enumerable.Repeat<HttpTimelinePostCreateRequestData>(new HttpTimelinePostCreateRequestData + { + ContentType = "text/plain", + Data = Convert.ToBase64String(Encoding.UTF8.GetBytes("a")) + }, 200).ToList(), + }; + + var testData = new List<HttpTimelinePostCreateRequestData?>() + { + null, + new HttpTimelinePostCreateRequestData + { + ContentType = null!, + Data = Convert.ToBase64String(Encoding.UTF8.GetBytes("a")) + }, + new HttpTimelinePostCreateRequestData + { + ContentType = "text/plain", + Data = null! + }, + new HttpTimelinePostCreateRequestData + { + ContentType = "text/xxxxxxx", + Data = Convert.ToBase64String(Encoding.UTF8.GetBytes("a")) + }, + new HttpTimelinePostCreateRequestData + { + ContentType = "text/plain", + Data = "aaa" + }, + new HttpTimelinePostCreateRequestData + { + ContentType = "text/plain", + Data = Convert.ToBase64String(new byte[] {0xE4, 0x1, 0xA0}) + }, + new HttpTimelinePostCreateRequestData + { + ContentType = "image/jpeg", + Data = Convert.ToBase64String(ImageHelper.CreatePngWithSize(100, 100)) + }, + new HttpTimelinePostCreateRequestData + { + ContentType = "image/jpeg", + Data = Convert.ToBase64String(new byte[] { 100, 200 }) + } + + }; + + testDataList.AddRange(testData.Select(d => new List<HttpTimelinePostCreateRequestData>() { d! })); + + return TimelineNameGeneratorTestData().AppendTestData(testDataList); + } + + [Theory] + [MemberData(nameof(CreatePost_InvalidModelTest_TestData))] + public async Task CreatePost_InvalidModel(TimelineNameGenerator generator, List<HttpTimelinePostCreateRequestData> dataList) + { + using var client = await CreateClientAsUser(); + + await client.TestPostAssertInvalidModelAsync( + $"timelines/{generator(1)}/posts", + new HttpTimelinePostCreateRequest + { + DataList = dataList + } + ); + } + + public static IEnumerable<object?[]> CreatePost_ShouldWork_TestData() + { + var testByteDatas = new List<ByteData>() + { + new ByteData(Encoding.UTF8.GetBytes("aaa"), MimeTypes.TextPlain), + new ByteData(Encoding.UTF8.GetBytes("aaa"), MimeTypes.TextMarkdown), + new ByteData(ImageHelper.CreateImageWithSize(100, 50, PngFormat.Instance), MimeTypes.ImagePng), + new ByteData(ImageHelper.CreateImageWithSize(100, 50, JpegFormat.Instance), MimeTypes.ImageJpeg), + new ByteData(ImageHelper.CreateImageWithSize(100, 50, GifFormat.Instance), MimeTypes.ImageGif), + }; + + return TimelineNameGeneratorTestData().AppendTestData(testByteDatas); + } + + [Theory] + [MemberData(nameof(CreatePost_ShouldWork_TestData))] + public async Task CreatePost_ShouldWork(TimelineNameGenerator generator, ByteData data) + { + using var client = await CreateClientAsUser(); + + var post = await client.TestPostAsync<HttpTimelinePost>( + $"timelines/{generator(1)}/posts", + new HttpTimelinePostCreateRequest + { + DataList = new List<HttpTimelinePostCreateRequestData> + { + new HttpTimelinePostCreateRequestData + { + ContentType = data.ContentType, + Data = Convert.ToBase64String(data.Data) + } + } + } + ); + + post.DataList.Should().NotBeNull().And.HaveCount(1); + var postData = post.DataList[0]; + postData.Should().NotBeNull(); + var postDataEtag = postData.ETag; + postDataEtag.Should().NotBeNullOrEmpty(); + + { + var response = await client.GetAsync($"timelines/{generator(1)}/posts/{post.Id}/data"); + response.StatusCode.Should().Be(HttpStatusCode.OK); + response.Headers.ETag.Should().NotBeNull(); + response.Headers.ETag!.Tag.Should().Be(postDataEtag); + response.Content.Headers.ContentType.Should().NotBeNull(); + response.Content.Headers.ContentType!.MediaType.Should().Be(data.ContentType); + + var body = await response.Content.ReadAsByteArrayAsync(); + body.Should().Equal(data.Data); + } + + { + var response = await client.GetAsync($"timelines/{generator(1)}/posts/{post.Id}/data/0"); + response.StatusCode.Should().Be(HttpStatusCode.OK); + response.Headers.ETag.Should().NotBeNull(); + response.Headers.ETag!.Tag.Should().Be(postDataEtag); + response.Content.Headers.ContentType.Should().NotBeNull(); + response.Content.Headers.ContentType!.MediaType.Should().Be(data.ContentType); + + var body = await response.Content.ReadAsByteArrayAsync(); + body.Should().Equal(data.Data); + } + } + + [Theory] + [MemberData(nameof(TimelineNameGeneratorTestData))] + public async Task CreatePost_MultipleData_ShouldWork(TimelineNameGenerator generator) + { + using var client = await CreateClientAsUser(); + + var textData = Encoding.UTF8.GetBytes("aaa"); + var imageData = ImageHelper.CreatePngWithSize(100, 50); + + var post = await client.TestPostAsync<HttpTimelinePost>( + $"timelines/{generator(1)}/posts", + new HttpTimelinePostCreateRequest + { + DataList = new List<HttpTimelinePostCreateRequestData> + { + new HttpTimelinePostCreateRequestData + { + ContentType = MimeTypes.TextMarkdown, + Data = Convert.ToBase64String(textData) + }, + new HttpTimelinePostCreateRequestData + { + ContentType = MimeTypes.ImagePng, + Data = Convert.ToBase64String(imageData) + } + } + } + ); + + post.DataList.Should().NotBeNull().And.HaveCount(2); + + var postData0 = post.DataList[0]; + postData0.Should().NotBeNull(); + var postDataEtag0 = postData0.ETag; + postDataEtag0.Should().NotBeNullOrEmpty(); + + var postData1 = post.DataList[1]; + postData1.Should().NotBeNull(); + var postDataEtag1 = postData1.ETag; + postDataEtag1.Should().NotBeNullOrEmpty(); + + { + var response = await client.GetAsync($"timelines/{generator(1)}/posts/{post.Id}/data"); + response.StatusCode.Should().Be(HttpStatusCode.OK); + response.Headers.ETag.Should().NotBeNull(); + response.Headers.ETag!.Tag.Should().Be(postDataEtag0); + response.Content.Headers.ContentType.Should().NotBeNull(); + response.Content.Headers.ContentType!.MediaType.Should().Be(MimeTypes.TextMarkdown); + + var body = await response.Content.ReadAsByteArrayAsync(); + body.Should().Equal(textData); + } + + { + var response = await client.GetAsync($"timelines/{generator(1)}/posts/{post.Id}/data/0"); + response.StatusCode.Should().Be(HttpStatusCode.OK); + response.Headers.ETag.Should().NotBeNull(); + response.Headers.ETag!.Tag.Should().Be(postDataEtag0); + response.Content.Headers.ContentType.Should().NotBeNull(); + response.Content.Headers.ContentType!.MediaType.Should().Be(MimeTypes.TextMarkdown); + + var body = await response.Content.ReadAsByteArrayAsync(); + body.Should().Equal(textData); + } + + { + var response = await client.GetAsync($"timelines/{generator(1)}/posts/{post.Id}/data/1"); + response.StatusCode.Should().Be(HttpStatusCode.OK); + response.Headers.ETag.Should().NotBeNull(); + response.Headers.ETag!.Tag.Should().Be(postDataEtag1); + response.Content.Headers.ContentType.Should().NotBeNull(); + response.Content.Headers.ContentType!.MediaType.Should().Be(MimeTypes.ImagePng); + + var body = await response.Content.ReadAsByteArrayAsync(); + body.Should().Equal(imageData); + } + } + + [Theory] + [MemberData(nameof(TimelineNameGeneratorTestData))] + public async Task Post_Editable(TimelineNameGenerator generator) + { + HttpTimelinePost post; + + { + using var client = await CreateClientAsUser(); + post = await client.TestPostAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts", CreateTextPostRequest("a")); + + post.Editable.Should().BeTrue(); + } + + { + using var client = await CreateClientAs(2); + var post2 = await client.TestGetAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts/{post.Id}"); + post2.Editable.Should().BeFalse(); + } + } + + [Theory] + [MemberData(nameof(TimelineNameGeneratorTestData))] + public async Task Post_Markdown_Url_Map(TimelineNameGenerator generator) + { + using var client = await CreateClientAsUser(); + var post = await client.TestPostAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts", CreateMarkdownPostRequest("[aaa](1) ")); + + var res = await client.GetAsync($"timelines/{generator(1)}/posts/{post.Id}/data"); + var markdown = await res.Content.ReadAsStringAsync(); + + markdown.Should().MatchRegex(@$"\[aaa\]\(https?://.*/timelines/{generator(1)}/posts/{post.Id}/data/1\)"); + markdown.Should().MatchRegex(@$"\[bbb\]\(https?://.*/timelines/{generator(1)}/posts/{post.Id}/data/2\)"); + } + + [Theory] + [MemberData(nameof(TimelineNameGeneratorTestData))] + public async Task Post_Markdown_Delete_Test(TimelineNameGenerator generator) + { + { + using var client = await CreateClientAs(2); + var post = await client.TestPostAsync<HttpTimelinePost>($"timelines/{generator(2)}/posts", CreateMarkdownPostRequest("[aaa](https://crupest.life)")); + await client.TestDeleteAsync($"timelines/{generator(2)}/posts/{post.Id}"); + } + + { + using var client = await CreateClientAsUser(); + var post = await client.TestPostAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts", CreateMarkdownPostRequest("[aaa](https://crupest.life)")); + await client.TestDeleteAsync($"timelines/{generator(1)}/posts/{post.Id}"); + } + } + + [Theory] + [MemberData(nameof(TimelineNameGeneratorTestData))] + public async Task Post_List_Pagination_Test(TimelineNameGenerator generator) + { + using var client = await CreateClientAsUser(); + var posts = new List<HttpTimelinePost>(); + for (int i = 0; i < 50; i++) + { + var post = await client.TestPostAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts", CreateTextPostRequest(i.ToString())); + posts.Add(post); + } + + { + var p = await client.TestGetAsync<List<HttpTimelinePost>>($"timelines/{generator(1)}/posts?page=2&numberPerPage=10"); + p.Should().BeEquivalentTo(posts.Skip(10).Take(10)); + } + } + + [Theory] + [InlineData("")] + [InlineData("default")] + public async Task Post_Color_Patch_Default(string value)
+ {
+ using var client = await CreateClientAsUser();
+
+ var post = await client.TestPostAsync<HttpTimelinePost>("timelines/t1/posts", CreateTextPostRequest("aaa", color: "#111111"));
+
+ var post2 = await client.TestPatchAsync<HttpTimelinePost>($"timelines/t1/posts/{post.Id}", new HttpTimelinePostPatchRequest { Color = value });
+ post2.Color.Should().BeNull();
+ } + } +} diff --git a/BackEnd/Timeline.Tests/IntegratedTests/TimelineTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/TimelineTest.cs index bbfe4ab3..d7832b60 100644 --- a/BackEnd/Timeline.Tests/IntegratedTests/TimelineTest.cs +++ b/BackEnd/Timeline.Tests/IntegratedTests/TimelineTest.cs @@ -438,5 +438,17 @@ namespace Timeline.Tests.IntegratedTests timeline.Postable.Should().Be(false);
}
}
+
+ [Theory]
+ [InlineData("")]
+ [InlineData("default")]
+ public async Task Patch_Timeline_Color_Default(string value)
+ {
+ using var client = await CreateClientAsUser();
+ var timeline = await client.TestPatchAsync<HttpTimeline>("timelines/t1", new HttpTimelinePatchRequest { Color = "#111111" });
+ timeline.Color.Should().NotBeNull();
+ var timeline2 = await client.TestPatchAsync<HttpTimeline>("timelines/t1", new HttpTimelinePatchRequest { Color = value });
+ timeline2.Color.Should().BeNull();
+ }
}
}
diff --git a/BackEnd/Timeline/Models/Http/HttpTimelinePatchRequest.cs b/BackEnd/Timeline/Models/Http/HttpTimelinePatchRequest.cs index 9accb6fc..35667af2 100644 --- a/BackEnd/Timeline/Models/Http/HttpTimelinePatchRequest.cs +++ b/BackEnd/Timeline/Models/Http/HttpTimelinePatchRequest.cs @@ -31,7 +31,7 @@ namespace Timeline.Models.Http /// <summary>
/// New color. Null for not change.
/// </summary>
- [Color]
+ [Color(PermitDefault = true, PermitEmpty = true)]
public string? Color { get; set; }
}
}
diff --git a/BackEnd/Timeline/Models/Http/HttpTimelinePostPatchRequest.cs b/BackEnd/Timeline/Models/Http/HttpTimelinePostPatchRequest.cs index 2c6edf66..cb576a74 100644 --- a/BackEnd/Timeline/Models/Http/HttpTimelinePostPatchRequest.cs +++ b/BackEnd/Timeline/Models/Http/HttpTimelinePostPatchRequest.cs @@ -13,7 +13,7 @@ namespace Timeline.Models.Http /// <summary>
/// Change the color. Null for not change.
/// </summary>
- [Color]
+ [Color(PermitEmpty = true, PermitDefault = true)]
public string? Color { get; set; }
}
}
diff --git a/BackEnd/Timeline/Models/Validation/ColorValidator.cs b/BackEnd/Timeline/Models/Validation/ColorValidator.cs index c5ad833d..4f7accc5 100644 --- a/BackEnd/Timeline/Models/Validation/ColorValidator.cs +++ b/BackEnd/Timeline/Models/Validation/ColorValidator.cs @@ -4,8 +4,22 @@ namespace Timeline.Models.Validation {
public class ColorValidator : Validator<string>
{
+ public bool PermitEmpty { get; set; } = false;
+ public bool PermitDefault { get; set; } = false;
+ public string DefaultValue { get; set; } = "default";
+
protected override (bool, string) DoValidate(string value)
{
+ if (PermitEmpty && value.Length == 0)
+ {
+ return (true, GetSuccessMessage());
+ }
+
+ if (PermitDefault && value == DefaultValue)
+ {
+ return (true, GetSuccessMessage());
+ }
+
if (!value.StartsWith('#'))
{
return (false, "Color must starts with '#'.");
@@ -32,9 +46,29 @@ namespace Timeline.Models.Validation [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
public class ColorAttribute : ValidateWithAttribute
{
+ private ColorValidator Validator => (ColorValidator)_validator;
+
public ColorAttribute() : base(typeof(ColorValidator))
{
}
+
+ public bool PermitEmpty
+ {
+ get => Validator.PermitEmpty;
+ set => Validator.PermitEmpty = value;
+ }
+
+ public bool PermitDefault
+ {
+ get => Validator.PermitDefault;
+ set => Validator.PermitDefault = value;
+ }
+
+ public string DefaultValue
+ {
+ get => Validator.DefaultValue;
+ set => Validator.DefaultValue = value;
+ }
}
}
diff --git a/BackEnd/Timeline/Models/Validation/Validator.cs b/BackEnd/Timeline/Models/Validation/Validator.cs index d334960e..0e1f7445 100644 --- a/BackEnd/Timeline/Models/Validation/Validator.cs +++ b/BackEnd/Timeline/Models/Validation/Validator.cs @@ -77,7 +77,7 @@ namespace Timeline.Models.Validation AllowMultiple = false)]
public class ValidateWithAttribute : ValidationAttribute
{
- private readonly IValidator _validator;
+ protected readonly IValidator _validator;
/// <summary>
/// Create with a given validator.
diff --git a/BackEnd/Timeline/Services/Timeline/TimelinePostService.cs b/BackEnd/Timeline/Services/Timeline/TimelinePostService.cs index ee1002e0..ae034767 100644 --- a/BackEnd/Timeline/Services/Timeline/TimelinePostService.cs +++ b/BackEnd/Timeline/Services/Timeline/TimelinePostService.cs @@ -26,6 +26,7 @@ namespace Timeline.Services.Timeline private readonly IImageService _imageValidator;
private readonly IClock _clock;
private readonly ColorValidator _colorValidator = new ColorValidator();
+ private readonly ColorValidator _colorValidatorAllowEmptyAndDefault = new ColorValidator() { PermitEmpty = true, PermitDefault = true };
public TimelinePostService(ILogger<TimelinePostService> logger, DatabaseContext database, IBasicTimelineService basicTimelineService, IBasicUserService basicUserService, IDataManager dataManager, IImageService imageValidator, IClock clock)
{
@@ -38,9 +39,9 @@ namespace Timeline.Services.Timeline _clock = clock;
}
- private void CheckColor(string color, string paramName)
+ private void CheckColor(string color, string paramName, bool allowEmptyOrDefault)
{
- if (!_colorValidator.Validate(color, out var message))
+ if (!(allowEmptyOrDefault ? _colorValidatorAllowEmptyAndDefault : _colorValidator).Validate(color, out var message))
throw new ArgumentException(string.Format(Resource.ExceptionColorInvalid, message), paramName);
}
@@ -166,7 +167,7 @@ namespace Timeline.Services.Timeline throw new ArgumentNullException(nameof(request));
if (request.Color is not null)
- CheckColor(request.Color, nameof(request));
+ CheckColor(request.Color, nameof(request), false);
if (request.DataList is null)
throw new ArgumentException(Resource.ExceptionDataListNull, nameof(request));
@@ -269,7 +270,7 @@ namespace Timeline.Services.Timeline throw new ArgumentNullException(nameof(request));
if (request.Color is not null)
- CheckColor(request.Color, nameof(request));
+ CheckColor(request.Color, nameof(request), true);
request.Time = request.Time?.MyToUtc();
@@ -287,7 +288,16 @@ namespace Timeline.Services.Timeline entity.Time = request.Time.Value;
if (request.Color is not null)
- entity.Color = request.Color;
+ {
+ if (request.Color.Length == 0 || request.Color == "default")
+ {
+ entity.Color = null;
+ }
+ else
+ {
+ entity.Color = request.Color;
+ }
+ }
entity.LastUpdated = _clock.GetCurrentTime();
diff --git a/BackEnd/Timeline/Services/Timeline/TimelineService.cs b/BackEnd/Timeline/Services/Timeline/TimelineService.cs index 920fcc74..6f22ff05 100644 --- a/BackEnd/Timeline/Services/Timeline/TimelineService.cs +++ b/BackEnd/Timeline/Services/Timeline/TimelineService.cs @@ -23,7 +23,7 @@ namespace Timeline.Services.Timeline private readonly IClock _clock;
private readonly TimelineNameValidator _timelineNameValidator = new TimelineNameValidator();
- private readonly ColorValidator _colorValidator = new ColorValidator();
+ private readonly ColorValidator _colorValidator = new ColorValidator() { PermitDefault = true, PermitEmpty = true };
public TimelineService(ILoggerFactory loggerFactory, DatabaseContext database, IBasicUserService userService, IClock clock)
: base(loggerFactory, database, userService, clock)
@@ -119,7 +119,14 @@ namespace Timeline.Services.Timeline if (newProperties.Color is not null)
{
changed = true;
- entity.Color = newProperties.Color;
+ if (newProperties.Color.Length == 0 || newProperties.Color == "default")
+ {
+ entity.Color = null;
+ }
+ else
+ {
+ entity.Color = newProperties.Color;
+ }
}
if (changed)
|