From 2f36e9a1c8d6db2a808f874134c9cb7d57c3ef16 Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Thu, 7 Nov 2019 22:06:06 +0800 Subject: Complete PersonalTimelineController and write attribute test. --- .../Controllers/PersonalTimelineControllerTest.cs | 111 +++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs (limited to 'Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs') diff --git a/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs b/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs new file mode 100644 index 00000000..d5c470ee --- /dev/null +++ b/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Controllers; +using Timeline.Services; +using Moq; +using Microsoft.Extensions.Logging.Abstractions; +using Xunit; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc; +using Timeline.Filters; +using Timeline.Tests.Helpers; +using Timeline.Models.Validation; +using System.Reflection; +using Microsoft.AspNetCore.Authorization; +using Timeline.Models.Http; + +namespace Timeline.Tests.Controllers +{ + public class PersonalTimelineControllerTest : IDisposable + { + private readonly Mock _service; + + private readonly PersonalTimelineController _controller; + + public PersonalTimelineControllerTest() + { + _service = new Mock(); + _controller = new PersonalTimelineController(NullLogger.Instance, _service.Object); + } + + public void Dispose() + { + _controller.Dispose(); + } + + [Fact] + public void AttributeTest() + { + static void AssertUsernameParameter(MethodInfo m) + { + m.GetParameter("username") + .Should().BeDecoratedWith() + .And.BeDecoratedWith(); + } + + static void AssertBodyParamter(MethodInfo m) + { + var p = m.GetParameter("body"); + p.Should().BeDecoratedWith(); + p.ParameterType.Should().Be(typeof(TBody)); + } + + var type = typeof(PersonalTimelineController); + type.Should().BeDecoratedWith(); + + { + var m = type.GetMethod(nameof(PersonalTimelineController.TimelineGet)); + m.Should().BeDecoratedWith() + .And.BeDecoratedWith(); + AssertUsernameParameter(m); + } + + { + var m = type.GetMethod(nameof(PersonalTimelineController.PostsGet)); + m.Should().BeDecoratedWith() + .And.BeDecoratedWith(); + AssertUsernameParameter(m); + } + + { + var m = type.GetMethod(nameof(PersonalTimelineController.TimelinePost)); + m.Should().BeDecoratedWith() + .And.BeDecoratedWith() + .And.BeDecoratedWith(); + AssertUsernameParameter(m); + AssertBodyParamter(m); + } + + { + var m = type.GetMethod(nameof(PersonalTimelineController.TimelinePostDelete)); + m.Should().BeDecoratedWith() + .And.BeDecoratedWith() + .And.BeDecoratedWith(); + AssertUsernameParameter(m); + AssertBodyParamter(m); + } + + { + var m = type.GetMethod(nameof(PersonalTimelineController.TimelineChangeProperty)); + m.Should().BeDecoratedWith() + .And.BeDecoratedWith() + .And.BeDecoratedWith() + .And.BeDecoratedWith(); + AssertUsernameParameter(m); + AssertBodyParamter(m); + } + + { + var m = type.GetMethod(nameof(PersonalTimelineController.TimelineChangeMember)); + m.Should().BeDecoratedWith() + .And.BeDecoratedWith() + .And.BeDecoratedWith() + .And.BeDecoratedWith(); + AssertUsernameParameter(m); + AssertBodyParamter(m); + } + } + } +} -- cgit v1.2.3 From e1757f98c0f5a337f1b5c44ef1638210a59f2811 Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Thu, 7 Nov 2019 22:57:04 +0800 Subject: Add Get method tests for PersonalTimelineController. --- .../Controllers/PersonalTimelineControllerTest.cs | 88 +++++++++++++++++++--- .../Helpers/Authentication/PrincipalHelper.cs | 23 ++++++ 2 files changed, 99 insertions(+), 12 deletions(-) create mode 100644 Timeline.Tests/Helpers/Authentication/PrincipalHelper.cs (limited to 'Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs') diff --git a/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs b/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs index d5c470ee..6857a27f 100644 --- a/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs +++ b/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs @@ -1,20 +1,22 @@ -using System; +using FluentAssertions; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using System; using System.Collections.Generic; -using System.Linq; +using System.Reflection; using System.Threading.Tasks; using Timeline.Controllers; -using Timeline.Services; -using Moq; -using Microsoft.Extensions.Logging.Abstractions; -using Xunit; -using FluentAssertions; -using Microsoft.AspNetCore.Mvc; using Timeline.Filters; -using Timeline.Tests.Helpers; -using Timeline.Models.Validation; -using System.Reflection; -using Microsoft.AspNetCore.Authorization; +using Timeline.Models; using Timeline.Models.Http; +using Timeline.Models.Validation; +using Timeline.Services; +using Timeline.Tests.Helpers; +using Timeline.Tests.Helpers.Authentication; +using Xunit; namespace Timeline.Tests.Controllers { @@ -107,5 +109,67 @@ namespace Timeline.Tests.Controllers AssertBodyParamter(m); } } + + const string authUsername = "authuser"; + private void SetUser(bool administrator) + { + _controller.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = PrincipalHelper.Create(authUsername, administrator) + } + }; + } + + [Fact] + public async Task TimelineGet() + { + const string username = "username"; + var timelineInfo = new BaseTimelineInfo(); + _service.Setup(s => s.GetTimeline(username)).ReturnsAsync(timelineInfo); + (await _controller.TimelineGet(username)).Value.Should().Be(timelineInfo); + _service.VerifyAll(); + } + + [Fact] + public async Task PostsGet_Forbid() + { + const string username = "username"; + SetUser(false); + _service.Setup(s => s.HasReadPermission(username, authUsername)).ReturnsAsync(false); + (await _controller.PostsGet(username)).Result + .Should().BeAssignableTo() + .Which.Value.Should().BeAssignableTo() + .Which.Code.Should().Be(ErrorCodes.Http.Timeline.PostsGetForbid); + _service.VerifyAll(); + } + + [Fact] + public async Task PostsGet_Admin_Success() + { + const string username = "username"; + SetUser(true); + _service.Setup(s => s.GetPosts(username)).ReturnsAsync(new List()); + (await _controller.PostsGet(username)).Value + .Should().BeAssignableTo>() + .Which.Should().NotBeNull().And.BeEmpty(); + _service.VerifyAll(); + } + + [Fact] + public async Task PostsGet_User_Success() + { + const string username = "username"; + SetUser(false); + _service.Setup(s => s.HasReadPermission(username, authUsername)).ReturnsAsync(true); + _service.Setup(s => s.GetPosts(username)).ReturnsAsync(new List()); + (await _controller.PostsGet(username)).Value + .Should().BeAssignableTo>() + .Which.Should().NotBeNull().And.BeEmpty(); + _service.VerifyAll(); + } + + //TODO! Write all the other tests. } } diff --git a/Timeline.Tests/Helpers/Authentication/PrincipalHelper.cs b/Timeline.Tests/Helpers/Authentication/PrincipalHelper.cs new file mode 100644 index 00000000..214472a2 --- /dev/null +++ b/Timeline.Tests/Helpers/Authentication/PrincipalHelper.cs @@ -0,0 +1,23 @@ +using System.Linq; +using System.Security.Claims; +using Timeline.Models; + +namespace Timeline.Tests.Helpers.Authentication +{ + public static class PrincipalHelper + { + internal const string AuthScheme = "TESTAUTH"; + + internal static ClaimsPrincipal Create(string username, bool administrator) + { + var identity = new ClaimsIdentity(AuthScheme); + identity.AddClaim(new Claim(identity.NameClaimType, username, ClaimValueTypes.String)); + identity.AddClaims(UserRoleConvert.ToArray(administrator).Select(role => new Claim(identity.RoleClaimType, role, ClaimValueTypes.String))); + + var principal = new ClaimsPrincipal(); + principal.AddIdentity(identity); + + return principal; + } + } +} -- cgit v1.2.3 From cc59e67f948d206a8bc466ed116d1bb870d3fb7b Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Sat, 9 Nov 2019 23:44:20 +0800 Subject: Add PersonalTimelineController PostOperationCreate unit tests. --- .../Controllers/PersonalTimelineControllerTest.cs | 89 +++++++++++++++++++--- Timeline/Controllers/PersonalTimelineController.cs | 6 +- Timeline/Models/Http/Timeline.cs | 2 +- 3 files changed, 82 insertions(+), 15 deletions(-) (limited to 'Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs') diff --git a/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs b/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs index 6857a27f..27b37f94 100644 --- a/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs +++ b/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs @@ -65,14 +65,14 @@ namespace Timeline.Tests.Controllers } { - var m = type.GetMethod(nameof(PersonalTimelineController.PostsGet)); + var m = type.GetMethod(nameof(PersonalTimelineController.PostListGet)); m.Should().BeDecoratedWith() .And.BeDecoratedWith(); AssertUsernameParameter(m); } { - var m = type.GetMethod(nameof(PersonalTimelineController.TimelinePost)); + var m = type.GetMethod(nameof(PersonalTimelineController.PostOperationCreate)); m.Should().BeDecoratedWith() .And.BeDecoratedWith() .And.BeDecoratedWith(); @@ -81,7 +81,7 @@ namespace Timeline.Tests.Controllers } { - var m = type.GetMethod(nameof(PersonalTimelineController.TimelinePostDelete)); + var m = type.GetMethod(nameof(PersonalTimelineController.PostOperationDelete)); m.Should().BeDecoratedWith() .And.BeDecoratedWith() .And.BeDecoratedWith(); @@ -133,43 +133,110 @@ namespace Timeline.Tests.Controllers } [Fact] - public async Task PostsGet_Forbid() + public async Task PostListGet_Forbid() { const string username = "username"; SetUser(false); _service.Setup(s => s.HasReadPermission(username, authUsername)).ReturnsAsync(false); - (await _controller.PostsGet(username)).Result + var result = (await _controller.PostListGet(username)).Result .Should().BeAssignableTo() - .Which.Value.Should().BeAssignableTo() - .Which.Code.Should().Be(ErrorCodes.Http.Timeline.PostsGetForbid); + .Which; + result.StatusCode.Should().Be(StatusCodes.Status403Forbidden); + result.Value.Should().BeAssignableTo() + .Which.Code.Should().Be(ErrorCodes.Http.Timeline.PostsGetForbid); _service.VerifyAll(); } [Fact] - public async Task PostsGet_Admin_Success() + public async Task PostListGet_Admin_Success() { const string username = "username"; SetUser(true); _service.Setup(s => s.GetPosts(username)).ReturnsAsync(new List()); - (await _controller.PostsGet(username)).Value + (await _controller.PostListGet(username)).Value .Should().BeAssignableTo>() .Which.Should().NotBeNull().And.BeEmpty(); _service.VerifyAll(); } [Fact] - public async Task PostsGet_User_Success() + public async Task PostListGet_User_Success() { const string username = "username"; SetUser(false); _service.Setup(s => s.HasReadPermission(username, authUsername)).ReturnsAsync(true); _service.Setup(s => s.GetPosts(username)).ReturnsAsync(new List()); - (await _controller.PostsGet(username)).Value + (await _controller.PostListGet(username)).Value .Should().BeAssignableTo>() .Which.Should().NotBeNull().And.BeEmpty(); _service.VerifyAll(); } + [Fact] + public async Task PostOperationCreate_Forbid() + { + const string username = "username"; + const string content = "cccc"; + SetUser(false); + _service.Setup(s => s.IsMemberOf(username, authUsername)).ReturnsAsync(false); + var result = (await _controller.PostOperationCreate(username, new TimelinePostCreateRequest + { + Content = content, + Time = null + })).Result.Should().NotBeNull().And.BeAssignableTo().Which; + result.StatusCode.Should().Be(StatusCodes.Status403Forbidden); + result.Value.Should().BeAssignableTo() + .Which.Code.Should().Be(ErrorCodes.Http.Timeline.PostsCreateForbid); + _service.VerifyAll(); + } + + [Fact] + public async Task PostOperationCreate_Admin_Success() + { + const string username = "username"; + const string content = "cccc"; + var response = new TimelinePostCreateResponse + { + Id = 3, + Time = DateTime.Now + }; + SetUser(true); + _service.Setup(s => s.CreatePost(username, authUsername, content, null)).ReturnsAsync(response); + var resultValue = (await _controller.PostOperationCreate(username, new TimelinePostCreateRequest + { + Content = content, + Time = null + })).Value; + resultValue.Should().NotBeNull() + .And.BeAssignableTo() + .And.BeEquivalentTo(response); + _service.VerifyAll(); + } + + [Fact] + public async Task PostOperationCreate_User_Success() + { + const string username = "username"; + const string content = "cccc"; + var response = new TimelinePostCreateResponse + { + Id = 3, + Time = DateTime.Now + }; + SetUser(false); + _service.Setup(s => s.IsMemberOf(username, authUsername)).ReturnsAsync(true); + _service.Setup(s => s.CreatePost(username, authUsername, content, null)).ReturnsAsync(response); + var resultValue = (await _controller.PostOperationCreate(username, new TimelinePostCreateRequest + { + Content = content, + Time = null + })).Value; + resultValue.Should().NotBeNull() + .And.BeAssignableTo() + .And.BeEquivalentTo(response); + _service.VerifyAll(); + } + //TODO! Write all the other tests. } } diff --git a/Timeline/Controllers/PersonalTimelineController.cs b/Timeline/Controllers/PersonalTimelineController.cs index f006ad47..f41e354b 100644 --- a/Timeline/Controllers/PersonalTimelineController.cs +++ b/Timeline/Controllers/PersonalTimelineController.cs @@ -74,7 +74,7 @@ namespace Timeline.Controllers [HttpGet("users/{username}/timeline/posts")] [CatchTimelineNotExistException] - public async Task>> PostsGet([FromRoute][Username] string username) + public async Task>> PostListGet([FromRoute][Username] string username) { if (!IsAdmin() && !await _service.HasReadPermission(username, GetAuthUsername())) { @@ -88,7 +88,7 @@ namespace Timeline.Controllers [HttpPost("user/{username}/timeline/postop/create")] [Authorize] [CatchTimelineNotExistException] - public async Task> TimelinePost([FromRoute][Username] string username, [FromBody] TimelinePostCreateRequest body) + public async Task> PostOperationCreate([FromRoute][Username] string username, [FromBody] TimelinePostCreateRequest body) { if (!IsAdmin() && !await _service.IsMemberOf(username, GetAuthUsername()!)) { @@ -103,7 +103,7 @@ namespace Timeline.Controllers [HttpPost("user/{username}/timeline/postop/delete")] [Authorize] [CatchTimelineNotExistException] - public async Task TimelinePostDelete([FromRoute][Username] string username, [FromBody] TimelinePostDeleteRequest body) + public async Task PostOperationDelete([FromRoute][Username] string username, [FromBody] TimelinePostDeleteRequest body) { var postId = body.Id!.Value; if (!IsAdmin() && !await _service.HasPostModifyPermission(username, postId, GetAuthUsername()!)) diff --git a/Timeline/Models/Http/Timeline.cs b/Timeline/Models/Http/Timeline.cs index f676afa0..06b88ad1 100644 --- a/Timeline/Models/Http/Timeline.cs +++ b/Timeline/Models/Http/Timeline.cs @@ -9,7 +9,7 @@ namespace Timeline.Models.Http { public class TimelinePostCreateRequest { - [Required(AllowEmptyStrings = false)] + [Required(AllowEmptyStrings = true)] public string Content { get; set; } = default!; public DateTime? Time { get; set; } -- cgit v1.2.3 From 299481eecc8c1b7bc40770d58c85ff1fddeddb96 Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Tue, 12 Nov 2019 20:09:41 +0800 Subject: Complete personal timeline controller unit tests. --- .../Controllers/PersonalTimelineControllerTest.cs | 153 ++++++++++++++++++++- Timeline/Controllers/PersonalTimelineController.cs | 25 +++- .../Controllers/TimelineController.Designer.cs | 28 +++- .../Resources/Controllers/TimelineController.resx | 12 +- .../Controllers/TimelineController.zh.resx | 12 +- 5 files changed, 209 insertions(+), 21 deletions(-) (limited to 'Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs') diff --git a/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs b/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs index 27b37f94..aecd10af 100644 --- a/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs +++ b/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs @@ -143,7 +143,7 @@ namespace Timeline.Tests.Controllers .Which; result.StatusCode.Should().Be(StatusCodes.Status403Forbidden); result.Value.Should().BeAssignableTo() - .Which.Code.Should().Be(ErrorCodes.Http.Timeline.PostsGetForbid); + .Which.Code.Should().Be(ErrorCodes.Http.Timeline.PostListGetForbid); _service.VerifyAll(); } @@ -186,7 +186,7 @@ namespace Timeline.Tests.Controllers })).Result.Should().NotBeNull().And.BeAssignableTo().Which; result.StatusCode.Should().Be(StatusCodes.Status403Forbidden); result.Value.Should().BeAssignableTo() - .Which.Code.Should().Be(ErrorCodes.Http.Timeline.PostsCreateForbid); + .Which.Code.Should().Be(ErrorCodes.Http.Timeline.PostOperationCreateForbid); _service.VerifyAll(); } @@ -237,6 +237,153 @@ namespace Timeline.Tests.Controllers _service.VerifyAll(); } - //TODO! Write all the other tests. + [Fact] + public async Task PostOperationDelete_Forbid() + { + const string username = "username"; + const long postId = 2; + SetUser(false); + _service.Setup(s => s.HasPostModifyPermission(username, postId, authUsername)).ReturnsAsync(false); + var result = (await _controller.PostOperationDelete(username, new TimelinePostDeleteRequest + { + Id = postId + })).Should().NotBeNull().And.BeAssignableTo().Which; + result.StatusCode.Should().Be(StatusCodes.Status403Forbidden); + result.Value.Should().BeAssignableTo() + .Which.Code.Should().Be(ErrorCodes.Http.Timeline.PostOperationDeleteForbid); + _service.VerifyAll(); + } + + [Fact] + public async Task PostOperationDelete_NotExist() + { + const string username = "username"; + const long postId = 2; + SetUser(true); + _service.Setup(s => s.DeletePost(username, postId)).ThrowsAsync(new TimelinePostNotExistException()); + var result = (await _controller.PostOperationDelete(username, new TimelinePostDeleteRequest + { + Id = postId + })).Should().NotBeNull().And.BeAssignableTo().Which; + result.StatusCode.Should().Be(StatusCodes.Status400BadRequest); + result.Value.Should().BeAssignableTo() + .Which.Code.Should().Be(ErrorCodes.Http.Timeline.PostOperationDeleteNotExist); + _service.VerifyAll(); + } + + [Fact] + public async Task PostOperationDelete_Admin_Success() + { + const string username = "username"; + const long postId = 2; + SetUser(true); + _service.Setup(s => s.DeletePost(username, postId)).Returns(Task.CompletedTask); + var result = await _controller.PostOperationDelete(username, new TimelinePostDeleteRequest + { + Id = postId + }); + result.Should().NotBeNull().And.BeAssignableTo(); + _service.VerifyAll(); + } + + [Fact] + public async Task PostOperationDelete_User_Success() + { + const string username = "username"; + const long postId = 2; + SetUser(false); + _service.Setup(s => s.DeletePost(username, postId)).Returns(Task.CompletedTask); + _service.Setup(s => s.HasPostModifyPermission(username, postId, authUsername)).ReturnsAsync(true); + var result = await _controller.PostOperationDelete(username, new TimelinePostDeleteRequest + { + Id = postId + }); + result.Should().NotBeNull().And.BeAssignableTo(); + _service.VerifyAll(); + } + + [Fact] + public async Task TimelineChangeProperty_Success() + { + const string username = "username"; + var req = new TimelinePropertyChangeRequest + { + Description = "", + Visibility = Entities.TimelineVisibility.Private + }; + _service.Setup(s => s.ChangeProperty(username, req)).Returns(Task.CompletedTask); + var result = await _controller.TimelineChangeProperty(username, req); + result.Should().NotBeNull().And.BeAssignableTo(); + _service.VerifyAll(); + } + + [Fact] + public async Task TimelineChangeMember_Success() + { + const string username = "username"; + var add = new List { "aaa" }; + var remove = new List { "rrr" }; + _service.Setup(s => s.ChangeMember(username, add, remove)).Returns(Task.CompletedTask); + var result = await _controller.TimelineChangeMember(username, new TimelineMemberChangeRequest + { + Add = add, + Remove = remove + }); + result.Should().NotBeNull().And.BeAssignableTo(); + _service.VerifyAll(); + } + + [Fact] + public async Task TimelineChangeMember_UsernameBadFormat() + { + const string username = "username"; + var add = new List { "aaa" }; + var remove = new List { "rrr" }; + _service.Setup(s => s.ChangeMember(username, add, remove)).ThrowsAsync( + new TimelineMemberOperationUserException("test", new UsernameBadFormatException())); + var result = await _controller.TimelineChangeMember(username, new TimelineMemberChangeRequest + { + Add = add, + Remove = remove + }); + result.Should().NotBeNull().And.BeAssignableTo() + .Which.Value.Should().BeAssignableTo() + .Which.Code.Should().Be(ErrorCodes.Http.Common.InvalidModel); + _service.VerifyAll(); + } + + [Fact] + public async Task TimelineChangeMember_AddNotExist() + { + const string username = "username"; + var add = new List { "aaa" }; + var remove = new List { "rrr" }; + _service.Setup(s => s.ChangeMember(username, add, remove)).ThrowsAsync( + new TimelineMemberOperationUserException("test", new UserNotExistException())); + var result = await _controller.TimelineChangeMember(username, new TimelineMemberChangeRequest + { + Add = add, + Remove = remove + }); + result.Should().NotBeNull().And.BeAssignableTo() + .Which.Value.Should().BeAssignableTo() + .Which.Code.Should().Be(ErrorCodes.Http.Timeline.MemberAddNotExist); + _service.VerifyAll(); + } + + [Fact] + public async Task TimelineChangeMember_UnknownTimelineMemberOperationUserException() + { + const string username = "username"; + var add = new List { "aaa" }; + var remove = new List { "rrr" }; + _service.Setup(s => s.ChangeMember(username, add, remove)).ThrowsAsync( + new TimelineMemberOperationUserException("test", null)); + await _controller.Awaiting(c => c.TimelineChangeMember(username, new TimelineMemberChangeRequest + { + Add = add, + Remove = remove + })).Should().ThrowAsync(); // Should rethrow. + } } } diff --git a/Timeline/Controllers/PersonalTimelineController.cs b/Timeline/Controllers/PersonalTimelineController.cs index f41e354b..f0f4e4c2 100644 --- a/Timeline/Controllers/PersonalTimelineController.cs +++ b/Timeline/Controllers/PersonalTimelineController.cs @@ -21,9 +21,11 @@ namespace Timeline { public static class Timeline // ccc = 004 { - public const int PostsGetForbid = 10040101; - public const int PostsCreateForbid = 10040102; - public const int MemberAddNotExist = 10040201; + public const int PostListGetForbid = 10040101; + public const int PostOperationCreateForbid = 10040102; + public const int PostOperationDeleteForbid = 10040103; + public const int PostOperationDeleteNotExist = 10040201; + public const int MemberAddNotExist = 10040301; } } } @@ -79,7 +81,7 @@ namespace Timeline.Controllers if (!IsAdmin() && !await _service.HasReadPermission(username, GetAuthUsername())) { return StatusCode(StatusCodes.Status403Forbidden, - new CommonResponse(ErrorCodes.Http.Timeline.PostsGetForbid, MessagePostsGetForbid)); + new CommonResponse(ErrorCodes.Http.Timeline.PostListGetForbid, MessagePostListGetForbid)); } return await _service.GetPosts(username); @@ -93,7 +95,7 @@ namespace Timeline.Controllers if (!IsAdmin() && !await _service.IsMemberOf(username, GetAuthUsername()!)) { return StatusCode(StatusCodes.Status403Forbidden, - new CommonResponse(ErrorCodes.Http.Timeline.PostsCreateForbid, MessagePostsCreateForbid)); + new CommonResponse(ErrorCodes.Http.Timeline.PostOperationCreateForbid, MessagePostOperationCreateForbid)); } var res = await _service.CreatePost(username, User.Identity.Name!, body.Content, body.Time); @@ -109,9 +111,18 @@ namespace Timeline.Controllers if (!IsAdmin() && !await _service.HasPostModifyPermission(username, postId, GetAuthUsername()!)) { return StatusCode(StatusCodes.Status403Forbidden, - new CommonResponse(ErrorCodes.Http.Timeline.PostsCreateForbid, MessagePostsCreateForbid)); + new CommonResponse(ErrorCodes.Http.Timeline.PostOperationDeleteForbid, MessagePostOperationCreateForbid)); + } + try + { + await _service.DeletePost(username, postId); + } + catch (TimelinePostNotExistException) + { + return BadRequest(new CommonResponse( + ErrorCodes.Http.Timeline.PostOperationDeleteNotExist, + MessagePostOperationDeleteNotExist)); } - await _service.DeletePost(username, postId); return Ok(); } diff --git a/Timeline/Resources/Controllers/TimelineController.Designer.cs b/Timeline/Resources/Controllers/TimelineController.Designer.cs index 5a4209c3..47c43fa2 100644 --- a/Timeline/Resources/Controllers/TimelineController.Designer.cs +++ b/Timeline/Resources/Controllers/TimelineController.Designer.cs @@ -96,21 +96,39 @@ namespace Timeline.Resources.Controllers { } } + /// + /// Looks up a localized string similar to You have no permission to read posts of the timeline.. + /// + internal static string MessagePostListGetForbid { + get { + return ResourceManager.GetString("MessagePostListGetForbid", resourceCulture); + } + } + /// /// Looks up a localized string similar to You have no permission to create posts in the timeline.. /// - internal static string MessagePostsCreateForbid { + internal static string MessagePostOperationCreateForbid { get { - return ResourceManager.GetString("MessagePostsCreateForbid", resourceCulture); + return ResourceManager.GetString("MessagePostOperationCreateForbid", resourceCulture); } } /// - /// Looks up a localized string similar to You have no permission to read posts of the timeline.. + /// Looks up a localized string similar to You have no permission to delete posts in the timeline.. + /// + internal static string MessagePostOperationDeleteForbid { + get { + return ResourceManager.GetString("MessagePostOperationDeleteForbid", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The post to delete does not exist.. /// - internal static string MessagePostsGetForbid { + internal static string MessagePostOperationDeleteNotExist { get { - return ResourceManager.GetString("MessagePostsGetForbid", resourceCulture); + return ResourceManager.GetString("MessagePostOperationDeleteNotExist", resourceCulture); } } } diff --git a/Timeline/Resources/Controllers/TimelineController.resx b/Timeline/Resources/Controllers/TimelineController.resx index 7e323164..0cf7e881 100644 --- a/Timeline/Resources/Controllers/TimelineController.resx +++ b/Timeline/Resources/Controllers/TimelineController.resx @@ -129,10 +129,16 @@ The {0}-st user to do operation {1} on does not exist. - + + You have no permission to read posts of the timeline. + + You have no permission to create posts in the timeline. - - You have no permission to read posts of the timeline. + + You have no permission to delete posts in the timeline. + + + The post to delete does not exist. \ No newline at end of file diff --git a/Timeline/Resources/Controllers/TimelineController.zh.resx b/Timeline/Resources/Controllers/TimelineController.zh.resx index cacce5fa..170ab4cd 100644 --- a/Timeline/Resources/Controllers/TimelineController.zh.resx +++ b/Timeline/Resources/Controllers/TimelineController.zh.resx @@ -123,10 +123,16 @@ 第{0}个做{1}操作的用户不存在。 - + + 你没有权限读取这个时间线消息。 + + 你没有权限在这个时间线中创建消息。 - - 你没有权限读取这个时间线消息。 + + 你没有权限在这个时间线中删除消息。 + + + 要删除的消息不存在。 \ No newline at end of file -- cgit v1.2.3 From 16b4720938ca42b777a10ba67d400531dcc1db35 Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Wed, 13 Nov 2019 22:55:31 +0800 Subject: WIP : Write timeline service. --- .../Controllers/PersonalTimelineControllerTest.cs | 2 +- Timeline/Controllers/PersonalTimelineController.cs | 4 +- Timeline/Entities/DatabaseContext.cs | 1 + Timeline/Services/TimelineService.cs | 336 ++++++++++++++++++++- Timeline/Services/UsernameBadFormatException.cs | 4 +- 5 files changed, 336 insertions(+), 11 deletions(-) (limited to 'Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs') diff --git a/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs b/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs index aecd10af..a7cbb37e 100644 --- a/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs +++ b/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs @@ -367,7 +367,7 @@ namespace Timeline.Tests.Controllers }); result.Should().NotBeNull().And.BeAssignableTo() .Which.Value.Should().BeAssignableTo() - .Which.Code.Should().Be(ErrorCodes.Http.Timeline.MemberAddNotExist); + .Which.Code.Should().Be(ErrorCodes.Http.Timeline.ChangeMemberUserNotExist); _service.VerifyAll(); } diff --git a/Timeline/Controllers/PersonalTimelineController.cs b/Timeline/Controllers/PersonalTimelineController.cs index f0f4e4c2..af6a70f8 100644 --- a/Timeline/Controllers/PersonalTimelineController.cs +++ b/Timeline/Controllers/PersonalTimelineController.cs @@ -25,7 +25,7 @@ namespace Timeline public const int PostOperationCreateForbid = 10040102; public const int PostOperationDeleteForbid = 10040103; public const int PostOperationDeleteNotExist = 10040201; - public const int MemberAddNotExist = 10040301; + public const int ChangeMemberUserNotExist = 10040301; } } } @@ -156,7 +156,7 @@ namespace Timeline.Controllers } else if (e.InnerException is UserNotExistException) { - return BadRequest(new CommonResponse(ErrorCodes.Http.Timeline.MemberAddNotExist, + return BadRequest(new CommonResponse(ErrorCodes.Http.Timeline.ChangeMemberUserNotExist, string.Format(CultureInfo.CurrentCulture, MessageMemberUserNotExist, e.Index, e.Operation))); } diff --git a/Timeline/Entities/DatabaseContext.cs b/Timeline/Entities/DatabaseContext.cs index 19df32c6..123ae0f3 100644 --- a/Timeline/Entities/DatabaseContext.cs +++ b/Timeline/Entities/DatabaseContext.cs @@ -22,5 +22,6 @@ namespace Timeline.Entities public DbSet UserDetails { get; set; } = default!; public DbSet Timelines { get; set; } = default!; public DbSet TimelinePosts { get; set; } = default!; + public DbSet TimelineMembers { get; set; } = default!; } } diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs index 28b1f91d..eff0c3fc 100644 --- a/Timeline/Services/TimelineService.cs +++ b/Timeline/Services/TimelineService.cs @@ -1,10 +1,12 @@ -using System; +using Microsoft.EntityFrameworkCore; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Timeline.Entities; using Timeline.Models; using Timeline.Models.Http; +using Timeline.Models.Validation; namespace Timeline.Services { @@ -135,14 +137,14 @@ namespace Timeline.Services /// The inner exception is /// when one of the username is invalid. /// The inner exception is - /// when one of the user to add does not exist. + /// when one of the user to change does not exist. /// /// - /// Operating on a username that is of bad format always throws. + /// Operating on a username that is of bad format or does not exist always throws. /// Add a user that already is a member has no effects. /// Remove a user that is not a member also has not effects. - /// Add a user that does not exist will throw . - /// But remove one does not throw. + /// Add and remove an identical user results in no effects. + /// More than one same usernames are regarded as one. /// Task ChangeMember(string name, IList? add, IList? remove); @@ -151,6 +153,7 @@ namespace Timeline.Services /// /// Username or the timeline name. See remarks of . /// The user to check on. Null means visitor without account. + /// True if can read, false if can't read. /// Thrown when is null. /// /// Thrown when timeline name is of bad format. @@ -164,7 +167,12 @@ namespace Timeline.Services /// For personal timeline, it means the user of that username does not exist /// and the inner exception should be a . /// - /// True if can read, false if can't read. + /// + /// Thrown when is of bad format. + /// + /// + /// Thrown when does not exist. + /// Task HasReadPermission(string name, string? username); /// @@ -285,4 +293,320 @@ namespace Timeline.Services /// Task GetTimeline(string username); } + + public abstract class BaseTimelineService : IBaseTimelineService + { + protected BaseTimelineService(DatabaseContext database, IClock clock) + { + Clock = clock; + Database = database; + } + + protected IClock Clock { get; } + + protected UsernameValidator UsernameValidator { get; } = new UsernameValidator(); + + protected DatabaseContext Database { get; } + + /// + /// Find the timeline id by the name. + /// For details, see remarks. + /// + /// The username or the timeline name. See remarks. + /// The id of the timeline entity. + /// Thrown when is null. + /// + /// Thrown when timeline name is of bad format. + /// For normal timeline, it means name is an empty string. + /// For personal timeline, it means the username is of bad format, + /// the inner exception should be a . + /// + /// + /// Thrown when timeline does not exist. + /// For normal timeline, it means the name does not exist. + /// For personal timeline, it means the user of that username does not exist + /// and the inner exception should be a . + /// + /// + /// This is the common but different part for both types of timeline service. + /// For class that implements , this method should + /// find the timeline entity id by the given as the username of the owner. + /// For class that implements , this method should + /// find the timeline entity id by the given as the timeline name. + /// This method should be called by many other method that follows the contract. + /// + protected abstract Task FindTimelineId(string name); + + public async Task> GetPosts(string name) + { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + var timelineId = await FindTimelineId(name); + var postEntities = await Database.TimelinePosts.Where(p => p.TimelineId == timelineId).ToListAsync(); + var posts = new List(await Task.WhenAll(postEntities.Select(async p => new TimelinePostInfo + { + Id = p.Id, + Content = p.Content, + Author = (await Database.Users.Where(u => u.Id == p.AuthorId).Select(u => new { u.Name }).SingleAsync()).Name, + Time = p.Time + }))); + return posts; + } + + public async Task CreatePost(string name, string author, string content, DateTime? time) + { + if (name == null) + throw new ArgumentNullException(nameof(name)); + if (author == null) + throw new ArgumentNullException(nameof(author)); + if (content == null) + throw new ArgumentNullException(nameof(content)); + + { + var (result, message) = UsernameValidator.Validate(author); + if (!result) + { + throw new UsernameBadFormatException(author, message); + } + } + + var timelineId = await FindTimelineId(name); + + var authorEntity = Database.Users.Where(u => u.Name == author).Select(u => new { u.Id }).SingleOrDefault(); + if (authorEntity == null) + { + throw new UserNotExistException(author); + } + var authorId = authorEntity.Id; + + var currentTime = Clock.GetCurrentTime(); + + var postEntity = new TimelinePostEntity + { + Content = content, + AuthorId = authorId, + TimelineId = timelineId, + Time = time ?? currentTime, + LastUpdated = currentTime + }; + + Database.TimelinePosts.Add(postEntity); + await Database.SaveChangesAsync(); + + return new TimelinePostCreateResponse + { + Id = postEntity.Id, + Time = postEntity.Time + }; + } + + public async Task DeletePost(string name, long id) + { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + var timelineId = FindTimelineId(name); + + var post = await Database.TimelinePosts.Where(p => p.Id == id).SingleOrDefaultAsync(); + + if (post == null) + throw new TimelinePostNotExistException(id); + + Database.TimelinePosts.Remove(post); + await Database.SaveChangesAsync(); + } + + public async Task ChangeProperty(string name, TimelinePropertyChangeRequest newProperties) + { + if (name == null) + throw new ArgumentNullException(nameof(name)); + if (newProperties == null) + throw new ArgumentNullException(nameof(newProperties)); + + var timelineId = await FindTimelineId(name); + + var timelineEntity = await Database.Timelines.Where(t => t.Id == timelineId).SingleAsync(); + + if (newProperties.Description != null) + { + timelineEntity.Description = newProperties.Description; + } + + if (newProperties.Visibility.HasValue) + { + timelineEntity.Visibility = newProperties.Visibility.Value; + } + + await Database.SaveChangesAsync(); + } + + public async Task ChangeMember(string name, IList? add, IList? remove) + { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + // remove duplication and check the format of each username. + // Return a username->index map. + Dictionary? RemoveDuplicateAndCheckFormat(IList? list, TimelineMemberOperationUserException.MemberOperation operation) + { + if (list != null) + { + Dictionary result = new Dictionary(); + var count = 0; + for (var index = 0; index < count; index++) + { + var username = list[index]; + if (result.ContainsKey(username)) + { + continue; + } + var (validationResult, message) = UsernameValidator.Validate(username); + if (!validationResult) + throw new TimelineMemberOperationUserException( + index, operation, username, + new UsernameBadFormatException(username, message)); + result.Add(username, index); + } + return result; + } + else + { + return null; + } + } + var simplifiedAdd = RemoveDuplicateAndCheckFormat(add, TimelineMemberOperationUserException.MemberOperation.Add); + var simplifiedRemove = RemoveDuplicateAndCheckFormat(remove, TimelineMemberOperationUserException.MemberOperation.Remove); + + // remove those both in add and remove + if (simplifiedAdd != null && simplifiedRemove != null) + { + var usersToClean = simplifiedRemove.Keys.Where(u => simplifiedAdd.ContainsKey(u)); + foreach (var u in usersToClean) + { + simplifiedAdd.Remove(u); + simplifiedRemove.Remove(u); + } + } + + var timelineId = await FindTimelineId(name); + + async Task?> CheckExistenceAndGetId(Dictionary? map, TimelineMemberOperationUserException.MemberOperation operation) + { + if (map == null) + return null; + + List result = new List(); + foreach (var (username, index) in map) + { + var user = await Database.Users.Where(u => u.Name == username).Select(u => new { u.Id }).SingleOrDefaultAsync(); + if (user == null) + { + throw new TimelineMemberOperationUserException(index, operation, username, + new UserNotExistException(username)); + } + result.Add(user.Id); + } + return result; + } + var userIdsAdd = await CheckExistenceAndGetId(simplifiedAdd, TimelineMemberOperationUserException.MemberOperation.Add); + var userIdsRemove = await CheckExistenceAndGetId(simplifiedRemove, TimelineMemberOperationUserException.MemberOperation.Remove); + + if (userIdsAdd != null) + { + var membersToAdd = userIdsAdd.Select(id => new TimelineMemberEntity { UserId = id, TimelineId = timelineId }).ToList(); + Database.TimelineMembers.AddRange(membersToAdd); + } + + if (userIdsRemove != null) + { + var membersToRemove = await Database.TimelineMembers.Where(m => m.TimelineId == timelineId && userIdsRemove.Contains(m.UserId)).ToListAsync(); + Database.TimelineMembers.RemoveRange(membersToRemove); + } + + await Database.SaveChangesAsync(); + } + + public async Task HasReadPermission(string name, string? username) + { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + long? userId = null; + if (username != null) + { + var (result, message) = UsernameValidator.Validate(username); + if (!result) + { + throw new UsernameBadFormatException(username); + } + + var user = await Database.Users.Where(u => u.Name == username).Select(u => new { u.Id }).SingleOrDefaultAsync(); + + if (user == null) + { + throw new UserNotExistException(username); + } + + userId = user.Id; + } + + var timelineId = await FindTimelineId(name); + + var timelineEntity = await Database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.Visibility }).SingleAsync(); + + if (timelineEntity.Visibility == TimelineVisibility.Public) + return true; + + if (timelineEntity.Visibility == TimelineVisibility.Register && username != null) + return true; + + if (userId == null) + { + return false; + } + else + { + var memberEntity = await Database.TimelineMembers.Where(m => m.UserId == userId && m.TimelineId == timelineId).SingleOrDefaultAsync(); + return memberEntity != null; + } + } + + public async Task HasPostModifyPermission(string name, long id, string username) + { + if (name == null) + throw new ArgumentNullException(nameof(name)); + if (username == null) + throw new ArgumentNullException(nameof(username)); + + { + var (result, message) = UsernameValidator.Validate(username); + if (!result) + { + throw new UsernameBadFormatException(username); + } + } + + var user = await Database.Users.Where(u => u.Name == username).Select(u => new { u.Id }).SingleOrDefaultAsync(); + + if (user == null) + { + throw new UserNotExistException(username); + } + + var userId = user.Id; + + var timelineId = await FindTimelineId(name); + + var timelineEntity = await Database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync(); + + var postEntitu = await Database.Timelines. // TODO! + + if (timelineEntity.OwnerId == userId) + { + return true; + } + } + + } } diff --git a/Timeline/Services/UsernameBadFormatException.cs b/Timeline/Services/UsernameBadFormatException.cs index 04354d22..d82bf962 100644 --- a/Timeline/Services/UsernameBadFormatException.cs +++ b/Timeline/Services/UsernameBadFormatException.cs @@ -9,8 +9,8 @@ namespace Timeline.Services public class UsernameBadFormatException : Exception { public UsernameBadFormatException() : base(Resources.Services.Exception.UsernameBadFormatException) { } - public UsernameBadFormatException(string message) : base(message) { } - public UsernameBadFormatException(string message, Exception inner) : base(message, inner) { } + public UsernameBadFormatException(string username) : this() { Username = username; } + public UsernameBadFormatException(string username, Exception inner) : base(Resources.Services.Exception.UsernameBadFormatException, inner) { Username = username; } public UsernameBadFormatException(string username, string message) : base(message) { Username = username; } public UsernameBadFormatException(string username, string message, Exception inner) : base(message, inner) { Username = username; } -- cgit v1.2.3 From 06a5d9aae4a348ff93aeaa40ac3d3ae2e7354f0f Mon Sep 17 00:00:00 2001 From: crupest Date: Mon, 18 Nov 2019 19:29:37 +0800 Subject: Write tests and fix bugs found via tests. --- .../Controllers/PersonalTimelineControllerTest.cs | 2 +- Timeline.Tests/Helpers/ResponseAssertions.cs | 2 +- .../IntegratedTests/PersonalTimelineTest.cs | 135 +++++++++++++++++++++ Timeline/Entities/TimelineEntity.cs | 17 +-- Timeline/Models/Timeline.cs | 18 ++- Timeline/Resources/Services/Exception.Designer.cs | 2 +- Timeline/Resources/Services/Exception.resx | 2 +- Timeline/Services/TimelineService.cs | 2 +- 8 files changed, 157 insertions(+), 23 deletions(-) (limited to 'Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs') diff --git a/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs b/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs index a7cbb37e..819017c2 100644 --- a/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs +++ b/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs @@ -309,7 +309,7 @@ namespace Timeline.Tests.Controllers var req = new TimelinePropertyChangeRequest { Description = "", - Visibility = Entities.TimelineVisibility.Private + Visibility = TimelineVisibility.Private }; _service.Setup(s => s.ChangeProperty(username, req)).Returns(Task.CompletedTask); var result = await _controller.TimelineChangeProperty(username, req); diff --git a/Timeline.Tests/Helpers/ResponseAssertions.cs b/Timeline.Tests/Helpers/ResponseAssertions.cs index 0e6f215b..6d764c68 100644 --- a/Timeline.Tests/Helpers/ResponseAssertions.cs +++ b/Timeline.Tests/Helpers/ResponseAssertions.cs @@ -88,7 +88,7 @@ namespace Timeline.Tests.Helpers return new AndWhichConstraint(this, null); } - var result = JsonConvert.DeserializeObject(body); + var result = JsonConvert.DeserializeObject(body); // TODO! catch and throw on bad format return new AndWhichConstraint(this, result); } } diff --git a/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs b/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs index 9629fc0a..aaa6215c 100644 --- a/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs +++ b/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Net.Http; using System.Text.Json; using System.Threading.Tasks; +using Timeline.Models; using Timeline.Models.Http; using Timeline.Tests.Helpers; using Timeline.Tests.Helpers.Authentication; @@ -22,6 +23,64 @@ namespace Timeline.Tests.IntegratedTests } + [Fact] + public async Task Member_Should_Work() + { + const string getUrl = "users/user/timeline"; + const string changeUrl = "users/user/timeline/op/member"; + using var client = await Factory.CreateClientAsUser(); + + async Task AssertMembers(IList members) + { + var res = await client.GetAsync(getUrl); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Members.Should().NotBeNull().And.BeEquivalentTo(members); + } + + async Task AssertEmptyMembers() + { + var res = await client.GetAsync(getUrl); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Members.Should().NotBeNull().And.BeEmpty(); + } + + await AssertEmptyMembers(); + { + var res = await client.PostAsJsonAsync(changeUrl, + new TimelineMemberChangeRequest { Add = new List { "admin", "usernotexist" } }); + res.Should().HaveStatusCode(400) + .And.HaveCommonBody() + .Which.Code.Should().Be(ErrorCodes.Http.Timeline.ChangeMemberUserNotExist); + } + { + var res = await client.PostAsJsonAsync(changeUrl, + new TimelineMemberChangeRequest { Remove = new List { "admin", "usernotexist" } }); + res.Should().HaveStatusCode(400) + .And.HaveCommonBody() + .Which.Code.Should().Be(ErrorCodes.Http.Timeline.ChangeMemberUserNotExist); + } + { + var res = await client.PostAsJsonAsync(changeUrl, + new TimelineMemberChangeRequest { Add = new List { "admin" }, Remove = new List { "admin" } }); + res.Should().HaveStatusCode(200); + await AssertEmptyMembers(); + } + { + var res = await client.PostAsJsonAsync(changeUrl, + new TimelineMemberChangeRequest { Add = new List { "admin" } }); + res.Should().HaveStatusCode(200); + await AssertMembers(new List { "admin" }); + } + { + var res = await client.PostAsJsonAsync(changeUrl, + new TimelineMemberChangeRequest { Remove = new List { "admin" } }); + res.Should().HaveStatusCode(200); + await AssertEmptyMembers(); + } + } + [Theory] [InlineData(AuthType.None, 200, 401, 401, 401, 401)] [InlineData(AuthType.User, 200, 200, 403, 200, 403)] @@ -58,5 +117,81 @@ namespace Timeline.Tests.IntegratedTests res.Should().HaveStatusCode(opMemberAdmin); } } + + [Fact] + public async Task Permission_GetPost() + { + const string userUrl = "users/user/timeline/posts"; + const string adminUrl = "users/admin/timeline/posts"; + { // default visibility is registered + { + using var client = Factory.CreateDefaultClient(); + var res = await client.GetAsync(userUrl); + res.Should().HaveStatusCode(403); + } + + { + using var client = await Factory.CreateClientAsUser(); + var res = await client.GetAsync(adminUrl); + res.Should().HaveStatusCode(200); + } + } + + { // change visibility to public + { + using var client = await Factory.CreateClientAsUser(); + var res = await client.PostAsJsonAsync("users/user/timeline/op/property", + new TimelinePropertyChangeRequest { Visibility = TimelineVisibility.Public }); + res.Should().HaveStatusCode(200); + } + { + using var client = Factory.CreateDefaultClient(); + var res = await client.GetAsync(userUrl); + res.Should().HaveStatusCode(200); + } + } + + { // change visibility to private + { + using var client = await Factory.CreateClientAsAdmin(); + { + var res = await client.PostAsJsonAsync("users/user/timeline/op/property", + new TimelinePropertyChangeRequest { Visibility = TimelineVisibility.Private }); + res.Should().HaveStatusCode(200); + } + { + var res = await client.PostAsJsonAsync("users/admin/timeline/op/property", + new TimelinePropertyChangeRequest { Visibility = TimelineVisibility.Private }); + res.Should().HaveStatusCode(200); + } + } + { + using var client = Factory.CreateDefaultClient(); + var res = await client.GetAsync(userUrl); + res.Should().HaveStatusCode(403); + } + { // user can't read admin's + using var client = await Factory.CreateClientAsUser(); + var res = await client.GetAsync(adminUrl); + res.Should().HaveStatusCode(403); + } + { // admin can read user's + using var client = await Factory.CreateClientAsAdmin(); + var res = await client.GetAsync(userUrl); + res.Should().HaveStatusCode(200); + } + { // add member + using var client = await Factory.CreateClientAsAdmin(); + var res = await client.PostAsJsonAsync("users/admin/timeline/op/member", + new TimelineMemberChangeRequest { Add = new List { "user" } }); + res.Should().HaveStatusCode(200); + } + { // now user can read admin's + using var client = await Factory.CreateClientAsUser(); + var res = await client.GetAsync(adminUrl); + res.Should().HaveStatusCode(200); + } + } + } } } diff --git a/Timeline/Entities/TimelineEntity.cs b/Timeline/Entities/TimelineEntity.cs index f5e22a54..9cacfcae 100644 --- a/Timeline/Entities/TimelineEntity.cs +++ b/Timeline/Entities/TimelineEntity.cs @@ -2,25 +2,10 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using Timeline.Models; namespace Timeline.Entities { - public enum TimelineVisibility - { - /// - /// All people including those without accounts. - /// - Public, - /// - /// Only people signed in. - /// - Register, - /// - /// Only member. - /// - Private - } - [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "This is entity object.")] [Table("timelines")] public class TimelineEntity diff --git a/Timeline/Models/Timeline.cs b/Timeline/Models/Timeline.cs index 26012878..85fefff5 100644 --- a/Timeline/Models/Timeline.cs +++ b/Timeline/Models/Timeline.cs @@ -1,11 +1,25 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Timeline.Entities; namespace Timeline.Models { + public enum TimelineVisibility + { + /// + /// All people including those without accounts. + /// + Public, + /// + /// Only people signed in. + /// + Register, + /// + /// Only member. + /// + Private + } + public class TimelinePostInfo { public long Id { get; set; } diff --git a/Timeline/Resources/Services/Exception.Designer.cs b/Timeline/Resources/Services/Exception.Designer.cs index 970c306d..1b46f9e9 100644 --- a/Timeline/Resources/Services/Exception.Designer.cs +++ b/Timeline/Resources/Services/Exception.Designer.cs @@ -286,7 +286,7 @@ namespace Timeline.Resources.Services { } /// - /// Looks up a localized string similar to An exception happened when do operation {} on the {} member on timeline.. + /// Looks up a localized string similar to An exception happened when do operation {0} on the {1} member on timeline.. /// internal static string TimelineMemberOperationExceptionDetail { get { diff --git a/Timeline/Resources/Services/Exception.resx b/Timeline/Resources/Services/Exception.resx index c8f6676a..1d9c0037 100644 --- a/Timeline/Resources/Services/Exception.resx +++ b/Timeline/Resources/Services/Exception.resx @@ -193,7 +193,7 @@ An exception happened when add or remove member on timeline. - An exception happened when do operation {} on the {} member on timeline. + An exception happened when do operation {0} on the {1} member on timeline. Timeline name is of bad format. If this is a personal timeline, it means the username is of bad format and inner exception should be a UsernameBadFormatException. diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs index 494beb11..1d199aae 100644 --- a/Timeline/Services/TimelineService.cs +++ b/Timeline/Services/TimelineService.cs @@ -458,7 +458,7 @@ namespace Timeline.Services if (list != null) { Dictionary result = new Dictionary(); - var count = 0; + var count = list.Count; for (var index = 0; index < count; index++) { var username = list[index]; -- cgit v1.2.3 From ed8bae9cf7fd22300678d718cfee1913209f2cd0 Mon Sep 17 00:00:00 2001 From: crupest Date: Wed, 20 Nov 2019 00:28:53 +0800 Subject: Clean and refactor tests. --- .../Controllers/PersonalTimelineControllerTest.cs | 1 - Timeline.Tests/Controllers/TokenControllerTest.cs | 3 +- Timeline.Tests/Controllers/UserControllerTest.cs | 1 - Timeline.Tests/DatabaseTest.cs | 2 +- .../Authentication/AuthenticationExtensions.cs | 75 ----------- .../Helpers/Authentication/PrincipalHelper.cs | 23 ---- Timeline.Tests/Helpers/MockUser.cs | 24 ++++ Timeline.Tests/Helpers/PrincipalHelper.cs | 23 ++++ Timeline.Tests/Helpers/TestApplication.cs | 2 - Timeline.Tests/Helpers/TestClock.cs | 15 +++ Timeline.Tests/Helpers/TestDatabase.cs | 89 +++++++++++++ Timeline.Tests/Helpers/UseCultureAttribute.cs | 143 +++++++++++---------- .../IntegratedTests/AuthorizationTest.cs | 23 +--- Timeline.Tests/IntegratedTests/I18nTest.cs | 16 +-- .../IntegratedTests/IntegratedTestBase.cs | 65 +++++++++- .../IntegratedTests/PersonalTimelineTest.cs | 62 +++++---- Timeline.Tests/IntegratedTests/TokenTest.cs | 46 +++---- Timeline.Tests/IntegratedTests/UserAvatarTest.cs | 21 +-- Timeline.Tests/IntegratedTests/UserDetailTest.cs | 25 +--- Timeline.Tests/IntegratedTests/UserTest.cs | 68 +++++----- Timeline.Tests/Mock/Data/TestDatabase.cs | 88 ------------- Timeline.Tests/Mock/Data/TestUsers.cs | 25 ---- Timeline.Tests/Mock/Services/TestClock.cs | 15 --- Timeline.Tests/Services/UserAvatarServiceTest.cs | 1 - Timeline.Tests/Services/UserDetailServiceTest.cs | 1 - Timeline.Tests/UsernameValidatorUnitTest.cs | 1 + 26 files changed, 396 insertions(+), 462 deletions(-) delete mode 100644 Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs delete mode 100644 Timeline.Tests/Helpers/Authentication/PrincipalHelper.cs create mode 100644 Timeline.Tests/Helpers/MockUser.cs create mode 100644 Timeline.Tests/Helpers/PrincipalHelper.cs create mode 100644 Timeline.Tests/Helpers/TestClock.cs create mode 100644 Timeline.Tests/Helpers/TestDatabase.cs delete mode 100644 Timeline.Tests/Mock/Data/TestDatabase.cs delete mode 100644 Timeline.Tests/Mock/Data/TestUsers.cs delete mode 100644 Timeline.Tests/Mock/Services/TestClock.cs (limited to 'Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs') diff --git a/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs b/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs index 819017c2..372ba8a7 100644 --- a/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs +++ b/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs @@ -15,7 +15,6 @@ using Timeline.Models.Http; using Timeline.Models.Validation; using Timeline.Services; using Timeline.Tests.Helpers; -using Timeline.Tests.Helpers.Authentication; using Xunit; namespace Timeline.Tests.Controllers diff --git a/Timeline.Tests/Controllers/TokenControllerTest.cs b/Timeline.Tests/Controllers/TokenControllerTest.cs index 4a08ca0f..238fc237 100644 --- a/Timeline.Tests/Controllers/TokenControllerTest.cs +++ b/Timeline.Tests/Controllers/TokenControllerTest.cs @@ -8,8 +8,7 @@ using System.Threading.Tasks; using Timeline.Controllers; using Timeline.Models.Http; using Timeline.Services; -using Timeline.Tests.Mock.Data; -using Timeline.Tests.Mock.Services; +using Timeline.Tests.Helpers; using Xunit; using static Timeline.ErrorCodes.Http.Token; diff --git a/Timeline.Tests/Controllers/UserControllerTest.cs b/Timeline.Tests/Controllers/UserControllerTest.cs index 83b8cdcf..a5ca7a2b 100644 --- a/Timeline.Tests/Controllers/UserControllerTest.cs +++ b/Timeline.Tests/Controllers/UserControllerTest.cs @@ -12,7 +12,6 @@ using Timeline.Models; using Timeline.Models.Http; using Timeline.Services; using Timeline.Tests.Helpers; -using Timeline.Tests.Mock.Data; using Xunit; using static Timeline.ErrorCodes.Http.User; diff --git a/Timeline.Tests/DatabaseTest.cs b/Timeline.Tests/DatabaseTest.cs index 20f57c40..a7b97c16 100644 --- a/Timeline.Tests/DatabaseTest.cs +++ b/Timeline.Tests/DatabaseTest.cs @@ -2,7 +2,7 @@ using System; using System.Linq; using Timeline.Entities; -using Timeline.Tests.Mock.Data; +using Timeline.Tests.Helpers; using Xunit; namespace Timeline.Tests diff --git a/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs b/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs deleted file mode 100644 index 4048bb73..00000000 --- a/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs +++ /dev/null @@ -1,75 +0,0 @@ -using Microsoft.AspNetCore.Mvc.Testing; -using System; -using System.Net.Http; -using System.Threading.Tasks; -using Timeline.Models.Http; -using Timeline.Tests.Mock.Data; - -namespace Timeline.Tests.Helpers.Authentication -{ - public enum AuthType - { - None, - User, - Admin - } - - public static class AuthenticationExtensions - { - private const string CreateTokenUrl = "/token/create"; - - public static async Task CreateUserTokenAsync(this HttpClient client, string username, string password, int? expireOffset = null) - { - var response = await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest { Username = username, Password = password, Expire = expireOffset }); - return response.Should().HaveStatusCode(200) - .And.HaveJsonBody().Which; - } - - public static async Task CreateClientWithCredential(this WebApplicationFactory factory, string username, string password) where T : class - { - var client = factory.CreateDefaultClient(); - var token = (await client.CreateUserTokenAsync(username, password)).Token; - client.DefaultRequestHeaders.Add("Authorization", "Bearer " + token); - return client; - } - - public static Task CreateClientAs(this WebApplicationFactory factory, MockUser user) where T : class - { - return CreateClientWithCredential(factory, user.Username, user.Password); - } - - public static Task CreateClientAsUser(this WebApplicationFactory factory) where T : class - { - return factory.CreateClientAs(MockUser.User); - } - - public static Task CreateClientAsAdmin(this WebApplicationFactory factory) where T : class - { - return factory.CreateClientAs(MockUser.Admin); - } - - public static Task CreateClientAs(this WebApplicationFactory factory, AuthType authType) where T : class - { - return authType switch - { - AuthType.None => Task.FromResult(factory.CreateDefaultClient()), - AuthType.User => factory.CreateClientAsUser(), - AuthType.Admin => factory.CreateClientAsAdmin(), - _ => throw new InvalidOperationException("Unknown auth type.") - }; - } - - public static MockUser GetMockUser(this AuthType authType) - { - return authType switch - { - AuthType.None => null, - AuthType.User => MockUser.User, - AuthType.Admin => MockUser.Admin, - _ => throw new InvalidOperationException("Unknown auth type.") - }; - } - - public static string GetUsername(this AuthType authType) => authType.GetMockUser().Username; - } -} diff --git a/Timeline.Tests/Helpers/Authentication/PrincipalHelper.cs b/Timeline.Tests/Helpers/Authentication/PrincipalHelper.cs deleted file mode 100644 index 214472a2..00000000 --- a/Timeline.Tests/Helpers/Authentication/PrincipalHelper.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Linq; -using System.Security.Claims; -using Timeline.Models; - -namespace Timeline.Tests.Helpers.Authentication -{ - public static class PrincipalHelper - { - internal const string AuthScheme = "TESTAUTH"; - - internal static ClaimsPrincipal Create(string username, bool administrator) - { - var identity = new ClaimsIdentity(AuthScheme); - identity.AddClaim(new Claim(identity.NameClaimType, username, ClaimValueTypes.String)); - identity.AddClaims(UserRoleConvert.ToArray(administrator).Select(role => new Claim(identity.RoleClaimType, role, ClaimValueTypes.String))); - - var principal = new ClaimsPrincipal(); - principal.AddIdentity(identity); - - return principal; - } - } -} diff --git a/Timeline.Tests/Helpers/MockUser.cs b/Timeline.Tests/Helpers/MockUser.cs new file mode 100644 index 00000000..8d738525 --- /dev/null +++ b/Timeline.Tests/Helpers/MockUser.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using Timeline.Models; + +namespace Timeline.Tests.Helpers +{ + public class MockUser + { + public MockUser(string username, string password, bool administrator) + { + Info = new UserInfo(username, administrator); + Password = password; + } + + public UserInfo Info { get; set; } + public string Username => Info.Username; + public string Password { get; set; } + public bool Administrator => Info.Administrator; + + public static MockUser User { get; } = new MockUser("user", "userpassword", false); + public static MockUser Admin { get; } = new MockUser("admin", "adminpassword", true); + + public static IReadOnlyList UserInfoList { get; } = new List { User.Info, Admin.Info }; + } +} diff --git a/Timeline.Tests/Helpers/PrincipalHelper.cs b/Timeline.Tests/Helpers/PrincipalHelper.cs new file mode 100644 index 00000000..89f3f7b1 --- /dev/null +++ b/Timeline.Tests/Helpers/PrincipalHelper.cs @@ -0,0 +1,23 @@ +using System.Linq; +using System.Security.Claims; +using Timeline.Models; + +namespace Timeline.Tests.Helpers +{ + public static class PrincipalHelper + { + internal const string AuthScheme = "TESTAUTH"; + + internal static ClaimsPrincipal Create(string username, bool administrator) + { + var identity = new ClaimsIdentity(AuthScheme); + identity.AddClaim(new Claim(identity.NameClaimType, username, ClaimValueTypes.String)); + identity.AddClaims(UserRoleConvert.ToArray(administrator).Select(role => new Claim(identity.RoleClaimType, role, ClaimValueTypes.String))); + + var principal = new ClaimsPrincipal(); + principal.AddIdentity(identity); + + return principal; + } + } +} diff --git a/Timeline.Tests/Helpers/TestApplication.cs b/Timeline.Tests/Helpers/TestApplication.cs index 5862f452..a624da6b 100644 --- a/Timeline.Tests/Helpers/TestApplication.cs +++ b/Timeline.Tests/Helpers/TestApplication.cs @@ -1,10 +1,8 @@ using Microsoft.AspNetCore.Mvc.Testing; -using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using System; using Timeline.Entities; -using Timeline.Tests.Mock.Data; namespace Timeline.Tests.Helpers { diff --git a/Timeline.Tests/Helpers/TestClock.cs b/Timeline.Tests/Helpers/TestClock.cs new file mode 100644 index 00000000..12b320d3 --- /dev/null +++ b/Timeline.Tests/Helpers/TestClock.cs @@ -0,0 +1,15 @@ +using System; +using Timeline.Services; + +namespace Timeline.Tests.Helpers +{ + public class TestClock : IClock + { + public DateTime? MockCurrentTime { get; set; } = null; + + public DateTime GetCurrentTime() + { + return MockCurrentTime.GetValueOrDefault(DateTime.Now); + } + } +} diff --git a/Timeline.Tests/Helpers/TestDatabase.cs b/Timeline.Tests/Helpers/TestDatabase.cs new file mode 100644 index 00000000..10224c27 --- /dev/null +++ b/Timeline.Tests/Helpers/TestDatabase.cs @@ -0,0 +1,89 @@ +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using Timeline.Entities; +using Timeline.Models; +using Timeline.Services; + +namespace Timeline.Tests.Helpers +{ + public class TestDatabase : IDisposable + { + // currently password service is thread safe, so we share a static one. + private static PasswordService PasswordService { get; } = new PasswordService(); + + private static User CreateEntityFromMock(MockUser user) + { + return new User + { + Name = user.Username, + EncryptedPassword = PasswordService.HashPassword(user.Password), + RoleString = UserRoleConvert.ToString(user.Administrator), + Avatar = null + }; + } + + private static IEnumerable CreateDefaultMockEntities() + { + // emmmmmmm. Never reuse the user instances because EF Core uses them, which will cause strange things. + yield return CreateEntityFromMock(MockUser.User); + yield return CreateEntityFromMock(MockUser.Admin); + } + + private static void InitDatabase(DatabaseContext context) + { + context.Database.EnsureCreated(); + context.Users.AddRange(CreateDefaultMockEntities()); + context.SaveChanges(); + } + + public SqliteConnection Connection { get; } + public DatabaseContext Context { get; } + + public TestDatabase() + { + Connection = new SqliteConnection("Data Source=:memory:;"); + Connection.Open(); + + var options = new DbContextOptionsBuilder() + .UseSqlite(Connection) + .Options; + + Context = new DatabaseContext(options); + + InitDatabase(Context); + } + + private List _extraMockUsers; + + public IReadOnlyList ExtraMockUsers => _extraMockUsers; + + public void CreateExtraMockUsers(int count) + { + if (count <= 0) + throw new ArgumentOutOfRangeException(nameof(count), count, "Additional user count must be bigger than 0."); + if (_extraMockUsers != null) + throw new InvalidOperationException("Already create mock users."); + + _extraMockUsers = new List(); + for (int i = 0; i < count; i++) + { + _extraMockUsers.Add(new MockUser($"user{i}", $"password", false)); + } + + Context.AddRange(_extraMockUsers.Select(u => CreateEntityFromMock(u))); + Context.SaveChanges(); + } + + public void Dispose() + { + Context.Dispose(); + + Connection.Close(); + Connection.Dispose(); + } + + } +} diff --git a/Timeline.Tests/Helpers/UseCultureAttribute.cs b/Timeline.Tests/Helpers/UseCultureAttribute.cs index f0064c01..017d77a8 100644 --- a/Timeline.Tests/Helpers/UseCultureAttribute.cs +++ b/Timeline.Tests/Helpers/UseCultureAttribute.cs @@ -1,91 +1,94 @@ using System; using System.Globalization; -using System.Linq; using System.Reflection; using System.Threading; using Xunit.Sdk; -// Copied from https://github.com/xunit/samples.xunit/blob/master/UseCulture/UseCultureAttribute.cs -/// -/// Apply this attribute to your test method to replace the -/// and -/// with another culture. -/// -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] -public class UseCultureAttribute : BeforeAfterTestAttribute +namespace Timeline.Tests.Helpers { - readonly Lazy culture; - readonly Lazy uiCulture; - - CultureInfo originalCulture; - CultureInfo originalUICulture; + // Copied from https://github.com/xunit/samples.xunit/blob/master/UseCulture/UseCultureAttribute.cs /// - /// Replaces the culture and UI culture of the current thread with - /// + /// Apply this attribute to your test method to replace the + /// and + /// with another culture. /// - /// The name of the culture. - /// - /// - /// This constructor overload uses for both - /// and . - /// - /// - public UseCultureAttribute(string culture) - : this(culture, culture) { } - - /// - /// Replaces the culture and UI culture of the current thread with - /// and - /// - /// The name of the culture. - /// The name of the UI culture. - public UseCultureAttribute(string culture, string uiCulture) + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public class UseCultureAttribute : BeforeAfterTestAttribute { - this.culture = new Lazy(() => new CultureInfo(culture, false)); - this.uiCulture = new Lazy(() => new CultureInfo(uiCulture, false)); - } + readonly Lazy culture; + readonly Lazy uiCulture; - /// - /// Gets the culture. - /// - public CultureInfo Culture { get { return culture.Value; } } + CultureInfo originalCulture; + CultureInfo originalUICulture; - /// - /// Gets the UI culture. - /// - public CultureInfo UICulture { get { return uiCulture.Value; } } + /// + /// Replaces the culture and UI culture of the current thread with + /// + /// + /// The name of the culture. + /// + /// + /// This constructor overload uses for both + /// and . + /// + /// + public UseCultureAttribute(string culture) + : this(culture, culture) { } - /// - /// Stores the current - /// and - /// and replaces them with the new cultures defined in the constructor. - /// - /// The method under test - public override void Before(MethodInfo methodUnderTest) - { - originalCulture = Thread.CurrentThread.CurrentCulture; - originalUICulture = Thread.CurrentThread.CurrentUICulture; + /// + /// Replaces the culture and UI culture of the current thread with + /// and + /// + /// The name of the culture. + /// The name of the UI culture. + public UseCultureAttribute(string culture, string uiCulture) + { + this.culture = new Lazy(() => new CultureInfo(culture, false)); + this.uiCulture = new Lazy(() => new CultureInfo(uiCulture, false)); + } - Thread.CurrentThread.CurrentCulture = Culture; - Thread.CurrentThread.CurrentUICulture = UICulture; + /// + /// Gets the culture. + /// + public CultureInfo Culture { get { return culture.Value; } } - CultureInfo.CurrentCulture.ClearCachedData(); - CultureInfo.CurrentUICulture.ClearCachedData(); - } + /// + /// Gets the UI culture. + /// + public CultureInfo UICulture { get { return uiCulture.Value; } } - /// - /// Restores the original and - /// to - /// - /// The method under test - public override void After(MethodInfo methodUnderTest) - { - Thread.CurrentThread.CurrentCulture = originalCulture; - Thread.CurrentThread.CurrentUICulture = originalUICulture; + /// + /// Stores the current + /// and + /// and replaces them with the new cultures defined in the constructor. + /// + /// The method under test + public override void Before(MethodInfo methodUnderTest) + { + originalCulture = Thread.CurrentThread.CurrentCulture; + originalUICulture = Thread.CurrentThread.CurrentUICulture; + + Thread.CurrentThread.CurrentCulture = Culture; + Thread.CurrentThread.CurrentUICulture = UICulture; + + CultureInfo.CurrentCulture.ClearCachedData(); + CultureInfo.CurrentUICulture.ClearCachedData(); + } + + /// + /// Restores the original and + /// to + /// + /// The method under test + public override void After(MethodInfo methodUnderTest) + { + Thread.CurrentThread.CurrentCulture = originalCulture; + Thread.CurrentThread.CurrentUICulture = originalUICulture; - CultureInfo.CurrentCulture.ClearCachedData(); - CultureInfo.CurrentUICulture.ClearCachedData(); + CultureInfo.CurrentCulture.ClearCachedData(); + CultureInfo.CurrentUICulture.ClearCachedData(); + } } } diff --git a/Timeline.Tests/IntegratedTests/AuthorizationTest.cs b/Timeline.Tests/IntegratedTests/AuthorizationTest.cs index a31d98f5..0bc094af 100644 --- a/Timeline.Tests/IntegratedTests/AuthorizationTest.cs +++ b/Timeline.Tests/IntegratedTests/AuthorizationTest.cs @@ -1,28 +1,17 @@ using FluentAssertions; using Microsoft.AspNetCore.Mvc.Testing; -using System; using System.Net; using System.Threading.Tasks; using Timeline.Tests.Helpers; -using Timeline.Tests.Helpers.Authentication; using Xunit; namespace Timeline.Tests.IntegratedTests { - public class AuthorizationTest : IClassFixture>, IDisposable + public class AuthorizationTest : IntegratedTestBase { - private readonly TestApplication _testApp; - private readonly WebApplicationFactory _factory; - public AuthorizationTest(WebApplicationFactory factory) + : base(factory) { - _testApp = new TestApplication(factory); - _factory = _testApp.Factory; - } - - public void Dispose() - { - _testApp.Dispose(); } private const string BaseUrl = "testing/auth/"; @@ -33,7 +22,7 @@ namespace Timeline.Tests.IntegratedTests [Fact] public async Task UnauthenticationTest() { - using var client = _factory.CreateDefaultClient(); + using var client = await CreateClientWithNoAuth(); var response = await client.GetAsync(AuthorizeUrl); response.Should().HaveStatusCode(HttpStatusCode.Unauthorized); } @@ -41,7 +30,7 @@ namespace Timeline.Tests.IntegratedTests [Fact] public async Task AuthenticationTest() { - using var client = await _factory.CreateClientAsUser(); + using var client = await CreateClientAsUser(); var response = await client.GetAsync(AuthorizeUrl); response.Should().HaveStatusCode(HttpStatusCode.OK); } @@ -49,7 +38,7 @@ namespace Timeline.Tests.IntegratedTests [Fact] public async Task UserAuthorizationTest() { - using var client = await _factory.CreateClientAsUser(); + using var client = await CreateClientAsUser(); var response1 = await client.GetAsync(UserUrl); response1.Should().HaveStatusCode(HttpStatusCode.OK); var response2 = await client.GetAsync(AdminUrl); @@ -59,7 +48,7 @@ namespace Timeline.Tests.IntegratedTests [Fact] public async Task AdminAuthorizationTest() { - using var client = await _factory.CreateClientAsAdmin(); + using var client = await CreateClientAsAdmin(); var response1 = await client.GetAsync(UserUrl); response1.Should().HaveStatusCode(HttpStatusCode.OK); var response2 = await client.GetAsync(AdminUrl); diff --git a/Timeline.Tests/IntegratedTests/I18nTest.cs b/Timeline.Tests/IntegratedTests/I18nTest.cs index 67bbea5c..855179af 100644 --- a/Timeline.Tests/IntegratedTests/I18nTest.cs +++ b/Timeline.Tests/IntegratedTests/I18nTest.cs @@ -1,32 +1,28 @@ -using Microsoft.AspNetCore.Mvc.Testing; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.Testing; using System; -using System.Collections.Generic; -using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; using Timeline.Tests.Helpers; using Xunit; -using FluentAssertions; namespace Timeline.Tests.IntegratedTests { [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1054:Uri parameters should not be strings")] - public class I18nTest : IClassFixture>, IDisposable + public class I18nTest : IntegratedTestBase { - private readonly TestApplication _testApp; private readonly HttpClient _client; public I18nTest(WebApplicationFactory factory) + : base(factory) { - _testApp = new TestApplication(factory); - _client = _testApp.Factory.CreateDefaultClient(); + _client = Factory.CreateDefaultClient(); } - public void Dispose() + protected override void OnDispose() { _client.Dispose(); - _testApp.Dispose(); } private const string DirectUrl = "testing/i18n/direct"; diff --git a/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs b/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs index 2dfaf82e..242a452d 100644 --- a/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs +++ b/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs @@ -1,14 +1,37 @@ using Microsoft.AspNetCore.Mvc.Testing; using System; using System.Collections.Generic; -using System.Linq; +using System.Net.Http; using System.Threading.Tasks; +using Timeline.Models.Http; using Timeline.Tests.Helpers; -using Timeline.Tests.Mock.Data; using Xunit; namespace Timeline.Tests.IntegratedTests { + public enum AuthType + { + None, + User, + Admin + } + + public static class AuthTypeExtensions + { + public static MockUser GetMockUser(this AuthType authType) + { + return authType switch + { + AuthType.None => null, + AuthType.User => MockUser.User, + AuthType.Admin => MockUser.Admin, + _ => throw new InvalidOperationException("Unknown auth type.") + }; + } + + public static string GetUsername(this AuthType authType) => authType.GetMockUser().Username; + } + public abstract class IntegratedTestBase : IClassFixture>, IDisposable { protected TestApplication TestApp { get; } @@ -20,8 +43,14 @@ namespace Timeline.Tests.IntegratedTests TestApp = new TestApplication(factory); } - public virtual void Dispose() + protected virtual void OnDispose() + { + + } + + public void Dispose() { + OnDispose(); TestApp.Dispose(); } @@ -31,5 +60,35 @@ namespace Timeline.Tests.IntegratedTests } protected IReadOnlyList ExtraMockUsers => TestApp.Database.ExtraMockUsers; + + public Task CreateClientWithNoAuth() + { + return Task.FromResult(Factory.CreateDefaultClient()); + } + + public async Task CreateClientWithCredential(string username, string password) + { + var client = Factory.CreateDefaultClient(); + var response = await client.PostAsJsonAsync("/token/create", + new CreateTokenRequest { Username = username, Password = password }); + var token = response.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which.Token; + client.DefaultRequestHeaders.Add("Authorization", "Bearer " + token); + return client; + } + + public Task CreateClientAs(MockUser user) + { + if (user == null) + return CreateClientWithNoAuth(); + return CreateClientWithCredential(user.Username, user.Password); + } + + public Task CreateClientAs(AuthType authType) => CreateClientAs(authType.GetMockUser()); + + + public Task CreateClientAsUser() => CreateClientAs(MockUser.User); + public Task CreateClientAsAdmin() => CreateClientAs(MockUser.Admin); + } } diff --git a/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs b/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs index 705675f9..9dae4c3e 100644 --- a/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs +++ b/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs @@ -1,17 +1,12 @@ using FluentAssertions; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Testing; using System; using System.Collections.Generic; -using System.Linq; using System.Net.Http; -using System.Text.Json; using System.Threading.Tasks; using Timeline.Models; using Timeline.Models.Http; using Timeline.Tests.Helpers; -using Timeline.Tests.Helpers.Authentication; -using Timeline.Tests.Mock.Data; using Xunit; namespace Timeline.Tests.IntegratedTests @@ -27,7 +22,7 @@ namespace Timeline.Tests.IntegratedTests [Fact] public async Task TimelineGet_Should_Work() { - using var client = Factory.CreateDefaultClient(); + using var client = await CreateClientWithNoAuth(); var res = await client.GetAsync("users/user/timeline"); var body = res.Should().HaveStatusCode(200) .And.HaveJsonBody().Which; @@ -40,7 +35,7 @@ namespace Timeline.Tests.IntegratedTests [Fact] public async Task Description_Should_Work() { - using var client = await Factory.CreateClientAsUser(); + using var client = await CreateClientAsUser(); async Task AssertDescription(string description) { @@ -78,7 +73,7 @@ namespace Timeline.Tests.IntegratedTests { const string getUrl = "users/user/timeline"; const string changeUrl = "users/user/timeline/op/member"; - using var client = await Factory.CreateClientAsUser(); + using var client = await CreateClientAsUser(); async Task AssertMembers(IList members) { @@ -137,7 +132,7 @@ namespace Timeline.Tests.IntegratedTests [InlineData(AuthType.Admin, 200, 200, 200, 200, 200)] public async Task Permission_Timeline(AuthType authType, int get, int opPropertyUser, int opPropertyAdmin, int opMemberUser, int opMemberAdmin) { - using var client = await Factory.CreateClientAs(authType); + using var client = await CreateClientAs(authType); { var res = await client.GetAsync("users/user/timeline"); res.Should().HaveStatusCode(get); @@ -175,13 +170,13 @@ namespace Timeline.Tests.IntegratedTests const string adminUrl = "users/admin/timeline/posts"; { // default visibility is registered { - using var client = Factory.CreateDefaultClient(); + using var client = await CreateClientWithNoAuth(); var res = await client.GetAsync(userUrl); res.Should().HaveStatusCode(403); } { - using var client = await Factory.CreateClientAsUser(); + using var client = await CreateClientAsUser(); var res = await client.GetAsync(adminUrl); res.Should().HaveStatusCode(200); } @@ -189,13 +184,13 @@ namespace Timeline.Tests.IntegratedTests { // change visibility to public { - using var client = await Factory.CreateClientAsUser(); + using var client = await CreateClientAsUser(); var res = await client.PostAsJsonAsync("users/user/timeline/op/property", new TimelinePropertyChangeRequest { Visibility = TimelineVisibility.Public }); res.Should().HaveStatusCode(200); } { - using var client = Factory.CreateDefaultClient(); + using var client = await CreateClientWithNoAuth(); var res = await client.GetAsync(userUrl); res.Should().HaveStatusCode(200); } @@ -203,7 +198,7 @@ namespace Timeline.Tests.IntegratedTests { // change visibility to private { - using var client = await Factory.CreateClientAsAdmin(); + using var client = await CreateClientAsAdmin(); { var res = await client.PostAsJsonAsync("users/user/timeline/op/property", new TimelinePropertyChangeRequest { Visibility = TimelineVisibility.Private }); @@ -216,28 +211,28 @@ namespace Timeline.Tests.IntegratedTests } } { - using var client = Factory.CreateDefaultClient(); + using var client = await CreateClientWithNoAuth(); var res = await client.GetAsync(userUrl); res.Should().HaveStatusCode(403); } { // user can't read admin's - using var client = await Factory.CreateClientAsUser(); + using var client = await CreateClientAsUser(); var res = await client.GetAsync(adminUrl); res.Should().HaveStatusCode(403); } { // admin can read user's - using var client = await Factory.CreateClientAsAdmin(); + using var client = await CreateClientAsAdmin(); var res = await client.GetAsync(userUrl); res.Should().HaveStatusCode(200); } { // add member - using var client = await Factory.CreateClientAsAdmin(); + using var client = await CreateClientAsAdmin(); var res = await client.PostAsJsonAsync("users/admin/timeline/op/member", new TimelineMemberChangeRequest { Add = new List { "user" } }); res.Should().HaveStatusCode(200); } { // now user can read admin's - using var client = await Factory.CreateClientAsUser(); + using var client = await CreateClientAsUser(); var res = await client.GetAsync(adminUrl); res.Should().HaveStatusCode(200); } @@ -250,14 +245,14 @@ namespace Timeline.Tests.IntegratedTests { CreateExtraMockUsers(1); - using (var client = await Factory.CreateClientAsUser()) + using (var client = await CreateClientAsUser()) { var res = await client.PostAsJsonAsync("users/user/timeline/op/member", new TimelineMemberChangeRequest { Add = new List { "user0" } }); res.Should().HaveStatusCode(200); } - using (var client = Factory.CreateDefaultClient()) + using (var client = await CreateClientWithNoAuth()) { { // no auth should get 401 var res = await client.PostAsJsonAsync("users/user/timeline/postop/create", @@ -266,7 +261,7 @@ namespace Timeline.Tests.IntegratedTests } } - using (var client = await Factory.CreateClientAsUser()) + using (var client = await CreateClientAsUser()) { { // post self's var res = await client.PostAsJsonAsync("users/user/timeline/postop/create", @@ -280,7 +275,7 @@ namespace Timeline.Tests.IntegratedTests } } - using (var client = await Factory.CreateClientAsAdmin()) + using (var client = await CreateClientAsAdmin()) { { // post as admin var res = await client.PostAsJsonAsync("users/user/timeline/postop/create", @@ -289,7 +284,7 @@ namespace Timeline.Tests.IntegratedTests } } - using (var client = await Factory.CreateClientAs(ExtraMockUsers[0])) + using (var client = await CreateClientAs(ExtraMockUsers[0])) { { // post as member var res = await client.PostAsJsonAsync("users/user/timeline/postop/create", @@ -306,7 +301,7 @@ namespace Timeline.Tests.IntegratedTests async Task CreatePost(MockUser auth, string timeline) { - using var client = await Factory.CreateClientAs(auth); + using var client = await CreateClientAs(auth); var res = await client.PostAsJsonAsync($"users/{timeline}/timeline/postop/create", new TimelinePostCreateRequest { Content = "aaa" }); return res.Should().HaveStatusCode(200) @@ -314,7 +309,7 @@ namespace Timeline.Tests.IntegratedTests .Which.Id; } - using (var client = await Factory.CreateClientAsUser()) + using (var client = await CreateClientAsUser()) { var res = await client.PostAsJsonAsync("users/user/timeline/op/member", new TimelineMemberChangeRequest { Add = new List { "user0", "user1" } }); @@ -322,7 +317,7 @@ namespace Timeline.Tests.IntegratedTests } { // no auth should get 401 - using var client = Factory.CreateDefaultClient(); + using var client = await CreateClientWithNoAuth(); var res = await client.PostAsJsonAsync("users/user/timeline/postop/delete", new TimelinePostDeleteRequest { Id = 12 }); res.Should().HaveStatusCode(401); @@ -330,7 +325,7 @@ namespace Timeline.Tests.IntegratedTests { // self can delete self var postId = await CreatePost(MockUser.User, "user"); - using var client = await Factory.CreateClientAsUser(); + using var client = await CreateClientAsUser(); var res = await client.PostAsJsonAsync("users/user/timeline/postop/delete", new TimelinePostDeleteRequest { Id = postId }); res.Should().HaveStatusCode(200); @@ -338,7 +333,7 @@ namespace Timeline.Tests.IntegratedTests { // admin can delete any var postId = await CreatePost(MockUser.User, "user"); - using var client = await Factory.CreateClientAsAdmin(); + using var client = await CreateClientAsAdmin(); var res = await client.PostAsJsonAsync("users/user/timeline/postop/delete", new TimelinePostDeleteRequest { Id = postId }); res.Should().HaveStatusCode(200); @@ -346,7 +341,7 @@ namespace Timeline.Tests.IntegratedTests { // owner can delete other var postId = await CreatePost(ExtraMockUsers[0], "user"); - using var client = await Factory.CreateClientAsUser(); + using var client = await CreateClientAsUser(); var res = await client.PostAsJsonAsync("users/user/timeline/postop/delete", new TimelinePostDeleteRequest { Id = postId }); res.Should().HaveStatusCode(200); @@ -354,7 +349,7 @@ namespace Timeline.Tests.IntegratedTests { // author can delete self var postId = await CreatePost(ExtraMockUsers[0], "user"); - using var client = await Factory.CreateClientAs(ExtraMockUsers[0]); + using var client = await CreateClientAs(ExtraMockUsers[0]); var res = await client.PostAsJsonAsync("users/user/timeline/postop/delete", new TimelinePostDeleteRequest { Id = postId }); res.Should().HaveStatusCode(200); @@ -362,7 +357,7 @@ namespace Timeline.Tests.IntegratedTests { // otherwise is forbidden var postId = await CreatePost(ExtraMockUsers[0], "user"); - using var client = await Factory.CreateClientAs(ExtraMockUsers[1]); + using var client = await CreateClientAs(ExtraMockUsers[1]); var res = await client.PostAsJsonAsync("users/user/timeline/postop/delete", new TimelinePostDeleteRequest { Id = postId }); res.Should().HaveStatusCode(403); @@ -373,7 +368,7 @@ namespace Timeline.Tests.IntegratedTests public async Task Post_Op_Should_Work() { { - using var client = await Factory.CreateClientAsUser(); + using var client = await CreateClientAsUser(); { var res = await client.GetAsync("users/user/timeline/posts"); res.Should().HaveStatusCode(200) @@ -459,6 +454,7 @@ namespace Timeline.Tests.IntegratedTests Time = createRes2.Time }); } + // TODO! Add post not exist tests. } } } diff --git a/Timeline.Tests/IntegratedTests/TokenTest.cs b/Timeline.Tests/IntegratedTests/TokenTest.cs index 111e8d8e..e62228fc 100644 --- a/Timeline.Tests/IntegratedTests/TokenTest.cs +++ b/Timeline.Tests/IntegratedTests/TokenTest.cs @@ -1,37 +1,33 @@ using FluentAssertions; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.DependencyInjection; -using System; using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; using Timeline.Models.Http; using Timeline.Services; using Timeline.Tests.Helpers; -using Timeline.Tests.Helpers.Authentication; -using Timeline.Tests.Mock.Data; using Xunit; using static Timeline.ErrorCodes.Http.Token; namespace Timeline.Tests.IntegratedTests { - public class TokenTest : IClassFixture>, IDisposable + public class TokenTest : IntegratedTestBase { private const string CreateTokenUrl = "token/create"; private const string VerifyTokenUrl = "token/verify"; - private readonly TestApplication _testApp; - private readonly WebApplicationFactory _factory; - public TokenTest(WebApplicationFactory factory) + : base(factory) { - _testApp = new TestApplication(factory); - _factory = _testApp.Factory; + } - public void Dispose() + private static async Task CreateUserTokenAsync(HttpClient client, string username, string password, int? expireOffset = null) { - _testApp.Dispose(); + var response = await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest { Username = username, Password = password, Expire = expireOffset }); + return response.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; } public static IEnumerable CreateToken_InvalidModel_Data() @@ -46,7 +42,7 @@ namespace Timeline.Tests.IntegratedTests [MemberData(nameof(CreateToken_InvalidModel_Data))] public async Task CreateToken_InvalidModel(string username, string password, int expire) { - using var client = _factory.CreateDefaultClient(); + using var client = await CreateClientWithNoAuth(); (await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest { Username = username, @@ -65,7 +61,7 @@ namespace Timeline.Tests.IntegratedTests [MemberData(nameof(CreateToken_UserCredential_Data))] public async void CreateToken_UserCredential(string username, string password) { - using var client = _factory.CreateDefaultClient(); + using var client = await CreateClientWithNoAuth(); var response = await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest { Username = username, Password = password }); response.Should().HaveStatusCode(400) @@ -76,7 +72,7 @@ namespace Timeline.Tests.IntegratedTests [Fact] public async Task CreateToken_Success() { - using var client = _factory.CreateDefaultClient(); + using var client = await CreateClientWithNoAuth(); var response = await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest { Username = MockUser.User.Username, Password = MockUser.User.Password }); var body = response.Should().HaveStatusCode(200) @@ -88,7 +84,7 @@ namespace Timeline.Tests.IntegratedTests [Fact] public async Task VerifyToken_InvalidModel() { - using var client = _factory.CreateDefaultClient(); + using var client = await CreateClientWithNoAuth(); (await client.PostAsJsonAsync(VerifyTokenUrl, new VerifyTokenRequest { Token = null })).Should().BeInvalidModel(); } @@ -96,7 +92,7 @@ namespace Timeline.Tests.IntegratedTests [Fact] public async Task VerifyToken_BadFormat() { - using var client = _factory.CreateDefaultClient(); + using var client = await CreateClientWithNoAuth(); var response = await client.PostAsJsonAsync(VerifyTokenUrl, new VerifyTokenRequest { Token = "bad token hahaha" }); response.Should().HaveStatusCode(400) @@ -107,10 +103,10 @@ namespace Timeline.Tests.IntegratedTests [Fact] public async Task VerifyToken_OldVersion() { - using var client = _factory.CreateDefaultClient(); - var token = (await client.CreateUserTokenAsync(MockUser.User.Username, MockUser.User.Password)).Token; + using var client = await CreateClientWithNoAuth(); + var token = (await CreateUserTokenAsync(client, MockUser.User.Username, MockUser.User.Password)).Token; - using (var scope = _factory.Server.Host.Services.CreateScope()) // UserService is scoped. + using (var scope = Factory.Server.Host.Services.CreateScope()) // UserService is scoped. { // create a user for test var userService = scope.ServiceProvider.GetRequiredService(); @@ -127,10 +123,10 @@ namespace Timeline.Tests.IntegratedTests [Fact] public async Task VerifyToken_UserNotExist() { - using var client = _factory.CreateDefaultClient(); - var token = (await client.CreateUserTokenAsync(MockUser.User.Username, MockUser.User.Password)).Token; + using var client = await CreateClientWithNoAuth(); + var token = (await CreateUserTokenAsync(client, MockUser.User.Username, MockUser.User.Password)).Token; - using (var scope = _factory.Server.Host.Services.CreateScope()) // UserService is scoped. + using (var scope = Factory.Server.Host.Services.CreateScope()) // UserService is scoped. { var userService = scope.ServiceProvider.GetRequiredService(); await userService.DeleteUser(MockUser.User.Username); @@ -146,7 +142,7 @@ namespace Timeline.Tests.IntegratedTests //[Fact] //public async Task VerifyToken_Expired() //{ - // using (var client = _factory.CreateDefaultClient()) + // 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. @@ -164,8 +160,8 @@ namespace Timeline.Tests.IntegratedTests [Fact] public async Task VerifyToken_Success() { - using var client = _factory.CreateDefaultClient(); - var createTokenResult = await client.CreateUserTokenAsync(MockUser.User.Username, MockUser.User.Password); + using var client = await CreateClientWithNoAuth(); + var createTokenResult = await CreateUserTokenAsync(client, MockUser.User.Username, MockUser.User.Password); var response = await client.PostAsJsonAsync(VerifyTokenUrl, new VerifyTokenRequest { Token = createTokenResult.Token }); response.Should().HaveStatusCode(200) diff --git a/Timeline.Tests/IntegratedTests/UserAvatarTest.cs b/Timeline.Tests/IntegratedTests/UserAvatarTest.cs index 2310fc66..25a7b675 100644 --- a/Timeline.Tests/IntegratedTests/UserAvatarTest.cs +++ b/Timeline.Tests/IntegratedTests/UserAvatarTest.cs @@ -15,27 +15,18 @@ using System.Net.Http.Headers; using System.Threading.Tasks; using Timeline.Services; using Timeline.Tests.Helpers; -using Timeline.Tests.Helpers.Authentication; using Xunit; using static Timeline.ErrorCodes.Http.Common; using static Timeline.ErrorCodes.Http.UserAvatar; namespace Timeline.Tests.IntegratedTests { - public class UserAvatarTest : IClassFixture>, IDisposable + public class UserAvatarTest : IntegratedTestBase { - private readonly TestApplication _testApp; - private readonly WebApplicationFactory _factory; - public UserAvatarTest(WebApplicationFactory factory) + : base(factory) { - _testApp = new TestApplication(factory); - _factory = _testApp.Factory; - } - public void Dispose() - { - _testApp.Dispose(); } [Fact] @@ -48,7 +39,7 @@ namespace Timeline.Tests.IntegratedTests Type = PngFormat.Instance.DefaultMimeType }; - using (var client = await _factory.CreateClientAsUser()) + using (var client = await CreateClientAsUser()) { { var res = await client.GetAsync("users/usernotexist/avatar"); @@ -57,7 +48,7 @@ namespace Timeline.Tests.IntegratedTests .Which.Code.Should().Be(Get.UserNotExist); } - var env = _factory.Server.Host.Services.GetRequiredService(); + var env = Factory.Server.Host.Services.GetRequiredService(); var defaultAvatarData = await File.ReadAllBytesAsync(Path.Combine(env.ContentRootPath, "default-avatar.png")); async Task GetReturnDefault(string username = "user") @@ -239,7 +230,7 @@ namespace Timeline.Tests.IntegratedTests } // Authorization check. - using (var client = await _factory.CreateClientAsAdmin()) + using (var client = await CreateClientAsAdmin()) { { var res = await client.PutByteArrayAsync("users/user/avatar", mockAvatar.Data, mockAvatar.Type); @@ -266,7 +257,7 @@ namespace Timeline.Tests.IntegratedTests } // bad username check - using (var client = await _factory.CreateClientAsAdmin()) + using (var client = await CreateClientAsAdmin()) { { var res = await client.GetAsync("users/u!ser/avatar"); diff --git a/Timeline.Tests/IntegratedTests/UserDetailTest.cs b/Timeline.Tests/IntegratedTests/UserDetailTest.cs index 8f2b6925..932c287e 100644 --- a/Timeline.Tests/IntegratedTests/UserDetailTest.cs +++ b/Timeline.Tests/IntegratedTests/UserDetailTest.cs @@ -1,38 +1,27 @@ using FluentAssertions; using Microsoft.AspNetCore.Mvc.Testing; -using System; using System.Net; using System.Net.Http.Headers; using System.Net.Mime; using System.Threading.Tasks; using Timeline.Tests.Helpers; -using Timeline.Tests.Helpers.Authentication; -using Timeline.Tests.Mock.Data; using Xunit; namespace Timeline.Tests.IntegratedTests { - public class UserDetailTest : IClassFixture>, IDisposable + public class UserDetailTest : IntegratedTestBase { - private readonly TestApplication _testApp; - private readonly WebApplicationFactory _factory; - public UserDetailTest(WebApplicationFactory factory) + : base(factory) { - _testApp = new TestApplication(factory); - _factory = _testApp.Factory; - } - public void Dispose() - { - _testApp.Dispose(); } [Fact] public async Task PermissionTest() { { // unauthorize - using var client = _factory.CreateDefaultClient(); + using var client = await CreateClientWithNoAuth(); { // GET var res = await client.GetAsync($"users/{MockUser.User.Username}/nickname"); res.Should().HaveStatusCode(HttpStatusCode.OK); @@ -47,7 +36,7 @@ namespace Timeline.Tests.IntegratedTests } } { // user - using var client = await _factory.CreateClientAsUser(); + using var client = await CreateClientAsUser(); { // GET var res = await client.GetAsync($"users/{MockUser.User.Username}/nickname"); res.Should().HaveStatusCode(HttpStatusCode.OK); @@ -70,7 +59,7 @@ namespace Timeline.Tests.IntegratedTests } } { // user - using var client = await _factory.CreateClientAsAdmin(); + using var client = await CreateClientAsAdmin(); { // PUT other var res = await client.PutStringAsync($"users/{MockUser.User.Username}/nickname", "aaa"); res.Should().HaveStatusCode(HttpStatusCode.OK); @@ -88,7 +77,7 @@ namespace Timeline.Tests.IntegratedTests var url = $"users/{MockUser.User.Username}/nickname"; var userNotExistUrl = "users/usernotexist/nickname"; { - using var client = await _factory.CreateClientAsUser(); + using var client = await CreateClientAsUser(); { var res = await client.GetAsync(userNotExistUrl); res.Should().HaveStatusCode(HttpStatusCode.NotFound) @@ -134,7 +123,7 @@ namespace Timeline.Tests.IntegratedTests } } { - using var client = await _factory.CreateClientAsAdmin(); + using var client = await CreateClientAsAdmin(); { var res = await client.PutStringAsync(userNotExistUrl, "aaa"); res.Should().HaveStatusCode(HttpStatusCode.BadRequest) diff --git a/Timeline.Tests/IntegratedTests/UserTest.cs b/Timeline.Tests/IntegratedTests/UserTest.cs index 7e99ddba..abfea18e 100644 --- a/Timeline.Tests/IntegratedTests/UserTest.cs +++ b/Timeline.Tests/IntegratedTests/UserTest.cs @@ -1,39 +1,28 @@ using FluentAssertions; using Microsoft.AspNetCore.Mvc.Testing; -using System; using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; using Timeline.Models; using Timeline.Models.Http; using Timeline.Tests.Helpers; -using Timeline.Tests.Helpers.Authentication; -using Timeline.Tests.Mock.Data; using Xunit; using static Timeline.ErrorCodes.Http.User; namespace Timeline.Tests.IntegratedTests { - public class UserTest : IClassFixture>, IDisposable + public class UserTest : IntegratedTestBase { - private readonly TestApplication _testApp; - private readonly WebApplicationFactory _factory; - public UserTest(WebApplicationFactory factory) + : base(factory) { - _testApp = new TestApplication(factory); - _factory = _testApp.Factory; - } - public void Dispose() - { - _testApp.Dispose(); } [Fact] public async Task Get_List_Success() { - using var client = await _factory.CreateClientAsAdmin(); + using var client = await CreateClientAsAdmin(); var res = await client.GetAsync("users"); res.Should().HaveStatusCode(200) .And.HaveJsonBody() @@ -43,7 +32,7 @@ namespace Timeline.Tests.IntegratedTests [Fact] public async Task Get_Single_Success() { - using var client = await _factory.CreateClientAsAdmin(); + using var client = await CreateClientAsAdmin(); var res = await client.GetAsync("users/" + MockUser.User.Username); res.Should().HaveStatusCode(200) .And.HaveJsonBody() @@ -53,7 +42,7 @@ namespace Timeline.Tests.IntegratedTests [Fact] public async Task Get_InvalidModel() { - using var client = await _factory.CreateClientAsAdmin(); + using var client = await CreateClientAsAdmin(); var res = await client.GetAsync("users/aaa!a"); res.Should().BeInvalidModel(); } @@ -61,7 +50,7 @@ namespace Timeline.Tests.IntegratedTests [Fact] public async Task Get_Users_404() { - using var client = await _factory.CreateClientAsAdmin(); + using var client = await CreateClientAsAdmin(); var res = await client.GetAsync("users/usernotexist"); res.Should().HaveStatusCode(404) .And.HaveCommonBody() @@ -79,7 +68,7 @@ namespace Timeline.Tests.IntegratedTests [MemberData(nameof(Put_InvalidModel_Data))] public async Task Put_InvalidModel(string username, string password, bool? administrator) { - using var client = await _factory.CreateClientAsAdmin(); + using var client = await CreateClientAsAdmin(); (await client.PutAsJsonAsync("users/" + username, new UserPutRequest { Password = password, Administrator = administrator })) .Should().BeInvalidModel(); @@ -96,7 +85,7 @@ namespace Timeline.Tests.IntegratedTests [Fact] public async Task Put_Modiefied() { - using var client = await _factory.CreateClientAsAdmin(); + using var client = await CreateClientAsAdmin(); var res = await client.PutAsJsonAsync("users/" + MockUser.User.Username, new UserPutRequest { Password = "password", @@ -109,7 +98,7 @@ namespace Timeline.Tests.IntegratedTests [Fact] public async Task Put_Created() { - using var client = await _factory.CreateClientAsAdmin(); + using var client = await CreateClientAsAdmin(); const string username = "puttest"; const string url = "users/" + username; @@ -125,7 +114,7 @@ namespace Timeline.Tests.IntegratedTests [Fact] public async Task Patch_NotExist() { - using var client = await _factory.CreateClientAsAdmin(); + using var client = await CreateClientAsAdmin(); var res = await client.PatchAsJsonAsync("users/usernotexist", new UserPatchRequest { }); res.Should().HaveStatusCode(404) .And.HaveCommonBody() @@ -135,7 +124,7 @@ namespace Timeline.Tests.IntegratedTests [Fact] public async Task Patch_InvalidModel() { - using var client = await _factory.CreateClientAsAdmin(); + using var client = await CreateClientAsAdmin(); var res = await client.PatchAsJsonAsync("users/aaa!a", new UserPatchRequest { }); res.Should().BeInvalidModel(); } @@ -143,7 +132,7 @@ namespace Timeline.Tests.IntegratedTests [Fact] public async Task Patch_Success() { - using var client = await _factory.CreateClientAsAdmin(); + using var client = await CreateClientAsAdmin(); { var res = await client.PatchAsJsonAsync("users/" + MockUser.User.Username, new UserPatchRequest { Administrator = false }); @@ -155,7 +144,7 @@ namespace Timeline.Tests.IntegratedTests [Fact] public async Task Delete_InvalidModel() { - using var client = await _factory.CreateClientAsAdmin(); + using var client = await CreateClientAsAdmin(); var url = "users/aaa!a"; var res = await client.DeleteAsync(url); res.Should().BeInvalidModel(); @@ -164,7 +153,7 @@ namespace Timeline.Tests.IntegratedTests [Fact] public async Task Delete_Deleted() { - using var client = await _factory.CreateClientAsAdmin(); + using var client = await CreateClientAsAdmin(); var url = "users/" + MockUser.User.Username; var res = await client.DeleteAsync(url); res.Should().BeDelete(true); @@ -176,7 +165,7 @@ namespace Timeline.Tests.IntegratedTests [Fact] public async Task Delete_NotExist() { - using var client = await _factory.CreateClientAsAdmin(); + using var client = await CreateClientAsAdmin(); var res = await client.DeleteAsync("users/usernotexist"); res.Should().BeDelete(false); } @@ -195,7 +184,7 @@ namespace Timeline.Tests.IntegratedTests [MemberData(nameof(Op_ChangeUsername_InvalidModel_Data))] public async Task Op_ChangeUsername_InvalidModel(string oldUsername, string newUsername) { - using var client = await _factory.CreateClientAsAdmin(); + using var client = await CreateClientAsAdmin(); (await client.PostAsJsonAsync(changeUsernameUrl, new ChangeUsernameRequest { OldUsername = oldUsername, NewUsername = newUsername })) .Should().BeInvalidModel(); @@ -204,7 +193,7 @@ namespace Timeline.Tests.IntegratedTests [Fact] public async Task Op_ChangeUsername_UserNotExist() { - using var client = await _factory.CreateClientAsAdmin(); + using var client = await CreateClientAsAdmin(); var res = await client.PostAsJsonAsync(changeUsernameUrl, new ChangeUsernameRequest { OldUsername = "usernotexist", NewUsername = "newUsername" }); res.Should().HaveStatusCode(400) @@ -215,7 +204,7 @@ namespace Timeline.Tests.IntegratedTests [Fact] public async Task Op_ChangeUsername_UserAlreadyExist() { - using var client = await _factory.CreateClientAsAdmin(); + using var client = await CreateClientAsAdmin(); var res = await client.PostAsJsonAsync(changeUsernameUrl, new ChangeUsernameRequest { OldUsername = MockUser.User.Username, NewUsername = MockUser.Admin.Username }); res.Should().HaveStatusCode(400) @@ -223,15 +212,23 @@ namespace Timeline.Tests.IntegratedTests .Which.Code.Should().Be(Op.ChangeUsername.AlreadyExist); } + private async Task TestLogin(string username, string password) + { + using var client = await CreateClientWithNoAuth(); + var response = await client.PostAsJsonAsync("token/create", new CreateTokenRequest { Username = username, Password = password }); + response.Should().HaveStatusCode(200) + .And.HaveJsonBody(); + } + [Fact] public async Task Op_ChangeUsername_Success() { - using var client = await _factory.CreateClientAsAdmin(); + using var client = await CreateClientAsAdmin(); const string newUsername = "hahaha"; var res = await client.PostAsJsonAsync(changeUsernameUrl, new ChangeUsernameRequest { OldUsername = MockUser.User.Username, NewUsername = newUsername }); res.Should().HaveStatusCode(200); - await client.CreateUserTokenAsync(newUsername, MockUser.User.Password); + await TestLogin(newUsername, MockUser.User.Password); } private const string changePasswordUrl = "userop/changepassword"; @@ -246,7 +243,7 @@ namespace Timeline.Tests.IntegratedTests [MemberData(nameof(Op_ChangePassword_InvalidModel_Data))] public async Task Op_ChangePassword_InvalidModel(string oldPassword, string newPassword) { - using var client = await _factory.CreateClientAsUser(); + using var client = await CreateClientAsUser(); (await client.PostAsJsonAsync(changePasswordUrl, new ChangePasswordRequest { OldPassword = oldPassword, NewPassword = newPassword })) .Should().BeInvalidModel(); @@ -255,7 +252,7 @@ namespace Timeline.Tests.IntegratedTests [Fact] public async Task Op_ChangePassword_BadOldPassword() { - using var client = await _factory.CreateClientAsUser(); + using var client = await CreateClientAsUser(); var res = await client.PostAsJsonAsync(changePasswordUrl, new ChangePasswordRequest { OldPassword = "???", NewPassword = "???" }); res.Should().HaveStatusCode(400) .And.HaveCommonBody() @@ -265,13 +262,12 @@ namespace Timeline.Tests.IntegratedTests [Fact] public async Task Op_ChangePassword_Success() { - using var client = await _factory.CreateClientAsUser(); + using var client = await CreateClientAsUser(); const string newPassword = "new"; var res = await client.PostAsJsonAsync(changePasswordUrl, new ChangePasswordRequest { OldPassword = MockUser.User.Password, NewPassword = newPassword }); res.Should().HaveStatusCode(200); - await _factory.CreateDefaultClient() // don't use client above, because it sets authorization header - .CreateUserTokenAsync(MockUser.User.Username, newPassword); + await TestLogin(MockUser.User.Username, newPassword); } } } diff --git a/Timeline.Tests/Mock/Data/TestDatabase.cs b/Timeline.Tests/Mock/Data/TestDatabase.cs deleted file mode 100644 index 1f396177..00000000 --- a/Timeline.Tests/Mock/Data/TestDatabase.cs +++ /dev/null @@ -1,88 +0,0 @@ -using Microsoft.Data.Sqlite; -using Microsoft.EntityFrameworkCore; -using System; -using System.Collections.Generic; -using System.Linq; -using Timeline.Entities; -using Timeline.Models; -using Timeline.Services; - -namespace Timeline.Tests.Mock.Data -{ - public class TestDatabase : IDisposable - { - // currently password service is thread safe, so we share a static one. - private static PasswordService PasswordService { get; } = new PasswordService(); - - private static User CreateEntityFromMock(MockUser user) - { - return new User - { - Name = user.Username, - EncryptedPassword = PasswordService.HashPassword(user.Password), - RoleString = UserRoleConvert.ToString(user.Administrator), - Avatar = null - }; - } - - private static IEnumerable CreateDefaultMockEntities() - { - // emmmmmmm. Never reuse the user instances because EF Core uses them, which will cause strange things. - yield return CreateEntityFromMock(MockUser.User); - yield return CreateEntityFromMock(MockUser.Admin); - } - - private static void InitDatabase(DatabaseContext context) - { - context.Database.EnsureCreated(); - context.Users.AddRange(CreateDefaultMockEntities()); - context.SaveChanges(); - } - - public TestDatabase() - { - Connection = new SqliteConnection("Data Source=:memory:;"); - Connection.Open(); - - var options = new DbContextOptionsBuilder() - .UseSqlite(Connection) - .Options; - - Context = new DatabaseContext(options); - - InitDatabase(Context); - } - - private List _extraMockUsers; - - public IReadOnlyList ExtraMockUsers => _extraMockUsers; - - public void CreateExtraMockUsers(int count) - { - if (count <= 0) - throw new ArgumentOutOfRangeException(nameof(count), count, "Additional user count must be bigger than 0."); - if (_extraMockUsers != null) - throw new InvalidOperationException("Already create mock users."); - - _extraMockUsers = new List(); - for (int i = 0; i < count; i++) - { - _extraMockUsers.Add(new MockUser($"user{i}", $"password", false)); - } - - Context.AddRange(_extraMockUsers.Select(u => CreateEntityFromMock(u))); - Context.SaveChanges(); - } - - public void Dispose() - { - Context.Dispose(); - - Connection.Close(); - Connection.Dispose(); - } - - public SqliteConnection Connection { get; } - public DatabaseContext Context { get; } - } -} diff --git a/Timeline.Tests/Mock/Data/TestUsers.cs b/Timeline.Tests/Mock/Data/TestUsers.cs deleted file mode 100644 index 443d3cf9..00000000 --- a/Timeline.Tests/Mock/Data/TestUsers.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Collections.Generic; -using Timeline.Models; - -namespace Timeline.Tests.Mock.Data -{ - public class MockUser - { - public MockUser(string username, string password, bool administrator) - { - Info = new UserInfo(username, administrator); - Password = password; - } - - public UserInfo Info { get; set; } - public string Username => Info.Username; - public string Password { get; set; } - public bool Administrator => Info.Administrator; - - - public static MockUser User { get; } = new MockUser("user", "userpassword", false); - public static MockUser Admin { get; } = new MockUser("admin", "adminpassword", true); - - public static IReadOnlyList UserInfoList { get; } = new List { User.Info, Admin.Info }; - } -} diff --git a/Timeline.Tests/Mock/Services/TestClock.cs b/Timeline.Tests/Mock/Services/TestClock.cs deleted file mode 100644 index 6671395a..00000000 --- a/Timeline.Tests/Mock/Services/TestClock.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using Timeline.Services; - -namespace Timeline.Tests.Mock.Services -{ - public class TestClock : IClock - { - public DateTime? MockCurrentTime { get; set; } = null; - - public DateTime GetCurrentTime() - { - return MockCurrentTime.GetValueOrDefault(DateTime.Now); - } - } -} diff --git a/Timeline.Tests/Services/UserAvatarServiceTest.cs b/Timeline.Tests/Services/UserAvatarServiceTest.cs index 033a5e90..2729aa6f 100644 --- a/Timeline.Tests/Services/UserAvatarServiceTest.cs +++ b/Timeline.Tests/Services/UserAvatarServiceTest.cs @@ -11,7 +11,6 @@ using System.Threading.Tasks; using Timeline.Entities; using Timeline.Services; using Timeline.Tests.Helpers; -using Timeline.Tests.Mock.Data; using Xunit; namespace Timeline.Tests.Services diff --git a/Timeline.Tests/Services/UserDetailServiceTest.cs b/Timeline.Tests/Services/UserDetailServiceTest.cs index bddb1494..9a869c89 100644 --- a/Timeline.Tests/Services/UserDetailServiceTest.cs +++ b/Timeline.Tests/Services/UserDetailServiceTest.cs @@ -7,7 +7,6 @@ using System.Threading.Tasks; using Timeline.Entities; using Timeline.Services; using Timeline.Tests.Helpers; -using Timeline.Tests.Mock.Data; using Xunit; namespace Timeline.Tests.Services diff --git a/Timeline.Tests/UsernameValidatorUnitTest.cs b/Timeline.Tests/UsernameValidatorUnitTest.cs index d02367be..e0f4633f 100644 --- a/Timeline.Tests/UsernameValidatorUnitTest.cs +++ b/Timeline.Tests/UsernameValidatorUnitTest.cs @@ -1,5 +1,6 @@ using FluentAssertions; using Timeline.Models.Validation; +using Timeline.Tests.Helpers; using Xunit; namespace Timeline.Tests -- cgit v1.2.3