From c5f3c69b3a008ab87542e523e2a59f37801bd65a Mon Sep 17 00:00:00 2001 From: crupest Date: Sat, 18 Jan 2020 00:50:31 +0800 Subject: ... --- .../ErrorResponseCodeGenerator.csproj | 12 + ErrorResponseCodeGenerator/Program.cs | 62 +++++ Timeline.ErrorCodes/ErrorCodes.cs | 65 +++++ Timeline.ErrorCodes/Timeline.ErrorCodes.csproj | 7 + Timeline.Tests/Controllers/TokenControllerTest.cs | 4 +- Timeline.Tests/Controllers/UserControllerTest.cs | 13 +- Timeline.sln | 18 +- Timeline/Controllers/PersonalTimelineController.cs | 40 +-- Timeline/Controllers/TokenController.cs | 50 +--- Timeline/Controllers/UserAvatarController.cs | 74 ++---- Timeline/Controllers/UserController.cs | 64 +---- Timeline/ErrorCodes.cs | 35 --- Timeline/Filters/Header.cs | 55 +---- Timeline/Filters/Timeline.cs | 25 +- Timeline/Filters/User.cs | 24 +- Timeline/Helpers/InvalidModelResponseFactory.cs | 2 +- Timeline/InvalidBranchException.cs | 16 -- Timeline/Models/Http/Common.cs | 33 --- Timeline/Models/Http/ErrorResponse.cs | 261 ++++++++++++++++++++ Timeline/Models/Http/Timeline.cs | 3 - Timeline/Resources/Common.Designer.cs | 72 ------ Timeline/Resources/Common.resx | 123 ---------- .../Controllers/TimelineController.Designer.cs | 54 ----- .../Resources/Controllers/TimelineController.resx | 18 -- .../Controllers/TimelineController.zh.resx | 138 ----------- .../Controllers/TokenController.Designer.cs | 45 ---- .../Resources/Controllers/TokenController.resx | 15 -- .../Resources/Controllers/TokenController.zh.resx | 135 ----------- .../Controllers/UserAvatarController.Designer.cs | 72 ------ .../Controllers/UserAvatarController.resx | 24 -- .../Controllers/UserAvatarController.zh.resx | 144 ----------- .../Controllers/UserController.Designer.cs | 118 +-------- Timeline/Resources/Controllers/UserController.resx | 42 +--- .../Resources/Controllers/UserController.zh.resx | 138 ----------- Timeline/Resources/Filters.Designer.cs | 63 ----- Timeline/Resources/Filters.resx | 21 -- Timeline/Resources/Filters.zh.resx | 141 ----------- Timeline/Resources/Messages.Designer.cs | 270 +++++++++++++++++++++ Timeline/Resources/Messages.resx | 189 +++++++++++++++ Timeline/Resources/Messages.zh.resx | 189 +++++++++++++++ Timeline/Resources/Models/Http/Common.Designer.cs | 36 --- Timeline/Resources/Models/Http/Common.resx | 12 - Timeline/Resources/Models/Http/Common.zh.resx | 12 - Timeline/Timeline.csproj | 22 +- 44 files changed, 1151 insertions(+), 1805 deletions(-) create mode 100644 ErrorResponseCodeGenerator/ErrorResponseCodeGenerator.csproj create mode 100644 ErrorResponseCodeGenerator/Program.cs create mode 100644 Timeline.ErrorCodes/ErrorCodes.cs create mode 100644 Timeline.ErrorCodes/Timeline.ErrorCodes.csproj delete mode 100644 Timeline/ErrorCodes.cs delete mode 100644 Timeline/InvalidBranchException.cs create mode 100644 Timeline/Models/Http/ErrorResponse.cs delete mode 100644 Timeline/Resources/Common.Designer.cs delete mode 100644 Timeline/Resources/Common.resx delete mode 100644 Timeline/Resources/Controllers/TimelineController.zh.resx delete mode 100644 Timeline/Resources/Controllers/TokenController.zh.resx delete mode 100644 Timeline/Resources/Controllers/UserAvatarController.zh.resx delete mode 100644 Timeline/Resources/Controllers/UserController.zh.resx delete mode 100644 Timeline/Resources/Filters.zh.resx create mode 100644 Timeline/Resources/Messages.Designer.cs create mode 100644 Timeline/Resources/Messages.resx create mode 100644 Timeline/Resources/Messages.zh.resx diff --git a/ErrorResponseCodeGenerator/ErrorResponseCodeGenerator.csproj b/ErrorResponseCodeGenerator/ErrorResponseCodeGenerator.csproj new file mode 100644 index 00000000..e77a1ba3 --- /dev/null +++ b/ErrorResponseCodeGenerator/ErrorResponseCodeGenerator.csproj @@ -0,0 +1,12 @@ + + + + Exe + netcoreapp3.1 + + + + + + + diff --git a/ErrorResponseCodeGenerator/Program.cs b/ErrorResponseCodeGenerator/Program.cs new file mode 100644 index 00000000..cf021927 --- /dev/null +++ b/ErrorResponseCodeGenerator/Program.cs @@ -0,0 +1,62 @@ +using System; +using System.Linq; +using System.Reflection; + +namespace ErrorResponseCodeGenerator +{ + class Program + { + static void Main(string[] args) + { + var code = ""; + + void RecursiveAddErrorCode(Type type, bool root) + { + code += $@" + public static class {(root ? "ErrorResponse" : type.Name)} + {{ +"; + + foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy) + .Where(fi => fi.IsLiteral && !fi.IsInitOnly && fi.FieldType == typeof(int))) + { + var path = type.FullName.Replace("+", ".").Replace("Timeline.Models.Http.ErrorCodes.", "") + "." + field.Name; + + code += $@" + public static CommonResponse {field.Name}(params object?[] formatArgs) + {{ + return new CommonResponse({"ErrorCodes." + path}, string.Format({path.Replace(".", "_")}, formatArgs)); + }} + + public static CommonResponse CustomMessage_{field.Name}(string message, params object?[] formatArgs) + {{ + return new CommonResponse({"ErrorCodes." + path}, string.Format(message, formatArgs)); + }} +"; + } + + foreach (var nestedType in type.GetNestedTypes()) + { + RecursiveAddErrorCode(nestedType, false); + } + + code += @" + } +"; + } + + RecursiveAddErrorCode(typeof(Timeline.Models.Http.ErrorCodes), true); + + code = @" +using static Timeline.Resources.Messages; + +namespace Timeline.Models.Http +{ +$ +} +".Replace("$", code); + + Console.WriteLine(code); + } + } +} diff --git a/Timeline.ErrorCodes/ErrorCodes.cs b/Timeline.ErrorCodes/ErrorCodes.cs new file mode 100644 index 00000000..730f42e0 --- /dev/null +++ b/Timeline.ErrorCodes/ErrorCodes.cs @@ -0,0 +1,65 @@ +namespace Timeline.Models.Http +{ + /// + /// All error code constants. + /// + /// + /// Format: 1bbbccdd + /// + public static class ErrorCodes + { + public static class Common + { + public const int InvalidModel = 1_000_0001; + public const int Forbid = 1_000_0002; + + public static class Header + { + public const int IfNonMatch_BadFormat = 1_000_01_01; + public const int ContentType_Missing = 1_000_02_01; + public const int ContentLength_Missing = 1_000_03_01; + public const int ContentLength_Zero = 1_000_03_02; + } + + public static class Content + { + public const int TooBig = 1_000_11_01; + public const int UnmatchedLength_Smaller = 1_000_11_02; + public const int UnmatchedLength_Bigger = 1_000_11_03; + } + } + + public static class UserCommon + { + public const int NotExist = 1_001_0001; + } + + public static class TokenController + { + public const int Create_BadCredential = 1_101_01_01; + public const int Verify_BadFormat = 1_101_02_01; + public const int Verify_UserNotExist = 1_101_02_02; + public const int Verify_OldVersion = 1_101_02_03; + public const int Verify_TimeExpired = 1_101_02_04; + } + + public static class UserController + { + public const int ChangeUsername_Conflict = 1_102_01_01; + public const int ChangePassword_BadOldPassword = 1_102_02_01; + } + + public static class UserAvatar + { + public const int BadFormat_CantDecode = 1_103_00_01; + public const int BadFormat_UnmatchedFormat = 1_103_00_02; + public const int BadFormat_BadSize = 1_103_00_03; + } + + public static class TimelineController + { + public const int PostOperationDelete_NotExist = 1_104_01_01; + } + } +} + diff --git a/Timeline.ErrorCodes/Timeline.ErrorCodes.csproj b/Timeline.ErrorCodes/Timeline.ErrorCodes.csproj new file mode 100644 index 00000000..01ca2568 --- /dev/null +++ b/Timeline.ErrorCodes/Timeline.ErrorCodes.csproj @@ -0,0 +1,7 @@ + + + + netcoreapp3.1 + + + diff --git a/Timeline.Tests/Controllers/TokenControllerTest.cs b/Timeline.Tests/Controllers/TokenControllerTest.cs index 238fc237..371884bb 100644 --- a/Timeline.Tests/Controllers/TokenControllerTest.cs +++ b/Timeline.Tests/Controllers/TokenControllerTest.cs @@ -10,7 +10,7 @@ using Timeline.Models.Http; using Timeline.Services; using Timeline.Tests.Helpers; using Xunit; -using static Timeline.ErrorCodes.Http.Token; +using static Timeline.ErrorCodes.Token; namespace Timeline.Tests.Controllers { @@ -98,7 +98,7 @@ namespace Timeline.Tests.Controllers public static IEnumerable Verify_BadRequest_Data() { - yield return new object[] { new JwtVerifyException(JwtVerifyException.ErrorCodes.Expired), Verify.Expired }; + yield return new object[] { new JwtVerifyException(JwtVerifyException.ErrorCodes.Expired), Verify.TimeExpired }; yield return new object[] { new JwtVerifyException(JwtVerifyException.ErrorCodes.IdClaimBadFormat), Verify.BadFormat }; yield return new object[] { new JwtVerifyException(JwtVerifyException.ErrorCodes.OldVersion), Verify.OldVersion }; yield return new object[] { new UserNotExistException(), Verify.UserNotExist }; diff --git a/Timeline.Tests/Controllers/UserControllerTest.cs b/Timeline.Tests/Controllers/UserControllerTest.cs index a5ca7a2b..7a6541fb 100644 --- a/Timeline.Tests/Controllers/UserControllerTest.cs +++ b/Timeline.Tests/Controllers/UserControllerTest.cs @@ -13,7 +13,8 @@ using Timeline.Models.Http; using Timeline.Services; using Timeline.Tests.Helpers; using Xunit; -using static Timeline.ErrorCodes.Http.User; +using static Timeline.ErrorCodes.User; +using static Timeline.ErrorCodes.UserCommon; namespace Timeline.Tests.Controllers { @@ -61,7 +62,7 @@ namespace Timeline.Tests.Controllers var action = await _controller.Get(username); action.Result.Should().BeAssignableTo() .Which.Value.Should().BeAssignableTo() - .Which.Code.Should().Be(Get.NotExist); + .Which.Code.Should().Be(UserNotExist); } [Theory] @@ -114,7 +115,7 @@ namespace Timeline.Tests.Controllers }, username); action.Should().BeAssignableTo() .Which.Value.Should().BeAssignableTo() - .Which.Code.Should().Be(Patch.NotExist); + .Which.Code.Should().Be(UserNotExist); } [Fact] @@ -154,8 +155,8 @@ namespace Timeline.Tests.Controllers } [Theory] - [InlineData(typeof(UserNotExistException), Op.ChangeUsername.NotExist)] - [InlineData(typeof(UsernameConfictException), Op.ChangeUsername.AlreadyExist)] + [InlineData(typeof(UserNotExistException), UserNotExist)] + [InlineData(typeof(UsernameConfictException), ChangeUsername_Conflict)] public async Task Op_ChangeUsername_Failure(Type exceptionType, int code) { const string oldUsername = "aaa"; @@ -212,7 +213,7 @@ namespace Timeline.Tests.Controllers var action = await _controller.ChangePassword(new ChangePasswordRequest { OldPassword = oldPassword, NewPassword = newPassword }); action.Should().BeAssignableTo() .Which.Value.Should().BeAssignableTo() - .Which.Code.Should().Be(Op.ChangePassword.BadOldPassword); + .Which.Code.Should().Be(ChangePassword_BadOldPassword); } } } diff --git a/Timeline.sln b/Timeline.sln index 055989e9..0e01871a 100644 --- a/Timeline.sln +++ b/Timeline.sln @@ -1,10 +1,14 @@ Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.28307.271 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29709.97 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Timeline", "Timeline\Timeline.csproj", "{A34D323C-5233-4754-B14F-4819CE9C27CA}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Timeline.Tests", "Timeline.Tests\Timeline.Tests.csproj", "{3D76D578-37BC-43C2-97BF-9C6DD3825F10}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Timeline.Tests", "Timeline.Tests\Timeline.Tests.csproj", "{3D76D578-37BC-43C2-97BF-9C6DD3825F10}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ErrorResponseCodeGenerator", "ErrorResponseCodeGenerator\ErrorResponseCodeGenerator.csproj", "{F325F802-75DE-4527-A299-F668281B0E4D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Timeline.ErrorCodes", "Timeline.ErrorCodes\Timeline.ErrorCodes.csproj", "{1044E3B0-1010-47CA-956E-B6E8FE87055B}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -20,6 +24,14 @@ Global {3D76D578-37BC-43C2-97BF-9C6DD3825F10}.Debug|Any CPU.Build.0 = Debug|Any CPU {3D76D578-37BC-43C2-97BF-9C6DD3825F10}.Release|Any CPU.ActiveCfg = Release|Any CPU {3D76D578-37BC-43C2-97BF-9C6DD3825F10}.Release|Any CPU.Build.0 = Release|Any CPU + {F325F802-75DE-4527-A299-F668281B0E4D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F325F802-75DE-4527-A299-F668281B0E4D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F325F802-75DE-4527-A299-F668281B0E4D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F325F802-75DE-4527-A299-F668281B0E4D}.Release|Any CPU.Build.0 = Release|Any CPU + {1044E3B0-1010-47CA-956E-B6E8FE87055B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1044E3B0-1010-47CA-956E-B6E8FE87055B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1044E3B0-1010-47CA-956E-B6E8FE87055B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1044E3B0-1010-47CA-956E-B6E8FE87055B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Timeline/Controllers/PersonalTimelineController.cs b/Timeline/Controllers/PersonalTimelineController.cs index c864ed39..e1e3aba0 100644 --- a/Timeline/Controllers/PersonalTimelineController.cs +++ b/Timeline/Controllers/PersonalTimelineController.cs @@ -12,24 +12,7 @@ using Timeline.Models.Http; using Timeline.Models.Validation; using Timeline.Services; using static Timeline.Resources.Controllers.TimelineController; - -namespace Timeline -{ - public static partial class ErrorCodes - { - public static partial class Http - { - public static class Timeline // ccc = 004 - { - public const int PostListGetForbid = 10040101; - public const int PostOperationCreateForbid = 10040102; - public const int PostOperationDeleteForbid = 10040103; - public const int PostOperationDeleteNotExist = 10040201; - public const int ChangeMemberUserNotExist = 10040301; - } - } - } -} +using static Timeline.Resources.Messages; namespace Timeline.Controllers { @@ -80,8 +63,7 @@ namespace Timeline.Controllers { if (!IsAdmin() && !await _service.HasReadPermission(username, GetAuthUsername())) { - return StatusCode(StatusCodes.Status403Forbidden, - new CommonResponse(ErrorCodes.Http.Timeline.PostListGetForbid, MessagePostListGetForbid)); + return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } return await _service.GetPosts(username); @@ -94,8 +76,7 @@ namespace Timeline.Controllers { if (!IsAdmin() && !await _service.IsMemberOf(username, GetAuthUsername()!)) { - return StatusCode(StatusCodes.Status403Forbidden, - new CommonResponse(ErrorCodes.Http.Timeline.PostOperationCreateForbid, MessagePostOperationCreateForbid)); + return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } var res = await _service.CreatePost(username, User.Identity.Name!, body.Content, body.Time); @@ -112,16 +93,13 @@ namespace Timeline.Controllers var postId = body.Id!.Value; if (!IsAdmin() && !await _service.HasPostModifyPermission(username, postId, GetAuthUsername()!)) { - return StatusCode(StatusCodes.Status403Forbidden, - new CommonResponse(ErrorCodes.Http.Timeline.PostOperationDeleteForbid, MessagePostOperationCreateForbid)); + return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } await _service.DeletePost(username, postId); } catch (TimelinePostNotExistException) { - return BadRequest(new CommonResponse( - ErrorCodes.Http.Timeline.PostOperationDeleteNotExist, - MessagePostOperationDeleteNotExist)); + return BadRequest(ErrorResponse.TimelineController.PostOperationDelete_NotExist()); } return Ok(); } @@ -151,13 +129,13 @@ namespace Timeline.Controllers { if (e.InnerException is UsernameBadFormatException) { - return BadRequest(CommonResponse.InvalidModel( - string.Format(CultureInfo.CurrentCulture, MessageMemberUsernameBadFormat, e.Index, e.Operation))); + return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel( + TimelineController_ChangeMember_UsernameBadFormat, e.Index, e.Operation)); } else if (e.InnerException is UserNotExistException) { - return BadRequest(new CommonResponse(ErrorCodes.Http.Timeline.ChangeMemberUserNotExist, - string.Format(CultureInfo.CurrentCulture, MessageMemberUserNotExist, e.Index, e.Operation))); + return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel( + TimelineController_ChangeMember_UserNotExist, e.Index, e.Operation)); } _logger.LogError(e, LogUnknownTimelineMemberOperationUserException); diff --git a/Timeline/Controllers/TokenController.cs b/Timeline/Controllers/TokenController.cs index 01f4778f..c360a109 100644 --- a/Timeline/Controllers/TokenController.cs +++ b/Timeline/Controllers/TokenController.cs @@ -10,31 +10,6 @@ using Timeline.Models.Http; using Timeline.Services; using static Timeline.Resources.Controllers.TokenController; -namespace Timeline -{ - public static partial class ErrorCodes - { - public static partial class Http - { - public static class Token // bbb = 001 - { - public static class Create // cc = 01 - { - public const int BadCredential = 10010101; - } - - public static class Verify // cc = 02 - { - public const int BadFormat = 10010201; - public const int UserNotExist = 10010202; - public const int OldVersion = 10010203; - public const int Expired = 10010204; - } - } - } - } -} - namespace Timeline.Controllers { [Route("token")] @@ -87,16 +62,12 @@ namespace Timeline.Controllers catch (UserNotExistException e) { LogFailure(LogUserNotExist, e); - return BadRequest(new CommonResponse( - ErrorCodes.Http.Token.Create.BadCredential, - ErrorBadCredential)); + return BadRequest(ErrorResponse.TokenController.Create_BadCredential()); } catch (BadPasswordException e) { LogFailure(LogBadPassword, e); - return BadRequest(new CommonResponse( - ErrorCodes.Http.Token.Create.BadCredential, - ErrorBadCredential)); + return BadRequest(ErrorResponse.TokenController.Create_BadCredential()); } } @@ -128,31 +99,28 @@ namespace Timeline.Controllers if (e.ErrorCode == JwtVerifyException.ErrorCodes.Expired) { var innerException = e.InnerException as SecurityTokenExpiredException; - LogFailure(LogVerifyExpire, e, ("Expires", innerException?.Expires), + LogFailure(LogVerifyExpire, e, ("Expires", innerException.Expires), ("Current Time", _clock.GetCurrentTime())); - return BadRequest(new CommonResponse( - ErrorCodes.Http.Token.Verify.Expired, ErrorVerifyExpire)); + return BadRequest(ErrorResponse.TokenController.Verify_TimeExpired()); } else if (e.ErrorCode == JwtVerifyException.ErrorCodes.OldVersion) { var innerException = e.InnerException as JwtBadVersionException; LogFailure(LogVerifyOldVersion, e, - ("Token Version", innerException?.TokenVersion), ("Required Version", innerException?.RequiredVersion)); - return BadRequest(new CommonResponse( - ErrorCodes.Http.Token.Verify.OldVersion, ErrorVerifyOldVersion)); + ("Token Version", innerException.TokenVersion), + ("Required Version", innerException?.RequiredVersion)); + return BadRequest(ErrorResponse.TokenController.Verify_OldVersion()); } else { LogFailure(LogVerifyBadFormat, e); - return BadRequest(new CommonResponse( - ErrorCodes.Http.Token.Verify.BadFormat, ErrorVerifyBadFormat)); + return BadRequest(ErrorResponse.TokenController.Verify_BadFormat()); } } catch (UserNotExistException e) { LogFailure(LogVerifyUserNotExist, e); - return BadRequest(new CommonResponse( - ErrorCodes.Http.Token.Verify.UserNotExist, ErrorVerifyUserNotExist)); + return BadRequest(ErrorResponse.TokenController.Verify_UserNotExist()); } } } diff --git a/Timeline/Controllers/UserAvatarController.cs b/Timeline/Controllers/UserAvatarController.cs index 7625f962..b4a6d8fd 100644 --- a/Timeline/Controllers/UserAvatarController.cs +++ b/Timeline/Controllers/UserAvatarController.cs @@ -14,39 +14,6 @@ using Timeline.Models.Validation; using Timeline.Services; using static Timeline.Resources.Controllers.UserAvatarController; -namespace Timeline -{ - public static partial class ErrorCodes - { - public static partial class Http - { - public static class UserAvatar // bbb = 003 - { - public static class Get // cc = 01 - { - public const int UserNotExist = 10030101; - } - - public static class Put // cc = 02 - { - public const int UserNotExist = 10030201; - public const int Forbid = 10030202; - public const int BadFormat_CantDecode = 10030203; - public const int BadFormat_UnmatchedFormat = 10030204; - public const int BadFormat_BadSize = 10030205; - - } - - public static class Delete // cc = 03 - { - public const int UserNotExist = 10030301; - public const int Forbid = 10030302; - } - } - } - } -} - namespace Timeline.Controllers { [ApiController] @@ -79,7 +46,7 @@ namespace Timeline.Controllers { _logger.LogInformation(Log.Format(LogGetBadIfNoneMatch, ("Username", username), ("If-None-Match", value))); - return BadRequest(HeaderErrorResponse.BadIfNonMatch()); + return BadRequest(ErrorResponse.Common.Header.IfNonMatch_BadFormat()); } if (eTagList.FirstOrDefault(e => e.Equals(eTag)) != null) @@ -99,7 +66,7 @@ namespace Timeline.Controllers catch (UserNotExistException e) { _logger.LogInformation(e, Log.Format(LogGetUserNotExist, ("Username", username))); - return NotFound(new CommonResponse(ErrorCodes.Http.UserAvatar.Get.UserNotExist, ErrorGetUserNotExist)); + return NotFound(ErrorResponse.UserController.ChangePassword_BadOldPassword()); } } @@ -111,14 +78,13 @@ namespace Timeline.Controllers { var contentLength = Request.ContentLength!.Value; if (contentLength > 1000 * 1000 * 10) - return BadRequest(ContentErrorResponse.TooBig("10MB")); + return BadRequest(ErrorResponse.Common.Content.TooBig("10MB")); if (!User.IsAdministrator() && User.Identity.Name != username) { _logger.LogInformation(Log.Format(LogPutForbid, ("Operator Username", User.Identity.Name), ("Username To Put Avatar", username))); - return StatusCode(StatusCodes.Status403Forbidden, - new CommonResponse(ErrorCodes.Http.UserAvatar.Put.Forbid, ErrorPutForbid)); + return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } try @@ -127,11 +93,11 @@ namespace Timeline.Controllers var bytesRead = await Request.Body.ReadAsync(data); if (bytesRead != contentLength) - return BadRequest(ContentErrorResponse.UnmatchedLength_Smaller()); + return BadRequest(ErrorResponse.Common.Content.UnmatchedLength_Smaller()); var extraByte = new byte[1]; if (await Request.Body.ReadAsync(extraByte) != 0) - return BadRequest(ContentErrorResponse.UnmatchedLength_Bigger()); + return BadRequest(ErrorResponse.Common.Content.UnmatchedLength_Bigger()); await _service.SetAvatar(username, new Avatar { @@ -146,24 +112,19 @@ namespace Timeline.Controllers catch (UserNotExistException e) { _logger.LogInformation(e, Log.Format(LogPutUserNotExist, ("Username", username))); - return BadRequest(new CommonResponse(ErrorCodes.Http.UserAvatar.Put.UserNotExist, ErrorPutUserNotExist)); + return BadRequest(ErrorResponse.UserController.ChangePassword_BadOldPassword()); } catch (AvatarFormatException e) { - var (code, message) = e.Error switch + _logger.LogInformation(e, Log.Format(LogPutUserBadFormat, ("Username", username))); + return BadRequest(e.Error switch { - AvatarFormatException.ErrorReason.CantDecode => - (ErrorCodes.Http.UserAvatar.Put.BadFormat_CantDecode, ErrorPutBadFormatCantDecode), - AvatarFormatException.ErrorReason.UnmatchedFormat => - (ErrorCodes.Http.UserAvatar.Put.BadFormat_UnmatchedFormat, ErrorPutBadFormatUnmatchedFormat), - AvatarFormatException.ErrorReason.BadSize => - (ErrorCodes.Http.UserAvatar.Put.BadFormat_BadSize, ErrorPutBadFormatBadSize), + AvatarFormatException.ErrorReason.CantDecode => ErrorResponse.UserAvatar.BadFormat_CantDecode(), + AvatarFormatException.ErrorReason.UnmatchedFormat => ErrorResponse.UserAvatar.BadFormat_UnmatchedFormat(), + AvatarFormatException.ErrorReason.BadSize => ErrorResponse.UserAvatar.BadFormat_BadSize(), _ => throw new Exception(ExceptionUnknownAvatarFormatError) - }; - - _logger.LogInformation(e, Log.Format(LogPutUserBadFormat, ("Username", username))); - return BadRequest(new CommonResponse(code, message)); + }); } } @@ -173,23 +134,20 @@ namespace Timeline.Controllers { if (!User.IsAdministrator() && User.Identity.Name != username) { - _logger.LogInformation(Log.Format(LogPutUserBadFormat, + _logger.LogInformation(Log.Format(LogDeleteForbid, ("Operator Username", User.Identity.Name), ("Username To Delete Avatar", username))); - return StatusCode(StatusCodes.Status403Forbidden, - new CommonResponse(ErrorCodes.Http.UserAvatar.Delete.Forbid, ErrorDeleteForbid)); + return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } try { await _service.SetAvatar(username, null); - - _logger.LogInformation(Log.Format(LogDeleteSuccess, ("Username", username))); return Ok(); } catch (UserNotExistException e) { _logger.LogInformation(e, Log.Format(LogDeleteNotExist, ("Username", username))); - return BadRequest(new CommonResponse(ErrorCodes.Http.UserAvatar.Delete.UserNotExist, ErrorDeleteUserNotExist)); + return BadRequest(ErrorResponse.UserCommon.NotExist()); } } } diff --git a/Timeline/Controllers/UserController.cs b/Timeline/Controllers/UserController.cs index 0d950cd7..956865dc 100644 --- a/Timeline/Controllers/UserController.cs +++ b/Timeline/Controllers/UserController.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -using System.Globalization; +using System; using System.Threading.Tasks; using Timeline.Auth; using Timeline.Helpers; @@ -11,43 +11,6 @@ using Timeline.Models.Validation; using Timeline.Services; using static Timeline.Resources.Controllers.UserController; -namespace Timeline -{ - public static partial class ErrorCodes - { - public static partial class Http - { - public static class User // bbb = 002 - { - public static class Get // cc = 01 - { - public const int NotExist = 10020101; // dd = 01 - } - - public static class Patch // cc = 03 - { - public const int NotExist = 10020301; // dd = 01 - } - - public static class Op // cc = 1x - { - public static class ChangeUsername // cc = 11 - { - public const int NotExist = 10021101; // dd = 01 - public const int AlreadyExist = 10021102; // dd = 02 - } - - public static class ChangePassword // cc = 12 - { - public const int BadOldPassword = 10021201; // dd = 01 - } - } - - } - } - } -} - namespace Timeline.Controllers { [ApiController] @@ -76,7 +39,7 @@ namespace Timeline.Controllers if (user == null) { _logger.LogInformation(Log.Format(LogGetUserNotExist, ("Username", username))); - return NotFound(new CommonResponse(ErrorCodes.Http.User.Get.NotExist, ErrorGetUserNotExist)); + return NotFound(ErrorResponse.UserCommon.NotExist()); } return Ok(user); } @@ -88,13 +51,11 @@ namespace Timeline.Controllers switch (result) { case PutResult.Create: - _logger.LogInformation(Log.Format(LogPutCreate, ("Username", username))); return CreatedAtAction("Get", new { username }, CommonPutResponse.Create()); case PutResult.Modify: - _logger.LogInformation(Log.Format(LogPutModify, ("Username", username))); return Ok(CommonPutResponse.Modify()); default: - throw new InvalidBranchException(); + throw new Exception(ExceptionUnknownPutResult); } } @@ -109,7 +70,7 @@ namespace Timeline.Controllers catch (UserNotExistException e) { _logger.LogInformation(e, Log.Format(LogPatchUserNotExist, ("Username", username))); - return NotFound(new CommonResponse(ErrorCodes.Http.User.Patch.NotExist, ErrorPatchUserNotExist)); + return NotFound(ErrorResponse.UserCommon.NotExist()); } } @@ -119,12 +80,10 @@ namespace Timeline.Controllers try { await _userService.DeleteUser(username); - _logger.LogInformation(Log.Format(LogDeleteDelete, ("Username", username))); return Ok(CommonDeleteResponse.Delete()); } - catch (UserNotExistException e) + catch (UserNotExistException) { - _logger.LogInformation(e, Log.Format(LogDeleteNotExist, ("Username", username))); return Ok(CommonDeleteResponse.NotExist()); } } @@ -135,22 +94,19 @@ namespace Timeline.Controllers try { await _userService.ChangeUsername(request.OldUsername, request.NewUsername); - _logger.LogInformation(Log.Format(LogChangeUsernameSuccess, - ("Old Username", request.OldUsername), ("New Username", request.NewUsername))); return Ok(); } catch (UserNotExistException e) { _logger.LogInformation(e, Log.Format(LogChangeUsernameNotExist, ("Old Username", request.OldUsername), ("New Username", request.NewUsername))); - return BadRequest(new CommonResponse(ErrorCodes.Http.User.Op.ChangeUsername.NotExist, - string.Format(CultureInfo.CurrentCulture, ErrorChangeUsernameNotExist, request.OldUsername))); + return BadRequest(ErrorResponse.UserCommon.NotExist()); } catch (UsernameConfictException e) { - _logger.LogInformation(e, Log.Format(LogChangeUsernameAlreadyExist, + _logger.LogInformation(e, Log.Format(LogChangeUsernameConflict, ("Old Username", request.OldUsername), ("New Username", request.NewUsername))); - return BadRequest(new CommonResponse(ErrorCodes.Http.User.Op.ChangeUsername.AlreadyExist, ErrorChangeUsernameAlreadyExist)); + return BadRequest(ErrorResponse.UserController.ChangeUsername_Conflict()); } // there is no need to catch bad format exception because it is already checked in model validation. } @@ -161,15 +117,13 @@ namespace Timeline.Controllers try { await _userService.ChangePassword(User.Identity.Name!, request.OldPassword, request.NewPassword); - _logger.LogInformation(Log.Format(LogChangePasswordSuccess, ("Username", User.Identity.Name))); return Ok(); } catch (BadPasswordException e) { _logger.LogInformation(e, Log.Format(LogChangePasswordBadPassword, ("Username", User.Identity.Name), ("Old Password", request.OldPassword))); - return BadRequest(new CommonResponse(ErrorCodes.Http.User.Op.ChangePassword.BadOldPassword, - ErrorChangePasswordBadPassword)); + return BadRequest(ErrorResponse.UserController.ChangePassword_BadOldPassword()); } // User can't be non-existent or the token is bad. } diff --git a/Timeline/ErrorCodes.cs b/Timeline/ErrorCodes.cs deleted file mode 100644 index c246953b..00000000 --- a/Timeline/ErrorCodes.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace Timeline -{ - /// - /// All error code constants. - /// - /// - /// Scheme: - /// abbbccdd - /// - public static partial class ErrorCodes - { - public static partial class Http // a = 1 - { - public static class Common // bbb = 000 - { - public const int InvalidModel = 10000000; - - public static class Header // cc = 0x - { - public static class IfNonMatch // cc = 01 - { - public const int BadFormat = 10000101; - } - } - - public static class Content // cc = 11 - { - public const int TooBig = 10001101; - public const int UnmatchedLength_Smaller = 10001102; - public const int UnmatchedLength_Bigger = 10001103; - } - } - } - } -} diff --git a/Timeline/Filters/Header.cs b/Timeline/Filters/Header.cs index f5fb16aa..843a619d 100644 --- a/Timeline/Filters/Header.cs +++ b/Timeline/Filters/Header.cs @@ -1,72 +1,23 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Timeline.Models.Http; -using static Timeline.Resources.Filters; - -namespace Timeline -{ - public static partial class ErrorCodes - { - public static partial class Http - { - public static partial class Filter // bxx = 1xx - { - public static partial class Header // bbb = 100 - { - public static class ContentType // cc = 01 - { - public const int Missing = 11000101; // dd = 01 - } - - public static class ContentLength // cc = 02 - { - public const int Missing = 11000201; // dd = 01 - public const int Zero = 11000202; // dd = 02 - } - } - } - - } - } -} namespace Timeline.Filters { public class RequireContentTypeAttribute : ActionFilterAttribute { - internal static CommonResponse CreateResponse() - { - return new CommonResponse( - ErrorCodes.Http.Filter.Header.ContentType.Missing, - MessageHeaderContentTypeMissing); - } - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")] public override void OnActionExecuting(ActionExecutingContext context) { if (context.HttpContext.Request.ContentType == null) { - context.Result = new BadRequestObjectResult(CreateResponse()); + context.Result = new BadRequestObjectResult(ErrorResponse.Common.Header.ContentType_Missing()); } } } public class RequireContentLengthAttribute : ActionFilterAttribute { - internal static CommonResponse CreateMissingResponse() - { - return new CommonResponse( - ErrorCodes.Http.Filter.Header.ContentLength.Missing, - MessageHeaderContentLengthMissing); - } - - internal static CommonResponse CreateZeroResponse() - { - return new CommonResponse( - ErrorCodes.Http.Filter.Header.ContentLength.Zero, - MessageHeaderContentLengthZero); - } - public RequireContentLengthAttribute() : this(true) { @@ -85,13 +36,13 @@ namespace Timeline.Filters { if (context.HttpContext.Request.ContentLength == null) { - context.Result = new BadRequestObjectResult(CreateMissingResponse()); + context.Result = new BadRequestObjectResult(ErrorResponse.Common.Header.ContentLength_Missing()); return; } if (RequireNonZero && context.HttpContext.Request.ContentLength.Value == 0) { - context.Result = new BadRequestObjectResult(CreateZeroResponse()); + context.Result = new BadRequestObjectResult(ErrorResponse.Common.Header.ContentLength_Zero()); return; } } diff --git a/Timeline/Filters/Timeline.cs b/Timeline/Filters/Timeline.cs index 7859d409..bc142db0 100644 --- a/Timeline/Filters/Timeline.cs +++ b/Timeline/Filters/Timeline.cs @@ -2,25 +2,6 @@ using Microsoft.AspNetCore.Mvc.Filters; using Timeline.Models.Http; using Timeline.Services; -using static Timeline.Resources.Filters; - -namespace Timeline -{ - public static partial class ErrorCodes - { - public static partial class Http - { - public static partial class Filter // bxx = 1xx - { - public static class Timeline // bbb = 102 - { - public const int UserNotExist = 11020101; - public const int NameNotExist = 11020102; - } - } - } - } -} namespace Timeline.Filters { @@ -33,13 +14,11 @@ namespace Timeline.Filters { if (e.InnerException is UserNotExistException) { - context.Result = new BadRequestObjectResult( - new CommonResponse(ErrorCodes.Http.Filter.Timeline.UserNotExist, MessageTimelineNotExistUser)); + context.Result = new BadRequestObjectResult(ErrorResponse.UserCommon.NotExist()); } else { - context.Result = new BadRequestObjectResult( - new CommonResponse(ErrorCodes.Http.Filter.Timeline.NameNotExist, MessageTimelineNotExist)); + throw new System.NotImplementedException(); } } } diff --git a/Timeline/Filters/User.cs b/Timeline/Filters/User.cs index 16c76750..12ed6155 100644 --- a/Timeline/Filters/User.cs +++ b/Timeline/Filters/User.cs @@ -9,25 +9,6 @@ using Timeline.Models.Http; using Timeline.Services; using static Timeline.Resources.Filters; -namespace Timeline -{ - public static partial class ErrorCodes - { - public static partial class Http - { - public static partial class Filter // bxx = 1xx - { - public static class User // bbb = 101 - { - public const int NotExist = 11010101; - - public const int NotSelfOrAdminForbid = 11010201; - } - } - } - } -} - namespace Timeline.Filters { public class SelfOrAdminAttribute : ActionFilterAttribute @@ -51,8 +32,7 @@ namespace Timeline.Filters { if (!user.IsAdministrator() && user.Identity.Name != username) { - context.Result = new ObjectResult( - new CommonResponse(ErrorCodes.Http.Filter.User.NotSelfOrAdminForbid, MessageSelfOrAdminForbid)) + context.Result = new ObjectResult(ErrorResponse.Common.Forbid()) { StatusCode = StatusCodes.Status403Forbidden }; } } @@ -76,7 +56,7 @@ namespace Timeline.Filters { if (context.Exception is UserNotExistException) { - var body = new CommonResponse(ErrorCodes.Http.Filter.User.NotExist, MessageUserNotExist); + var body = ErrorResponse.UserCommon.NotExist(); if (context.HttpContext.Request.Method == "GET") context.Result = new NotFoundObjectResult(body); diff --git a/Timeline/Helpers/InvalidModelResponseFactory.cs b/Timeline/Helpers/InvalidModelResponseFactory.cs index 643c99ac..71ee44a9 100644 --- a/Timeline/Helpers/InvalidModelResponseFactory.cs +++ b/Timeline/Helpers/InvalidModelResponseFactory.cs @@ -20,7 +20,7 @@ namespace Timeline.Helpers messageBuilder.AppendLine(error.ErrorMessage); } - return new BadRequestObjectResult(CommonResponse.InvalidModel(messageBuilder.ToString())); + return new BadRequestObjectResult(ErrorResponse.Common.CustomMessage_InvalidModel(messageBuilder.ToString())); } } } diff --git a/Timeline/InvalidBranchException.cs b/Timeline/InvalidBranchException.cs deleted file mode 100644 index 32937c5d..00000000 --- a/Timeline/InvalidBranchException.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; - -namespace Timeline -{ - - [Serializable] - public class InvalidBranchException : Exception - { - public InvalidBranchException() : base(Resources.Common.ExceptionInvalidBranch) { } - public InvalidBranchException(string message) : base(message) { } - public InvalidBranchException(string message, Exception inner) : base(message, inner) { } - protected InvalidBranchException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - } -} diff --git a/Timeline/Models/Http/Common.cs b/Timeline/Models/Http/Common.cs index d1e95397..a9fc8a79 100644 --- a/Timeline/Models/Http/Common.cs +++ b/Timeline/Models/Http/Common.cs @@ -1,15 +1,9 @@ -using System.Globalization; using static Timeline.Resources.Models.Http.Common; namespace Timeline.Models.Http { public class CommonResponse { - internal static CommonResponse InvalidModel(string message) - { - return new CommonResponse(ErrorCodes.Http.Common.InvalidModel, message); - } - public CommonResponse() { @@ -25,33 +19,6 @@ namespace Timeline.Models.Http public string? Message { get; set; } } - internal static class HeaderErrorResponse - { - internal static CommonResponse BadIfNonMatch() - { - return new CommonResponse(ErrorCodes.Http.Common.Header.IfNonMatch.BadFormat, MessageHeaderIfNonMatchBad); - } - } - - internal static class ContentErrorResponse - { - internal static CommonResponse TooBig(string maxLength) - { - return new CommonResponse(ErrorCodes.Http.Common.Content.TooBig, - string.Format(CultureInfo.CurrentCulture, MessageContentTooBig, maxLength)); - } - - internal static CommonResponse UnmatchedLength_Smaller() - { - return new CommonResponse(ErrorCodes.Http.Common.Content.UnmatchedLength_Smaller, MessageContentUnmatchedLengthSmaller); - } - internal static CommonResponse UnmatchedLength_Bigger() - { - return new CommonResponse(ErrorCodes.Http.Common.Content.UnmatchedLength_Bigger, MessageContentUnmatchedLengthBigger); - } - } - - public class CommonDataResponse : CommonResponse { public CommonDataResponse() diff --git a/Timeline/Models/Http/ErrorResponse.cs b/Timeline/Models/Http/ErrorResponse.cs new file mode 100644 index 00000000..6a53e0c3 --- /dev/null +++ b/Timeline/Models/Http/ErrorResponse.cs @@ -0,0 +1,261 @@ +using static Timeline.Resources.Messages; + +namespace Timeline.Models.Http +{ + + public static class ErrorResponse + { + + public static class Common + { + + public static CommonResponse InvalidModel(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.Common.InvalidModel, string.Format(Common_InvalidModel, formatArgs)); + } + + public static CommonResponse CustomMessage_InvalidModel(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.Common.InvalidModel, string.Format(message, formatArgs)); + } + + public static CommonResponse Forbid(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.Common.Forbid, string.Format(Common_Forbid, formatArgs)); + } + + public static CommonResponse CustomMessage_Forbid(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.Common.Forbid, string.Format(message, formatArgs)); + } + + public static class Header + { + + public static CommonResponse IfNonMatch_BadFormat(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.Common.Header.IfNonMatch_BadFormat, string.Format(Common_Header_IfNonMatch_BadFormat, formatArgs)); + } + + public static CommonResponse CustomMessage_IfNonMatch_BadFormat(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.Common.Header.IfNonMatch_BadFormat, string.Format(message, formatArgs)); + } + + public static CommonResponse ContentType_Missing(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.Common.Header.ContentType_Missing, string.Format(Common_Header_ContentType_Missing, formatArgs)); + } + + public static CommonResponse CustomMessage_ContentType_Missing(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.Common.Header.ContentType_Missing, string.Format(message, formatArgs)); + } + + public static CommonResponse ContentLength_Missing(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.Common.Header.ContentLength_Missing, string.Format(Common_Header_ContentLength_Missing, formatArgs)); + } + + public static CommonResponse CustomMessage_ContentLength_Missing(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.Common.Header.ContentLength_Missing, string.Format(message, formatArgs)); + } + + public static CommonResponse ContentLength_Zero(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.Common.Header.ContentLength_Zero, string.Format(Common_Header_ContentLength_Zero, formatArgs)); + } + + public static CommonResponse CustomMessage_ContentLength_Zero(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.Common.Header.ContentLength_Zero, string.Format(message, formatArgs)); + } + + } + + public static class Content + { + + public static CommonResponse TooBig(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.Common.Content.TooBig, string.Format(Common_Content_TooBig, formatArgs)); + } + + public static CommonResponse CustomMessage_TooBig(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.Common.Content.TooBig, string.Format(message, formatArgs)); + } + + public static CommonResponse UnmatchedLength_Smaller(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.Common.Content.UnmatchedLength_Smaller, string.Format(Common_Content_UnmatchedLength_Smaller, formatArgs)); + } + + public static CommonResponse CustomMessage_UnmatchedLength_Smaller(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.Common.Content.UnmatchedLength_Smaller, string.Format(message, formatArgs)); + } + + public static CommonResponse UnmatchedLength_Bigger(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.Common.Content.UnmatchedLength_Bigger, string.Format(Common_Content_UnmatchedLength_Bigger, formatArgs)); + } + + public static CommonResponse CustomMessage_UnmatchedLength_Bigger(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.Common.Content.UnmatchedLength_Bigger, string.Format(message, formatArgs)); + } + + } + + } + + public static class UserCommon + { + + public static CommonResponse NotExist(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.UserCommon.NotExist, string.Format(UserCommon_NotExist, formatArgs)); + } + + public static CommonResponse CustomMessage_NotExist(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.UserCommon.NotExist, string.Format(message, formatArgs)); + } + + } + + public static class TokenController + { + + public static CommonResponse Create_BadCredential(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TokenController.Create_BadCredential, string.Format(TokenController_Create_BadCredential, formatArgs)); + } + + public static CommonResponse CustomMessage_Create_BadCredential(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TokenController.Create_BadCredential, string.Format(message, formatArgs)); + } + + public static CommonResponse Verify_BadFormat(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TokenController.Verify_BadFormat, string.Format(TokenController_Verify_BadFormat, formatArgs)); + } + + public static CommonResponse CustomMessage_Verify_BadFormat(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TokenController.Verify_BadFormat, string.Format(message, formatArgs)); + } + + public static CommonResponse Verify_UserNotExist(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TokenController.Verify_UserNotExist, string.Format(TokenController_Verify_UserNotExist, formatArgs)); + } + + public static CommonResponse CustomMessage_Verify_UserNotExist(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TokenController.Verify_UserNotExist, string.Format(message, formatArgs)); + } + + public static CommonResponse Verify_OldVersion(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TokenController.Verify_OldVersion, string.Format(TokenController_Verify_OldVersion, formatArgs)); + } + + public static CommonResponse CustomMessage_Verify_OldVersion(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TokenController.Verify_OldVersion, string.Format(message, formatArgs)); + } + + public static CommonResponse Verify_TimeExpired(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TokenController.Verify_TimeExpired, string.Format(TokenController_Verify_TimeExpired, formatArgs)); + } + + public static CommonResponse CustomMessage_Verify_TimeExpired(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TokenController.Verify_TimeExpired, string.Format(message, formatArgs)); + } + + } + + public static class UserController + { + + public static CommonResponse ChangeUsername_Conflict(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.UserController.ChangeUsername_Conflict, string.Format(UserController_ChangeUsername_Conflict, formatArgs)); + } + + public static CommonResponse CustomMessage_ChangeUsername_Conflict(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.UserController.ChangeUsername_Conflict, string.Format(message, formatArgs)); + } + + public static CommonResponse ChangePassword_BadOldPassword(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.UserController.ChangePassword_BadOldPassword, string.Format(UserController_ChangePassword_BadOldPassword, formatArgs)); + } + + public static CommonResponse CustomMessage_ChangePassword_BadOldPassword(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.UserController.ChangePassword_BadOldPassword, string.Format(message, formatArgs)); + } + + } + + public static class UserAvatar + { + + public static CommonResponse BadFormat_CantDecode(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.UserAvatar.BadFormat_CantDecode, string.Format(UserAvatar_BadFormat_CantDecode, formatArgs)); + } + + public static CommonResponse CustomMessage_BadFormat_CantDecode(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.UserAvatar.BadFormat_CantDecode, string.Format(message, formatArgs)); + } + + public static CommonResponse BadFormat_UnmatchedFormat(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.UserAvatar.BadFormat_UnmatchedFormat, string.Format(UserAvatar_BadFormat_UnmatchedFormat, formatArgs)); + } + + public static CommonResponse CustomMessage_BadFormat_UnmatchedFormat(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.UserAvatar.BadFormat_UnmatchedFormat, string.Format(message, formatArgs)); + } + + public static CommonResponse BadFormat_BadSize(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.UserAvatar.BadFormat_BadSize, string.Format(UserAvatar_BadFormat_BadSize, formatArgs)); + } + + public static CommonResponse CustomMessage_BadFormat_BadSize(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.UserAvatar.BadFormat_BadSize, string.Format(message, formatArgs)); + } + + } + + public static class TimelineController + { + + public static CommonResponse PostOperationDelete_NotExist(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TimelineController.PostOperationDelete_NotExist, string.Format(TimelineController_PostOperationDelete_NotExist, formatArgs)); + } + + public static CommonResponse CustomMessage_PostOperationDelete_NotExist(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TimelineController.PostOperationDelete_NotExist, string.Format(message, formatArgs)); + } + + } + + } + +} diff --git a/Timeline/Models/Http/Timeline.cs b/Timeline/Models/Http/Timeline.cs index 06b88ad1..3029434e 100644 --- a/Timeline/Models/Http/Timeline.cs +++ b/Timeline/Models/Http/Timeline.cs @@ -1,9 +1,6 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Threading.Tasks; -using Timeline.Entities; namespace Timeline.Models.Http { diff --git a/Timeline/Resources/Common.Designer.cs b/Timeline/Resources/Common.Designer.cs deleted file mode 100644 index 4f1c8e3f..00000000 --- a/Timeline/Resources/Common.Designer.cs +++ /dev/null @@ -1,72 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Timeline.Resources { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Common { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Common() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Common", typeof(Common).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to The branch is invalid. Normally this branch is not reachable.. - /// - internal static string ExceptionInvalidBranch { - get { - return ResourceManager.GetString("ExceptionInvalidBranch", resourceCulture); - } - } - } -} diff --git a/Timeline/Resources/Common.resx b/Timeline/Resources/Common.resx deleted file mode 100644 index 8a036996..00000000 --- a/Timeline/Resources/Common.resx +++ /dev/null @@ -1,123 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - The branch is invalid. Normally this branch is not reachable. - - \ No newline at end of file diff --git a/Timeline/Resources/Controllers/TimelineController.Designer.cs b/Timeline/Resources/Controllers/TimelineController.Designer.cs index 47c43fa2..ae6414e6 100644 --- a/Timeline/Resources/Controllers/TimelineController.Designer.cs +++ b/Timeline/Resources/Controllers/TimelineController.Designer.cs @@ -77,59 +77,5 @@ namespace Timeline.Resources.Controllers { return ResourceManager.GetString("LogUnknownTimelineMemberOperationUserException", resourceCulture); } } - - /// - /// Looks up a localized string similar to The {0}-st username to do operation {1} on is of bad format.. - /// - internal static string MessageMemberUsernameBadFormat { - get { - return ResourceManager.GetString("MessageMemberUsernameBadFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The {0}-st user to do operation {1} on does not exist.. - /// - internal static string MessageMemberUserNotExist { - get { - return ResourceManager.GetString("MessageMemberUserNotExist", resourceCulture); - } - } - - /// - /// 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 MessagePostOperationCreateForbid { - get { - return ResourceManager.GetString("MessagePostOperationCreateForbid", resourceCulture); - } - } - - /// - /// 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 MessagePostOperationDeleteNotExist { - get { - return ResourceManager.GetString("MessagePostOperationDeleteNotExist", resourceCulture); - } - } } } diff --git a/Timeline/Resources/Controllers/TimelineController.resx b/Timeline/Resources/Controllers/TimelineController.resx index 0cf7e881..4cf3d6fb 100644 --- a/Timeline/Resources/Controllers/TimelineController.resx +++ b/Timeline/Resources/Controllers/TimelineController.resx @@ -123,22 +123,4 @@ An unknown TimelineMemberOperationUserException is thrown. Can't recognize its inner exception. It is rethrown. - - The {0}-st username to do operation {1} on is of bad format. - - - 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 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 deleted file mode 100644 index 170ab4cd..00000000 --- a/Timeline/Resources/Controllers/TimelineController.zh.resx +++ /dev/null @@ -1,138 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - 第{0}个做{1}操作的用户名格式错误。 - - - 第{0}个做{1}操作的用户不存在。 - - - 你没有权限读取这个时间线消息。 - - - 你没有权限在这个时间线中创建消息。 - - - 你没有权限在这个时间线中删除消息。 - - - 要删除的消息不存在。 - - \ No newline at end of file diff --git a/Timeline/Resources/Controllers/TokenController.Designer.cs b/Timeline/Resources/Controllers/TokenController.Designer.cs index 22e6a8be..a7c2864b 100644 --- a/Timeline/Resources/Controllers/TokenController.Designer.cs +++ b/Timeline/Resources/Controllers/TokenController.Designer.cs @@ -60,51 +60,6 @@ namespace Timeline.Resources.Controllers { } } - /// - /// Looks up a localized string similar to Username or password is invalid.. - /// - internal static string ErrorBadCredential { - get { - return ResourceManager.GetString("ErrorBadCredential", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The token is of bad format. It might not be created by the server.. - /// - internal static string ErrorVerifyBadFormat { - get { - return ResourceManager.GetString("ErrorVerifyBadFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The token is expired.. - /// - internal static string ErrorVerifyExpire { - get { - return ResourceManager.GetString("ErrorVerifyExpire", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Token has an old version. User might have update some info.. - /// - internal static string ErrorVerifyOldVersion { - get { - return ResourceManager.GetString("ErrorVerifyOldVersion", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to User does not exist. Administrator might have deleted this user.. - /// - internal static string ErrorVerifyUserNotExist { - get { - return ResourceManager.GetString("ErrorVerifyUserNotExist", resourceCulture); - } - } - /// /// Looks up a localized string similar to The password is wrong.. /// diff --git a/Timeline/Resources/Controllers/TokenController.resx b/Timeline/Resources/Controllers/TokenController.resx index 42e1ff92..683d6cc9 100644 --- a/Timeline/Resources/Controllers/TokenController.resx +++ b/Timeline/Resources/Controllers/TokenController.resx @@ -117,21 +117,6 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Username or password is invalid. - - - The token is of bad format. It might not be created by the server. - - - The token is expired. - - - Token has an old version. User might have update some info. - - - User does not exist. Administrator might have deleted this user. - The password is wrong. diff --git a/Timeline/Resources/Controllers/TokenController.zh.resx b/Timeline/Resources/Controllers/TokenController.zh.resx deleted file mode 100644 index 51e0f25b..00000000 --- a/Timeline/Resources/Controllers/TokenController.zh.resx +++ /dev/null @@ -1,135 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - 用户名或密码错误。 - - - 符号格式错误。这个符号可能不是这个服务器创建的。 - - - 符号过期了。 - - - 符号是一个旧版本。用户可能已经更新了信息。 - - - 用户不存在。管理员可能已经删除了这个用户。 - - \ No newline at end of file diff --git a/Timeline/Resources/Controllers/UserAvatarController.Designer.cs b/Timeline/Resources/Controllers/UserAvatarController.Designer.cs index 1dacb19f..e6eeb1e8 100644 --- a/Timeline/Resources/Controllers/UserAvatarController.Designer.cs +++ b/Timeline/Resources/Controllers/UserAvatarController.Designer.cs @@ -60,78 +60,6 @@ namespace Timeline.Resources.Controllers { } } - /// - /// Looks up a localized string similar to Normal user can't delete other's avatar.. - /// - internal static string ErrorDeleteForbid { - get { - return ResourceManager.GetString("ErrorDeleteForbid", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to User does not exist.. - /// - internal static string ErrorDeleteUserNotExist { - get { - return ResourceManager.GetString("ErrorDeleteUserNotExist", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to User does not exist.. - /// - internal static string ErrorGetUserNotExist { - get { - return ResourceManager.GetString("ErrorGetUserNotExist", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Image is not a square.. - /// - internal static string ErrorPutBadFormatBadSize { - get { - return ResourceManager.GetString("ErrorPutBadFormatBadSize", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Decoding image failed.. - /// - internal static string ErrorPutBadFormatCantDecode { - get { - return ResourceManager.GetString("ErrorPutBadFormatCantDecode", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Image format is not the one in header.. - /// - internal static string ErrorPutBadFormatUnmatchedFormat { - get { - return ResourceManager.GetString("ErrorPutBadFormatUnmatchedFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Normal user can't change other's avatar.. - /// - internal static string ErrorPutForbid { - get { - return ResourceManager.GetString("ErrorPutForbid", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to User does not exist.. - /// - internal static string ErrorPutUserNotExist { - get { - return ResourceManager.GetString("ErrorPutUserNotExist", resourceCulture); - } - } - /// /// Looks up a localized string similar to Unknown AvatarDataException.ErrorReason value.. /// diff --git a/Timeline/Resources/Controllers/UserAvatarController.resx b/Timeline/Resources/Controllers/UserAvatarController.resx index 3f444b04..58860c83 100644 --- a/Timeline/Resources/Controllers/UserAvatarController.resx +++ b/Timeline/Resources/Controllers/UserAvatarController.resx @@ -117,30 +117,6 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Normal user can't delete other's avatar. - - - User does not exist. - - - User does not exist. - - - Image is not a square. - - - Decoding image failed. - - - Image format is not the one in header. - - - Normal user can't change other's avatar. - - - User does not exist. - Unknown AvatarDataException.ErrorReason value. diff --git a/Timeline/Resources/Controllers/UserAvatarController.zh.resx b/Timeline/Resources/Controllers/UserAvatarController.zh.resx deleted file mode 100644 index 94de1606..00000000 --- a/Timeline/Resources/Controllers/UserAvatarController.zh.resx +++ /dev/null @@ -1,144 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - 普通用户不能删除其他用户的头像。 - - - 用户不存在。 - - - 用户不存在。 - - - 图片不是正方形。 - - - 解码图片失败。 - - - 图片格式与请求头中指示的不一样。 - - - 普通用户不能修改其他用户的头像。 - - - 用户不存在。 - - \ No newline at end of file diff --git a/Timeline/Resources/Controllers/UserController.Designer.cs b/Timeline/Resources/Controllers/UserController.Designer.cs index 0c9ac0d7..c8067614 100644 --- a/Timeline/Resources/Controllers/UserController.Designer.cs +++ b/Timeline/Resources/Controllers/UserController.Designer.cs @@ -61,56 +61,11 @@ namespace Timeline.Resources.Controllers { } /// - /// Looks up a localized string similar to Old password is wrong.. + /// Looks up a localized string similar to Unknown PutResult.. /// - internal static string ErrorChangePasswordBadPassword { + internal static string ExceptionUnknownPutResult { get { - return ResourceManager.GetString("ErrorChangePasswordBadPassword", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The new username {0} already exists.. - /// - internal static string ErrorChangeUsernameAlreadyExist { - get { - return ResourceManager.GetString("ErrorChangeUsernameAlreadyExist", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The old username {0} does not exist.. - /// - internal static string ErrorChangeUsernameNotExist { - get { - return ResourceManager.GetString("ErrorChangeUsernameNotExist", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The user does not exist.. - /// - internal static string ErrorGetUserNotExist { - get { - return ResourceManager.GetString("ErrorGetUserNotExist", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Can't patch a user that does not exist.. - /// - internal static string ErrorPatchUserNotExist { - get { - return ResourceManager.GetString("ErrorPatchUserNotExist", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Username is of bad format.. - /// - internal static string ErrorPutBadUsername { - get { - return ResourceManager.GetString("ErrorPutBadUsername", resourceCulture); + return ResourceManager.GetString("ExceptionUnknownPutResult", resourceCulture); } } @@ -123,21 +78,12 @@ namespace Timeline.Resources.Controllers { } } - /// - /// Looks up a localized string similar to A user has changed password.. - /// - internal static string LogChangePasswordSuccess { - get { - return ResourceManager.GetString("LogChangePasswordSuccess", resourceCulture); - } - } - /// /// Looks up a localized string similar to Attempt to change a user's username to a existent one failed.. /// - internal static string LogChangeUsernameAlreadyExist { + internal static string LogChangeUsernameConflict { get { - return ResourceManager.GetString("LogChangeUsernameAlreadyExist", resourceCulture); + return ResourceManager.GetString("LogChangeUsernameConflict", resourceCulture); } } @@ -150,33 +96,6 @@ namespace Timeline.Resources.Controllers { } } - /// - /// Looks up a localized string similar to A user has changed username.. - /// - internal static string LogChangeUsernameSuccess { - get { - return ResourceManager.GetString("LogChangeUsernameSuccess", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A user has been deleted.. - /// - internal static string LogDeleteDelete { - get { - return ResourceManager.GetString("LogDeleteDelete", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Attempt to delete a user that does not exist.. - /// - internal static string LogDeleteNotExist { - get { - return ResourceManager.GetString("LogDeleteNotExist", resourceCulture); - } - } - /// /// Looks up a localized string similar to Attempt to retrieve info of a user that does not exist failed.. /// @@ -194,32 +113,5 @@ namespace Timeline.Resources.Controllers { return ResourceManager.GetString("LogPatchUserNotExist", resourceCulture); } } - - /// - /// Looks up a localized string similar to Attempt to create a user with bad username failed.. - /// - internal static string LogPutBadUsername { - get { - return ResourceManager.GetString("LogPutBadUsername", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A user has been created.. - /// - internal static string LogPutCreate { - get { - return ResourceManager.GetString("LogPutCreate", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A user has been modified.. - /// - internal static string LogPutModify { - get { - return ResourceManager.GetString("LogPutModify", resourceCulture); - } - } } } diff --git a/Timeline/Resources/Controllers/UserController.resx b/Timeline/Resources/Controllers/UserController.resx index 50aa13d6..0bdf4845 100644 --- a/Timeline/Resources/Controllers/UserController.resx +++ b/Timeline/Resources/Controllers/UserController.resx @@ -117,58 +117,22 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Old password is wrong. - - - The new username {0} already exists. - - - The old username {0} does not exist. - - - The user does not exist. - - - Can't patch a user that does not exist. - - - Username is of bad format. + + Unknown PutResult. Attempt to change password with wrong old password failed. - - A user has changed password. - - + Attempt to change a user's username to a existent one failed. Attempt to change a username of a user that does not exist failed. - - A user has changed username. - - - A user has been deleted. - - - Attempt to delete a user that does not exist. - Attempt to retrieve info of a user that does not exist failed. Attempt to patch a user that does not exist failed. - - Attempt to create a user with bad username failed. - - - A user has been created. - - - A user has been modified. - \ No newline at end of file diff --git a/Timeline/Resources/Controllers/UserController.zh.resx b/Timeline/Resources/Controllers/UserController.zh.resx deleted file mode 100644 index 3556083e..00000000 --- a/Timeline/Resources/Controllers/UserController.zh.resx +++ /dev/null @@ -1,138 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - 旧密码错误。 - - - 新用户名{0}已经存在。 - - - 旧用户名{0}不存在。 - - - 用户不存在。 - - - 不能修改一个不存在的用户。 - - - 用户名格式错误。 - - \ No newline at end of file diff --git a/Timeline/Resources/Filters.Designer.cs b/Timeline/Resources/Filters.Designer.cs index 5576190d..dedfe498 100644 --- a/Timeline/Resources/Filters.Designer.cs +++ b/Timeline/Resources/Filters.Designer.cs @@ -86,68 +86,5 @@ namespace Timeline.Resources { return ResourceManager.GetString("LogSelfOrAdminUsernameNotString", resourceCulture); } } - - /// - /// Looks up a localized string similar to Header Content-Length is missing or of bad format.. - /// - internal static string MessageHeaderContentLengthMissing { - get { - return ResourceManager.GetString("MessageHeaderContentLengthMissing", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Header Content-Length must not be 0.. - /// - internal static string MessageHeaderContentLengthZero { - get { - return ResourceManager.GetString("MessageHeaderContentLengthZero", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Header Content-Type is required.. - /// - internal static string MessageHeaderContentTypeMissing { - get { - return ResourceManager.GetString("MessageHeaderContentTypeMissing", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You can't access the resource unless you are the owner or administrator.. - /// - internal static string MessageSelfOrAdminForbid { - get { - return ResourceManager.GetString("MessageSelfOrAdminForbid", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The requested timeline does not exist.. - /// - internal static string MessageTimelineNotExist { - get { - return ResourceManager.GetString("MessageTimelineNotExist", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The requested personal timeline does not exist because the user does not exist.. - /// - internal static string MessageTimelineNotExistUser { - get { - return ResourceManager.GetString("MessageTimelineNotExistUser", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The user does not exist.. - /// - internal static string MessageUserNotExist { - get { - return ResourceManager.GetString("MessageUserNotExist", resourceCulture); - } - } } } diff --git a/Timeline/Resources/Filters.resx b/Timeline/Resources/Filters.resx index 7bfbc703..22620889 100644 --- a/Timeline/Resources/Filters.resx +++ b/Timeline/Resources/Filters.resx @@ -126,25 +126,4 @@ You apply a SelfOrAdminAttribute on an action, found a model named username, but it is not string. - - Header Content-Length is missing or of bad format. - - - Header Content-Length must not be 0. - - - Header Content-Type is required. - - - You can't access the resource unless you are the owner or administrator. - - - The requested timeline does not exist. - - - The requested personal timeline does not exist because the user does not exist. - - - The user does not exist. - \ No newline at end of file diff --git a/Timeline/Resources/Filters.zh.resx b/Timeline/Resources/Filters.zh.resx deleted file mode 100644 index 36aac788..00000000 --- a/Timeline/Resources/Filters.zh.resx +++ /dev/null @@ -1,141 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - 请求头Content-Length缺失或者格式不对。 - - - 请求头Content-Length不能为0。 - - - 缺少必需的请求头Content-Type。 - - - 你无权访问该资源除非你是资源的拥有者或者管理员。 - - - 请求的时间线不存在。 - - - 请求的个人时间线不存在因为该用户不存在。 - - - 用户不存在。 - - \ No newline at end of file diff --git a/Timeline/Resources/Messages.Designer.cs b/Timeline/Resources/Messages.Designer.cs new file mode 100644 index 00000000..8c13374f --- /dev/null +++ b/Timeline/Resources/Messages.Designer.cs @@ -0,0 +1,270 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Resources { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Messages { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Messages() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Messages", typeof(Messages).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Body is too big. It can't be bigger than {0}.. + /// + internal static string Common_Content_TooBig { + get { + return ResourceManager.GetString("Common_Content_TooBig", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Actual body length is bigger than it in header.. + /// + internal static string Common_Content_UnmatchedLength_Bigger { + get { + return ResourceManager.GetString("Common_Content_UnmatchedLength_Bigger", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Actual body length is smaller than it in header.. + /// + internal static string Common_Content_UnmatchedLength_Smaller { + get { + return ResourceManager.GetString("Common_Content_UnmatchedLength_Smaller", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You have no permission to do the operation.. + /// + internal static string Common_Forbid { + get { + return ResourceManager.GetString("Common_Forbid", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Header Content-Length is missing or of bad format.. + /// + internal static string Common_Header_ContentLength_Missing { + get { + return ResourceManager.GetString("Common_Header_ContentLength_Missing", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Header Content-Length must not be 0.. + /// + internal static string Common_Header_ContentLength_Zero { + get { + return ResourceManager.GetString("Common_Header_ContentLength_Zero", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Header Content-Type is missing.. + /// + internal static string Common_Header_ContentType_Missing { + get { + return ResourceManager.GetString("Common_Header_ContentType_Missing", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Header If-Non-Match is of bad format.. + /// + internal static string Common_Header_IfNonMatch_BadFormat { + get { + return ResourceManager.GetString("Common_Header_IfNonMatch_BadFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Model is of bad format.. + /// + internal static string Common_InvalidModel { + get { + return ResourceManager.GetString("Common_InvalidModel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The {0}-st username to do operation {1} on is of bad format.. + /// + internal static string TimelineController_ChangeMember_UsernameBadFormat { + get { + return ResourceManager.GetString("TimelineController_ChangeMember_UsernameBadFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The {0}-st user to do operation {1} on does not exist.. + /// + internal static string TimelineController_ChangeMember_UserNotExist { + get { + return ResourceManager.GetString("TimelineController_ChangeMember_UserNotExist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The post to delete does not exist.. + /// + internal static string TimelineController_PostOperationDelete_NotExist { + get { + return ResourceManager.GetString("TimelineController_PostOperationDelete_NotExist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Username or password is invalid.. + /// + internal static string TokenController_Create_BadCredential { + get { + return ResourceManager.GetString("TokenController_Create_BadCredential", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The token is of bad format. It might not be created by the server.. + /// + internal static string TokenController_Verify_BadFormat { + get { + return ResourceManager.GetString("TokenController_Verify_BadFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Token has an old version. User might have update some info.. + /// + internal static string TokenController_Verify_OldVersion { + get { + return ResourceManager.GetString("TokenController_Verify_OldVersion", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The token is expired.. + /// + internal static string TokenController_Verify_TimeExpired { + get { + return ResourceManager.GetString("TokenController_Verify_TimeExpired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to User does not exist. Administrator might have deleted this user.. + /// + internal static string TokenController_Verify_UserNotExist { + get { + return ResourceManager.GetString("TokenController_Verify_UserNotExist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Image is not a square.. + /// + internal static string UserAvatar_BadFormat_BadSize { + get { + return ResourceManager.GetString("UserAvatar_BadFormat_BadSize", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Image decode failed.. + /// + internal static string UserAvatar_BadFormat_CantDecode { + get { + return ResourceManager.GetString("UserAvatar_BadFormat_CantDecode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Image format does not match the one in header.. + /// + internal static string UserAvatar_BadFormat_UnmatchedFormat { + get { + return ResourceManager.GetString("UserAvatar_BadFormat_UnmatchedFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The user to operate on does not exist.. + /// + internal static string UserCommon_NotExist { + get { + return ResourceManager.GetString("UserCommon_NotExist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Old password is wrong.. + /// + internal static string UserController_ChangePassword_BadOldPassword { + get { + return ResourceManager.GetString("UserController_ChangePassword_BadOldPassword", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The new username already exists.. + /// + internal static string UserController_ChangeUsername_Conflict { + get { + return ResourceManager.GetString("UserController_ChangeUsername_Conflict", resourceCulture); + } + } + } +} diff --git a/Timeline/Resources/Messages.resx b/Timeline/Resources/Messages.resx new file mode 100644 index 00000000..c5228ed5 --- /dev/null +++ b/Timeline/Resources/Messages.resx @@ -0,0 +1,189 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Body is too big. It can't be bigger than {0}. + + + Actual body length is bigger than it in header. + + + Actual body length is smaller than it in header. + + + You have no permission to do the operation. + + + Header Content-Length is missing or of bad format. + + + Header Content-Length must not be 0. + + + Header Content-Type is missing. + + + Header If-Non-Match is of bad format. + + + Model is of bad format. + + + The {0}-st username to do operation {1} on is of bad format. + + + The {0}-st user to do operation {1} on does not exist. + + + The post to delete does not exist. + + + Username or password is invalid. + + + The token is of bad format. It might not be created by the server. + + + Token has an old version. User might have update some info. + + + The token is expired. + + + User does not exist. Administrator might have deleted this user. + + + Image is not a square. + + + Image decode failed. + + + Image format does not match the one in header. + + + The user to operate on does not exist. + + + Old password is wrong. + + + The new username already exists. + + \ No newline at end of file diff --git a/Timeline/Resources/Messages.zh.resx b/Timeline/Resources/Messages.zh.resx new file mode 100644 index 00000000..6e52befd --- /dev/null +++ b/Timeline/Resources/Messages.zh.resx @@ -0,0 +1,189 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 请求体太大。它不能超过{0}. + + + 实际的请求体长度比头中指示的大。 + + + 实际的请求体长度比头中指示的小。 + + + 你没有权限做此操作。 + + + 请求头Content-Length缺失或者格式不对。 + + + 请求头Content-Length不能为0。 + + + 请求头Content-Type缺失。 + + + 请求头If-Non-Match格式不对。 + + + 请求模型格式不对。 + + + 第{0}个做{1}操作的用户名格式错误。 + + + 第{0}个做{1}操作的用户不存在。 + + + 要删除的消息不存在。 + + + 用户名或密码错误。 + + + 符号格式错误。这个符号可能不是这个服务器创建的。 + + + 符号是一个旧版本。用户可能已经更新了信息。 + + + 符号过期了。 + + + 用户不存在。管理员可能已经删除了这个用户。 + + + 图片不是正方形。 + + + 解码图片失败。 + + + 图片格式与请求头中指示的不一样。 + + + 要操作的用户不存在。 + + + 旧密码错误。 + + + 新用户名已经存在。 + + \ No newline at end of file diff --git a/Timeline/Resources/Models/Http/Common.Designer.cs b/Timeline/Resources/Models/Http/Common.Designer.cs index 4eebd2bc..5165463e 100644 --- a/Timeline/Resources/Models/Http/Common.Designer.cs +++ b/Timeline/Resources/Models/Http/Common.Designer.cs @@ -60,33 +60,6 @@ namespace Timeline.Resources.Models.Http { } } - /// - /// Looks up a localized string similar to Body is too big. It can't be bigger than {0}.. - /// - internal static string MessageContentTooBig { - get { - return ResourceManager.GetString("MessageContentTooBig", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Actual body length is bigger than it in header.. - /// - internal static string MessageContentUnmatchedLengthBigger { - get { - return ResourceManager.GetString("MessageContentUnmatchedLengthBigger", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Actual body length is smaller than it in header.. - /// - internal static string MessageContentUnmatchedLengthSmaller { - get { - return ResourceManager.GetString("MessageContentUnmatchedLengthSmaller", resourceCulture); - } - } - /// /// Looks up a localized string similar to An existent item is deleted.. /// @@ -105,15 +78,6 @@ namespace Timeline.Resources.Models.Http { } } - /// - /// Looks up a localized string similar to Header If-Non-Match is of bad format.. - /// - internal static string MessageHeaderIfNonMatchBad { - get { - return ResourceManager.GetString("MessageHeaderIfNonMatchBad", resourceCulture); - } - } - /// /// Looks up a localized string similar to A new item is created.. /// diff --git a/Timeline/Resources/Models/Http/Common.resx b/Timeline/Resources/Models/Http/Common.resx index 540c6c58..85ec4d32 100644 --- a/Timeline/Resources/Models/Http/Common.resx +++ b/Timeline/Resources/Models/Http/Common.resx @@ -117,24 +117,12 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Body is too big. It can't be bigger than {0}. - - - Actual body length is bigger than it in header. - - - Actual body length is smaller than it in header. - An existent item is deleted. The item does not exist, so nothing is changed. - - Header If-Non-Match is of bad format. - A new item is created. diff --git a/Timeline/Resources/Models/Http/Common.zh.resx b/Timeline/Resources/Models/Http/Common.zh.resx index 467916a2..de74ac3b 100644 --- a/Timeline/Resources/Models/Http/Common.zh.resx +++ b/Timeline/Resources/Models/Http/Common.zh.resx @@ -117,24 +117,12 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - 请求体太大。它不能超过{0}. - - - 实际的请求体长度比头中指示的大。 - - - 实际的请求体长度比头中指示的小。 - 删除了一个项目。 要删除的项目不存在,什么都没有修改。 - - 请求头If-Non-Match格式不对。 - 创建了一个新项目。 diff --git a/Timeline/Timeline.csproj b/Timeline/Timeline.csproj index 681f49cb..27897177 100644 --- a/Timeline/Timeline.csproj +++ b/Timeline/Timeline.csproj @@ -32,17 +32,16 @@ + + + + True True AuthHandler.resx - - True - True - Common.resx - True True @@ -73,6 +72,11 @@ True Filters.resx + + True + True + Messages.resx + True True @@ -115,10 +119,6 @@ ResXFileCodeGenerator AuthHandler.Designer.cs - - ResXFileCodeGenerator - Common.Designer.cs - ResXFileCodeGenerator TestingI18nController.Designer.cs @@ -144,6 +144,10 @@ ResXFileCodeGenerator Filters.Designer.cs + + ResXFileCodeGenerator + Messages.Designer.cs + ResXFileCodeGenerator Common.Designer.cs -- cgit v1.2.3 From e6069a6980ec6d2505e19026d4c84a9588f153dc Mon Sep 17 00:00:00 2001 From: crupest Date: Sun, 19 Jan 2020 22:45:43 +0800 Subject: Basically finish refactor of error codes. --- .../Controllers/PersonalTimelineControllerTest.cs | 12 ++++---- Timeline.Tests/Controllers/TokenControllerTest.cs | 13 ++++----- Timeline.Tests/Controllers/UserControllerTest.cs | 12 ++++---- Timeline.Tests/ErrorCodeTest.cs | 1 + Timeline.Tests/Helpers/ResponseAssertions.cs | 12 ++++++-- .../IntegratedTests/PersonalTimelineTest.cs | 6 ++-- Timeline.Tests/IntegratedTests/TokenTest.cs | 9 +++--- Timeline.Tests/IntegratedTests/UserAvatarTest.cs | 33 +++++++++++----------- Timeline.Tests/IntegratedTests/UserDetailTest.cs | 7 +++-- Timeline.Tests/IntegratedTests/UserTest.cs | 11 ++++---- Timeline/Controllers/PersonalTimelineController.cs | 3 +- Timeline/Controllers/TokenController.cs | 4 +-- Timeline/Controllers/UserAvatarController.cs | 4 +-- 13 files changed, 65 insertions(+), 62 deletions(-) diff --git a/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs b/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs index 372ba8a7..bbc8ba75 100644 --- a/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs +++ b/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs @@ -142,7 +142,7 @@ namespace Timeline.Tests.Controllers .Which; result.StatusCode.Should().Be(StatusCodes.Status403Forbidden); result.Value.Should().BeAssignableTo() - .Which.Code.Should().Be(ErrorCodes.Http.Timeline.PostListGetForbid); + .Which.Code.Should().Be(ErrorCodes.Common.Forbid); _service.VerifyAll(); } @@ -185,7 +185,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.PostOperationCreateForbid); + .Which.Code.Should().Be(ErrorCodes.Common.Forbid); _service.VerifyAll(); } @@ -249,7 +249,7 @@ namespace Timeline.Tests.Controllers })).Should().NotBeNull().And.BeAssignableTo().Which; result.StatusCode.Should().Be(StatusCodes.Status403Forbidden); result.Value.Should().BeAssignableTo() - .Which.Code.Should().Be(ErrorCodes.Http.Timeline.PostOperationDeleteForbid); + .Which.Code.Should().Be(ErrorCodes.Common.Forbid); _service.VerifyAll(); } @@ -266,7 +266,7 @@ namespace Timeline.Tests.Controllers })).Should().NotBeNull().And.BeAssignableTo().Which; result.StatusCode.Should().Be(StatusCodes.Status400BadRequest); result.Value.Should().BeAssignableTo() - .Which.Code.Should().Be(ErrorCodes.Http.Timeline.PostOperationDeleteNotExist); + .Which.Code.Should().Be(ErrorCodes.TimelineController.PostOperationDelete_NotExist); _service.VerifyAll(); } @@ -347,7 +347,7 @@ namespace Timeline.Tests.Controllers }); result.Should().NotBeNull().And.BeAssignableTo() .Which.Value.Should().BeAssignableTo() - .Which.Code.Should().Be(ErrorCodes.Http.Common.InvalidModel); + .Which.Code.Should().Be(ErrorCodes.Common.InvalidModel); _service.VerifyAll(); } @@ -366,7 +366,7 @@ namespace Timeline.Tests.Controllers }); result.Should().NotBeNull().And.BeAssignableTo() .Which.Value.Should().BeAssignableTo() - .Which.Code.Should().Be(ErrorCodes.Http.Timeline.ChangeMemberUserNotExist); + .Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist); _service.VerifyAll(); } diff --git a/Timeline.Tests/Controllers/TokenControllerTest.cs b/Timeline.Tests/Controllers/TokenControllerTest.cs index 371884bb..2b3547ea 100644 --- a/Timeline.Tests/Controllers/TokenControllerTest.cs +++ b/Timeline.Tests/Controllers/TokenControllerTest.cs @@ -10,7 +10,6 @@ using Timeline.Models.Http; using Timeline.Services; using Timeline.Tests.Helpers; using Xunit; -using static Timeline.ErrorCodes.Token; namespace Timeline.Tests.Controllers { @@ -67,7 +66,7 @@ namespace Timeline.Tests.Controllers }); action.Result.Should().BeAssignableTo() .Which.Value.Should().BeAssignableTo() - .Which.Code.Should().Be(Create.BadCredential); + .Which.Code.Should().Be(ErrorCodes.TokenController.Create_BadCredential); } [Fact] @@ -82,7 +81,7 @@ namespace Timeline.Tests.Controllers }); action.Result.Should().BeAssignableTo() .Which.Value.Should().BeAssignableTo() - .Which.Code.Should().Be(Create.BadCredential); + .Which.Code.Should().Be(ErrorCodes.TokenController.Create_BadCredential); } [Fact] @@ -98,10 +97,10 @@ namespace Timeline.Tests.Controllers public static IEnumerable Verify_BadRequest_Data() { - yield return new object[] { new JwtVerifyException(JwtVerifyException.ErrorCodes.Expired), Verify.TimeExpired }; - yield return new object[] { new JwtVerifyException(JwtVerifyException.ErrorCodes.IdClaimBadFormat), Verify.BadFormat }; - yield return new object[] { new JwtVerifyException(JwtVerifyException.ErrorCodes.OldVersion), Verify.OldVersion }; - yield return new object[] { new UserNotExistException(), Verify.UserNotExist }; + yield return new object[] { new JwtVerifyException(JwtVerifyException.ErrorCodes.Expired), ErrorCodes.TokenController.Verify_TimeExpired }; + yield return new object[] { new JwtVerifyException(JwtVerifyException.ErrorCodes.IdClaimBadFormat), ErrorCodes.TokenController.Verify_BadFormat }; + yield return new object[] { new JwtVerifyException(JwtVerifyException.ErrorCodes.OldVersion), ErrorCodes.TokenController.Verify_OldVersion }; + yield return new object[] { new UserNotExistException(), ErrorCodes.TokenController.Verify_UserNotExist }; } [Theory] diff --git a/Timeline.Tests/Controllers/UserControllerTest.cs b/Timeline.Tests/Controllers/UserControllerTest.cs index 7a6541fb..043062c3 100644 --- a/Timeline.Tests/Controllers/UserControllerTest.cs +++ b/Timeline.Tests/Controllers/UserControllerTest.cs @@ -13,8 +13,6 @@ using Timeline.Models.Http; using Timeline.Services; using Timeline.Tests.Helpers; using Xunit; -using static Timeline.ErrorCodes.User; -using static Timeline.ErrorCodes.UserCommon; namespace Timeline.Tests.Controllers { @@ -62,7 +60,7 @@ namespace Timeline.Tests.Controllers var action = await _controller.Get(username); action.Result.Should().BeAssignableTo() .Which.Value.Should().BeAssignableTo() - .Which.Code.Should().Be(UserNotExist); + .Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist); } [Theory] @@ -115,7 +113,7 @@ namespace Timeline.Tests.Controllers }, username); action.Should().BeAssignableTo() .Which.Value.Should().BeAssignableTo() - .Which.Code.Should().Be(UserNotExist); + .Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist); } [Fact] @@ -155,8 +153,8 @@ namespace Timeline.Tests.Controllers } [Theory] - [InlineData(typeof(UserNotExistException), UserNotExist)] - [InlineData(typeof(UsernameConfictException), ChangeUsername_Conflict)] + [InlineData(typeof(UserNotExistException), ErrorCodes.UserCommon.NotExist)] + [InlineData(typeof(UsernameConfictException), ErrorCodes.UserController.ChangeUsername_Conflict)] public async Task Op_ChangeUsername_Failure(Type exceptionType, int code) { const string oldUsername = "aaa"; @@ -213,7 +211,7 @@ namespace Timeline.Tests.Controllers var action = await _controller.ChangePassword(new ChangePasswordRequest { OldPassword = oldPassword, NewPassword = newPassword }); action.Should().BeAssignableTo() .Which.Value.Should().BeAssignableTo() - .Which.Code.Should().Be(ChangePassword_BadOldPassword); + .Which.Code.Should().Be(ErrorCodes.UserController.ChangePassword_BadOldPassword); } } } diff --git a/Timeline.Tests/ErrorCodeTest.cs b/Timeline.Tests/ErrorCodeTest.cs index 78a58131..258ebf4e 100644 --- a/Timeline.Tests/ErrorCodeTest.cs +++ b/Timeline.Tests/ErrorCodeTest.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Reflection; +using Timeline.Models.Http; using Xunit; using Xunit.Abstractions; diff --git a/Timeline.Tests/Helpers/ResponseAssertions.cs b/Timeline.Tests/Helpers/ResponseAssertions.cs index 301ceef6..be0043dc 100644 --- a/Timeline.Tests/Helpers/ResponseAssertions.cs +++ b/Timeline.Tests/Helpers/ResponseAssertions.cs @@ -3,6 +3,7 @@ using FluentAssertions.Execution; using FluentAssertions.Formatting; using FluentAssertions.Primitives; using System; +using System.Globalization; using System.Net; using System.Net.Http; using System.Text; @@ -147,14 +148,21 @@ namespace Timeline.Tests.Helpers body.Data.Delete.Should().Be(delete); } + public static void HaveCommonResponseBody(this HttpResponseMessageAssertions assertions, int code, string message = null, params object[] messageArgs) + { + message = string.IsNullOrEmpty(message) ? "" : ", " + string.Format(CultureInfo.CurrentCulture, message, messageArgs); + var body = assertions.HaveJsonBody("Response body should be CommonResponse{0}", message).Which; + body.Code.Should().Be(code, "Response body code is not the specified one{0}", message); + } + public static void BeInvalidModel(this HttpResponseMessageAssertions assertions, string message = null) { message = string.IsNullOrEmpty(message) ? "" : ", " + message; assertions.HaveStatusCode(400, "Invalid Model Error must have 400 status code{0}", message) .And.HaveCommonBody("Invalid Model Error must have CommonResponse body{0}", message) - .Which.Code.Should().Be(ErrorCodes.Http.Common.InvalidModel, + .Which.Code.Should().Be(ErrorCodes.Common.InvalidModel, "Invalid Model Error must have code {0} in body{1}", - ErrorCodes.Http.Common.InvalidModel, message); + ErrorCodes.Common.InvalidModel, message); } } } diff --git a/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs b/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs index c5d0addd..51e2d05e 100644 --- a/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs +++ b/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs @@ -98,14 +98,14 @@ namespace Timeline.Tests.IntegratedTests new TimelineMemberChangeRequest { Add = new List { "admin", "usernotexist" } }); res.Should().HaveStatusCode(400) .And.HaveCommonBody() - .Which.Code.Should().Be(ErrorCodes.Http.Timeline.ChangeMemberUserNotExist); + .Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist); } { 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); + .Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist); } { var res = await client.PostAsJsonAsync(changeUrl, @@ -453,7 +453,7 @@ namespace Timeline.Tests.IntegratedTests new TimelinePostDeleteRequest { Id = 30000 }); res.Should().HaveStatusCode(400) .And.HaveCommonBody() - .Which.Code.Should().Be(ErrorCodes.Http.Timeline.PostOperationDeleteNotExist); + .Which.Code.Should().Be(ErrorCodes.TimelineController.PostOperationDelete_NotExist); } { var res = await client.GetAsync("users/user/timeline/posts"); diff --git a/Timeline.Tests/IntegratedTests/TokenTest.cs b/Timeline.Tests/IntegratedTests/TokenTest.cs index e62228fc..ecd5d0b8 100644 --- a/Timeline.Tests/IntegratedTests/TokenTest.cs +++ b/Timeline.Tests/IntegratedTests/TokenTest.cs @@ -8,7 +8,6 @@ using Timeline.Models.Http; using Timeline.Services; using Timeline.Tests.Helpers; using Xunit; -using static Timeline.ErrorCodes.Http.Token; namespace Timeline.Tests.IntegratedTests { @@ -66,7 +65,7 @@ namespace Timeline.Tests.IntegratedTests new CreateTokenRequest { Username = username, Password = password }); response.Should().HaveStatusCode(400) .And.HaveCommonBody() - .Which.Code.Should().Be(Create.BadCredential); + .Which.Code.Should().Be(ErrorCodes.TokenController.Create_BadCredential); } [Fact] @@ -97,7 +96,7 @@ namespace Timeline.Tests.IntegratedTests new VerifyTokenRequest { Token = "bad token hahaha" }); response.Should().HaveStatusCode(400) .And.HaveCommonBody() - .Which.Code.Should().Be(Verify.BadFormat); + .Which.Code.Should().Be(ErrorCodes.TokenController.Verify_BadFormat); } [Fact] @@ -117,7 +116,7 @@ namespace Timeline.Tests.IntegratedTests new VerifyTokenRequest { Token = token })) .Should().HaveStatusCode(400) .And.HaveCommonBody() - .Which.Code.Should().Be(Verify.OldVersion); + .Which.Code.Should().Be(ErrorCodes.TokenController.Verify_OldVersion); } [Fact] @@ -136,7 +135,7 @@ namespace Timeline.Tests.IntegratedTests new VerifyTokenRequest { Token = token })) .Should().HaveStatusCode(400) .And.HaveCommonBody() - .Which.Code.Should().Be(Verify.UserNotExist); + .Which.Code.Should().Be(ErrorCodes.TokenController.Verify_UserNotExist); } //[Fact] diff --git a/Timeline.Tests/IntegratedTests/UserAvatarTest.cs b/Timeline.Tests/IntegratedTests/UserAvatarTest.cs index 25a7b675..a4e10634 100644 --- a/Timeline.Tests/IntegratedTests/UserAvatarTest.cs +++ b/Timeline.Tests/IntegratedTests/UserAvatarTest.cs @@ -13,11 +13,10 @@ using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; +using Timeline.Models.Http; using Timeline.Services; using Timeline.Tests.Helpers; using Xunit; -using static Timeline.ErrorCodes.Http.Common; -using static Timeline.ErrorCodes.Http.UserAvatar; namespace Timeline.Tests.IntegratedTests { @@ -45,7 +44,7 @@ namespace Timeline.Tests.IntegratedTests var res = await client.GetAsync("users/usernotexist/avatar"); res.Should().HaveStatusCode(404) .And.HaveCommonBody() - .Which.Code.Should().Be(Get.UserNotExist); + .Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist); } var env = Factory.Server.Host.Services.GetRequiredService(); @@ -85,7 +84,7 @@ namespace Timeline.Tests.IntegratedTests request.Headers.TryAddWithoutValidation("If-None-Match", "\"dsdfd"); var res = await client.SendAsync(request); res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.HaveCommonBody().Which.Code.Should().Be(Header.IfNonMatch.BadFormat); + .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Common.Header.IfNonMatch_BadFormat); } { @@ -115,7 +114,7 @@ namespace Timeline.Tests.IntegratedTests content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); var res = await client.PutAsync("users/user/avatar", content); res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Http.Filter.Header.ContentLength.Missing); ; + .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Common.Header.ContentLength_Missing); ; } { @@ -123,7 +122,7 @@ namespace Timeline.Tests.IntegratedTests content.Headers.ContentLength = 1; var res = await client.PutAsync("users/user/avatar", content); res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Http.Filter.Header.ContentType.Missing); + .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Common.Header.ContentType_Missing); } { @@ -132,7 +131,7 @@ namespace Timeline.Tests.IntegratedTests content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); var res = await client.PutAsync("users/user/avatar", content); res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Http.Filter.Header.ContentLength.Zero); + .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Common.Header.ContentLength_Zero); } { @@ -146,7 +145,7 @@ namespace Timeline.Tests.IntegratedTests content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); var res = await client.PutAsync("users/user/avatar", content); res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.HaveCommonBody().Which.Code.Should().Be(Content.TooBig); + .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Common.Content.TooBig); } { @@ -155,7 +154,7 @@ namespace Timeline.Tests.IntegratedTests content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); var res = await client.PutAsync("users/user/avatar", content); res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.HaveCommonBody().Which.Code.Should().Be(Content.UnmatchedLength_Smaller); + .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Common.Content.UnmatchedLength_Smaller); } { @@ -164,25 +163,25 @@ namespace Timeline.Tests.IntegratedTests content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); var res = await client.PutAsync("users/user/avatar", content); res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.HaveCommonBody().Which.Code.Should().Be(Content.UnmatchedLength_Bigger); + .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Common.Content.UnmatchedLength_Bigger); } { var res = await client.PutByteArrayAsync("users/user/avatar", new[] { (byte)0x00 }, "image/png"); res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.HaveCommonBody().Which.Code.Should().Be(Put.BadFormat_CantDecode); + .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.UserAvatar.BadFormat_CantDecode); } { var res = await client.PutByteArrayAsync("users/user/avatar", mockAvatar.Data, "image/jpeg"); res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.HaveCommonBody().Which.Code.Should().Be(Put.BadFormat_UnmatchedFormat); + .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.UserAvatar.BadFormat_UnmatchedFormat); } { var res = await client.PutByteArrayAsync("users/user/avatar", ImageHelper.CreatePngWithSize(100, 200), "image/png"); res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.HaveCommonBody().Which.Code.Should().Be(Put.BadFormat_BadSize); + .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.UserAvatar.BadFormat_BadSize); } { @@ -212,13 +211,13 @@ namespace Timeline.Tests.IntegratedTests { var res = await client.PutByteArrayAsync("users/admin/avatar", new[] { (byte)0x00 }, "image/png"); res.Should().HaveStatusCode(HttpStatusCode.Forbidden) - .And.HaveCommonBody().Which.Code.Should().Be(Put.Forbid); + .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Common.Forbid); } { var res = await client.DeleteAsync("users/admin/avatar"); res.Should().HaveStatusCode(HttpStatusCode.Forbidden) - .And.HaveCommonBody().Which.Code.Should().Be(Delete.Forbid); + .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Common.Forbid); } for (int i = 0; i < 2; i++) // double delete should work. @@ -246,13 +245,13 @@ namespace Timeline.Tests.IntegratedTests var res = await client.PutByteArrayAsync("users/usernotexist/avatar", new[] { (byte)0x00 }, "image/png"); res.Should().HaveStatusCode(400) .And.HaveCommonBody() - .Which.Code.Should().Be(Put.UserNotExist); + .Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist); } { var res = await client.DeleteAsync("users/usernotexist/avatar"); res.Should().HaveStatusCode(400) - .And.HaveCommonBody().Which.Code.Should().Be(Delete.UserNotExist); + .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist); } } diff --git a/Timeline.Tests/IntegratedTests/UserDetailTest.cs b/Timeline.Tests/IntegratedTests/UserDetailTest.cs index 932c287e..3781a816 100644 --- a/Timeline.Tests/IntegratedTests/UserDetailTest.cs +++ b/Timeline.Tests/IntegratedTests/UserDetailTest.cs @@ -4,6 +4,7 @@ using System.Net; using System.Net.Http.Headers; using System.Net.Mime; using System.Threading.Tasks; +using Timeline.Models.Http; using Timeline.Tests.Helpers; using Xunit; @@ -82,7 +83,7 @@ namespace Timeline.Tests.IntegratedTests var res = await client.GetAsync(userNotExistUrl); res.Should().HaveStatusCode(HttpStatusCode.NotFound) .And.HaveCommonBody() - .Which.Code.Should().Be(ErrorCodes.Http.Filter.User.NotExist); + .Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist); } { @@ -128,13 +129,13 @@ namespace Timeline.Tests.IntegratedTests var res = await client.PutStringAsync(userNotExistUrl, "aaa"); res.Should().HaveStatusCode(HttpStatusCode.BadRequest) .And.HaveCommonBody() - .Which.Code.Should().Be(ErrorCodes.Http.Filter.User.NotExist); + .Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist); } { var res = await client.DeleteAsync(userNotExistUrl); res.Should().HaveStatusCode(HttpStatusCode.BadRequest) .And.HaveCommonBody() - .Which.Code.Should().Be(ErrorCodes.Http.Filter.User.NotExist); + .Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist); } var nickname = "nnn"; { diff --git a/Timeline.Tests/IntegratedTests/UserTest.cs b/Timeline.Tests/IntegratedTests/UserTest.cs index abfea18e..fbef6da3 100644 --- a/Timeline.Tests/IntegratedTests/UserTest.cs +++ b/Timeline.Tests/IntegratedTests/UserTest.cs @@ -7,7 +7,6 @@ using Timeline.Models; using Timeline.Models.Http; using Timeline.Tests.Helpers; using Xunit; -using static Timeline.ErrorCodes.Http.User; namespace Timeline.Tests.IntegratedTests { @@ -54,7 +53,7 @@ namespace Timeline.Tests.IntegratedTests var res = await client.GetAsync("users/usernotexist"); res.Should().HaveStatusCode(404) .And.HaveCommonBody() - .Which.Code.Should().Be(Get.NotExist); + .Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist); } public static IEnumerable Put_InvalidModel_Data() @@ -118,7 +117,7 @@ namespace Timeline.Tests.IntegratedTests var res = await client.PatchAsJsonAsync("users/usernotexist", new UserPatchRequest { }); res.Should().HaveStatusCode(404) .And.HaveCommonBody() - .Which.Code.Should().Be(Patch.NotExist); + .Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist); } [Fact] @@ -198,7 +197,7 @@ namespace Timeline.Tests.IntegratedTests new ChangeUsernameRequest { OldUsername = "usernotexist", NewUsername = "newUsername" }); res.Should().HaveStatusCode(400) .And.HaveCommonBody() - .Which.Code.Should().Be(Op.ChangeUsername.NotExist); + .Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist); } [Fact] @@ -209,7 +208,7 @@ namespace Timeline.Tests.IntegratedTests new ChangeUsernameRequest { OldUsername = MockUser.User.Username, NewUsername = MockUser.Admin.Username }); res.Should().HaveStatusCode(400) .And.HaveCommonBody() - .Which.Code.Should().Be(Op.ChangeUsername.AlreadyExist); + .Which.Code.Should().Be(ErrorCodes.UserController.ChangeUsername_Conflict); } private async Task TestLogin(string username, string password) @@ -256,7 +255,7 @@ namespace Timeline.Tests.IntegratedTests var res = await client.PostAsJsonAsync(changePasswordUrl, new ChangePasswordRequest { OldPassword = "???", NewPassword = "???" }); res.Should().HaveStatusCode(400) .And.HaveCommonBody() - .Which.Code.Should().Be(Op.ChangePassword.BadOldPassword); + .Which.Code.Should().Be(ErrorCodes.UserController.ChangePassword_BadOldPassword); } [Fact] diff --git a/Timeline/Controllers/PersonalTimelineController.cs b/Timeline/Controllers/PersonalTimelineController.cs index e1e3aba0..2c70fad1 100644 --- a/Timeline/Controllers/PersonalTimelineController.cs +++ b/Timeline/Controllers/PersonalTimelineController.cs @@ -3,7 +3,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using System.Collections.Generic; -using System.Globalization; using System.Threading.Tasks; using Timeline.Auth; using Timeline.Filters; @@ -134,7 +133,7 @@ namespace Timeline.Controllers } else if (e.InnerException is UserNotExistException) { - return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel( + return BadRequest(ErrorResponse.UserCommon.CustomMessage_NotExist( TimelineController_ChangeMember_UserNotExist, e.Index, e.Operation)); } diff --git a/Timeline/Controllers/TokenController.cs b/Timeline/Controllers/TokenController.cs index c360a109..67001e87 100644 --- a/Timeline/Controllers/TokenController.cs +++ b/Timeline/Controllers/TokenController.cs @@ -99,7 +99,7 @@ namespace Timeline.Controllers if (e.ErrorCode == JwtVerifyException.ErrorCodes.Expired) { var innerException = e.InnerException as SecurityTokenExpiredException; - LogFailure(LogVerifyExpire, e, ("Expires", innerException.Expires), + LogFailure(LogVerifyExpire, e, ("Expires", innerException?.Expires), ("Current Time", _clock.GetCurrentTime())); return BadRequest(ErrorResponse.TokenController.Verify_TimeExpired()); } @@ -107,7 +107,7 @@ namespace Timeline.Controllers { var innerException = e.InnerException as JwtBadVersionException; LogFailure(LogVerifyOldVersion, e, - ("Token Version", innerException.TokenVersion), + ("Token Version", innerException?.TokenVersion), ("Required Version", innerException?.RequiredVersion)); return BadRequest(ErrorResponse.TokenController.Verify_OldVersion()); } diff --git a/Timeline/Controllers/UserAvatarController.cs b/Timeline/Controllers/UserAvatarController.cs index b4a6d8fd..62f1d78c 100644 --- a/Timeline/Controllers/UserAvatarController.cs +++ b/Timeline/Controllers/UserAvatarController.cs @@ -66,7 +66,7 @@ namespace Timeline.Controllers catch (UserNotExistException e) { _logger.LogInformation(e, Log.Format(LogGetUserNotExist, ("Username", username))); - return NotFound(ErrorResponse.UserController.ChangePassword_BadOldPassword()); + return NotFound(ErrorResponse.UserCommon.NotExist()); } } @@ -112,7 +112,7 @@ namespace Timeline.Controllers catch (UserNotExistException e) { _logger.LogInformation(e, Log.Format(LogPutUserNotExist, ("Username", username))); - return BadRequest(ErrorResponse.UserController.ChangePassword_BadOldPassword()); + return BadRequest(ErrorResponse.UserCommon.NotExist()); } catch (AvatarFormatException e) { -- cgit v1.2.3 From 4aadb05cd5718c7d16bf432c96e23ae4e7db4783 Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 21 Jan 2020 01:11:17 +0800 Subject: ... --- Timeline.Tests/Controllers/TokenControllerTest.cs | 6 +- Timeline.Tests/Controllers/UserControllerTest.cs | 4 +- Timeline.Tests/DatabaseTest.cs | 4 +- Timeline.Tests/Helpers/TestDatabase.cs | 6 +- Timeline.Tests/IntegratedTests/UserTest.cs | 6 +- Timeline.Tests/Services/UserAvatarServiceTest.cs | 6 +- Timeline.Tests/Services/UserDetailServiceTest.cs | 2 +- Timeline/Controllers/TokenController.cs | 6 +- Timeline/Controllers/UserController.cs | 6 +- Timeline/Entities/DatabaseContext.cs | 10 +- Timeline/Entities/TimelineEntity.cs | 2 +- Timeline/Entities/TimelineMemberEntity.cs | 2 +- Timeline/Entities/TimelinePostEntity.cs | 2 +- Timeline/Entities/User.cs | 42 ------ Timeline/Entities/UserAvatar.cs | 28 ---- Timeline/Entities/UserAvatarEntity.cs | 28 ++++ Timeline/Entities/UserDetail.cs | 21 --- Timeline/Entities/UserDetailEntity.cs | 17 +++ Timeline/Entities/UserEntity.cs | 42 ++++++ Timeline/Models/Http/Token.cs | 4 +- Timeline/Models/Http/User.cs | 6 + Timeline/Models/UserConvert.cs | 67 ---------- Timeline/Models/UserInfo.cs | 23 +--- Timeline/Models/UserRoleConvert.cs | 44 ++++++ Timeline/Resources/Services/Exception.Designer.cs | 96 +++++++------- Timeline/Resources/Services/Exception.resx | 46 +++---- Timeline/Services/DatabaseExtensions.cs | 2 +- Timeline/Services/JwtService.cs | 132 ------------------ .../Services/JwtUserTokenBadFormatException.cs | 48 +++++++ Timeline/Services/JwtVerifyException.cs | 59 --------- Timeline/Services/UserAvatarService.cs | 2 +- Timeline/Services/UserDetailService.cs | 2 +- Timeline/Services/UserService.cs | 68 +++------- Timeline/Services/UserTokenException.cs | 71 ++++++++++ Timeline/Services/UserTokenManager.cs | 93 +++++++++++++ Timeline/Services/UserTokenService.cs | 147 +++++++++++++++++++++ Timeline/Services/UsernameBadFormatException.cs | 2 +- Timeline/Startup.cs | 2 +- 38 files changed, 632 insertions(+), 522 deletions(-) delete mode 100644 Timeline/Entities/User.cs delete mode 100644 Timeline/Entities/UserAvatar.cs create mode 100644 Timeline/Entities/UserAvatarEntity.cs delete mode 100644 Timeline/Entities/UserDetail.cs create mode 100644 Timeline/Entities/UserDetailEntity.cs create mode 100644 Timeline/Entities/UserEntity.cs delete mode 100644 Timeline/Models/UserConvert.cs create mode 100644 Timeline/Models/UserRoleConvert.cs delete mode 100644 Timeline/Services/JwtService.cs create mode 100644 Timeline/Services/JwtUserTokenBadFormatException.cs delete mode 100644 Timeline/Services/JwtVerifyException.cs create mode 100644 Timeline/Services/UserTokenException.cs create mode 100644 Timeline/Services/UserTokenManager.cs create mode 100644 Timeline/Services/UserTokenService.cs diff --git a/Timeline.Tests/Controllers/TokenControllerTest.cs b/Timeline.Tests/Controllers/TokenControllerTest.cs index 2b3547ea..740d8377 100644 --- a/Timeline.Tests/Controllers/TokenControllerTest.cs +++ b/Timeline.Tests/Controllers/TokenControllerTest.cs @@ -97,9 +97,9 @@ namespace Timeline.Tests.Controllers public static IEnumerable Verify_BadRequest_Data() { - yield return new object[] { new JwtVerifyException(JwtVerifyException.ErrorCodes.Expired), ErrorCodes.TokenController.Verify_TimeExpired }; - yield return new object[] { new JwtVerifyException(JwtVerifyException.ErrorCodes.IdClaimBadFormat), ErrorCodes.TokenController.Verify_BadFormat }; - yield return new object[] { new JwtVerifyException(JwtVerifyException.ErrorCodes.OldVersion), ErrorCodes.TokenController.Verify_OldVersion }; + yield return new object[] { new JwtUserTokenBadFormatException(JwtUserTokenBadFormatException.ErrorCodes.Expired), ErrorCodes.TokenController.Verify_TimeExpired }; + yield return new object[] { new JwtUserTokenBadFormatException(JwtUserTokenBadFormatException.ErrorCodes.IdClaimBadFormat), ErrorCodes.TokenController.Verify_BadFormat }; + yield return new object[] { new JwtUserTokenBadFormatException(JwtUserTokenBadFormatException.ErrorCodes.OldVersion), ErrorCodes.TokenController.Verify_OldVersion }; yield return new object[] { new UserNotExistException(), ErrorCodes.TokenController.Verify_UserNotExist }; } diff --git a/Timeline.Tests/Controllers/UserControllerTest.cs b/Timeline.Tests/Controllers/UserControllerTest.cs index 043062c3..262dbe11 100644 --- a/Timeline.Tests/Controllers/UserControllerTest.cs +++ b/Timeline.Tests/Controllers/UserControllerTest.cs @@ -46,7 +46,7 @@ namespace Timeline.Tests.Controllers public async Task Get_Success() { const string username = "aaa"; - _mockUserService.Setup(s => s.GetUser(username)).ReturnsAsync(MockUser.User.Info); + _mockUserService.Setup(s => s.GetUserByUsername(username)).ReturnsAsync(MockUser.User.Info); var action = await _controller.Get(username); action.Result.Should().BeAssignableTo() .Which.Value.Should().BeEquivalentTo(MockUser.User.Info); @@ -56,7 +56,7 @@ namespace Timeline.Tests.Controllers public async Task Get_NotFound() { const string username = "aaa"; - _mockUserService.Setup(s => s.GetUser(username)).Returns(Task.FromResult(null)); + _mockUserService.Setup(s => s.GetUserByUsername(username)).Returns(Task.FromResult(null)); var action = await _controller.Get(username); action.Result.Should().BeAssignableTo() .Which.Value.Should().BeAssignableTo() diff --git a/Timeline.Tests/DatabaseTest.cs b/Timeline.Tests/DatabaseTest.cs index a7b97c16..a15823a9 100644 --- a/Timeline.Tests/DatabaseTest.cs +++ b/Timeline.Tests/DatabaseTest.cs @@ -28,7 +28,7 @@ namespace Timeline.Tests { var user = _context.Users.First(); _context.UserAvatars.Count().Should().Be(0); - _context.UserAvatars.Add(new UserAvatar + _context.UserAvatars.Add(new UserAvatarEntity { Data = null, Type = null, @@ -48,7 +48,7 @@ namespace Timeline.Tests { var user = _context.Users.First(); _context.UserDetails.Count().Should().Be(0); - _context.UserDetails.Add(new UserDetail + _context.UserDetails.Add(new UserDetailEntity { Nickname = null, UserId = user.Id diff --git a/Timeline.Tests/Helpers/TestDatabase.cs b/Timeline.Tests/Helpers/TestDatabase.cs index 9560f353..3163279a 100644 --- a/Timeline.Tests/Helpers/TestDatabase.cs +++ b/Timeline.Tests/Helpers/TestDatabase.cs @@ -14,9 +14,9 @@ namespace Timeline.Tests.Helpers // 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) + private static UserEntity CreateEntityFromMock(MockUser user) { - return new User + return new UserEntity { Name = user.Username, EncryptedPassword = PasswordService.HashPassword(user.Password), @@ -25,7 +25,7 @@ namespace Timeline.Tests.Helpers }; } - private static IEnumerable CreateDefaultMockEntities() + 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); diff --git a/Timeline.Tests/IntegratedTests/UserTest.cs b/Timeline.Tests/IntegratedTests/UserTest.cs index fbef6da3..ea9f1177 100644 --- a/Timeline.Tests/IntegratedTests/UserTest.cs +++ b/Timeline.Tests/IntegratedTests/UserTest.cs @@ -24,7 +24,7 @@ namespace Timeline.Tests.IntegratedTests using var client = await CreateClientAsAdmin(); var res = await client.GetAsync("users"); res.Should().HaveStatusCode(200) - .And.HaveJsonBody() + .And.HaveJsonBody() .Which.Should().BeEquivalentTo(MockUser.UserInfoList); } @@ -34,7 +34,7 @@ namespace Timeline.Tests.IntegratedTests using var client = await CreateClientAsAdmin(); var res = await client.GetAsync("users/" + MockUser.User.Username); res.Should().HaveStatusCode(200) - .And.HaveJsonBody() + .And.HaveJsonBody() .Which.Should().BeEquivalentTo(MockUser.User.Info); } @@ -77,7 +77,7 @@ namespace Timeline.Tests.IntegratedTests { var res = await client.GetAsync("users/" + username); res.Should().HaveStatusCode(200) - .And.HaveJsonBody() + .And.HaveJsonBody() .Which.Administrator.Should().Be(administrator); } diff --git a/Timeline.Tests/Services/UserAvatarServiceTest.cs b/Timeline.Tests/Services/UserAvatarServiceTest.cs index 2729aa6f..d4371c48 100644 --- a/Timeline.Tests/Services/UserAvatarServiceTest.cs +++ b/Timeline.Tests/Services/UserAvatarServiceTest.cs @@ -78,7 +78,7 @@ namespace Timeline.Tests.Services public class UserAvatarServiceTest : IDisposable { - private UserAvatar CreateMockAvatarEntity(string key) => new UserAvatar + private UserAvatarEntity CreateMockAvatarEntity(string key) => new UserAvatarEntity { Type = $"image/test{key}", Data = Encoding.ASCII.GetBytes($"mock{key}"), @@ -102,7 +102,7 @@ namespace Timeline.Tests.Services Data = Encoding.ASCII.GetBytes($"mock{key}") }; - private static Avatar ToAvatar(UserAvatar entity) + private static Avatar ToAvatar(UserAvatarEntity entity) { return new Avatar { @@ -111,7 +111,7 @@ namespace Timeline.Tests.Services }; } - private static AvatarInfo ToAvatarInfo(UserAvatar entity) + private static AvatarInfo ToAvatarInfo(UserAvatarEntity entity) { return new AvatarInfo { diff --git a/Timeline.Tests/Services/UserDetailServiceTest.cs b/Timeline.Tests/Services/UserDetailServiceTest.cs index 9a869c89..e6eabadf 100644 --- a/Timeline.Tests/Services/UserDetailServiceTest.cs +++ b/Timeline.Tests/Services/UserDetailServiceTest.cs @@ -52,7 +52,7 @@ namespace Timeline.Tests.Services { var context = _testDatabase.Context; var userId = (await context.Users.Where(u => u.Name == MockUser.User.Username).Select(u => new { u.Id }).SingleAsync()).Id; - context.UserDetails.Add(new UserDetail + context.UserDetails.Add(new UserDetailEntity { Nickname = nickname, UserId = userId diff --git a/Timeline/Controllers/TokenController.cs b/Timeline/Controllers/TokenController.cs index 67001e87..851c7606 100644 --- a/Timeline/Controllers/TokenController.cs +++ b/Timeline/Controllers/TokenController.cs @@ -94,16 +94,16 @@ namespace Timeline.Controllers User = result }); } - catch (JwtVerifyException e) + catch (JwtUserTokenBadFormatException e) { - if (e.ErrorCode == JwtVerifyException.ErrorCodes.Expired) + if (e.ErrorCode == JwtUserTokenBadFormatException.ErrorCodes.Expired) { var innerException = e.InnerException as SecurityTokenExpiredException; LogFailure(LogVerifyExpire, e, ("Expires", innerException?.Expires), ("Current Time", _clock.GetCurrentTime())); return BadRequest(ErrorResponse.TokenController.Verify_TimeExpired()); } - else if (e.ErrorCode == JwtVerifyException.ErrorCodes.OldVersion) + else if (e.ErrorCode == JwtUserTokenBadFormatException.ErrorCodes.OldVersion) { var innerException = e.InnerException as JwtBadVersionException; LogFailure(LogVerifyOldVersion, e, diff --git a/Timeline/Controllers/UserController.cs b/Timeline/Controllers/UserController.cs index 956865dc..65ee3a0f 100644 --- a/Timeline/Controllers/UserController.cs +++ b/Timeline/Controllers/UserController.cs @@ -27,15 +27,15 @@ namespace Timeline.Controllers } [HttpGet("users"), AdminAuthorize] - public async Task> List() + public async Task> List() { return Ok(await _userService.ListUsers()); } [HttpGet("users/{username}"), AdminAuthorize] - public async Task> Get([FromRoute][Username] string username) + public async Task> Get([FromRoute][Username] string username) { - var user = await _userService.GetUser(username); + var user = await _userService.GetUserByUsername(username); if (user == null) { _logger.LogInformation(Log.Format(LogGetUserNotExist, ("Username", username))); diff --git a/Timeline/Entities/DatabaseContext.cs b/Timeline/Entities/DatabaseContext.cs index ffb6158a..738440b2 100644 --- a/Timeline/Entities/DatabaseContext.cs +++ b/Timeline/Entities/DatabaseContext.cs @@ -13,13 +13,13 @@ namespace Timeline.Entities [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")] protected override void OnModelCreating(ModelBuilder modelBuilder) { - modelBuilder.Entity().Property(e => e.Version).HasDefaultValue(0); - modelBuilder.Entity().HasIndex(e => e.Name).IsUnique(); + modelBuilder.Entity().Property(e => e.Version).HasDefaultValue(0); + modelBuilder.Entity().HasIndex(e => e.Name).IsUnique(); } - public DbSet Users { get; set; } = default!; - public DbSet UserAvatars { get; set; } = default!; - public DbSet UserDetails { get; set; } = default!; + public DbSet Users { get; set; } = default!; + public DbSet UserAvatars { get; set; } = default!; + 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/Entities/TimelineEntity.cs b/Timeline/Entities/TimelineEntity.cs index 9cacfcae..2bfd6107 100644 --- a/Timeline/Entities/TimelineEntity.cs +++ b/Timeline/Entities/TimelineEntity.cs @@ -26,7 +26,7 @@ namespace Timeline.Entities public long OwnerId { get; set; } [ForeignKey(nameof(OwnerId))] - public User Owner { get; set; } = default!; + public UserEntity Owner { get; set; } = default!; [Column("visibility")] public TimelineVisibility Visibility { get; set; } diff --git a/Timeline/Entities/TimelineMemberEntity.cs b/Timeline/Entities/TimelineMemberEntity.cs index dbe861bd..e76f2099 100644 --- a/Timeline/Entities/TimelineMemberEntity.cs +++ b/Timeline/Entities/TimelineMemberEntity.cs @@ -13,7 +13,7 @@ namespace Timeline.Entities public long UserId { get; set; } [ForeignKey(nameof(UserId))] - public User User { get; set; } = default!; + public UserEntity User { get; set; } = default!; [Column("timeline")] public long TimelineId { get; set; } diff --git a/Timeline/Entities/TimelinePostEntity.cs b/Timeline/Entities/TimelinePostEntity.cs index efef3ab5..a615c61f 100644 --- a/Timeline/Entities/TimelinePostEntity.cs +++ b/Timeline/Entities/TimelinePostEntity.cs @@ -20,7 +20,7 @@ namespace Timeline.Entities public long AuthorId { get; set; } [ForeignKey(nameof(AuthorId))] - public User Author { get; set; } = default!; + public UserEntity Author { get; set; } = default!; [Column("content")] public string? Content { get; set; } diff --git a/Timeline/Entities/User.cs b/Timeline/Entities/User.cs deleted file mode 100644 index e725a69a..00000000 --- a/Timeline/Entities/User.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Timeline.Entities -{ - public static class UserRoles - { - public const string Admin = "admin"; - public const string User = "user"; - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "This is an entity class.")] - [Table("users")] - public class User - { - [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public long Id { get; set; } - - [Column("name"), MaxLength(26), Required] - public string Name { get; set; } = default!; - - [Column("password"), Required] - public string EncryptedPassword { get; set; } = default!; - - [Column("roles"), Required] - public string RoleString { get; set; } = default!; - - [Column("version"), Required] - public long Version { get; set; } - - public UserAvatar? Avatar { get; set; } - - public UserDetail? Detail { get; set; } - - public List Timelines { get; set; } = default!; - - public List TimelinePosts { get; set; } = default!; - - public List TimelinesJoined { get; set; } = default!; - } -} diff --git a/Timeline/Entities/UserAvatar.cs b/Timeline/Entities/UserAvatar.cs deleted file mode 100644 index 114246f3..00000000 --- a/Timeline/Entities/UserAvatar.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Timeline.Entities -{ - [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1819:Properties should not return arrays", Justification = "This is data base entity.")] - [Table("user_avatars")] - public class UserAvatar - { - [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public long Id { get; set; } - - [Column("data")] - public byte[]? Data { get; set; } - - [Column("type")] - public string? Type { get; set; } - - [Column("etag"), MaxLength(30)] - public string? ETag { get; set; } - - [Column("last_modified"), Required] - public DateTime LastModified { get; set; } - - public long UserId { get; set; } - } -} diff --git a/Timeline/Entities/UserAvatarEntity.cs b/Timeline/Entities/UserAvatarEntity.cs new file mode 100644 index 00000000..eed819bc --- /dev/null +++ b/Timeline/Entities/UserAvatarEntity.cs @@ -0,0 +1,28 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Timeline.Entities +{ + [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1819:Properties should not return arrays", Justification = "This is data base entity.")] + [Table("user_avatars")] + public class UserAvatarEntity + { + [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + + [Column("data")] + public byte[]? Data { get; set; } + + [Column("type")] + public string? Type { get; set; } + + [Column("etag"), MaxLength(30)] + public string? ETag { get; set; } + + [Column("last_modified"), Required] + public DateTime LastModified { get; set; } + + public long UserId { get; set; } + } +} diff --git a/Timeline/Entities/UserDetail.cs b/Timeline/Entities/UserDetail.cs deleted file mode 100644 index 45f87e2b..00000000 --- a/Timeline/Entities/UserDetail.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using System.Linq; -using System.Threading.Tasks; - -namespace Timeline.Entities -{ - [Table("user_details")] - public class UserDetail - { - [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public long Id { get; set; } - - [Column("nickname"), MaxLength(26)] - public string? Nickname { get; set; } - - public long UserId { get; set; } - } -} diff --git a/Timeline/Entities/UserDetailEntity.cs b/Timeline/Entities/UserDetailEntity.cs new file mode 100644 index 00000000..7a525294 --- /dev/null +++ b/Timeline/Entities/UserDetailEntity.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Timeline.Entities +{ + [Table("user_details")] + public class UserDetailEntity + { + [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + + [Column("nickname"), MaxLength(26)] + public string? Nickname { get; set; } + + public long UserId { get; set; } + } +} diff --git a/Timeline/Entities/UserEntity.cs b/Timeline/Entities/UserEntity.cs new file mode 100644 index 00000000..83ef5621 --- /dev/null +++ b/Timeline/Entities/UserEntity.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Timeline.Entities +{ + public static class UserRoles + { + public const string Admin = "admin"; + public const string User = "user"; + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "This is an entity class.")] + [Table("users")] + public class UserEntity + { + [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + + [Column("name"), MaxLength(26), Required] + public string Name { get; set; } = default!; + + [Column("password"), Required] + public string EncryptedPassword { get; set; } = default!; + + [Column("roles"), Required] + public string RoleString { get; set; } = default!; + + [Column("version"), Required] + public long Version { get; set; } + + public UserAvatarEntity? Avatar { get; set; } + + public UserDetailEntity? Detail { get; set; } + + public List Timelines { get; set; } = default!; + + public List TimelinePosts { get; set; } = default!; + + public List TimelinesJoined { get; set; } = default!; + } +} diff --git a/Timeline/Models/Http/Token.cs b/Timeline/Models/Http/Token.cs index ea8b59ed..0649f1d1 100644 --- a/Timeline/Models/Http/Token.cs +++ b/Timeline/Models/Http/Token.cs @@ -16,7 +16,7 @@ namespace Timeline.Models.Http public class CreateTokenResponse { public string Token { get; set; } = default!; - public UserInfo User { get; set; } = default!; + public User User { get; set; } = default!; } public class VerifyTokenRequest @@ -27,6 +27,6 @@ namespace Timeline.Models.Http public class VerifyTokenResponse { - public UserInfo User { get; set; } = default!; + public User User { get; set; } = default!; } } diff --git a/Timeline/Models/Http/User.cs b/Timeline/Models/Http/User.cs index 516c1329..69bfacf2 100644 --- a/Timeline/Models/Http/User.cs +++ b/Timeline/Models/Http/User.cs @@ -3,6 +3,12 @@ using Timeline.Models.Validation; namespace Timeline.Models.Http { + public class User + { + public string Username { get; set; } = default!; + public bool Administrator { get; set; } + } + public class UserPutRequest { [Required] diff --git a/Timeline/Models/UserConvert.cs b/Timeline/Models/UserConvert.cs deleted file mode 100644 index 5b132421..00000000 --- a/Timeline/Models/UserConvert.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Timeline.Entities; -using Timeline.Services; - -namespace Timeline.Models -{ - public static class UserConvert - { - public static UserInfo CreateUserInfo(User user) - { - if (user == null) - throw new ArgumentNullException(nameof(user)); - return new UserInfo(user.Name, UserRoleConvert.ToBool(user.RoleString)); - } - - internal static UserCache CreateUserCache(User user) - { - if (user == null) - throw new ArgumentNullException(nameof(user)); - return new UserCache - { - Username = user.Name, - Administrator = UserRoleConvert.ToBool(user.RoleString), - Version = user.Version - }; - } - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "No need.")] - public static class UserRoleConvert - { - public const string UserRole = UserRoles.User; - public const string AdminRole = UserRoles.Admin; - - public static string[] ToArray(bool administrator) - { - return administrator ? new string[] { UserRole, AdminRole } : new string[] { UserRole }; - } - - public static string[] ToArray(string s) - { - return s.Split(',').ToArray(); - } - - public static bool ToBool(IReadOnlyCollection roles) - { - return roles.Contains(AdminRole); - } - - public static string ToString(IReadOnlyCollection roles) - { - return string.Join(',', roles); - } - - public static string ToString(bool administrator) - { - return administrator ? UserRole + "," + AdminRole : UserRole; - } - - public static bool ToBool(string s) - { - return s.Contains("admin", StringComparison.InvariantCulture); - } - } -} diff --git a/Timeline/Models/UserInfo.cs b/Timeline/Models/UserInfo.cs index b60bdfa2..eff47329 100644 --- a/Timeline/Models/UserInfo.cs +++ b/Timeline/Models/UserInfo.cs @@ -1,23 +1,10 @@ -namespace Timeline.Models +namespace Timeline.Models { - public sealed class UserInfo + public class UserInfo { - public UserInfo() - { - } - - public UserInfo(string username, bool administrator) - { - Username = username; - Administrator = administrator; - } - + public long Id { get; set; } + public long Version { get; set; } public string Username { get; set; } = default!; - public bool Administrator { get; set; } = default!; - - public override string ToString() - { - return $"Username: {Username} ; Administrator: {Administrator}"; - } + public bool Administrator { get; set; } } } diff --git a/Timeline/Models/UserRoleConvert.cs b/Timeline/Models/UserRoleConvert.cs new file mode 100644 index 00000000..ade9a799 --- /dev/null +++ b/Timeline/Models/UserRoleConvert.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Timeline.Entities; + +namespace Timeline.Models +{ + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "No need.")] + public static class UserRoleConvert + { + public const string UserRole = UserRoles.User; + public const string AdminRole = UserRoles.Admin; + + public static string[] ToArray(bool administrator) + { + return administrator ? new string[] { UserRole, AdminRole } : new string[] { UserRole }; + } + + public static string[] ToArray(string s) + { + return s.Split(',').ToArray(); + } + + public static bool ToBool(IReadOnlyCollection roles) + { + return roles.Contains(AdminRole); + } + + public static string ToString(IReadOnlyCollection roles) + { + return string.Join(',', roles); + } + + public static string ToString(bool administrator) + { + return administrator ? UserRole + "," + AdminRole : UserRole; + } + + public static bool ToBool(string s) + { + return s.Contains("admin", StringComparison.InvariantCulture); + } + } +} diff --git a/Timeline/Resources/Services/Exception.Designer.cs b/Timeline/Resources/Services/Exception.Designer.cs index 1b46f9e9..0a3325d4 100644 --- a/Timeline/Resources/Services/Exception.Designer.cs +++ b/Timeline/Resources/Services/Exception.Designer.cs @@ -178,92 +178,65 @@ namespace Timeline.Resources.Services { } /// - /// Looks up a localized string similar to The version of the jwt token is old.. + /// Looks up a localized string similar to The token didn't pass verification because {0}.. /// - internal static string JwtBadVersionException { + internal static string JwtUserTokenBadFormatException { get { - return ResourceManager.GetString("JwtBadVersionException", resourceCulture); + return ResourceManager.GetString("JwtUserTokenBadFormatException", resourceCulture); } } /// - /// Looks up a localized string similar to The token didn't pass verification because {0}, see inner exception for information.. + /// Looks up a localized string similar to id claim is not a number. /// - internal static string JwtVerifyException { + internal static string JwtUserTokenBadFormatExceptionIdBadFormat { get { - return ResourceManager.GetString("JwtVerifyException", resourceCulture); + return ResourceManager.GetString("JwtUserTokenBadFormatExceptionIdBadFormat", resourceCulture); } } /// - /// Looks up a localized string similar to token is expired.. + /// Looks up a localized string similar to id claim does not exist. /// - internal static string JwtVerifyExceptionExpired { + internal static string JwtUserTokenBadFormatExceptionIdMissing { get { - return ResourceManager.GetString("JwtVerifyExceptionExpired", resourceCulture); + return ResourceManager.GetString("JwtUserTokenBadFormatExceptionIdMissing", resourceCulture); } } /// - /// Looks up a localized string similar to id claim is not a number.. + /// Looks up a localized string similar to other error, see inner exception for information. /// - internal static string JwtVerifyExceptionIdClaimBadFormat { + internal static string JwtUserTokenBadFormatExceptionOthers { get { - return ResourceManager.GetString("JwtVerifyExceptionIdClaimBadFormat", resourceCulture); + return ResourceManager.GetString("JwtUserTokenBadFormatExceptionOthers", resourceCulture); } } /// - /// Looks up a localized string similar to id claim does not exist.. - /// - internal static string JwtVerifyExceptionNoIdClaim { - get { - return ResourceManager.GetString("JwtVerifyExceptionNoIdClaim", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to version claim does not exist.. - /// - internal static string JwtVerifyExceptionNoVersionClaim { - get { - return ResourceManager.GetString("JwtVerifyExceptionNoVersionClaim", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to version of token is old.. - /// - internal static string JwtVerifyExceptionOldVersion { - get { - return ResourceManager.GetString("JwtVerifyExceptionOldVersion", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to uncommon error.. + /// Looks up a localized string similar to unknown error. /// - internal static string JwtVerifyExceptionOthers { + internal static string JwtUserTokenBadFormatExceptionUnknown { get { - return ResourceManager.GetString("JwtVerifyExceptionOthers", resourceCulture); + return ResourceManager.GetString("JwtUserTokenBadFormatExceptionUnknown", resourceCulture); } } /// - /// Looks up a localized string similar to unknown error code.. + /// Looks up a localized string similar to version claim is not a number.. /// - internal static string JwtVerifyExceptionUnknown { + internal static string JwtUserTokenBadFormatExceptionVersionBadFormat { get { - return ResourceManager.GetString("JwtVerifyExceptionUnknown", resourceCulture); + return ResourceManager.GetString("JwtUserTokenBadFormatExceptionVersionBadFormat", resourceCulture); } } /// - /// Looks up a localized string similar to version claim is not a number.. + /// Looks up a localized string similar to version claim does not exist.. /// - internal static string JwtVerifyExceptionVersionClaimBadFormat { + internal static string JwtUserTokenBadFormatExceptionVersionMissing { get { - return ResourceManager.GetString("JwtVerifyExceptionVersionClaimBadFormat", resourceCulture); + return ResourceManager.GetString("JwtUserTokenBadFormatExceptionVersionMissing", resourceCulture); } } @@ -356,5 +329,32 @@ namespace Timeline.Resources.Services { return ResourceManager.GetString("UserNotExistException", resourceCulture); } } + + /// + /// Looks up a localized string similar to The token is of bad format, which means it may not be created by the server.. + /// + internal static string UserTokenBadFormatException { + get { + return ResourceManager.GetString("UserTokenBadFormatException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The token is of bad version.. + /// + internal static string UserTokenBadVersionException { + get { + return ResourceManager.GetString("UserTokenBadVersionException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The token is expired because its expiration time has passed.. + /// + internal static string UserTokenTimeExpireException { + get { + return ResourceManager.GetString("UserTokenTimeExpireException", resourceCulture); + } + } } } diff --git a/Timeline/Resources/Services/Exception.resx b/Timeline/Resources/Services/Exception.resx index 1d9c0037..bc96248d 100644 --- a/Timeline/Resources/Services/Exception.resx +++ b/Timeline/Resources/Services/Exception.resx @@ -156,36 +156,27 @@ Unknown format marker. - - The version of the jwt token is old. + + The token didn't pass verification because {0}. - - The token didn't pass verification because {0}, see inner exception for information. + + id claim is not a number - - token is expired. + + id claim does not exist - - id claim is not a number. + + other error, see inner exception for information - - id claim does not exist. - - - version claim does not exist. - - - version of token is old. - - - uncommon error. - - - unknown error code. + + unknown error - + version claim is not a number. + + version claim does not exist. + The timeline with that name already exists. @@ -216,4 +207,13 @@ The user does not exist. + + The token is of bad format, which means it may not be created by the server. + + + The token is of bad version. + + + The token is expired because its expiration time has passed. + \ No newline at end of file diff --git a/Timeline/Services/DatabaseExtensions.cs b/Timeline/Services/DatabaseExtensions.cs index 140c3146..c5c96d8c 100644 --- a/Timeline/Services/DatabaseExtensions.cs +++ b/Timeline/Services/DatabaseExtensions.cs @@ -19,7 +19,7 @@ namespace Timeline.Services /// Thrown if is null. /// Thrown if is of bad format. /// Thrown if user does not exist. - internal static async Task CheckAndGetUser(DbSet userDbSet, string? username) + internal static async Task CheckAndGetUser(DbSet userDbSet, string? username) { if (username == null) throw new ArgumentNullException(nameof(username)); diff --git a/Timeline/Services/JwtService.cs b/Timeline/Services/JwtService.cs deleted file mode 100644 index bf92966a..00000000 --- a/Timeline/Services/JwtService.cs +++ /dev/null @@ -1,132 +0,0 @@ -using Microsoft.Extensions.Options; -using Microsoft.IdentityModel.Tokens; -using System; -using System.Globalization; -using System.IdentityModel.Tokens.Jwt; -using System.Security.Claims; -using System.Text; -using Timeline.Configs; - -namespace Timeline.Services -{ - public class TokenInfo - { - public long Id { get; set; } - public long Version { get; set; } - } - - public interface IJwtService - { - /// - /// Create a JWT token for a given token info. - /// - /// The info to generate token. - /// The expire time. If null then use current time with offset in config. - /// Return the generated token. - /// Thrown when is null. - string GenerateJwtToken(TokenInfo tokenInfo, DateTime? expires = null); - - /// - /// Verify a JWT token. - /// Return null is is null. - /// - /// The token string to verify. - /// Return the saved info in token. - /// Thrown when is null. - /// Thrown when the token is invalid. - TokenInfo VerifyJwtToken(string token); - - } - - public class JwtService : IJwtService - { - private const string VersionClaimType = "timeline_version"; - - private readonly IOptionsMonitor _jwtConfig; - private readonly JwtSecurityTokenHandler _tokenHandler = new JwtSecurityTokenHandler(); - private readonly IClock _clock; - - public JwtService(IOptionsMonitor jwtConfig, IClock clock) - { - _jwtConfig = jwtConfig; - _clock = clock; - } - - public string GenerateJwtToken(TokenInfo tokenInfo, DateTime? expires = null) - { - if (tokenInfo == null) - throw new ArgumentNullException(nameof(tokenInfo)); - - var config = _jwtConfig.CurrentValue; - - var identity = new ClaimsIdentity(); - identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, tokenInfo.Id.ToString(CultureInfo.InvariantCulture.NumberFormat), ClaimValueTypes.Integer64)); - identity.AddClaim(new Claim(VersionClaimType, tokenInfo.Version.ToString(CultureInfo.InvariantCulture.NumberFormat), ClaimValueTypes.Integer64)); - - var tokenDescriptor = new SecurityTokenDescriptor() - { - Subject = identity, - Issuer = config.Issuer, - Audience = config.Audience, - SigningCredentials = new SigningCredentials( - new SymmetricSecurityKey(Encoding.ASCII.GetBytes(config.SigningKey)), SecurityAlgorithms.HmacSha384), - IssuedAt = _clock.GetCurrentTime(), - Expires = expires.GetValueOrDefault(_clock.GetCurrentTime().AddSeconds(config.DefaultExpireOffset)), - NotBefore = _clock.GetCurrentTime() // I must explicitly set this or it will use the current time by default and mock is not work in which case test will not pass. - }; - - var token = _tokenHandler.CreateToken(tokenDescriptor); - var tokenString = _tokenHandler.WriteToken(token); - - return tokenString; - } - - - public TokenInfo VerifyJwtToken(string token) - { - if (token == null) - throw new ArgumentNullException(nameof(token)); - - var config = _jwtConfig.CurrentValue; - try - { - var principal = _tokenHandler.ValidateToken(token, new TokenValidationParameters - { - ValidateIssuer = true, - ValidateAudience = true, - ValidateIssuerSigningKey = true, - ValidateLifetime = true, - ValidIssuer = config.Issuer, - ValidAudience = config.Audience, - IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(config.SigningKey)) - }, out _); - - var idClaim = principal.FindFirstValue(ClaimTypes.NameIdentifier); - if (idClaim == null) - throw new JwtVerifyException(JwtVerifyException.ErrorCodes.NoIdClaim); - if (!long.TryParse(idClaim, out var id)) - throw new JwtVerifyException(JwtVerifyException.ErrorCodes.IdClaimBadFormat); - - var versionClaim = principal.FindFirstValue(VersionClaimType); - if (versionClaim == null) - throw new JwtVerifyException(JwtVerifyException.ErrorCodes.NoVersionClaim); - if (!long.TryParse(versionClaim, out var version)) - throw new JwtVerifyException(JwtVerifyException.ErrorCodes.VersionClaimBadFormat); - - return new TokenInfo - { - Id = id, - Version = version - }; - } - catch (SecurityTokenExpiredException e) - { - throw new JwtVerifyException(e, JwtVerifyException.ErrorCodes.Expired); - } - catch (Exception e) - { - throw new JwtVerifyException(e, JwtVerifyException.ErrorCodes.Others); - } - } - } -} diff --git a/Timeline/Services/JwtUserTokenBadFormatException.cs b/Timeline/Services/JwtUserTokenBadFormatException.cs new file mode 100644 index 00000000..c528c3e3 --- /dev/null +++ b/Timeline/Services/JwtUserTokenBadFormatException.cs @@ -0,0 +1,48 @@ +using System; +using System.Globalization; +using static Timeline.Resources.Services.Exception; + +namespace Timeline.Services +{ + [Serializable] + public class JwtUserTokenBadFormatException : UserTokenBadFormatException + { + public enum ErrorKind + { + NoIdClaim, + IdClaimBadFormat, + NoVersionClaim, + VersionClaimBadFormat, + Other + } + + public JwtUserTokenBadFormatException() : this("", ErrorKind.Other) { } + public JwtUserTokenBadFormatException(string message) : base(message) { } + public JwtUserTokenBadFormatException(string message, Exception inner) : base(message, inner) { } + + public JwtUserTokenBadFormatException(string token, ErrorKind type) : base(token, GetErrorMessage(type)) { ErrorType = type; } + public JwtUserTokenBadFormatException(string token, ErrorKind type, Exception inner) : base(token, GetErrorMessage(type), inner) { ErrorType = type; } + public JwtUserTokenBadFormatException(string token, ErrorKind type, string message, Exception inner) : base(token, message, inner) { ErrorType = type; } + protected JwtUserTokenBadFormatException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + public ErrorKind ErrorType { get; set; } + + private static string GetErrorMessage(ErrorKind type) + { + var reason = type switch + { + ErrorKind.NoIdClaim => JwtUserTokenBadFormatExceptionIdMissing, + ErrorKind.IdClaimBadFormat => JwtUserTokenBadFormatExceptionIdBadFormat, + ErrorKind.NoVersionClaim => JwtUserTokenBadFormatExceptionVersionMissing, + ErrorKind.VersionClaimBadFormat => JwtUserTokenBadFormatExceptionVersionBadFormat, + ErrorKind.Other => JwtUserTokenBadFormatExceptionOthers, + _ => JwtUserTokenBadFormatExceptionUnknown + }; + + return string.Format(CultureInfo.CurrentCulture, + Resources.Services.Exception.JwtUserTokenBadFormatException, reason); + } + } +} diff --git a/Timeline/Services/JwtVerifyException.cs b/Timeline/Services/JwtVerifyException.cs deleted file mode 100644 index a915b51a..00000000 --- a/Timeline/Services/JwtVerifyException.cs +++ /dev/null @@ -1,59 +0,0 @@ -using Microsoft.IdentityModel.Tokens; -using System; -using System.Globalization; -using static Timeline.Resources.Services.Exception; - -namespace Timeline.Services -{ - [Serializable] - public class JwtVerifyException : Exception - { - public static class ErrorCodes - { - // Codes in -1000 ~ -1999 usually means the user provides a token that is not created by this server. - - public const int Others = -1001; - public const int NoIdClaim = -1002; - public const int IdClaimBadFormat = -1003; - public const int NoVersionClaim = -1004; - public const int VersionClaimBadFormat = -1005; - - /// - /// Corresponds to . - /// - public const int Expired = -2001; - public const int OldVersion = -2002; - } - - public JwtVerifyException() : base(GetErrorMessage(0)) { } - public JwtVerifyException(string message) : base(message) { } - public JwtVerifyException(string message, Exception inner) : base(message, inner) { } - - public JwtVerifyException(int code) : base(GetErrorMessage(code)) { ErrorCode = code; } - public JwtVerifyException(string message, int code) : base(message) { ErrorCode = code; } - public JwtVerifyException(Exception inner, int code) : base(GetErrorMessage(code), inner) { ErrorCode = code; } - public JwtVerifyException(string message, Exception inner, int code) : base(message, inner) { ErrorCode = code; } - protected JwtVerifyException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - public int ErrorCode { get; set; } - - private static string GetErrorMessage(int errorCode) - { - var reason = errorCode switch - { - ErrorCodes.Others => JwtVerifyExceptionOthers, - ErrorCodes.NoIdClaim => JwtVerifyExceptionNoIdClaim, - ErrorCodes.IdClaimBadFormat => JwtVerifyExceptionIdClaimBadFormat, - ErrorCodes.NoVersionClaim => JwtVerifyExceptionNoVersionClaim, - ErrorCodes.VersionClaimBadFormat => JwtVerifyExceptionVersionClaimBadFormat, - ErrorCodes.Expired => JwtVerifyExceptionExpired, - ErrorCodes.OldVersion => JwtVerifyExceptionOldVersion, - _ => JwtVerifyExceptionUnknown - }; - - return string.Format(CultureInfo.InvariantCulture, Resources.Services.Exception.JwtVerifyException, reason); - } - } -} diff --git a/Timeline/Services/UserAvatarService.cs b/Timeline/Services/UserAvatarService.cs index 01201864..ac7dd857 100644 --- a/Timeline/Services/UserAvatarService.cs +++ b/Timeline/Services/UserAvatarService.cs @@ -275,7 +275,7 @@ namespace Timeline.Services var create = avatarEntity == null; if (create) { - avatarEntity = new UserAvatar(); + avatarEntity = new UserAvatarEntity(); } avatarEntity!.Type = avatar.Type; avatarEntity.Data = avatar.Data; diff --git a/Timeline/Services/UserDetailService.cs b/Timeline/Services/UserDetailService.cs index 0b24e4e2..4f4a7942 100644 --- a/Timeline/Services/UserDetailService.cs +++ b/Timeline/Services/UserDetailService.cs @@ -77,7 +77,7 @@ namespace Timeline.Services var create = userDetail == null; if (create) { - userDetail = new UserDetail + userDetail = new UserDetailEntity { UserId = userId }; diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs index 4012539f..db2350a2 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -11,47 +11,37 @@ using Timeline.Models.Validation; namespace Timeline.Services { - public class CreateTokenResult - { - public string Token { get; set; } = default!; - public UserInfo User { get; set; } = default!; - } - public interface IUserService { /// - /// Try to anthenticate with the given username and password. - /// If success, create a token and return the user info. + /// Try to verify the given username and password. /// - /// The username of the user to anthenticate. - /// The password of the user to anthenticate. - /// The expired time point. Null then use default. See for what is default. - /// An containing the created token and user info. + /// The username of the user to verify. + /// The password of the user to verify. + /// The user info. /// Thrown when or is null. /// Thrown when username is of bad format. /// Thrown when the user with given username does not exist. /// Thrown when password is wrong. - Task CreateToken(string username, string password, DateTime? expires = null); + Task VerifyCredential(string username, string password); /// - /// Verify the given token. - /// If success, return the user info. + /// Try to get a user by id. /// - /// The token to verify. - /// The user info specified by the token. - /// Thrown when is null. - /// Thrown when the token is of bad format. Thrown by . - /// Thrown when the user specified by the token does not exist. Usually it has been deleted after the token was issued. - Task VerifyToken(string token); + /// The id of the user. + /// The user info. + /// Thrown when the user with given id does not exist. + Task GetUserById(long id); /// /// Get the user info of given username. /// /// Username of the user. - /// The info of the user. Null if the user of given username does not exists. + /// The info of the user. /// Thrown when is null. /// Thrown when is of bad format. - Task GetUser(string username); + /// Thrown when the user with given username does not exist. + Task GetUserByUsername(string username); /// /// List all users. @@ -120,39 +110,24 @@ namespace Timeline.Services Task ChangeUsername(string oldUsername, string newUsername); } - internal class UserCache - { - public string Username { get; set; } = default!; - public bool Administrator { get; set; } - public long Version { get; set; } - - public UserInfo ToUserInfo() - { - return new UserInfo(Username, Administrator); - } - } - public class UserService : IUserService { private readonly ILogger _logger; - private readonly IMemoryCache _memoryCache; private readonly DatabaseContext _databaseContext; - private readonly IJwtService _jwtService; + private readonly IMemoryCache _memoryCache; + private readonly IPasswordService _passwordService; - private readonly UsernameValidator _usernameValidator; + private readonly UsernameValidator _usernameValidator = new UsernameValidator(); - public UserService(ILogger logger, IMemoryCache memoryCache, DatabaseContext databaseContext, IJwtService jwtService, IPasswordService passwordService) + public UserService(ILogger logger, IMemoryCache memoryCache, DatabaseContext databaseContext, IPasswordService passwordService) { _logger = logger; _memoryCache = memoryCache; _databaseContext = databaseContext; - _jwtService = jwtService; _passwordService = passwordService; - - _usernameValidator = new UsernameValidator(); } private static string GenerateCacheKeyByUserId(long id) => $"user:{id}"; @@ -176,12 +151,13 @@ namespace Timeline.Services } } - public async Task CreateToken(string username, string password, DateTime? expires) + public async Task CheckCredential(string username, string password) { if (username == null) throw new ArgumentNullException(nameof(username)); if (password == null) throw new ArgumentNullException(nameof(password)); + CheckUsernameFormat(username); // We need password info, so always check the database. @@ -231,12 +207,12 @@ namespace Timeline.Services } if (tokenInfo.Version != cache.Version) - throw new JwtVerifyException(new JwtBadVersionException(tokenInfo.Version, cache.Version), JwtVerifyException.ErrorCodes.OldVersion); + throw new JwtUserTokenBadFormatException(new JwtBadVersionException(tokenInfo.Version, cache.Version), JwtUserTokenBadFormatException.ErrorCodes.OldVersion); return cache.ToUserInfo(); } - public async Task GetUser(string username) + public async Task GetUserByUsername(string username) { if (username == null) throw new ArgumentNullException(nameof(username)); @@ -267,7 +243,7 @@ namespace Timeline.Services if (user == null) { - var newUser = new User + var newUser = new UserEntity { Name = username, EncryptedPassword = _passwordService.HashPassword(password), diff --git a/Timeline/Services/UserTokenException.cs b/Timeline/Services/UserTokenException.cs new file mode 100644 index 00000000..e63305b1 --- /dev/null +++ b/Timeline/Services/UserTokenException.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Timeline.Services +{ + + [Serializable] + public class UserTokenException : Exception + { + public UserTokenException() { } + public UserTokenException(string message) : base(message) { } + public UserTokenException(string message, Exception inner) : base(message, inner) { } + public UserTokenException(string token, string message) : base(message) { Token = token; } + public UserTokenException(string token, string message, Exception inner) : base(message, inner) { Token = token; } + protected UserTokenException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + public string Token { get; private set; } = ""; + } + + + [Serializable] + public class UserTokenTimeExpireException : UserTokenException + { + public UserTokenTimeExpireException() : base(Resources.Services.Exception.UserTokenTimeExpireException) { } + public UserTokenTimeExpireException(string message) : base(message) { } + public UserTokenTimeExpireException(string message, Exception inner) : base(message, inner) { } + public UserTokenTimeExpireException(string token, DateTime expireTime, DateTime verifyTime) : base(token, Resources.Services.Exception.UserTokenTimeExpireException) { ExpireTime = expireTime; VerifyTime = verifyTime; } + public UserTokenTimeExpireException(string token, DateTime expireTime, DateTime verifyTime, Exception inner) : base(token, Resources.Services.Exception.UserTokenTimeExpireException, inner) { ExpireTime = expireTime; VerifyTime = verifyTime; } + protected UserTokenTimeExpireException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + public DateTime ExpireTime { get; private set; } = default; + + public DateTime VerifyTime { get; private set; } = default; + } + + [Serializable] + public class UserTokenBadVersionException : UserTokenException + { + public UserTokenBadVersionException() : base(Resources.Services.Exception.UserTokenBadVersionException) { } + public UserTokenBadVersionException(string message) : base(message) { } + public UserTokenBadVersionException(string message, Exception inner) : base(message, inner) { } + public UserTokenBadVersionException(string token, long tokenVersion, long requiredVersion) : base(token, Resources.Services.Exception.UserTokenBadVersionException) { TokenVersion = tokenVersion; RequiredVersion = requiredVersion; } + public UserTokenBadVersionException(string token, long tokenVersion, long requiredVersion, Exception inner) : base(token, Resources.Services.Exception.UserTokenBadVersionException, inner) { TokenVersion = tokenVersion; RequiredVersion = requiredVersion; } + protected UserTokenBadVersionException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + public long TokenVersion { get; set; } + + public long RequiredVersion { get; set; } + } + + [Serializable] + public class UserTokenBadFormatException : UserTokenException + { + public UserTokenBadFormatException() : base(Resources.Services.Exception.UserTokenBadFormatException) { } + public UserTokenBadFormatException(string token) : base(token, Resources.Services.Exception.UserTokenBadFormatException) { } + public UserTokenBadFormatException(string token, string message) : base(token, message) { } + public UserTokenBadFormatException(string token, Exception inner) : base(token, Resources.Services.Exception.UserTokenBadFormatException, inner) { } + public UserTokenBadFormatException(string token, string message, Exception inner) : base(token, message, inner) { } + protected UserTokenBadFormatException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + } +} diff --git a/Timeline/Services/UserTokenManager.cs b/Timeline/Services/UserTokenManager.cs new file mode 100644 index 00000000..c3cb51c9 --- /dev/null +++ b/Timeline/Services/UserTokenManager.cs @@ -0,0 +1,93 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Threading.Tasks; +using Timeline.Models; + +namespace Timeline.Services +{ + public class UserTokenCreateResult + { + public string Token { get; set; } = default!; + public UserInfo User { get; set; } = default!; + } + + public interface IUserTokenManager + { + /// + /// Try to create a token for given username and password. + /// + /// The username. + /// The password. + /// The expire time of the token. + /// The created token and the user info. + /// Thrown when or is null. + /// Thrown when is of bad format. + /// Thrown when the user with does not exist. + /// Thrown when is wrong. + public Task CreateToken(string username, string password, DateTime? expireAt = null); + + /// + /// Verify a token and get the saved user info. This also check the database for existence of the user. + /// + /// The token. + /// The user stored in token. + /// Thrown when is null. + /// Thrown when the token is expired. + /// Thrown when the token is of bad version. + /// Thrown when the token is of bad format. + /// Thrown when the user specified by the token does not exist. Usually the user had been deleted after the token was issued. + public Task VerifyToken(string token); + } + + public class UserTokenManager : IUserTokenManager + { + private readonly ILogger _logger; + private readonly IUserService _userService; + private readonly IUserTokenService _userTokenService; + private readonly IClock _clock; + + public UserTokenManager(ILogger logger, IUserService userService, IUserTokenService userTokenService, IClock clock) + { + _logger = logger; + _userService = userService; + _userTokenService = userTokenService; + _clock = clock; + } + + public async Task CreateToken(string username, string password, DateTime? expireAt = null) + { + if (username == null) + throw new ArgumentNullException(nameof(username)); + if (password == null) + throw new ArgumentNullException(nameof(password)); + + var user = await _userService.VerifyCredential(username, password); + var token = _userTokenService.GenerateToken(new UserTokenInfo { Id = user.Id, Version = user.Version, ExpireAt = expireAt }); + + return new UserTokenCreateResult { Token = token, User = user }; + } + + + public async Task VerifyToken(string token) + { + if (token == null) + throw new ArgumentNullException(nameof(token)); + + var tokenInfo = _userTokenService.VerifyToken(token); + + if (tokenInfo.ExpireAt.HasValue) + { + var currentTime = _clock.GetCurrentTime(); + if (tokenInfo.ExpireAt < currentTime) + throw new UserTokenTimeExpireException(token, tokenInfo.ExpireAt.Value, currentTime); + } + + var user = await _userService.GetUserById(tokenInfo.Id); + + if (tokenInfo.Version < user.Version) + throw new UserTokenBadVersionException(token, tokenInfo.Version, user.Version); + + return user; + } + } +} diff --git a/Timeline/Services/UserTokenService.cs b/Timeline/Services/UserTokenService.cs new file mode 100644 index 00000000..c246fdff --- /dev/null +++ b/Timeline/Services/UserTokenService.cs @@ -0,0 +1,147 @@ +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using System; +using System.Globalization; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using Timeline.Configs; + +namespace Timeline.Services +{ + public class UserTokenInfo + { + public long Id { get; set; } + public long Version { get; set; } + public DateTime? ExpireAt { get; set; } + } + + public interface IUserTokenService + { + /// + /// Create a token for a given token info. + /// + /// The info to generate token. + /// Return the generated token. + /// Thrown when is null. + string GenerateToken(UserTokenInfo tokenInfo); + + /// + /// Verify a token and get the saved info. + /// + /// The token to verify. + /// The saved info in token. + /// Thrown when is null. + /// Thrown when the token is of bad format. + /// + /// If this method throw , it usually means the token is not created by this service. + /// + UserTokenInfo VerifyToken(string token); + } + + public class JwtUserTokenService : IUserTokenService + { + private const string VersionClaimType = "timeline_version"; + + private readonly IOptionsMonitor _jwtConfig; + private readonly IClock _clock; + + private readonly JwtSecurityTokenHandler _tokenHandler = new JwtSecurityTokenHandler(); + private SymmetricSecurityKey _tokenSecurityKey; + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "")] + public JwtUserTokenService(IOptionsMonitor jwtConfig, IClock clock) + { + _jwtConfig = jwtConfig; + _clock = clock; + + _tokenSecurityKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwtConfig.CurrentValue.SigningKey)); + jwtConfig.OnChange(config => + { + _tokenSecurityKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(config.SigningKey)); + }); + } + + public string GenerateToken(UserTokenInfo tokenInfo) + { + if (tokenInfo == null) + throw new ArgumentNullException(nameof(tokenInfo)); + + var config = _jwtConfig.CurrentValue; + + var identity = new ClaimsIdentity(); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, tokenInfo.Id.ToString(CultureInfo.InvariantCulture.NumberFormat), ClaimValueTypes.Integer64)); + identity.AddClaim(new Claim(VersionClaimType, tokenInfo.Version.ToString(CultureInfo.InvariantCulture.NumberFormat), ClaimValueTypes.Integer64)); + + var tokenDescriptor = new SecurityTokenDescriptor() + { + Subject = identity, + Issuer = config.Issuer, + Audience = config.Audience, + SigningCredentials = new SigningCredentials( + new SymmetricSecurityKey(Encoding.ASCII.GetBytes(config.SigningKey)), SecurityAlgorithms.HmacSha384), + IssuedAt = _clock.GetCurrentTime(), + Expires = tokenInfo.ExpireAt.GetValueOrDefault(_clock.GetCurrentTime().AddSeconds(config.DefaultExpireOffset)), + NotBefore = _clock.GetCurrentTime() // I must explicitly set this or it will use the current time by default and mock is not work in which case test will not pass. + }; + + var token = _tokenHandler.CreateToken(tokenDescriptor); + var tokenString = _tokenHandler.WriteToken(token); + + return tokenString; + } + + + public UserTokenInfo VerifyToken(string token) + { + if (token == null) + throw new ArgumentNullException(nameof(token)); + + var config = _jwtConfig.CurrentValue; + try + { + var principal = _tokenHandler.ValidateToken(token, new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateIssuerSigningKey = true, + ValidateLifetime = false, + ValidIssuer = config.Issuer, + ValidAudience = config.Audience, + IssuerSigningKey = _tokenSecurityKey + }, out var t); + + var idClaim = principal.FindFirstValue(ClaimTypes.NameIdentifier); + if (idClaim == null) + throw new JwtUserTokenBadFormatException(token, JwtUserTokenBadFormatException.ErrorKind.NoIdClaim); + if (!long.TryParse(idClaim, out var id)) + throw new JwtUserTokenBadFormatException(token, JwtUserTokenBadFormatException.ErrorKind.IdClaimBadFormat); + + var versionClaim = principal.FindFirstValue(VersionClaimType); + if (versionClaim == null) + throw new JwtUserTokenBadFormatException(token, JwtUserTokenBadFormatException.ErrorKind.NoVersionClaim); + if (!long.TryParse(versionClaim, out var version)) + throw new JwtUserTokenBadFormatException(token, JwtUserTokenBadFormatException.ErrorKind.VersionClaimBadFormat); + + var decodedToken = (JwtSecurityToken)t; + var exp = decodedToken.Payload.Exp; + DateTime? expireAt = null; + if (exp.HasValue) + { + expireAt = EpochTime.DateTime(exp.Value); + } + + return new UserTokenInfo + { + Id = id, + Version = version, + ExpireAt = expireAt + }; + } + catch (Exception e) when (e is SecurityTokenException || e is ArgumentException) + { + throw new JwtUserTokenBadFormatException(token, JwtUserTokenBadFormatException.ErrorKind.Other, e); + } + } + } +} diff --git a/Timeline/Services/UsernameBadFormatException.cs b/Timeline/Services/UsernameBadFormatException.cs index d82bf962..991be7df 100644 --- a/Timeline/Services/UsernameBadFormatException.cs +++ b/Timeline/Services/UsernameBadFormatException.cs @@ -22,6 +22,6 @@ namespace Timeline.Services /// /// Username of bad format. /// - public string? Username { get; private set; } + public string Username { get; private set; } = ""; } } diff --git a/Timeline/Startup.cs b/Timeline/Startup.cs index 5b6499a4..c1e73182 100644 --- a/Timeline/Startup.cs +++ b/Timeline/Startup.cs @@ -84,7 +84,7 @@ namespace Timeline }); services.AddScoped(); - services.AddScoped(); + services.AddScoped(); services.AddTransient(); services.AddTransient(); services.AddUserAvatarService(); -- cgit v1.2.3 From c72996ff9854aededc4ecfab92ef5d42167edd86 Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 23 Jan 2020 20:51:02 +0800 Subject: ... --- Timeline.Tests/Controllers/TokenControllerTest.cs | 35 +++++++++--- Timeline.Tests/Controllers/UserControllerTest.cs | 21 +++++-- Timeline.Tests/Helpers/MockUser.cs | 10 ++-- Timeline/Auth/MyAuthenticationHandler.cs | 10 ++-- Timeline/Controllers/TokenController.cs | 60 ++++++++++---------- Timeline/Controllers/UserController.cs | 11 ++-- Timeline/Entities/UserAvatarEntity.cs | 4 ++ Timeline/Entities/UserDetailEntity.cs | 4 ++ Timeline/GlobalSuppressions.cs | 6 +- Timeline/Services/JwtBadVersionException.cs | 36 ------------ Timeline/Services/UserService.cs | 67 +++++++++++------------ Timeline/Services/UsernameBadFormatException.cs | 11 ++-- Timeline/Startup.cs | 3 +- 13 files changed, 142 insertions(+), 136 deletions(-) delete mode 100644 Timeline/Services/JwtBadVersionException.cs diff --git a/Timeline.Tests/Controllers/TokenControllerTest.cs b/Timeline.Tests/Controllers/TokenControllerTest.cs index 740d8377..61fbe950 100644 --- a/Timeline.Tests/Controllers/TokenControllerTest.cs +++ b/Timeline.Tests/Controllers/TokenControllerTest.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using Timeline.Controllers; +using Timeline.Models; using Timeline.Models.Http; using Timeline.Services; using Timeline.Tests.Helpers; @@ -15,7 +16,7 @@ namespace Timeline.Tests.Controllers { public class TokenControllerTest : IDisposable { - private readonly Mock _mockUserService = new Mock(); + private readonly Mock _mockUserService = new Mock(); private readonly TestClock _mockClock = new TestClock(); @@ -38,12 +39,18 @@ namespace Timeline.Tests.Controllers { var mockCurrentTime = DateTime.Now; _mockClock.MockCurrentTime = mockCurrentTime; - var createResult = new CreateTokenResult + var mockCreateResult = new UserTokenCreateResult { Token = "mocktokenaaaaa", - User = MockUser.User.Info + User = new UserInfo + { + Id = 1, + Username = MockUser.User.Username, + Administrator = MockUser.User.Administrator, + Version = 1 + } }; - _mockUserService.Setup(s => s.CreateToken("u", "p", expire == null ? null : (DateTime?)mockCurrentTime.AddDays(expire.Value))).ReturnsAsync(createResult); + _mockUserService.Setup(s => s.CreateToken("u", "p", expire == null ? null : (DateTime?)mockCurrentTime.AddDays(expire.Value))).ReturnsAsync(mockCreateResult); var action = await _controller.Create(new CreateTokenRequest { Username = "u", @@ -51,7 +58,11 @@ namespace Timeline.Tests.Controllers Expire = expire }); action.Result.Should().BeAssignableTo() - .Which.Value.Should().BeEquivalentTo(createResult); + .Which.Value.Should().BeEquivalentTo(new CreateTokenResponse + { + Token = mockCreateResult.Token, + User = MockUser.User.Info + }); } [Fact] @@ -88,7 +99,13 @@ namespace Timeline.Tests.Controllers public async Task Verify_Ok() { const string token = "aaaaaaaaaaaaaa"; - _mockUserService.Setup(s => s.VerifyToken(token)).ReturnsAsync(MockUser.User.Info); + _mockUserService.Setup(s => s.VerifyToken(token)).ReturnsAsync(new UserInfo + { + Id = 1, + Username = MockUser.User.Username, + Administrator = MockUser.User.Administrator, + Version = 1 + }); var action = await _controller.Verify(new VerifyTokenRequest { Token = token }); action.Result.Should().BeAssignableTo() .Which.Value.Should().BeAssignableTo() @@ -97,9 +114,9 @@ namespace Timeline.Tests.Controllers public static IEnumerable Verify_BadRequest_Data() { - yield return new object[] { new JwtUserTokenBadFormatException(JwtUserTokenBadFormatException.ErrorCodes.Expired), ErrorCodes.TokenController.Verify_TimeExpired }; - yield return new object[] { new JwtUserTokenBadFormatException(JwtUserTokenBadFormatException.ErrorCodes.IdClaimBadFormat), ErrorCodes.TokenController.Verify_BadFormat }; - yield return new object[] { new JwtUserTokenBadFormatException(JwtUserTokenBadFormatException.ErrorCodes.OldVersion), ErrorCodes.TokenController.Verify_OldVersion }; + yield return new object[] { new UserTokenTimeExpireException(), ErrorCodes.TokenController.Verify_TimeExpired }; + yield return new object[] { new UserTokenBadVersionException(), ErrorCodes.TokenController.Verify_OldVersion }; + yield return new object[] { new UserTokenBadFormatException(), ErrorCodes.TokenController.Verify_BadFormat }; yield return new object[] { new UserNotExistException(), ErrorCodes.TokenController.Verify_UserNotExist }; } diff --git a/Timeline.Tests/Controllers/UserControllerTest.cs b/Timeline.Tests/Controllers/UserControllerTest.cs index 262dbe11..a1035675 100644 --- a/Timeline.Tests/Controllers/UserControllerTest.cs +++ b/Timeline.Tests/Controllers/UserControllerTest.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging.Abstractions; using Moq; using System; +using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; @@ -35,18 +36,28 @@ namespace Timeline.Tests.Controllers [Fact] public async Task GetList_Success() { - var array = MockUser.UserInfoList.ToArray(); - _mockUserService.Setup(s => s.ListUsers()).ReturnsAsync(array); + var mockUserList = new UserInfo[] { + new UserInfo { Id = 1, Username = "aaa", Administrator = true, Version = 1 }, + new UserInfo { Id = 2, Username = "bbb", Administrator = false, Version = 1 } + }; + _mockUserService.Setup(s => s.ListUsers()).ReturnsAsync(mockUserList); var action = await _controller.List(); action.Result.Should().BeAssignableTo() - .Which.Value.Should().BeEquivalentTo(array); + .Which.Value.Should().BeEquivalentTo( + mockUserList.Select(u => new User { Username = u.Username, Administrator = u.Administrator }).ToArray()); } [Fact] public async Task Get_Success() { const string username = "aaa"; - _mockUserService.Setup(s => s.GetUserByUsername(username)).ReturnsAsync(MockUser.User.Info); + _mockUserService.Setup(s => s.GetUserByUsername(username)).ReturnsAsync(new UserInfo + { + Id = 1, + Username = MockUser.User.Username, + Administrator = MockUser.User.Administrator, + Version = 1 + }); var action = await _controller.Get(username); action.Result.Should().BeAssignableTo() .Which.Value.Should().BeEquivalentTo(MockUser.User.Info); @@ -56,7 +67,7 @@ namespace Timeline.Tests.Controllers public async Task Get_NotFound() { const string username = "aaa"; - _mockUserService.Setup(s => s.GetUserByUsername(username)).Returns(Task.FromResult(null)); + _mockUserService.Setup(s => s.GetUserByUsername(username)).ThrowsAsync(new UserNotExistException()); var action = await _controller.Get(username); action.Result.Should().BeAssignableTo() .Which.Value.Should().BeAssignableTo() diff --git a/Timeline.Tests/Helpers/MockUser.cs b/Timeline.Tests/Helpers/MockUser.cs index 8d738525..49576842 100644 --- a/Timeline.Tests/Helpers/MockUser.cs +++ b/Timeline.Tests/Helpers/MockUser.cs @@ -1,17 +1,17 @@ using System.Collections.Generic; -using Timeline.Models; - +using Timeline.Models.Http; + namespace Timeline.Tests.Helpers { public class MockUser { public MockUser(string username, string password, bool administrator) { - Info = new UserInfo(username, administrator); + Info = new User { Username = username, Administrator = administrator }; Password = password; } - public UserInfo Info { get; set; } + public User Info { get; set; } public string Username => Info.Username; public string Password { get; set; } public bool Administrator => Info.Administrator; @@ -19,6 +19,6 @@ namespace Timeline.Tests.Helpers 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 }; + public static IReadOnlyList UserInfoList { get; } = new List { User.Info, Admin.Info }; } } diff --git a/Timeline/Auth/MyAuthenticationHandler.cs b/Timeline/Auth/MyAuthenticationHandler.cs index f5dcd697..5bae5117 100644 --- a/Timeline/Auth/MyAuthenticationHandler.cs +++ b/Timeline/Auth/MyAuthenticationHandler.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; using System; +using System.Globalization; using System.Linq; using System.Security.Claims; using System.Text.Encodings.Web; @@ -30,13 +31,13 @@ namespace Timeline.Auth public class MyAuthenticationHandler : AuthenticationHandler { private readonly ILogger _logger; - private readonly IUserService _userService; + private readonly IUserTokenManager _userTokenManager; - public MyAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IUserService userService) + public MyAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IUserTokenManager userTokenManager) : base(options, logger, encoder, clock) { _logger = logger.CreateLogger(); - _userService = userService; + _userTokenManager = userTokenManager; } // return null if no token is found @@ -78,9 +79,10 @@ namespace Timeline.Auth try { - var userInfo = await _userService.VerifyToken(token); + var userInfo = await _userTokenManager.VerifyToken(token); var identity = new ClaimsIdentity(AuthenticationConstants.Scheme); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userInfo.Id.ToString(CultureInfo.InvariantCulture), ClaimValueTypes.Integer64)); identity.AddClaim(new Claim(identity.NameClaimType, userInfo.Username, ClaimValueTypes.String)); identity.AddClaims(UserRoleConvert.ToArray(userInfo.Administrator).Select(role => new Claim(identity.RoleClaimType, role, ClaimValueTypes.String))); diff --git a/Timeline/Controllers/TokenController.cs b/Timeline/Controllers/TokenController.cs index 851c7606..a96b6fa9 100644 --- a/Timeline/Controllers/TokenController.cs +++ b/Timeline/Controllers/TokenController.cs @@ -1,11 +1,11 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -using Microsoft.IdentityModel.Tokens; using System; using System.Globalization; using System.Threading.Tasks; using Timeline.Helpers; +using Timeline.Models; using Timeline.Models.Http; using Timeline.Services; using static Timeline.Resources.Controllers.TokenController; @@ -16,13 +16,22 @@ namespace Timeline.Controllers [ApiController] public class TokenController : Controller { - private readonly IUserService _userService; + private readonly IUserTokenManager _userTokenManager; private readonly ILogger _logger; private readonly IClock _clock; - public TokenController(IUserService userService, ILogger logger, IClock clock) + private static User CreateUserFromUserInfo(UserInfo userInfo) { - _userService = userService; + return new User + { + Username = userInfo.Username, + Administrator = userInfo.Administrator + }; + } + + public TokenController(IUserTokenManager userTokenManager, ILogger logger, IClock clock) + { + _userTokenManager = userTokenManager; _logger = logger; _clock = clock; } @@ -47,7 +56,7 @@ namespace Timeline.Controllers if (request.Expire != null) expireTime = _clock.GetCurrentTime().AddDays(request.Expire.Value); - var result = await _userService.CreateToken(request.Username, request.Password, expireTime); + var result = await _userTokenManager.CreateToken(request.Username, request.Password, expireTime); _logger.LogInformation(Log.Format(LogCreateSuccess, ("Username", request.Username), @@ -56,7 +65,7 @@ namespace Timeline.Controllers return Ok(new CreateTokenResponse { Token = result.Token, - User = result.User + User = CreateUserFromUserInfo(result.User) }); } catch (UserNotExistException e) @@ -86,36 +95,29 @@ namespace Timeline.Controllers try { - var result = await _userService.VerifyToken(request.Token); + var result = await _userTokenManager.VerifyToken(request.Token); _logger.LogInformation(Log.Format(LogVerifySuccess, ("Username", result.Username), ("Token", request.Token))); return Ok(new VerifyTokenResponse { - User = result + User = CreateUserFromUserInfo(result) }); } - catch (JwtUserTokenBadFormatException e) + catch (UserTokenTimeExpireException e) { - if (e.ErrorCode == JwtUserTokenBadFormatException.ErrorCodes.Expired) - { - var innerException = e.InnerException as SecurityTokenExpiredException; - LogFailure(LogVerifyExpire, e, ("Expires", innerException?.Expires), - ("Current Time", _clock.GetCurrentTime())); - return BadRequest(ErrorResponse.TokenController.Verify_TimeExpired()); - } - else if (e.ErrorCode == JwtUserTokenBadFormatException.ErrorCodes.OldVersion) - { - var innerException = e.InnerException as JwtBadVersionException; - LogFailure(LogVerifyOldVersion, e, - ("Token Version", innerException?.TokenVersion), - ("Required Version", innerException?.RequiredVersion)); - return BadRequest(ErrorResponse.TokenController.Verify_OldVersion()); - } - else - { - LogFailure(LogVerifyBadFormat, e); - return BadRequest(ErrorResponse.TokenController.Verify_BadFormat()); - } + LogFailure(LogVerifyExpire, e, ("Expire Time", e.ExpireTime), ("Verify Time", e.VerifyTime)); + return BadRequest(ErrorResponse.TokenController.Verify_TimeExpired()); + } + catch (UserTokenBadVersionException e) + { + LogFailure(LogVerifyOldVersion, e, ("Token Version", e.TokenVersion), ("Required Version", e.RequiredVersion)); + return BadRequest(ErrorResponse.TokenController.Verify_OldVersion()); + + } + catch (UserTokenBadFormatException e) + { + LogFailure(LogVerifyBadFormat, e); + return BadRequest(ErrorResponse.TokenController.Verify_BadFormat()); } catch (UserNotExistException e) { diff --git a/Timeline/Controllers/UserController.cs b/Timeline/Controllers/UserController.cs index 65ee3a0f..5f1b7bd7 100644 --- a/Timeline/Controllers/UserController.cs +++ b/Timeline/Controllers/UserController.cs @@ -35,13 +35,16 @@ namespace Timeline.Controllers [HttpGet("users/{username}"), AdminAuthorize] public async Task> Get([FromRoute][Username] string username) { - var user = await _userService.GetUserByUsername(username); - if (user == null) + try + { + var user = await _userService.GetUserByUsername(username); + return Ok(user); + } + catch (UserNotExistException e) { - _logger.LogInformation(Log.Format(LogGetUserNotExist, ("Username", username))); + _logger.LogInformation(e, Log.Format(LogGetUserNotExist, ("Username", username))); return NotFound(ErrorResponse.UserCommon.NotExist()); } - return Ok(user); } [HttpPut("users/{username}"), AdminAuthorize] diff --git a/Timeline/Entities/UserAvatarEntity.cs b/Timeline/Entities/UserAvatarEntity.cs index eed819bc..6cecce1a 100644 --- a/Timeline/Entities/UserAvatarEntity.cs +++ b/Timeline/Entities/UserAvatarEntity.cs @@ -23,6 +23,10 @@ namespace Timeline.Entities [Column("last_modified"), Required] public DateTime LastModified { get; set; } + [Column("user"), Required] public long UserId { get; set; } + + [ForeignKey(nameof(UserId))] + public UserEntity User { get; set; } = default!; } } diff --git a/Timeline/Entities/UserDetailEntity.cs b/Timeline/Entities/UserDetailEntity.cs index 7a525294..1d9957f9 100644 --- a/Timeline/Entities/UserDetailEntity.cs +++ b/Timeline/Entities/UserDetailEntity.cs @@ -12,6 +12,10 @@ namespace Timeline.Entities [Column("nickname"), MaxLength(26)] public string? Nickname { get; set; } + [Column("user")] public long UserId { get; set; } + + [ForeignKey(nameof(UserId))] + public UserEntity User { get; set; } = default!; } } diff --git a/Timeline/GlobalSuppressions.cs b/Timeline/GlobalSuppressions.cs index 076c2005..c0754071 100644 --- a/Timeline/GlobalSuppressions.cs +++ b/Timeline/GlobalSuppressions.cs @@ -7,6 +7,6 @@ [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1034:Nested types should not be visible", Justification = "This is not bad.")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "No need to check the null because it's ASP.Net's duty.", Scope = "namespaceanddescendants", Target = "Timeline.Controllers")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "Migrations code are auto generated.", Scope = "namespaceanddescendants", Target = "Timeline.Migrations")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Error code constant identifiers.", Scope = "type", Target = "Timeline.ErrorCodes")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1716:Identifiers should not match keywords", Justification = "Error code constant identifiers.", Scope = "type", Target = "Timeline.ErrorCodes")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1724:Type names should not match namespaces", Justification = "Error code constant identifiers.", Scope = "type", Target = "Timeline.ErrorCodes")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Generated error response identifiers.", Scope = "type", Target = "Timeline.Models.Http.ErrorResponse")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1724:Type names should not match namespaces", Justification = "Generated error response identifiers.", Scope = "type", Target = "Timeline.Models.Http.ErrorResponse")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "Generated error response.", Scope = "type", Target = "Timeline.Models.Http.ErrorResponse")] diff --git a/Timeline/Services/JwtBadVersionException.cs b/Timeline/Services/JwtBadVersionException.cs deleted file mode 100644 index 4ce17710..00000000 --- a/Timeline/Services/JwtBadVersionException.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using Timeline.Helpers; - -namespace Timeline.Services -{ - [Serializable] - public class JwtBadVersionException : Exception - { - public JwtBadVersionException() : base(Resources.Services.Exception.JwtBadVersionException) { } - public JwtBadVersionException(string message) : base(message) { } - public JwtBadVersionException(string message, Exception inner) : base(message, inner) { } - - public JwtBadVersionException(long tokenVersion, long requiredVersion) - : base(Log.Format(Resources.Services.Exception.JwtBadVersionException, - ("Token Version", tokenVersion), - ("Required Version", requiredVersion))) - { - TokenVersion = tokenVersion; - RequiredVersion = requiredVersion; - } - - protected JwtBadVersionException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - /// - /// The version in the token. - /// - public long? TokenVersion { get; set; } - - /// - /// The version required. - /// - public long? RequiredVersion { get; set; } - } -} diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs index db2350a2..104db1b0 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -139,19 +139,30 @@ namespace Timeline.Services _logger.LogInformation(Log.Format(Resources.Services.UserService.LogCacheRemove, ("Key", key))); } - private void CheckUsernameFormat(string username, string? additionalMessage = null) + private void CheckUsernameFormat(string username, string? message = null) { - var (result, message) = _usernameValidator.Validate(username); + var (result, validationMessage) = _usernameValidator.Validate(username); if (!result) { - if (additionalMessage == null) - throw new UsernameBadFormatException(username, message); + if (message == null) + throw new UsernameBadFormatException(username, validationMessage); else - throw new UsernameBadFormatException(username, additionalMessage + message); + throw new UsernameBadFormatException(username, validationMessage, message); } } - public async Task CheckCredential(string username, string password) + private static UserInfo CreateUserInfoFromEntity(UserEntity user) + { + return new UserInfo + { + Id = user.Id, + Username = user.Name, + Administrator = UserRoleConvert.ToBool(user.RoleString), + Version = user.Version + }; + } + + public async Task VerifyCredential(string username, string password) { if (username == null) throw new ArgumentNullException(nameof(username)); @@ -169,30 +180,13 @@ namespace Timeline.Services if (!_passwordService.VerifyPassword(user.EncryptedPassword, password)) throw new BadPasswordException(password); - var token = _jwtService.GenerateJwtToken(new TokenInfo - { - Id = user.Id, - Version = user.Version - }, expires); - - return new CreateTokenResult - { - Token = token, - User = UserConvert.CreateUserInfo(user) - }; + return CreateUserInfoFromEntity(user); } - public async Task VerifyToken(string token) + public async Task GetUserById(long id) { - if (token == null) - throw new ArgumentNullException(nameof(token)); - - TokenInfo tokenInfo; - tokenInfo = _jwtService.VerifyJwtToken(token); - - var id = tokenInfo.Id; var key = GenerateCacheKeyByUserId(id); - if (!_memoryCache.TryGetValue(key, out var cache)) + if (!_memoryCache.TryGetValue(key, out var cache)) { // no cache, check the database var user = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync(); @@ -201,34 +195,35 @@ namespace Timeline.Services throw new UserNotExistException(id); // create cache - cache = UserConvert.CreateUserCache(user); + cache = CreateUserInfoFromEntity(user); _memoryCache.CreateEntry(key).SetValue(cache); _logger.LogInformation(Log.Format(Resources.Services.UserService.LogCacheCreate, ("Key", key))); } - if (tokenInfo.Version != cache.Version) - throw new JwtUserTokenBadFormatException(new JwtBadVersionException(tokenInfo.Version, cache.Version), JwtUserTokenBadFormatException.ErrorCodes.OldVersion); - - return cache.ToUserInfo(); + return cache; } public async Task GetUserByUsername(string username) { if (username == null) throw new ArgumentNullException(nameof(username)); + CheckUsernameFormat(username); - return await _databaseContext.Users + var entity = await _databaseContext.Users .Where(user => user.Name == username) - .Select(user => UserConvert.CreateUserInfo(user)) .SingleOrDefaultAsync(); + + if (entity == null) + throw new UserNotExistException(username); + + return CreateUserInfoFromEntity(entity); } public async Task ListUsers() { - return await _databaseContext.Users - .Select(user => UserConvert.CreateUserInfo(user)) - .ToArrayAsync(); + var entities = await _databaseContext.Users.ToArrayAsync(); + return entities.Select(user => CreateUserInfoFromEntity(user)).ToArray(); } public async Task PutUser(string username, string password, bool administrator) diff --git a/Timeline/Services/UsernameBadFormatException.cs b/Timeline/Services/UsernameBadFormatException.cs index 991be7df..ad0350b5 100644 --- a/Timeline/Services/UsernameBadFormatException.cs +++ b/Timeline/Services/UsernameBadFormatException.cs @@ -9,11 +9,12 @@ namespace Timeline.Services public class UsernameBadFormatException : Exception { public UsernameBadFormatException() : base(Resources.Services.Exception.UsernameBadFormatException) { } - public UsernameBadFormatException(string username) : this() { Username = username; } - public UsernameBadFormatException(string username, Exception inner) : base(Resources.Services.Exception.UsernameBadFormatException, inner) { Username = username; } + public UsernameBadFormatException(string message) : base(message) { } + public UsernameBadFormatException(string message, Exception inner) : base(message, inner) { } - public UsernameBadFormatException(string username, string message) : base(message) { Username = username; } - public UsernameBadFormatException(string username, string message, Exception inner) : base(message, inner) { Username = username; } + public UsernameBadFormatException(string username, string validationMessage) : this() { Username = username; ValidationMessage = validationMessage; } + + public UsernameBadFormatException(string username, string validationMessage, string message) : this(message) { Username = username; ValidationMessage = validationMessage; } protected UsernameBadFormatException( System.Runtime.Serialization.SerializationInfo info, @@ -23,5 +24,7 @@ namespace Timeline.Services /// Username of bad format. /// public string Username { get; private set; } = ""; + + public string ValidationMessage { get; private set; } = ""; } } diff --git a/Timeline/Startup.cs b/Timeline/Startup.cs index c1e73182..379ce6ea 100644 --- a/Timeline/Startup.cs +++ b/Timeline/Startup.cs @@ -84,7 +84,8 @@ namespace Timeline }); services.AddScoped(); - services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddTransient(); services.AddTransient(); services.AddUserAvatarService(); -- cgit v1.2.3 From 6829ad0903aa3310ad29b74fc435611442569d5c Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 23 Jan 2020 21:21:32 +0800 Subject: Add some unit tests for token manager. --- Timeline.Tests/Helpers/ParameterInfoAssertions.cs | 3 -- Timeline.Tests/Services/UserTokenManagerTest.cs | 52 +++++++++++++++++++++++ 2 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 Timeline.Tests/Services/UserTokenManagerTest.cs diff --git a/Timeline.Tests/Helpers/ParameterInfoAssertions.cs b/Timeline.Tests/Helpers/ParameterInfoAssertions.cs index e3becee1..d3e5a41e 100644 --- a/Timeline.Tests/Helpers/ParameterInfoAssertions.cs +++ b/Timeline.Tests/Helpers/ParameterInfoAssertions.cs @@ -3,10 +3,7 @@ using FluentAssertions.Execution; using FluentAssertions.Formatting; using FluentAssertions.Primitives; using System; -using System.Collections.Generic; -using System.Linq; using System.Reflection; -using System.Threading.Tasks; namespace Timeline.Tests.Helpers { diff --git a/Timeline.Tests/Services/UserTokenManagerTest.cs b/Timeline.Tests/Services/UserTokenManagerTest.cs new file mode 100644 index 00000000..86ad84b3 --- /dev/null +++ b/Timeline.Tests/Services/UserTokenManagerTest.cs @@ -0,0 +1,52 @@ +using FluentAssertions; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Services; +using Timeline.Tests.Helpers; +using Xunit; + +namespace Timeline.Tests.Services +{ + public class UserTokenManagerTest + { + private readonly UserTokenManager _service; + + private readonly Mock _mockUserService; + private readonly Mock _mockUserTokenService; + private readonly TestClock _mockClock; + + public UserTokenManagerTest() + { + _mockUserService = new Mock(); + _mockUserTokenService = new Mock(); + _mockClock = new TestClock(); + + _service = new UserTokenManager(NullLogger.Instance, _mockUserService.Object, _mockUserTokenService.Object, _mockClock); + } + + [Theory] + [InlineData(null, "aaa", "username")] + [InlineData("aaa", null, "password")] + public void CreateToken_NullArgument(string username, string password, string paramName) + { + _service.Invoking(s => s.CreateToken(username, password)).Should().Throw() + .Which.ParamName.Should().Be(paramName); + } + + [Theory] + [InlineData(typeof(UsernameBadFormatException))] + [InlineData(typeof(UserNotExistException))] + [InlineData(typeof(BadPasswordException))] + public async Task CreateToken_VerifyCredential_Throw(Type exceptionType) + { + const string username = "uuu"; + const string password = "ppp"; + _mockUserService.Setup(s => s.VerifyCredential(username, password)).ThrowsAsync((Exception)Activator.CreateInstance(exceptionType)); + await _service.Awaiting(s => s.CreateToken(username, password)).Should().ThrowAsync(exceptionType); + } + } +} -- cgit v1.2.3 From 21588701f25860145847c0740299867d9285d872 Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 24 Jan 2020 19:49:57 +0800 Subject: Upgrade library version. --- Timeline.Tests/Services/UserTokenManagerTest.cs | 32 +++++++++++++++++++++++++ Timeline.Tests/Timeline.Tests.csproj | 8 +++---- Timeline/Timeline.csproj | 8 +++---- 3 files changed, 40 insertions(+), 8 deletions(-) diff --git a/Timeline.Tests/Services/UserTokenManagerTest.cs b/Timeline.Tests/Services/UserTokenManagerTest.cs index 86ad84b3..a8ace778 100644 --- a/Timeline.Tests/Services/UserTokenManagerTest.cs +++ b/Timeline.Tests/Services/UserTokenManagerTest.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Timeline.Models; using Timeline.Services; using Timeline.Tests.Helpers; using Xunit; @@ -48,5 +49,36 @@ namespace Timeline.Tests.Services _mockUserService.Setup(s => s.VerifyCredential(username, password)).ThrowsAsync((Exception)Activator.CreateInstance(exceptionType)); await _service.Awaiting(s => s.CreateToken(username, password)).Should().ThrowAsync(exceptionType); } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task CreateToken_Success(bool setExpireTime) + { + const string username = "uuu"; + const string password = "ppp"; + var mockExpireTime = setExpireTime ? (DateTime?)DateTime.Now : null; + var mockUserInfo = new UserInfo + { + Id = 1, + Username = username, + Administrator = false, + Version = 1 + }; + const string mockToken = "mocktokenaaaaaaa"; + + _mockUserService.Setup(s => s.VerifyCredential(username, password)).ReturnsAsync(mockUserInfo); + _mockUserTokenService.Setup(s => s.GenerateToken( + It.Is(userTokenInfo => + userTokenInfo.Id == mockUserInfo.Id && + userTokenInfo.Version == mockUserInfo.Version && + userTokenInfo.ExpireAt == mockExpireTime))).Returns(mockToken); + (await _service.CreateToken(username, password, mockExpireTime)) + .Should().BeEquivalentTo(new UserTokenCreateResult + { + Token = mockToken, + User = mockUserInfo + }); + } } } diff --git a/Timeline.Tests/Timeline.Tests.csproj b/Timeline.Tests/Timeline.Tests.csproj index 0845d1ab..bde4f430 100644 --- a/Timeline.Tests/Timeline.Tests.csproj +++ b/Timeline.Tests/Timeline.Tests.csproj @@ -7,18 +7,18 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Timeline/Timeline.csproj b/Timeline/Timeline.csproj index 27897177..90588f70 100644 --- a/Timeline/Timeline.csproj +++ b/Timeline/Timeline.csproj @@ -19,10 +19,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive -- cgit v1.2.3 From 1a653fca9e4e3371dd65782c987a736e2259d66a Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 24 Jan 2020 23:15:44 +0800 Subject: Finish UserTokenManagerTest. --- Timeline.Tests/Services/UserTokenManagerTest.cs | 81 +++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/Timeline.Tests/Services/UserTokenManagerTest.cs b/Timeline.Tests/Services/UserTokenManagerTest.cs index a8ace778..19122d31 100644 --- a/Timeline.Tests/Services/UserTokenManagerTest.cs +++ b/Timeline.Tests/Services/UserTokenManagerTest.cs @@ -80,5 +80,86 @@ namespace Timeline.Tests.Services User = mockUserInfo }); } + + [Fact] + public void VerifyToken_NullArgument() + { + _service.Invoking(s => s.VerifyToken(null)).Should().ThrowAsync(); + } + + [Fact] + public async Task VerifyToken_BadFormat() + { + const string mockToken = "mocktokenaaaaaa"; + _mockUserTokenService.Setup(s => s.VerifyToken(mockToken)).Throws(new UserTokenBadFormatException()); + + await _service.Awaiting(s => s.VerifyToken(mockToken)).Should().ThrowAsync(); + } + + [Fact] + public async Task VerifyToken_TimeExpire() + { + const string mockToken = "mocktokenaaaaaa"; + var mockTime = DateTime.Now; + _mockClock.MockCurrentTime = mockTime; + var mockTokenInfo = new UserTokenInfo + { + Id = 1, + Version = 1, + ExpireAt = mockTime.AddDays(-1) + }; + _mockUserTokenService.Setup(s => s.VerifyToken(mockToken)).Returns(mockTokenInfo); + + await _service.Awaiting(s => s.VerifyToken(mockToken)).Should().ThrowAsync(); + } + + [Fact] + public async Task VerifyToken_BadVersion() + { + const string mockToken = "mocktokenaaaaaa"; + var mockTime = DateTime.Now; + _mockClock.MockCurrentTime = mockTime; + var mockTokenInfo = new UserTokenInfo + { + Id = 1, + Version = 1, + ExpireAt = mockTime.AddDays(1) + }; + _mockUserTokenService.Setup(s => s.VerifyToken(mockToken)).Returns(mockTokenInfo); + _mockUserService.Setup(s => s.GetUserById(1)).ReturnsAsync(new UserInfo + { + Id = 1, + Username = "aaa", + Administrator = false, + Version = 2 + }); + + await _service.Awaiting(s => s.VerifyToken(mockToken)).Should().ThrowAsync(); + } + + [Fact] + public async Task VerifyToken_Success() + { + const string mockToken = "mocktokenaaaaaa"; + var mockTime = DateTime.Now; + _mockClock.MockCurrentTime = mockTime; + var mockTokenInfo = new UserTokenInfo + { + Id = 1, + Version = 1, + ExpireAt = mockTime.AddDays(1) + }; + var mockUserInfo = new UserInfo + { + Id = 1, + Username = "aaa", + Administrator = false, + Version = 1 + }; + _mockUserTokenService.Setup(s => s.VerifyToken(mockToken)).Returns(mockTokenInfo); + _mockUserService.Setup(s => s.GetUserById(1)).ReturnsAsync(mockUserInfo); + + (await _service.VerifyToken(mockToken)).Should().BeEquivalentTo(mockUserInfo); + } } } -- cgit v1.2.3 From b6043126fae039c58512f60a576b10925b06df4c Mon Sep 17 00:00:00 2001 From: crupest Date: Wed, 29 Jan 2020 00:17:45 +0800 Subject: ... --- Timeline.Tests/Controllers/TokenControllerTest.cs | 4 +- Timeline.Tests/Controllers/UserControllerTest.cs | 8 +- Timeline.Tests/Helpers/TestDatabase.cs | 6 +- Timeline.Tests/Services/UserAvatarServiceTest.cs | 6 +- Timeline.Tests/Services/UserDetailServiceTest.cs | 4 +- Timeline.Tests/Services/UserTokenManagerTest.cs | 8 +- Timeline/Controllers/TokenController.cs | 4 +- Timeline/Controllers/UserDetailController.cs | 49 ---- Timeline/Entities/DatabaseContext.cs | 2 +- Timeline/Entities/UserDetailEntity.cs | 21 -- Timeline/Entities/UserEntity.cs | 13 +- Timeline/GlobalSuppressions.cs | 1 + Timeline/Models/Http/User.cs | 9 +- Timeline/Models/User.cs | 19 ++ Timeline/Models/UserInfo.cs | 10 - Timeline/Models/Validation/Validator.cs | 28 ++- .../Validation/PasswordValidator.Designer.cs | 72 ++++++ .../Models/Validation/PasswordValidator.resx | 123 ++++++++++ .../Models/Validation/PasswordValidator.zh.resx | 123 ++++++++++ Timeline/Resources/Services/Exception.Designer.cs | 18 +- Timeline/Resources/Services/Exception.resx | 6 +- Timeline/Resources/Services/UserCache.Designer.cs | 99 ++++++++ Timeline/Resources/Services/UserCache.resx | 132 ++++++++++ .../Resources/Services/UserManager.Designer.cs | 72 ++++++ Timeline/Resources/Services/UserManager.resx | 123 ++++++++++ .../Resources/Services/UserService.Designer.cs | 36 +++ Timeline/Resources/Services/UserService.resx | 12 + Timeline/Services/DatabaseExtensions.cs | 2 +- Timeline/Services/PasswordBadFormatException.cs | 27 ++ Timeline/Services/TimelineService.cs | 16 +- Timeline/Services/UserDetailService.cs | 102 -------- Timeline/Services/UserNotExistException.cs | 4 +- Timeline/Services/UserService.cs | 271 +++++++++++---------- Timeline/Services/UserTokenManager.cs | 6 +- Timeline/Services/UsernameBadFormatException.cs | 30 --- Timeline/Timeline.csproj | 27 ++ 36 files changed, 1086 insertions(+), 407 deletions(-) delete mode 100644 Timeline/Controllers/UserDetailController.cs delete mode 100644 Timeline/Entities/UserDetailEntity.cs create mode 100644 Timeline/Models/User.cs delete mode 100644 Timeline/Models/UserInfo.cs create mode 100644 Timeline/Resources/Models/Validation/PasswordValidator.Designer.cs create mode 100644 Timeline/Resources/Models/Validation/PasswordValidator.resx create mode 100644 Timeline/Resources/Models/Validation/PasswordValidator.zh.resx create mode 100644 Timeline/Resources/Services/UserCache.Designer.cs create mode 100644 Timeline/Resources/Services/UserCache.resx create mode 100644 Timeline/Resources/Services/UserManager.Designer.cs create mode 100644 Timeline/Resources/Services/UserManager.resx create mode 100644 Timeline/Services/PasswordBadFormatException.cs delete mode 100644 Timeline/Services/UserDetailService.cs delete mode 100644 Timeline/Services/UsernameBadFormatException.cs diff --git a/Timeline.Tests/Controllers/TokenControllerTest.cs b/Timeline.Tests/Controllers/TokenControllerTest.cs index 61fbe950..43e1a413 100644 --- a/Timeline.Tests/Controllers/TokenControllerTest.cs +++ b/Timeline.Tests/Controllers/TokenControllerTest.cs @@ -42,7 +42,7 @@ namespace Timeline.Tests.Controllers var mockCreateResult = new UserTokenCreateResult { Token = "mocktokenaaaaa", - User = new UserInfo + User = new Models.User { Id = 1, Username = MockUser.User.Username, @@ -99,7 +99,7 @@ namespace Timeline.Tests.Controllers public async Task Verify_Ok() { const string token = "aaaaaaaaaaaaaa"; - _mockUserService.Setup(s => s.VerifyToken(token)).ReturnsAsync(new UserInfo + _mockUserService.Setup(s => s.VerifyToken(token)).ReturnsAsync(new Models.User { Id = 1, Username = MockUser.User.Username, diff --git a/Timeline.Tests/Controllers/UserControllerTest.cs b/Timeline.Tests/Controllers/UserControllerTest.cs index a1035675..192d53dd 100644 --- a/Timeline.Tests/Controllers/UserControllerTest.cs +++ b/Timeline.Tests/Controllers/UserControllerTest.cs @@ -36,9 +36,9 @@ namespace Timeline.Tests.Controllers [Fact] public async Task GetList_Success() { - var mockUserList = new UserInfo[] { - new UserInfo { Id = 1, Username = "aaa", Administrator = true, Version = 1 }, - new UserInfo { Id = 2, Username = "bbb", Administrator = false, Version = 1 } + var mockUserList = new Models.User[] { + new Models.User { Id = 1, Username = "aaa", Administrator = true, Version = 1 }, + new Models.User { Id = 2, Username = "bbb", Administrator = false, Version = 1 } }; _mockUserService.Setup(s => s.ListUsers()).ReturnsAsync(mockUserList); var action = await _controller.List(); @@ -51,7 +51,7 @@ namespace Timeline.Tests.Controllers public async Task Get_Success() { const string username = "aaa"; - _mockUserService.Setup(s => s.GetUserByUsername(username)).ReturnsAsync(new UserInfo + _mockUserService.Setup(s => s.GetUserByUsername(username)).ReturnsAsync(new Models.User { Id = 1, Username = MockUser.User.Username, diff --git a/Timeline.Tests/Helpers/TestDatabase.cs b/Timeline.Tests/Helpers/TestDatabase.cs index 3163279a..e29a71fa 100644 --- a/Timeline.Tests/Helpers/TestDatabase.cs +++ b/Timeline.Tests/Helpers/TestDatabase.cs @@ -18,9 +18,9 @@ namespace Timeline.Tests.Helpers { return new UserEntity { - Name = user.Username, - EncryptedPassword = PasswordService.HashPassword(user.Password), - RoleString = UserRoleConvert.ToString(user.Administrator), + Username = user.Username, + Password = PasswordService.HashPassword(user.Password), + Roles = UserRoleConvert.ToString(user.Administrator), Avatar = null }; } diff --git a/Timeline.Tests/Services/UserAvatarServiceTest.cs b/Timeline.Tests/Services/UserAvatarServiceTest.cs index d4371c48..2dca7ccf 100644 --- a/Timeline.Tests/Services/UserAvatarServiceTest.cs +++ b/Timeline.Tests/Services/UserAvatarServiceTest.cs @@ -171,7 +171,7 @@ namespace Timeline.Tests.Services var mockAvatarEntity = CreateMockAvatarEntity("aaa"); { var context = _database.Context; - var user = await context.Users.Where(u => u.Name == username).Include(u => u.Avatar).SingleAsync(); + var user = await context.Users.Where(u => u.Username == username).Include(u => u.Avatar).SingleAsync(); user.Avatar = mockAvatarEntity; await context.SaveChangesAsync(); } @@ -205,7 +205,7 @@ namespace Timeline.Tests.Services var mockAvatarEntity = CreateMockAvatarEntity("aaa"); { var context = _database.Context; - var user = await context.Users.Where(u => u.Name == username).Include(u => u.Avatar).SingleAsync(); + var user = await context.Users.Where(u => u.Username == username).Include(u => u.Avatar).SingleAsync(); user.Avatar = mockAvatarEntity; await context.SaveChangesAsync(); } @@ -236,7 +236,7 @@ namespace Timeline.Tests.Services { string username = MockUser.User.Username; - var user = await _database.Context.Users.Where(u => u.Name == username).Include(u => u.Avatar).SingleAsync(); + var user = await _database.Context.Users.Where(u => u.Username == username).Include(u => u.Avatar).SingleAsync(); var avatar1 = CreateMockAvatar("aaa"); var avatar2 = CreateMockAvatar("bbb"); diff --git a/Timeline.Tests/Services/UserDetailServiceTest.cs b/Timeline.Tests/Services/UserDetailServiceTest.cs index e6eabadf..dbff2705 100644 --- a/Timeline.Tests/Services/UserDetailServiceTest.cs +++ b/Timeline.Tests/Services/UserDetailServiceTest.cs @@ -51,7 +51,7 @@ namespace Timeline.Tests.Services const string nickname = "aaaaaa"; { var context = _testDatabase.Context; - var userId = (await context.Users.Where(u => u.Name == MockUser.User.Username).Select(u => new { u.Id }).SingleAsync()).Id; + var userId = (await context.Users.Where(u => u.Username == MockUser.User.Username).Select(u => new { u.Id }).SingleAsync()).Id; context.UserDetails.Add(new UserDetailEntity { Nickname = nickname, @@ -83,7 +83,7 @@ namespace Timeline.Tests.Services public async Task SetNickname_ShouldWork() { var username = MockUser.User.Username; - var user = await _testDatabase.Context.Users.Where(u => u.Name == username).Include(u => u.Detail).SingleAsync(); + var user = await _testDatabase.Context.Users.Where(u => u.Username == username).Include(u => u.Detail).SingleAsync(); var nickname1 = "nickname1"; var nickname2 = "nickname2"; diff --git a/Timeline.Tests/Services/UserTokenManagerTest.cs b/Timeline.Tests/Services/UserTokenManagerTest.cs index 19122d31..e649fbab 100644 --- a/Timeline.Tests/Services/UserTokenManagerTest.cs +++ b/Timeline.Tests/Services/UserTokenManagerTest.cs @@ -2,8 +2,6 @@ using Microsoft.Extensions.Logging.Abstractions; using Moq; using System; -using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using Timeline.Models; using Timeline.Services; @@ -58,7 +56,7 @@ namespace Timeline.Tests.Services const string username = "uuu"; const string password = "ppp"; var mockExpireTime = setExpireTime ? (DateTime?)DateTime.Now : null; - var mockUserInfo = new UserInfo + var mockUserInfo = new User { Id = 1, Username = username, @@ -126,7 +124,7 @@ namespace Timeline.Tests.Services ExpireAt = mockTime.AddDays(1) }; _mockUserTokenService.Setup(s => s.VerifyToken(mockToken)).Returns(mockTokenInfo); - _mockUserService.Setup(s => s.GetUserById(1)).ReturnsAsync(new UserInfo + _mockUserService.Setup(s => s.GetUserById(1)).ReturnsAsync(new User { Id = 1, Username = "aaa", @@ -149,7 +147,7 @@ namespace Timeline.Tests.Services Version = 1, ExpireAt = mockTime.AddDays(1) }; - var mockUserInfo = new UserInfo + var mockUserInfo = new User { Id = 1, Username = "aaa", diff --git a/Timeline/Controllers/TokenController.cs b/Timeline/Controllers/TokenController.cs index a96b6fa9..9724c1a6 100644 --- a/Timeline/Controllers/TokenController.cs +++ b/Timeline/Controllers/TokenController.cs @@ -20,9 +20,9 @@ namespace Timeline.Controllers private readonly ILogger _logger; private readonly IClock _clock; - private static User CreateUserFromUserInfo(UserInfo userInfo) + private static Models.Http.User CreateUserFromUserInfo(Models.User userInfo) { - return new User + return new Models.Http.User { Username = userInfo.Username, Administrator = userInfo.Administrator diff --git a/Timeline/Controllers/UserDetailController.cs b/Timeline/Controllers/UserDetailController.cs deleted file mode 100644 index 9de9899e..00000000 --- a/Timeline/Controllers/UserDetailController.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using System.Threading.Tasks; -using Timeline.Filters; -using Timeline.Models.Validation; -using Timeline.Services; -using System.ComponentModel.DataAnnotations; -using Microsoft.AspNetCore.Authorization; - -namespace Timeline.Controllers -{ - [ApiController] - public class UserDetailController : Controller - { - private readonly IUserDetailService _service; - - public UserDetailController(IUserDetailService service) - { - _service = service; - } - - [HttpGet("users/{username}/nickname")] - [CatchUserNotExistException] - public async Task> GetNickname([FromRoute][Username] string username) - { - return Ok(await _service.GetNickname(username)); - } - - [HttpPut("users/{username}/nickname")] - [Authorize] - [SelfOrAdmin] - [CatchUserNotExistException] - public async Task PutNickname([FromRoute][Username] string username, - [FromBody][StringLength(10, MinimumLength = 1)] string body) - { - await _service.SetNickname(username, body); - return Ok(); - } - - [HttpDelete("users/{username}/nickname")] - [Authorize] - [SelfOrAdmin] - [CatchUserNotExistException] - public async Task DeleteNickname([FromRoute][Username] string username) - { - await _service.SetNickname(username, null); - return Ok(); - } - } -} diff --git a/Timeline/Entities/DatabaseContext.cs b/Timeline/Entities/DatabaseContext.cs index 738440b2..ac4ad7b2 100644 --- a/Timeline/Entities/DatabaseContext.cs +++ b/Timeline/Entities/DatabaseContext.cs @@ -14,7 +14,7 @@ namespace Timeline.Entities protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity().Property(e => e.Version).HasDefaultValue(0); - modelBuilder.Entity().HasIndex(e => e.Name).IsUnique(); + modelBuilder.Entity().HasIndex(e => e.Username).IsUnique(); } public DbSet Users { get; set; } = default!; diff --git a/Timeline/Entities/UserDetailEntity.cs b/Timeline/Entities/UserDetailEntity.cs deleted file mode 100644 index 1d9957f9..00000000 --- a/Timeline/Entities/UserDetailEntity.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Timeline.Entities -{ - [Table("user_details")] - public class UserDetailEntity - { - [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public long Id { get; set; } - - [Column("nickname"), MaxLength(26)] - public string? Nickname { get; set; } - - [Column("user")] - public long UserId { get; set; } - - [ForeignKey(nameof(UserId))] - public UserEntity User { get; set; } = default!; - } -} diff --git a/Timeline/Entities/UserEntity.cs b/Timeline/Entities/UserEntity.cs index 83ef5621..dae6979f 100644 --- a/Timeline/Entities/UserEntity.cs +++ b/Timeline/Entities/UserEntity.cs @@ -17,21 +17,22 @@ namespace Timeline.Entities [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] public long Id { get; set; } - [Column("name"), MaxLength(26), Required] - public string Name { get; set; } = default!; + [Column("username"), MaxLength(26), Required] + public string Username { get; set; } = default!; [Column("password"), Required] - public string EncryptedPassword { get; set; } = default!; + public string Password { get; set; } = default!; [Column("roles"), Required] - public string RoleString { get; set; } = default!; + public string Roles { get; set; } = default!; [Column("version"), Required] public long Version { get; set; } - public UserAvatarEntity? Avatar { get; set; } + [Column("nickname"), MaxLength(40)] + public string? Nickname { get; set; } - public UserDetailEntity? Detail { get; set; } + public UserAvatarEntity? Avatar { get; set; } public List Timelines { get; set; } = default!; diff --git a/Timeline/GlobalSuppressions.cs b/Timeline/GlobalSuppressions.cs index c0754071..d27b3c16 100644 --- a/Timeline/GlobalSuppressions.cs +++ b/Timeline/GlobalSuppressions.cs @@ -10,3 +10,4 @@ [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Generated error response identifiers.", Scope = "type", Target = "Timeline.Models.Http.ErrorResponse")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1724:Type names should not match namespaces", Justification = "Generated error response identifiers.", Scope = "type", Target = "Timeline.Models.Http.ErrorResponse")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "Generated error response.", Scope = "type", Target = "Timeline.Models.Http.ErrorResponse")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1056:Uri properties should not be strings", Justification = "That's unnecessary.")] diff --git a/Timeline/Models/Http/User.cs b/Timeline/Models/Http/User.cs index 69bfacf2..b3812f48 100644 --- a/Timeline/Models/Http/User.cs +++ b/Timeline/Models/Http/User.cs @@ -1,14 +1,10 @@ +using System; using System.ComponentModel.DataAnnotations; using Timeline.Models.Validation; namespace Timeline.Models.Http { - public class User - { - public string Username { get; set; } = default!; - public bool Administrator { get; set; } - } - + [Obsolete("Remove this.")] public class UserPutRequest { [Required] @@ -17,6 +13,7 @@ namespace Timeline.Models.Http public bool? Administrator { get; set; } } + [Obsolete("Remove this.")] public class UserPatchRequest { public string? Password { get; set; } diff --git a/Timeline/Models/User.cs b/Timeline/Models/User.cs new file mode 100644 index 00000000..05395022 --- /dev/null +++ b/Timeline/Models/User.cs @@ -0,0 +1,19 @@ +using Timeline.Models.Validation; + +namespace Timeline.Models +{ + public class User + { + [Username] + public string? Username { get; set; } + public bool? Administrator { get; set; } + public string? Nickname { get; set; } + public string? AvatarUrl { get; set; } + + + #region secret + public string? Password { get; set; } + public long? Version { get; set; } + #endregion secret + } +} diff --git a/Timeline/Models/UserInfo.cs b/Timeline/Models/UserInfo.cs deleted file mode 100644 index eff47329..00000000 --- a/Timeline/Models/UserInfo.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Timeline.Models -{ - public class UserInfo - { - public long Id { get; set; } - public long Version { get; set; } - public string Username { get; set; } = default!; - public bool Administrator { get; set; } - } -} diff --git a/Timeline/Models/Validation/Validator.cs b/Timeline/Models/Validation/Validator.cs index a16f6f81..ead7dbef 100644 --- a/Timeline/Models/Validation/Validator.cs +++ b/Timeline/Models/Validation/Validator.cs @@ -20,24 +20,46 @@ namespace Timeline.Models.Validation (bool, string) Validate(object? value); } + public static class ValidatorExtensions + { + public static bool Validate(this IValidator validator, object? value, out string message) + { + if (validator == null) + throw new ArgumentNullException(nameof(validator)); + + var (r, m) = validator.Validate(value); + message = m; + return r; + } + } + /// /// Convenient base class for validator. /// /// The type of accepted value. /// /// Subclass should override to do the real validation. - /// This class will check the nullity and type of value. If value is null or not of type - /// it will return false and not call . + /// This class will check the nullity and type of value. + /// If value is null, it will pass or fail depending on . + /// If value is not null and not of type + /// it will fail and not call . + /// + /// is true by default. /// /// If you want some other behaviours, write the validator from scratch. /// public abstract class Validator : IValidator { + protected bool PermitNull { get; set; } = true; + public (bool, string) Validate(object? value) { if (value == null) { - return (false, ValidatorMessageNull); + if (PermitNull) + return (true, GetSuccessMessage()); + else + return (false, ValidatorMessageNull); } if (value is T v) diff --git a/Timeline/Resources/Models/Validation/PasswordValidator.Designer.cs b/Timeline/Resources/Models/Validation/PasswordValidator.Designer.cs new file mode 100644 index 00000000..e7630d26 --- /dev/null +++ b/Timeline/Resources/Models/Validation/PasswordValidator.Designer.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Resources.Models.Validation { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class PasswordValidator { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal PasswordValidator() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Models.Validation.PasswordValidator", typeof(PasswordValidator).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Password can't be empty.. + /// + internal static string MessageEmptyString { + get { + return ResourceManager.GetString("MessageEmptyString", resourceCulture); + } + } + } +} diff --git a/Timeline/Resources/Models/Validation/PasswordValidator.resx b/Timeline/Resources/Models/Validation/PasswordValidator.resx new file mode 100644 index 00000000..f445cc75 --- /dev/null +++ b/Timeline/Resources/Models/Validation/PasswordValidator.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Password can't be empty. + + \ No newline at end of file diff --git a/Timeline/Resources/Models/Validation/PasswordValidator.zh.resx b/Timeline/Resources/Models/Validation/PasswordValidator.zh.resx new file mode 100644 index 00000000..9eab7b4e --- /dev/null +++ b/Timeline/Resources/Models/Validation/PasswordValidator.zh.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 密码不能为空。 + + \ No newline at end of file diff --git a/Timeline/Resources/Services/Exception.Designer.cs b/Timeline/Resources/Services/Exception.Designer.cs index 0a3325d4..671c4b93 100644 --- a/Timeline/Resources/Services/Exception.Designer.cs +++ b/Timeline/Resources/Services/Exception.Designer.cs @@ -240,6 +240,15 @@ namespace Timeline.Resources.Services { } } + /// + /// Looks up a localized string similar to Password is of bad format.. + /// + internal static string PasswordBadFormatException { + get { + return ResourceManager.GetString("PasswordBadFormatException", resourceCulture); + } + } + /// /// Looks up a localized string similar to The timeline with that name already exists.. /// @@ -303,15 +312,6 @@ namespace Timeline.Resources.Services { } } - /// - /// Looks up a localized string similar to The username is of bad format.. - /// - internal static string UsernameBadFormatException { - get { - return ResourceManager.GetString("UsernameBadFormatException", resourceCulture); - } - } - /// /// Looks up a localized string similar to The username already exists.. /// diff --git a/Timeline/Resources/Services/Exception.resx b/Timeline/Resources/Services/Exception.resx index bc96248d..3ae14d4e 100644 --- a/Timeline/Resources/Services/Exception.resx +++ b/Timeline/Resources/Services/Exception.resx @@ -177,6 +177,9 @@ version claim does not exist. + + Password is of bad format. + The timeline with that name already exists. @@ -198,9 +201,6 @@ The use is not a member of the timeline. - - The username is of bad format. - The username already exists. diff --git a/Timeline/Resources/Services/UserCache.Designer.cs b/Timeline/Resources/Services/UserCache.Designer.cs new file mode 100644 index 00000000..28a74a6c --- /dev/null +++ b/Timeline/Resources/Services/UserCache.Designer.cs @@ -0,0 +1,99 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Resources.Services { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class UserCache { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal UserCache() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Services.UserCache", typeof(UserCache).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Found user info from cache. Entry: {0} .. + /// + internal static string LogGetCacheExist { + get { + return ResourceManager.GetString("LogGetCacheExist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to User info not exist in cache. Id: {0} .. + /// + internal static string LogGetCacheNotExist { + get { + return ResourceManager.GetString("LogGetCacheNotExist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to User info remove in cache. Id: {0} .. + /// + internal static string LogRemoveCache { + get { + return ResourceManager.GetString("LogRemoveCache", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to User info set in cache. Entry: {0} .. + /// + internal static string LogSetCache { + get { + return ResourceManager.GetString("LogSetCache", resourceCulture); + } + } + } +} diff --git a/Timeline/Resources/Services/UserCache.resx b/Timeline/Resources/Services/UserCache.resx new file mode 100644 index 00000000..1102108b --- /dev/null +++ b/Timeline/Resources/Services/UserCache.resx @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Found user info from cache. Entry: {0} . + + + User info not exist in cache. Id: {0} . + + + User info remove in cache. Id: {0} . + + + User info set in cache. Entry: {0} . + + \ No newline at end of file diff --git a/Timeline/Resources/Services/UserManager.Designer.cs b/Timeline/Resources/Services/UserManager.Designer.cs new file mode 100644 index 00000000..424499f8 --- /dev/null +++ b/Timeline/Resources/Services/UserManager.Designer.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Resources.Services { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class UserManager { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal UserManager() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Services.UserManager", typeof(UserManager).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to A user has been created.. + /// + internal static string LogUserCreate { + get { + return ResourceManager.GetString("LogUserCreate", resourceCulture); + } + } + } +} diff --git a/Timeline/Resources/Services/UserManager.resx b/Timeline/Resources/Services/UserManager.resx new file mode 100644 index 00000000..ecb89179 --- /dev/null +++ b/Timeline/Resources/Services/UserManager.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + A user has been created. + + \ No newline at end of file diff --git a/Timeline/Resources/Services/UserService.Designer.cs b/Timeline/Resources/Services/UserService.Designer.cs index 2a04dded..1b85546d 100644 --- a/Timeline/Resources/Services/UserService.Designer.cs +++ b/Timeline/Resources/Services/UserService.Designer.cs @@ -78,6 +78,42 @@ namespace Timeline.Resources.Services { } } + /// + /// Looks up a localized string similar to Password can't be empty.. + /// + internal static string ExceptionPasswordEmpty { + get { + return ResourceManager.GetString("ExceptionPasswordEmpty", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Password can't be null or empty.. + /// + internal static string ExceptionPasswordNullOrEmpty { + get { + return ResourceManager.GetString("ExceptionPasswordNullOrEmpty", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Username is of bad format, because {}.. + /// + internal static string ExceptionUsernameBadFormat { + get { + return ResourceManager.GetString("ExceptionUsernameBadFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Username can't be null or empty.. + /// + internal static string ExceptionUsernameNullOrEmpty { + get { + return ResourceManager.GetString("ExceptionUsernameNullOrEmpty", resourceCulture); + } + } + /// /// Looks up a localized string similar to A cache entry is created.. /// diff --git a/Timeline/Resources/Services/UserService.resx b/Timeline/Resources/Services/UserService.resx index 3670d8f9..26221770 100644 --- a/Timeline/Resources/Services/UserService.resx +++ b/Timeline/Resources/Services/UserService.resx @@ -123,6 +123,18 @@ Old username is of bad format. + + Password can't be empty. + + + Password can't be null or empty. + + + Username is of bad format, because {}. + + + Username can't be null or empty. + A cache entry is created. diff --git a/Timeline/Services/DatabaseExtensions.cs b/Timeline/Services/DatabaseExtensions.cs index c5c96d8c..e77dd01a 100644 --- a/Timeline/Services/DatabaseExtensions.cs +++ b/Timeline/Services/DatabaseExtensions.cs @@ -27,7 +27,7 @@ namespace Timeline.Services if (!result) throw new UsernameBadFormatException(username, message); - var userId = await userDbSet.Where(u => u.Name == username).Select(u => u.Id).SingleOrDefaultAsync(); + var userId = await userDbSet.Where(u => u.Username == username).Select(u => u.Id).SingleOrDefaultAsync(); if (userId == 0) throw new UserNotExistException(username); return userId; diff --git a/Timeline/Services/PasswordBadFormatException.cs b/Timeline/Services/PasswordBadFormatException.cs new file mode 100644 index 00000000..2029ebb4 --- /dev/null +++ b/Timeline/Services/PasswordBadFormatException.cs @@ -0,0 +1,27 @@ +using System; + +namespace Timeline.Services +{ + + [Serializable] + public class PasswordBadFormatException : Exception + { + public PasswordBadFormatException() : base(Resources.Services.Exception.PasswordBadFormatException) { } + public PasswordBadFormatException(string message) : base(message) { } + public PasswordBadFormatException(string message, Exception inner) : base(message, inner) { } + + public PasswordBadFormatException(string password, string validationMessage) : this() + { + Password = password; + ValidationMessage = validationMessage; + } + + protected PasswordBadFormatException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + public string Password { get; set; } = ""; + + public string ValidationMessage { get; set; } = ""; + } +} diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs index f7b0e0e9..f43d2de5 100644 --- a/Timeline/Services/TimelineService.cs +++ b/Timeline/Services/TimelineService.cs @@ -356,7 +356,7 @@ namespace Timeline.Services { Id = entity.Id, Content = entity.Content, - Author = (await Database.Users.Where(u => u.Id == entity.AuthorId).Select(u => new { u.Name }).SingleAsync()).Name, + Author = (await Database.Users.Where(u => u.Id == entity.AuthorId).Select(u => new { u.Username }).SingleAsync()).Name, Time = entity.Time }); } @@ -382,7 +382,7 @@ namespace Timeline.Services var timelineId = await FindTimelineId(name); - var authorEntity = Database.Users.Where(u => u.Name == author).Select(u => new { u.Id }).SingleOrDefault(); + var authorEntity = Database.Users.Where(u => u.Username == author).Select(u => new { u.Id }).SingleOrDefault(); if (authorEntity == null) { throw new UserNotExistException(author); @@ -508,7 +508,7 @@ namespace Timeline.Services 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(); + var user = await Database.Users.Where(u => u.Username == username).Select(u => new { u.Id }).SingleOrDefaultAsync(); if (user == null) { throw new TimelineMemberOperationUserException(index, operation, username, @@ -550,7 +550,7 @@ namespace Timeline.Services throw new UsernameBadFormatException(username); } - var user = await Database.Users.Where(u => u.Name == username).Select(u => new { u.Id }).SingleOrDefaultAsync(); + var user = await Database.Users.Where(u => u.Username == username).Select(u => new { u.Id }).SingleOrDefaultAsync(); if (user == null) { @@ -596,7 +596,7 @@ namespace Timeline.Services } } - var user = await Database.Users.Where(u => u.Name == username).Select(u => new { u.Id }).SingleOrDefaultAsync(); + var user = await Database.Users.Where(u => u.Username == username).Select(u => new { u.Id }).SingleOrDefaultAsync(); if (user == null) { @@ -632,7 +632,7 @@ namespace Timeline.Services } } - var user = await Database.Users.Where(u => u.Name == username).Select(u => new { u.Id }).SingleOrDefaultAsync(); + var user = await Database.Users.Where(u => u.Username == username).Select(u => new { u.Id }).SingleOrDefaultAsync(); if (user == null) { @@ -672,7 +672,7 @@ namespace Timeline.Services } } - var userEntity = await Database.Users.Where(u => u.Name == name).Select(u => new { u.Id }).SingleOrDefaultAsync(); + var userEntity = await Database.Users.Where(u => u.Username == name).Select(u => new { u.Id }).SingleOrDefaultAsync(); if (userEntity == null) { @@ -715,7 +715,7 @@ namespace Timeline.Services var timelineMemberEntities = await Database.TimelineMembers.Where(m => m.TimelineId == timelineId).Select(m => new { m.UserId }).ToListAsync(); - var memberUsernameTasks = timelineMemberEntities.Select(m => Database.Users.Where(u => u.Id == m.UserId).Select(u => u.Name).SingleAsync()).ToArray(); + var memberUsernameTasks = timelineMemberEntities.Select(m => Database.Users.Where(u => u.Id == m.UserId).Select(u => u.Username).SingleAsync()).ToArray(); var memberUsernames = await Task.WhenAll(memberUsernameTasks); diff --git a/Timeline/Services/UserDetailService.cs b/Timeline/Services/UserDetailService.cs deleted file mode 100644 index 4f4a7942..00000000 --- a/Timeline/Services/UserDetailService.cs +++ /dev/null @@ -1,102 +0,0 @@ -using Microsoft.Extensions.Logging; -using System; -using System.Linq; -using System.Threading.Tasks; -using Timeline.Entities; -using static Timeline.Resources.Services.UserDetailService; - -namespace Timeline.Services -{ - public interface IUserDetailService - { - /// - /// Get the nickname of the user with given username. - /// If the user does not set a nickname, the username is returned as the nickname. - /// - /// The username of the user to get nickname of. - /// The nickname of the user. - /// Thrown when is null. - /// Thrown when is of bad format. - /// Thrown when the user does not exist. - Task GetNickname(string username); - - /// - /// Set the nickname of the user with given username. - /// - /// The username of the user to set nickname of. - /// The nickname. Pass null to unset. - /// Thrown when is null. - /// Thrown when is not null but its length is bigger than 10. - /// Thrown when is of bad format. - /// Thrown when the user does not exist. - Task SetNickname(string username, string? nickname); - } - - public class UserDetailService : IUserDetailService - { - private readonly DatabaseContext _database; - - private readonly ILogger _logger; - - public UserDetailService(DatabaseContext database, ILogger logger) - { - _database = database; - _logger = logger; - } - - public async Task GetNickname(string username) - { - var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username); - var nickname = _database.UserDetails.Where(d => d.UserId == userId).Select(d => new { d.Nickname }).SingleOrDefault()?.Nickname; - return nickname ?? username; - } - - public async Task SetNickname(string username, string? nickname) - { - if (nickname != null && nickname.Length > 10) - { - throw new ArgumentException(ExceptionNicknameTooLong, nameof(nickname)); - } - var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username); - var userDetail = _database.UserDetails.Where(d => d.UserId == userId).SingleOrDefault(); - if (nickname == null) - { - if (userDetail == null || userDetail.Nickname == null) - { - return; - } - else - { - userDetail.Nickname = null; - await _database.SaveChangesAsync(); - _logger.LogInformation(LogEntityNicknameSetToNull, userId); - } - } - else - { - var create = userDetail == null; - if (create) - { - userDetail = new UserDetailEntity - { - UserId = userId - }; - } - userDetail!.Nickname = nickname; - if (create) - { - _database.UserDetails.Add(userDetail); - } - await _database.SaveChangesAsync(); - if (create) - { - _logger.LogInformation(LogEntityNicknameCreate, userId, nickname); - } - else - { - _logger.LogInformation(LogEntityNicknameSetNotNull, userId, nickname); - } - } - } - } -} diff --git a/Timeline/Services/UserNotExistException.cs b/Timeline/Services/UserNotExistException.cs index c7317f56..fd0b5ecf 100644 --- a/Timeline/Services/UserNotExistException.cs +++ b/Timeline/Services/UserNotExistException.cs @@ -31,11 +31,11 @@ namespace Timeline.Services /// /// The username of the user that does not exist. /// - public string? Username { get; set; } + public string Username { get; set; } = ""; /// /// The id of the user that does not exist. /// - public long? Id { get; set; } + public long Id { get; set; } } } diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs index 104db1b0..c5595c99 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -1,13 +1,14 @@ using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using System; +using System.Globalization; using System.Linq; using System.Threading.Tasks; using Timeline.Entities; using Timeline.Helpers; using Timeline.Models; using Timeline.Models.Validation; +using static Timeline.Resources.Services.UserService; namespace Timeline.Services { @@ -18,12 +19,12 @@ namespace Timeline.Services /// /// The username of the user to verify. /// The password of the user to verify. - /// The user info. + /// The user info and auth info. /// Thrown when or is null. - /// Thrown when username is of bad format. + /// Thrown when username is of bad format. /// Thrown when the user with given username does not exist. /// Thrown when password is wrong. - Task VerifyCredential(string username, string password); + Task VerifyCredential(string username, string password); /// /// Try to get a user by id. @@ -31,7 +32,7 @@ namespace Timeline.Services /// The id of the user. /// The user info. /// Thrown when the user with given id does not exist. - Task GetUserById(long id); + Task GetUserById(long id); /// /// Get the user info of given username. @@ -39,30 +40,51 @@ namespace Timeline.Services /// Username of the user. /// The info of the user. /// Thrown when is null. - /// Thrown when is of bad format. + /// Thrown when is of bad format. /// Thrown when the user with given username does not exist. - Task GetUserByUsername(string username); + Task GetUserByUsername(string username); /// /// List all users. /// /// The user info of users. - Task ListUsers(); + Task ListUsers(); /// - /// Create or modify a user with given username. - /// Username must be match with [a-zA-z0-9-_]. + /// Create a user with given info. /// - /// Username of user. - /// Password of user. - /// Whether the user is administrator. - /// - /// Return if a new user is created. - /// Return if a existing user is modified. - /// - /// Thrown when or is null. - /// Thrown when is of bad format. - Task PutUser(string username, string password, bool administrator); + /// The info of new user. + /// The password, can't be null or empty. + /// The id of the new user. + /// Thrown when is null. + /// Thrown when some fields in is bad. + /// Thrown when a user with given username already exists. + /// + /// must not be null and must be a valid username. + /// must not be null or empty. + /// is false by default (null). + /// Other fields are ignored. + /// + Task CreateUser(User info); + + /// + /// Modify a user's info. + /// + /// The id of the user. + /// The new info. May be null. + /// Thrown when some fields in is bad. + /// Thrown when user with given id does not exist. + /// + /// Only , and will be used. + /// If null, then not change. + /// Other fields are ignored. + /// After modified, even if nothing is changed, version will increase. + /// + /// can't be empty. + /// + /// Note: Whether is set or not, version will increase and not set to the specified value if there is one. + /// + Task ModifyUser(long id, User? info); /// /// Partially modify a user of given username. @@ -116,181 +138,164 @@ namespace Timeline.Services private readonly DatabaseContext _databaseContext; - private readonly IMemoryCache _memoryCache; private readonly IPasswordService _passwordService; private readonly UsernameValidator _usernameValidator = new UsernameValidator(); - public UserService(ILogger logger, IMemoryCache memoryCache, DatabaseContext databaseContext, IPasswordService passwordService) + public UserService(ILogger logger, DatabaseContext databaseContext, IPasswordService passwordService) { _logger = logger; - _memoryCache = memoryCache; _databaseContext = databaseContext; _passwordService = passwordService; } - private static string GenerateCacheKeyByUserId(long id) => $"user:{id}"; - - private void RemoveCache(long id) + private void CheckUsernameFormat(string username, string? paramName, Func? messageBuilder = null) { - var key = GenerateCacheKeyByUserId(id); - _memoryCache.Remove(key); - _logger.LogInformation(Log.Format(Resources.Services.UserService.LogCacheRemove, ("Key", key))); - } - - private void CheckUsernameFormat(string username, string? message = null) - { - var (result, validationMessage) = _usernameValidator.Validate(username); - if (!result) + if (!_usernameValidator.Validate(username, out var message)) { - if (message == null) - throw new UsernameBadFormatException(username, validationMessage); + if (messageBuilder == null) + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, ExceptionUsernameBadFormat, message), paramName); else - throw new UsernameBadFormatException(username, validationMessage, message); + throw new ArgumentException(messageBuilder(message), paramName); } } - private static UserInfo CreateUserInfoFromEntity(UserEntity user) + private static User CreateUserFromEntity(UserEntity entity) { - return new UserInfo + return new User { - Id = user.Id, - Username = user.Name, - Administrator = UserRoleConvert.ToBool(user.RoleString), - Version = user.Version + Username = entity.Username, + Administrator = UserRoleConvert.ToBool(entity.Roles), + Nickname = string.IsNullOrEmpty(entity.Nickname) ? entity.Username : entity.Nickname, + Version = entity.Version }; } - public async Task VerifyCredential(string username, string password) + public async Task VerifyCredential(string username, string password) { if (username == null) throw new ArgumentNullException(nameof(username)); if (password == null) throw new ArgumentNullException(nameof(password)); - CheckUsernameFormat(username); + CheckUsernameFormat(username, nameof(username)); - // We need password info, so always check the database. - var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync(); + var entity = await _databaseContext.Users.Where(u => u.Username == username).SingleOrDefaultAsync(); - if (user == null) + if (entity == null) throw new UserNotExistException(username); - if (!_passwordService.VerifyPassword(user.EncryptedPassword, password)) + if (!_passwordService.VerifyPassword(entity.Password, password)) throw new BadPasswordException(password); - return CreateUserInfoFromEntity(user); + return CreateUserFromEntity(entity); } - public async Task GetUserById(long id) + public async Task GetUserById(long id) { - var key = GenerateCacheKeyByUserId(id); - if (!_memoryCache.TryGetValue(key, out var cache)) - { - // no cache, check the database - var user = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync(); - - if (user == null) - throw new UserNotExistException(id); + var user = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync(); - // create cache - cache = CreateUserInfoFromEntity(user); - _memoryCache.CreateEntry(key).SetValue(cache); - _logger.LogInformation(Log.Format(Resources.Services.UserService.LogCacheCreate, ("Key", key))); - } + if (user == null) + throw new UserNotExistException(id); - return cache; + return CreateUserFromEntity(user); } - public async Task GetUserByUsername(string username) + public async Task GetUserByUsername(string username) { if (username == null) throw new ArgumentNullException(nameof(username)); - CheckUsernameFormat(username); + CheckUsernameFormat(username, nameof(username)); - var entity = await _databaseContext.Users - .Where(user => user.Name == username) - .SingleOrDefaultAsync(); + var entity = await _databaseContext.Users.Where(user => user.Username == username).SingleOrDefaultAsync(); if (entity == null) throw new UserNotExistException(username); - return CreateUserInfoFromEntity(entity); + return CreateUserFromEntity(entity); } - public async Task ListUsers() + public async Task ListUsers() { var entities = await _databaseContext.Users.ToArrayAsync(); - return entities.Select(user => CreateUserInfoFromEntity(user)).ToArray(); + return entities.Select(user => CreateUserFromEntity(user)).ToArray(); } - public async Task PutUser(string username, string password, bool administrator) + public async Task CreateUser(User info) { - if (username == null) - throw new ArgumentNullException(nameof(username)); - if (password == null) - throw new ArgumentNullException(nameof(password)); - CheckUsernameFormat(username); + if (info == null) + throw new ArgumentNullException(nameof(info)); - var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync(); + if (string.IsNullOrEmpty(info.Username)) + throw new ArgumentException(ExceptionUsernameNullOrEmpty, nameof(info)); - if (user == null) - { - var newUser = new UserEntity - { - Name = username, - EncryptedPassword = _passwordService.HashPassword(password), - RoleString = UserRoleConvert.ToString(administrator), - Avatar = null - }; - await _databaseContext.AddAsync(newUser); - await _databaseContext.SaveChangesAsync(); - _logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseCreate, - ("Id", newUser.Id), ("Username", username), ("Administrator", administrator))); - return PutResult.Create; - } + CheckUsernameFormat(info.Username, nameof(info)); - user.EncryptedPassword = _passwordService.HashPassword(password); - user.RoleString = UserRoleConvert.ToString(administrator); - user.Version += 1; + if (string.IsNullOrEmpty(info.Password)) + throw new ArgumentException(ExceptionPasswordNullOrEmpty); + + var username = info.Username; + + var conflict = await _databaseContext.Users.AnyAsync(u => u.Username == username); + + if (conflict) + throw new UsernameConfictException(username); + + var administrator = info.Administrator ?? false; + var password = info.Password; + + var newEntity = new UserEntity + { + Username = username, + Password = _passwordService.HashPassword(password), + Roles = UserRoleConvert.ToString(administrator), + Version = 1 + }; + _databaseContext.Users.Add(newEntity); await _databaseContext.SaveChangesAsync(); - _logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseUpdate, - ("Id", user.Id), ("Username", username), ("Administrator", administrator))); - //clear cache - RemoveCache(user.Id); + _logger.LogInformation(Log.Format(LogDatabaseCreate, + ("Id", newEntity.Id), ("Username", username), ("Administrator", administrator))); - return PutResult.Modify; + return newEntity.Id; } - public async Task PatchUser(string username, string? password, bool? administrator) + public async Task ModifyUser(long id, User? info) { - if (username == null) - throw new ArgumentNullException(nameof(username)); - CheckUsernameFormat(username); + if (info != null && info.Password != null && info.Password.Length == 0) + throw new ArgumentException(ExceptionPasswordEmpty, nameof(info)); - var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync(); - if (user == null) - throw new UserNotExistException(username); + var entity = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync(); + if (entity == null) + throw new UserNotExistException(id); - if (password != null) + if (info != null) { - user.EncryptedPassword = _passwordService.HashPassword(password); - } + var password = info.Password; + if (password != null) + { + entity.Password = _passwordService.HashPassword(password); + } - if (administrator != null) - { - user.RoleString = UserRoleConvert.ToString(administrator.Value); + var administrator = info.Administrator; + if (administrator.HasValue) + { + entity.Roles = UserRoleConvert.ToString(administrator.Value); + } + + var nickname = info.Nickname; + if (nickname != null) + { + entity.Nickname = nickname; + } } - user.Version += 1; - await _databaseContext.SaveChangesAsync(); - _logger.LogInformation(Resources.Services.UserService.LogDatabaseUpdate, ("Id", user.Id)); + entity.Version += 1; - //clear cache - RemoveCache(user.Id); + await _databaseContext.SaveChangesAsync(); + _logger.LogInformation(LogDatabaseUpdate, ("Id", id)); } public async Task DeleteUser(string username) @@ -299,7 +304,7 @@ namespace Timeline.Services throw new ArgumentNullException(nameof(username)); CheckUsernameFormat(username); - var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync(); + var user = await _databaseContext.Users.Where(u => u.Username == username).SingleOrDefaultAsync(); if (user == null) throw new UserNotExistException(username); @@ -309,7 +314,7 @@ namespace Timeline.Services ("Id", user.Id))); //clear cache - RemoveCache(user.Id); + await _cache.RemoveCache(user.Id); } public async Task ChangePassword(string username, string oldPassword, string newPassword) @@ -322,21 +327,21 @@ namespace Timeline.Services throw new ArgumentNullException(nameof(newPassword)); CheckUsernameFormat(username); - var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync(); + var user = await _databaseContext.Users.Where(u => u.Username == username).SingleOrDefaultAsync(); if (user == null) throw new UserNotExistException(username); - var verifyResult = _passwordService.VerifyPassword(user.EncryptedPassword, oldPassword); + var verifyResult = _passwordService.VerifyPassword(user.Password, oldPassword); if (!verifyResult) throw new BadPasswordException(oldPassword); - user.EncryptedPassword = _passwordService.HashPassword(newPassword); + user.Password = _passwordService.HashPassword(newPassword); user.Version += 1; await _databaseContext.SaveChangesAsync(); _logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseUpdate, ("Id", user.Id), ("Operation", "Change password"))); //clear cache - RemoveCache(user.Id); + await _cache.RemoveCache(user.Id); } public async Task ChangeUsername(string oldUsername, string newUsername) @@ -348,20 +353,22 @@ namespace Timeline.Services CheckUsernameFormat(oldUsername, Resources.Services.UserService.ExceptionOldUsernameBadFormat); CheckUsernameFormat(newUsername, Resources.Services.UserService.ExceptionNewUsernameBadFormat); - var user = await _databaseContext.Users.Where(u => u.Name == oldUsername).SingleOrDefaultAsync(); + var user = await _databaseContext.Users.Where(u => u.Username == oldUsername).SingleOrDefaultAsync(); if (user == null) throw new UserNotExistException(oldUsername); - var conflictUser = await _databaseContext.Users.Where(u => u.Name == newUsername).SingleOrDefaultAsync(); + var conflictUser = await _databaseContext.Users.Where(u => u.Username == newUsername).SingleOrDefaultAsync(); if (conflictUser != null) throw new UsernameConfictException(newUsername); - user.Name = newUsername; + user.Username = newUsername; user.Version += 1; await _databaseContext.SaveChangesAsync(); _logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseUpdate, ("Id", user.Id), ("Old Username", oldUsername), ("New Username", newUsername))); - RemoveCache(user.Id); + await _cache.RemoveCache(user.Id); } + + } } diff --git a/Timeline/Services/UserTokenManager.cs b/Timeline/Services/UserTokenManager.cs index c3cb51c9..a2c2980d 100644 --- a/Timeline/Services/UserTokenManager.cs +++ b/Timeline/Services/UserTokenManager.cs @@ -8,7 +8,7 @@ namespace Timeline.Services public class UserTokenCreateResult { public string Token { get; set; } = default!; - public UserInfo User { get; set; } = default!; + public User User { get; set; } = default!; } public interface IUserTokenManager @@ -36,7 +36,7 @@ namespace Timeline.Services /// Thrown when the token is of bad version. /// Thrown when the token is of bad format. /// Thrown when the user specified by the token does not exist. Usually the user had been deleted after the token was issued. - public Task VerifyToken(string token); + public Task VerifyToken(string token); } public class UserTokenManager : IUserTokenManager @@ -68,7 +68,7 @@ namespace Timeline.Services } - public async Task VerifyToken(string token) + public async Task VerifyToken(string token) { if (token == null) throw new ArgumentNullException(nameof(token)); diff --git a/Timeline/Services/UsernameBadFormatException.cs b/Timeline/Services/UsernameBadFormatException.cs deleted file mode 100644 index ad0350b5..00000000 --- a/Timeline/Services/UsernameBadFormatException.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; - -namespace Timeline.Services -{ - /// - /// Thrown when username is of bad format. - /// - [Serializable] - 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, string validationMessage) : this() { Username = username; ValidationMessage = validationMessage; } - - public UsernameBadFormatException(string username, string validationMessage, string message) : this(message) { Username = username; ValidationMessage = validationMessage; } - - protected UsernameBadFormatException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - /// - /// Username of bad format. - /// - public string Username { get; private set; } = ""; - - public string ValidationMessage { get; private set; } = ""; - } -} diff --git a/Timeline/Timeline.csproj b/Timeline/Timeline.csproj index 90588f70..195252d9 100644 --- a/Timeline/Timeline.csproj +++ b/Timeline/Timeline.csproj @@ -82,6 +82,11 @@ True Common.resx + + True + True + PasswordValidator.resx + True True @@ -102,11 +107,21 @@ True UserAvatarService.resx + + True + True + UserCache.resx + True True UserDetailService.resx + + True + True + UserManager.resx + True True @@ -152,6 +167,10 @@ ResXFileCodeGenerator Common.Designer.cs + + ResXFileCodeGenerator + PasswordValidator.Designer.cs + ResXFileCodeGenerator UsernameValidator.Designer.cs @@ -168,10 +187,18 @@ ResXFileCodeGenerator UserAvatarService.Designer.cs + + ResXFileCodeGenerator + UserCache.Designer.cs + ResXFileCodeGenerator UserDetailService.Designer.cs + + ResXFileCodeGenerator + UserManager.Designer.cs + ResXFileCodeGenerator UserService.Designer.cs -- cgit v1.2.3 From 401a0c86054711bf5ebdce7d7717c9b59bffc2fa Mon Sep 17 00:00:00 2001 From: crupest Date: Wed, 29 Jan 2020 18:38:41 +0800 Subject: ... --- Timeline.Tests/Controllers/UserControllerTest.cs | 4 +- Timeline/Auth/MyAuthenticationHandler.cs | 4 +- Timeline/Controllers/UserController.cs | 4 +- Timeline/Models/User.cs | 1 + Timeline/Models/Validation/NicknameValidator.cs | 26 ++ Timeline/Resources/Messages.zh.resx | 189 --------------- Timeline/Resources/Models/Http/Common.zh.resx | 132 ----------- .../Validation/NicknameValidator.Designer.cs | 72 ++++++ .../Models/Validation/NicknameValidator.resx | 123 ++++++++++ .../Validation/PasswordValidator.Designer.cs | 72 ------ .../Models/Validation/PasswordValidator.resx | 123 ---------- .../Models/Validation/PasswordValidator.zh.resx | 123 ---------- .../Models/Validation/UsernameValidator.zh.resx | 129 ---------- .../Resources/Models/Validation/Validator.zh.resx | 129 ---------- Timeline/Resources/Services/Exception.Designer.cs | 18 +- Timeline/Resources/Services/Exception.resx | 6 +- .../Resources/Services/UserManager.Designer.cs | 72 ------ Timeline/Resources/Services/UserManager.resx | 123 ---------- .../Resources/Services/UserService.Designer.cs | 36 +-- Timeline/Resources/Services/UserService.resx | 18 +- Timeline/Services/ConfictException.cs | 21 ++ Timeline/Services/UserService.cs | 263 +++++++++++++-------- Timeline/Services/UserTokenException.cs | 3 - Timeline/Services/UserTokenManager.cs | 4 +- Timeline/Services/UsernameConfictException.cs | 25 -- Timeline/Startup.cs | 19 -- Timeline/Timeline.csproj | 17 +- 27 files changed, 454 insertions(+), 1302 deletions(-) create mode 100644 Timeline/Models/Validation/NicknameValidator.cs delete mode 100644 Timeline/Resources/Messages.zh.resx delete mode 100644 Timeline/Resources/Models/Http/Common.zh.resx create mode 100644 Timeline/Resources/Models/Validation/NicknameValidator.Designer.cs create mode 100644 Timeline/Resources/Models/Validation/NicknameValidator.resx delete mode 100644 Timeline/Resources/Models/Validation/PasswordValidator.Designer.cs delete mode 100644 Timeline/Resources/Models/Validation/PasswordValidator.resx delete mode 100644 Timeline/Resources/Models/Validation/PasswordValidator.zh.resx delete mode 100644 Timeline/Resources/Models/Validation/UsernameValidator.zh.resx delete mode 100644 Timeline/Resources/Models/Validation/Validator.zh.resx delete mode 100644 Timeline/Resources/Services/UserManager.Designer.cs delete mode 100644 Timeline/Resources/Services/UserManager.resx create mode 100644 Timeline/Services/ConfictException.cs delete mode 100644 Timeline/Services/UsernameConfictException.cs diff --git a/Timeline.Tests/Controllers/UserControllerTest.cs b/Timeline.Tests/Controllers/UserControllerTest.cs index 192d53dd..3890712a 100644 --- a/Timeline.Tests/Controllers/UserControllerTest.cs +++ b/Timeline.Tests/Controllers/UserControllerTest.cs @@ -40,7 +40,7 @@ namespace Timeline.Tests.Controllers new Models.User { Id = 1, Username = "aaa", Administrator = true, Version = 1 }, new Models.User { Id = 2, Username = "bbb", Administrator = false, Version = 1 } }; - _mockUserService.Setup(s => s.ListUsers()).ReturnsAsync(mockUserList); + _mockUserService.Setup(s => s.GetUsers()).ReturnsAsync(mockUserList); var action = await _controller.List(); action.Result.Should().BeAssignableTo() .Which.Value.Should().BeEquivalentTo( @@ -165,7 +165,7 @@ namespace Timeline.Tests.Controllers [Theory] [InlineData(typeof(UserNotExistException), ErrorCodes.UserCommon.NotExist)] - [InlineData(typeof(UsernameConfictException), ErrorCodes.UserController.ChangeUsername_Conflict)] + [InlineData(typeof(ConfictException), ErrorCodes.UserController.ChangeUsername_Conflict)] public async Task Op_ChangeUsername_Failure(Type exceptionType, int code) { const string oldUsername = "aaa"; diff --git a/Timeline/Auth/MyAuthenticationHandler.cs b/Timeline/Auth/MyAuthenticationHandler.cs index 5bae5117..e6b26c4b 100644 --- a/Timeline/Auth/MyAuthenticationHandler.cs +++ b/Timeline/Auth/MyAuthenticationHandler.cs @@ -82,9 +82,9 @@ namespace Timeline.Auth var userInfo = await _userTokenManager.VerifyToken(token); var identity = new ClaimsIdentity(AuthenticationConstants.Scheme); - identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userInfo.Id.ToString(CultureInfo.InvariantCulture), ClaimValueTypes.Integer64)); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userInfo.Id!.Value.ToString(CultureInfo.InvariantCulture), ClaimValueTypes.Integer64)); identity.AddClaim(new Claim(identity.NameClaimType, userInfo.Username, ClaimValueTypes.String)); - identity.AddClaims(UserRoleConvert.ToArray(userInfo.Administrator).Select(role => new Claim(identity.RoleClaimType, role, ClaimValueTypes.String))); + identity.AddClaims(UserRoleConvert.ToArray(userInfo.Administrator!.Value).Select(role => new Claim(identity.RoleClaimType, role, ClaimValueTypes.String))); var principal = new ClaimsPrincipal(); principal.AddIdentity(identity); diff --git a/Timeline/Controllers/UserController.cs b/Timeline/Controllers/UserController.cs index 5f1b7bd7..3305952a 100644 --- a/Timeline/Controllers/UserController.cs +++ b/Timeline/Controllers/UserController.cs @@ -29,7 +29,7 @@ namespace Timeline.Controllers [HttpGet("users"), AdminAuthorize] public async Task> List() { - return Ok(await _userService.ListUsers()); + return Ok(await _userService.GetUsers()); } [HttpGet("users/{username}"), AdminAuthorize] @@ -105,7 +105,7 @@ namespace Timeline.Controllers ("Old Username", request.OldUsername), ("New Username", request.NewUsername))); return BadRequest(ErrorResponse.UserCommon.NotExist()); } - catch (UsernameConfictException e) + catch (ConfictException e) { _logger.LogInformation(e, Log.Format(LogChangeUsernameConflict, ("Old Username", request.OldUsername), ("New Username", request.NewUsername))); diff --git a/Timeline/Models/User.cs b/Timeline/Models/User.cs index 05395022..2cead892 100644 --- a/Timeline/Models/User.cs +++ b/Timeline/Models/User.cs @@ -12,6 +12,7 @@ namespace Timeline.Models #region secret + public long? Id { get; set; } public string? Password { get; set; } public long? Version { get; set; } #endregion secret diff --git a/Timeline/Models/Validation/NicknameValidator.cs b/Timeline/Models/Validation/NicknameValidator.cs new file mode 100644 index 00000000..f6626a2a --- /dev/null +++ b/Timeline/Models/Validation/NicknameValidator.cs @@ -0,0 +1,26 @@ +using System; +using static Timeline.Resources.Models.Validation.NicknameValidator; + +namespace Timeline.Models.Validation +{ + public class NicknameValidator : Validator + { + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "Already checked in base.")] + protected override (bool, string) DoValidate(string value) + { + if (value.Length > 10) + return (false, MessageTooLong); + + return (true, GetSuccessMessage()); + } + } + + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)] + public class NicknameAttribute : ValidateWithAttribute + { + public NicknameAttribute() : base(typeof(NicknameValidator)) + { + + } + } +} diff --git a/Timeline/Resources/Messages.zh.resx b/Timeline/Resources/Messages.zh.resx deleted file mode 100644 index 6e52befd..00000000 --- a/Timeline/Resources/Messages.zh.resx +++ /dev/null @@ -1,189 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - 请求体太大。它不能超过{0}. - - - 实际的请求体长度比头中指示的大。 - - - 实际的请求体长度比头中指示的小。 - - - 你没有权限做此操作。 - - - 请求头Content-Length缺失或者格式不对。 - - - 请求头Content-Length不能为0。 - - - 请求头Content-Type缺失。 - - - 请求头If-Non-Match格式不对。 - - - 请求模型格式不对。 - - - 第{0}个做{1}操作的用户名格式错误。 - - - 第{0}个做{1}操作的用户不存在。 - - - 要删除的消息不存在。 - - - 用户名或密码错误。 - - - 符号格式错误。这个符号可能不是这个服务器创建的。 - - - 符号是一个旧版本。用户可能已经更新了信息。 - - - 符号过期了。 - - - 用户不存在。管理员可能已经删除了这个用户。 - - - 图片不是正方形。 - - - 解码图片失败。 - - - 图片格式与请求头中指示的不一样。 - - - 要操作的用户不存在。 - - - 旧密码错误。 - - - 新用户名已经存在。 - - \ No newline at end of file diff --git a/Timeline/Resources/Models/Http/Common.zh.resx b/Timeline/Resources/Models/Http/Common.zh.resx deleted file mode 100644 index de74ac3b..00000000 --- a/Timeline/Resources/Models/Http/Common.zh.resx +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - 删除了一个项目。 - - - 要删除的项目不存在,什么都没有修改。 - - - 创建了一个新项目。 - - - 修改了一个已存在的项目。 - - \ No newline at end of file diff --git a/Timeline/Resources/Models/Validation/NicknameValidator.Designer.cs b/Timeline/Resources/Models/Validation/NicknameValidator.Designer.cs new file mode 100644 index 00000000..522f305a --- /dev/null +++ b/Timeline/Resources/Models/Validation/NicknameValidator.Designer.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Resources.Models.Validation { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class NicknameValidator { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal NicknameValidator() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Models.Validation.NicknameValidator", typeof(NicknameValidator).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Nickname is too long.. + /// + internal static string MessageTooLong { + get { + return ResourceManager.GetString("MessageTooLong", resourceCulture); + } + } + } +} diff --git a/Timeline/Resources/Models/Validation/NicknameValidator.resx b/Timeline/Resources/Models/Validation/NicknameValidator.resx new file mode 100644 index 00000000..b191b505 --- /dev/null +++ b/Timeline/Resources/Models/Validation/NicknameValidator.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Nickname is too long. + + \ No newline at end of file diff --git a/Timeline/Resources/Models/Validation/PasswordValidator.Designer.cs b/Timeline/Resources/Models/Validation/PasswordValidator.Designer.cs deleted file mode 100644 index e7630d26..00000000 --- a/Timeline/Resources/Models/Validation/PasswordValidator.Designer.cs +++ /dev/null @@ -1,72 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Timeline.Resources.Models.Validation { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class PasswordValidator { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal PasswordValidator() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Models.Validation.PasswordValidator", typeof(PasswordValidator).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to Password can't be empty.. - /// - internal static string MessageEmptyString { - get { - return ResourceManager.GetString("MessageEmptyString", resourceCulture); - } - } - } -} diff --git a/Timeline/Resources/Models/Validation/PasswordValidator.resx b/Timeline/Resources/Models/Validation/PasswordValidator.resx deleted file mode 100644 index f445cc75..00000000 --- a/Timeline/Resources/Models/Validation/PasswordValidator.resx +++ /dev/null @@ -1,123 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Password can't be empty. - - \ No newline at end of file diff --git a/Timeline/Resources/Models/Validation/PasswordValidator.zh.resx b/Timeline/Resources/Models/Validation/PasswordValidator.zh.resx deleted file mode 100644 index 9eab7b4e..00000000 --- a/Timeline/Resources/Models/Validation/PasswordValidator.zh.resx +++ /dev/null @@ -1,123 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - 密码不能为空。 - - \ No newline at end of file diff --git a/Timeline/Resources/Models/Validation/UsernameValidator.zh.resx b/Timeline/Resources/Models/Validation/UsernameValidator.zh.resx deleted file mode 100644 index 89d519b0..00000000 --- a/Timeline/Resources/Models/Validation/UsernameValidator.zh.resx +++ /dev/null @@ -1,129 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - 空字符串是不允许的。 - - - 无效的字符,只能使用字母、数字、下划线和连字符。 - - - 太长了,不能大于26个字符。 - - \ No newline at end of file diff --git a/Timeline/Resources/Models/Validation/Validator.zh.resx b/Timeline/Resources/Models/Validation/Validator.zh.resx deleted file mode 100644 index 2f98e7e3..00000000 --- a/Timeline/Resources/Models/Validation/Validator.zh.resx +++ /dev/null @@ -1,129 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - 值不是类型{0}的实例。 - - - 值不能为null. - - - 验证成功。 - - \ No newline at end of file diff --git a/Timeline/Resources/Services/Exception.Designer.cs b/Timeline/Resources/Services/Exception.Designer.cs index 671c4b93..cada1788 100644 --- a/Timeline/Resources/Services/Exception.Designer.cs +++ b/Timeline/Resources/Services/Exception.Designer.cs @@ -114,6 +114,15 @@ namespace Timeline.Resources.Services { } } + /// + /// Looks up a localized string similar to A present resource conflicts with the given resource.. + /// + internal static string ConfictException { + get { + return ResourceManager.GetString("ConfictException", resourceCulture); + } + } + /// /// Looks up a localized string similar to The hashes password is of bad format. It might not be created by server.. /// @@ -312,15 +321,6 @@ namespace Timeline.Resources.Services { } } - /// - /// Looks up a localized string similar to The username already exists.. - /// - internal static string UsernameConfictException { - get { - return ResourceManager.GetString("UsernameConfictException", resourceCulture); - } - } - /// /// Looks up a localized string similar to The user does not exist.. /// diff --git a/Timeline/Resources/Services/Exception.resx b/Timeline/Resources/Services/Exception.resx index 3ae14d4e..2cb0f11a 100644 --- a/Timeline/Resources/Services/Exception.resx +++ b/Timeline/Resources/Services/Exception.resx @@ -135,6 +135,9 @@ The password is wrong. + + A present resource conflicts with the given resource. + The hashes password is of bad format. It might not be created by server. @@ -201,9 +204,6 @@ The use is not a member of the timeline. - - The username already exists. - The user does not exist. diff --git a/Timeline/Resources/Services/UserManager.Designer.cs b/Timeline/Resources/Services/UserManager.Designer.cs deleted file mode 100644 index 424499f8..00000000 --- a/Timeline/Resources/Services/UserManager.Designer.cs +++ /dev/null @@ -1,72 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Timeline.Resources.Services { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class UserManager { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal UserManager() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Services.UserManager", typeof(UserManager).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to A user has been created.. - /// - internal static string LogUserCreate { - get { - return ResourceManager.GetString("LogUserCreate", resourceCulture); - } - } - } -} diff --git a/Timeline/Resources/Services/UserManager.resx b/Timeline/Resources/Services/UserManager.resx deleted file mode 100644 index ecb89179..00000000 --- a/Timeline/Resources/Services/UserManager.resx +++ /dev/null @@ -1,123 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - A user has been created. - - \ No newline at end of file diff --git a/Timeline/Resources/Services/UserService.Designer.cs b/Timeline/Resources/Services/UserService.Designer.cs index 1b85546d..cdf7f390 100644 --- a/Timeline/Resources/Services/UserService.Designer.cs +++ b/Timeline/Resources/Services/UserService.Designer.cs @@ -69,6 +69,15 @@ namespace Timeline.Resources.Services { } } + /// + /// Looks up a localized string similar to Nickname is of bad format, because {}.. + /// + internal static string ExceptionNicknameBadFormat { + get { + return ResourceManager.GetString("ExceptionNicknameBadFormat", resourceCulture); + } + } + /// /// Looks up a localized string similar to Old username is of bad format.. /// @@ -88,11 +97,11 @@ namespace Timeline.Resources.Services { } /// - /// Looks up a localized string similar to Password can't be null or empty.. + /// Looks up a localized string similar to Password can't be null.. /// - internal static string ExceptionPasswordNullOrEmpty { + internal static string ExceptionPasswordNull { get { - return ResourceManager.GetString("ExceptionPasswordNullOrEmpty", resourceCulture); + return ResourceManager.GetString("ExceptionPasswordNull", resourceCulture); } } @@ -106,29 +115,20 @@ namespace Timeline.Resources.Services { } /// - /// Looks up a localized string similar to Username can't be null or empty.. - /// - internal static string ExceptionUsernameNullOrEmpty { - get { - return ResourceManager.GetString("ExceptionUsernameNullOrEmpty", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A cache entry is created.. + /// Looks up a localized string similar to A user with given username already exists.. /// - internal static string LogCacheCreate { + internal static string ExceptionUsernameConflict { get { - return ResourceManager.GetString("LogCacheCreate", resourceCulture); + return ResourceManager.GetString("ExceptionUsernameConflict", resourceCulture); } } /// - /// Looks up a localized string similar to A cache entry is removed.. + /// Looks up a localized string similar to Username can't be null.. /// - internal static string LogCacheRemove { + internal static string ExceptionUsernameNull { get { - return ResourceManager.GetString("LogCacheRemove", resourceCulture); + return ResourceManager.GetString("ExceptionUsernameNull", resourceCulture); } } diff --git a/Timeline/Resources/Services/UserService.resx b/Timeline/Resources/Services/UserService.resx index 26221770..09bd4abb 100644 --- a/Timeline/Resources/Services/UserService.resx +++ b/Timeline/Resources/Services/UserService.resx @@ -120,26 +120,26 @@ New username is of bad format. + + Nickname is of bad format, because {}. + Old username is of bad format. Password can't be empty. - - Password can't be null or empty. + + Password can't be null. Username is of bad format, because {}. - - Username can't be null or empty. - - - A cache entry is created. + + A user with given username already exists. - - A cache entry is removed. + + Username can't be null. A new user entry is added to the database. diff --git a/Timeline/Services/ConfictException.cs b/Timeline/Services/ConfictException.cs new file mode 100644 index 00000000..dcd77366 --- /dev/null +++ b/Timeline/Services/ConfictException.cs @@ -0,0 +1,21 @@ +using System; + +namespace Timeline.Services +{ + /// + /// Thrown when a resource already exists and conflicts with the given resource. + /// + /// + /// For example a username already exists and conflicts with the given username. + /// + [Serializable] + public class ConfictException : Exception + { + public ConfictException() : base(Resources.Services.Exception.ConfictException) { } + public ConfictException(string message) : base(message) { } + public ConfictException(string message, Exception inner) : base(message, inner) { } + protected ConfictException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + } +} diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs index c5595c99..616e70ba 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -21,7 +21,7 @@ namespace Timeline.Services /// The password of the user to verify. /// The user info and auth info. /// Thrown when or is null. - /// Thrown when username is of bad format. + /// Thrown when is of bad format or is empty. /// Thrown when the user with given username does not exist. /// Thrown when password is wrong. Task VerifyCredential(string username, string password); @@ -48,7 +48,7 @@ namespace Timeline.Services /// List all users. /// /// The user info of users. - Task ListUsers(); + Task GetUsers(); /// /// Create a user with given info. @@ -58,11 +58,12 @@ namespace Timeline.Services /// The id of the new user. /// Thrown when is null. /// Thrown when some fields in is bad. - /// Thrown when a user with given username already exists. + /// Thrown when a user with given username already exists. /// /// must not be null and must be a valid username. /// must not be null or empty. /// is false by default (null). + /// must be a valid nickname if set. It is empty by default. /// Other fields are ignored. /// Task CreateUser(User info); @@ -75,61 +76,70 @@ namespace Timeline.Services /// Thrown when some fields in is bad. /// Thrown when user with given id does not exist. /// - /// Only , and will be used. + /// Only , , and will be used. /// If null, then not change. /// Other fields are ignored. /// After modified, even if nothing is changed, version will increase. /// - /// can't be empty. + /// must be a valid username if set. + /// can't be empty if set. + /// must be a valid nickname if set. /// /// Note: Whether is set or not, version will increase and not set to the specified value if there is one. /// + /// Task ModifyUser(long id, User? info); /// - /// Partially modify a user of given username. + /// Modify a user's info. + /// + /// The username of the user. + /// The new info. May be null. + /// Thrown when is null. + /// Thrown when is of bad format or some fields in is bad. + /// Thrown when user with given id does not exist. + /// + /// Only , and will be used. + /// If null, then not change. + /// Other fields are ignored. + /// After modified, even if nothing is changed, version will increase. + /// + /// must be a valid username if set. + /// can't be empty if set. + /// must be a valid nickname if set. /// - /// Note that whether actually modified or not, Version of the user will always increase. + /// Note: Whether is set or not, version will increase and not set to the specified value if there is one. + /// + /// + Task ModifyUser(string username, User? info); + + /// + /// Delete a user of given id. /// - /// Username of the user to modify. Can't be null. - /// New password. Null if not modify. - /// Whether the user is administrator. Null if not modify. - /// Thrown if is null. - /// Thrown when is of bad format. - /// Thrown if the user with given username does not exist. - Task PatchUser(string username, string? password, bool? administrator); + /// Id of the user to delete. + /// True if user is deleted, false if user not exist. + Task DeleteUser(long id); /// /// Delete a user of given username. /// - /// Username of thet user to delete. Can't be null. + /// Username of the user to delete. Can't be null. + /// True if user is deleted, false if user not exist. /// Thrown if is null. - /// Thrown when is of bad format. - /// Thrown if the user with given username does not exist. - Task DeleteUser(string username); + /// Thrown when is of bad format. + Task DeleteUser(string username); /// /// Try to change a user's password with old password. /// - /// The name of user to change password of. - /// The user's old password. - /// The user's new password. - /// Thrown if or or is null. - /// Thrown when is of bad format. + /// The id of user to change password of. + /// Old password. + /// New password. + /// Thrown if or is null. + /// Thrown if or is empty. /// Thrown if the user with given username does not exist. /// Thrown if the old password is wrong. - Task ChangePassword(string username, string oldPassword, string newPassword); - - /// - /// Change a user's username. - /// - /// The user's old username. - /// The new username. - /// Thrown if or is null. - /// Thrown if the user with old username does not exist. - /// Thrown if the or is of bad format. - /// Thrown if user with the new username already exists. - Task ChangeUsername(string oldUsername, string newUsername); + Task ChangePassword(long id, string oldPassword, string newPassword); } public class UserService : IUserService @@ -138,11 +148,10 @@ namespace Timeline.Services private readonly DatabaseContext _databaseContext; - private readonly IPasswordService _passwordService; private readonly UsernameValidator _usernameValidator = new UsernameValidator(); - + private readonly NicknameValidator _nicknameValidator = new NicknameValidator(); public UserService(ILogger logger, DatabaseContext databaseContext, IPasswordService passwordService) { _logger = logger; @@ -150,17 +159,35 @@ namespace Timeline.Services _passwordService = passwordService; } - private void CheckUsernameFormat(string username, string? paramName, Func? messageBuilder = null) + private void CheckUsernameFormat(string username, string? paramName) { if (!_usernameValidator.Validate(username, out var message)) { - if (messageBuilder == null) - throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, ExceptionUsernameBadFormat, message), paramName); - else - throw new ArgumentException(messageBuilder(message), paramName); + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, ExceptionUsernameBadFormat, message), paramName); + } + } + + private static void CheckPasswordFormat(string password, string? paramName) + { + if (password.Length == 0) + { + throw new ArgumentException(ExceptionPasswordEmpty, paramName); } } + private void CheckNicknameFormat(string nickname, string? paramName) + { + if (!_nicknameValidator.Validate(nickname, out var message)) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, ExceptionNicknameBadFormat, message), paramName); + } + } + + private static void ThrowUsernameConflict() + { + throw new ConfictException(ExceptionUsernameConflict); + } + private static User CreateUserFromEntity(UserEntity entity) { return new User @@ -168,6 +195,7 @@ namespace Timeline.Services Username = entity.Username, Administrator = UserRoleConvert.ToBool(entity.Roles), Nickname = string.IsNullOrEmpty(entity.Nickname) ? entity.Username : entity.Nickname, + Id = entity.Id, Version = entity.Version }; } @@ -180,6 +208,7 @@ namespace Timeline.Services throw new ArgumentNullException(nameof(password)); CheckUsernameFormat(username, nameof(username)); + CheckPasswordFormat(password, nameof(password)); var entity = await _databaseContext.Users.Where(u => u.Username == username).SingleOrDefaultAsync(); @@ -217,7 +246,7 @@ namespace Timeline.Services return CreateUserFromEntity(entity); } - public async Task ListUsers() + public async Task GetUsers() { var entities = await _databaseContext.Users.ToArrayAsync(); return entities.Select(user => CreateUserFromEntity(user)).ToArray(); @@ -228,20 +257,22 @@ namespace Timeline.Services if (info == null) throw new ArgumentNullException(nameof(info)); - if (string.IsNullOrEmpty(info.Username)) - throw new ArgumentException(ExceptionUsernameNullOrEmpty, nameof(info)); - + if (info.Username == null) + throw new ArgumentException(ExceptionUsernameNull, nameof(info)); CheckUsernameFormat(info.Username, nameof(info)); - if (string.IsNullOrEmpty(info.Password)) - throw new ArgumentException(ExceptionPasswordNullOrEmpty); + if (info.Password == null) + throw new ArgumentException(ExceptionPasswordNull, nameof(info)); + CheckPasswordFormat(info.Password, nameof(info)); + + if (info.Nickname != null) + CheckNicknameFormat(info.Nickname, nameof(info)); var username = info.Username; var conflict = await _databaseContext.Users.AnyAsync(u => u.Username == username); - if (conflict) - throw new UsernameConfictException(username); + ThrowUsernameConflict(); var administrator = info.Administrator ?? false; var password = info.Password; @@ -262,17 +293,35 @@ namespace Timeline.Services return newEntity.Id; } - public async Task ModifyUser(long id, User? info) + private void ValidateModifyUserInfo(User? info) { - if (info != null && info.Password != null && info.Password.Length == 0) - throw new ArgumentException(ExceptionPasswordEmpty, nameof(info)); + if (info != null) + { + if (info.Username != null) + CheckUsernameFormat(info.Username, nameof(info)); - var entity = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync(); - if (entity == null) - throw new UserNotExistException(id); + if (info.Password != null) + CheckPasswordFormat(info.Password, nameof(info)); + + if (info.Nickname != null) + CheckNicknameFormat(info.Nickname, nameof(info)); + } + } + private async Task UpdateUserEntity(UserEntity entity, User? info) + { if (info != null) { + var username = info.Username; + if (username != null) + { + var conflict = await _databaseContext.Users.AnyAsync(u => u.Username == username); + if (conflict) + ThrowUsernameConflict(); + + entity.Username = username; + } + var password = info.Password; if (password != null) { @@ -293,82 +342,90 @@ namespace Timeline.Services } entity.Version += 1; + } + + + public async Task ModifyUser(long id, User? info) + { + ValidateModifyUserInfo(info); + + var entity = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync(); + if (entity == null) + throw new UserNotExistException(id); + + await UpdateUserEntity(entity, info); await _databaseContext.SaveChangesAsync(); _logger.LogInformation(LogDatabaseUpdate, ("Id", id)); } - public async Task DeleteUser(string username) + public async Task ModifyUser(string username, User? info) { if (username == null) throw new ArgumentNullException(nameof(username)); - CheckUsernameFormat(username); + CheckUsernameFormat(username, nameof(username)); - var user = await _databaseContext.Users.Where(u => u.Username == username).SingleOrDefaultAsync(); - if (user == null) + ValidateModifyUserInfo(info); + + var entity = await _databaseContext.Users.Where(u => u.Username == username).SingleOrDefaultAsync(); + if (entity == null) throw new UserNotExistException(username); - _databaseContext.Users.Remove(user); + await UpdateUserEntity(entity, info); + await _databaseContext.SaveChangesAsync(); - _logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseRemove, - ("Id", user.Id))); + _logger.LogInformation(LogDatabaseUpdate, ("Username", username)); + } + + public async Task DeleteUser(long id) + { + var user = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync(); + if (user == null) + return false; - //clear cache - await _cache.RemoveCache(user.Id); + _databaseContext.Users.Remove(user); + await _databaseContext.SaveChangesAsync(); + _logger.LogInformation(Log.Format(LogDatabaseRemove, ("Id", id), ("Username", user.Username))); + return true; } - public async Task ChangePassword(string username, string oldPassword, string newPassword) + public async Task DeleteUser(string username) { if (username == null) throw new ArgumentNullException(nameof(username)); - if (oldPassword == null) - throw new ArgumentNullException(nameof(oldPassword)); - if (newPassword == null) - throw new ArgumentNullException(nameof(newPassword)); - CheckUsernameFormat(username); + CheckUsernameFormat(username, nameof(username)); var user = await _databaseContext.Users.Where(u => u.Username == username).SingleOrDefaultAsync(); if (user == null) - throw new UserNotExistException(username); - - var verifyResult = _passwordService.VerifyPassword(user.Password, oldPassword); - if (!verifyResult) - throw new BadPasswordException(oldPassword); + return false; - user.Password = _passwordService.HashPassword(newPassword); - user.Version += 1; + _databaseContext.Users.Remove(user); await _databaseContext.SaveChangesAsync(); - _logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseUpdate, - ("Id", user.Id), ("Operation", "Change password"))); - //clear cache - await _cache.RemoveCache(user.Id); + _logger.LogInformation(Log.Format(LogDatabaseRemove, ("Id", user.Id), ("Username", username))); + return true; } - public async Task ChangeUsername(string oldUsername, string newUsername) + public async Task ChangePassword(long id, string oldPassword, string newPassword) { - if (oldUsername == null) - throw new ArgumentNullException(nameof(oldUsername)); - if (newUsername == null) - throw new ArgumentNullException(nameof(newUsername)); - CheckUsernameFormat(oldUsername, Resources.Services.UserService.ExceptionOldUsernameBadFormat); - CheckUsernameFormat(newUsername, Resources.Services.UserService.ExceptionNewUsernameBadFormat); - - var user = await _databaseContext.Users.Where(u => u.Username == oldUsername).SingleOrDefaultAsync(); - if (user == null) - throw new UserNotExistException(oldUsername); + if (oldPassword == null) + throw new ArgumentNullException(nameof(oldPassword)); + if (newPassword == null) + throw new ArgumentNullException(nameof(newPassword)); + CheckPasswordFormat(oldPassword, nameof(oldPassword)); + CheckPasswordFormat(newPassword, nameof(newPassword)); - var conflictUser = await _databaseContext.Users.Where(u => u.Username == newUsername).SingleOrDefaultAsync(); - if (conflictUser != null) - throw new UsernameConfictException(newUsername); + var entity = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync(); - user.Username = newUsername; - user.Version += 1; - await _databaseContext.SaveChangesAsync(); - _logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseUpdate, - ("Id", user.Id), ("Old Username", oldUsername), ("New Username", newUsername))); - await _cache.RemoveCache(user.Id); - } + if (entity == null) + throw new UserNotExistException(id); + if (!_passwordService.VerifyPassword(entity.Password, oldPassword)) + throw new BadPasswordException(oldPassword); + entity.Password = _passwordService.HashPassword(newPassword); + entity.Version += 1; + await _databaseContext.SaveChangesAsync(); + _logger.LogInformation(Log.Format(LogDatabaseUpdate, ("Id", id), ("Operation", "Change password"))); + } } } diff --git a/Timeline/Services/UserTokenException.cs b/Timeline/Services/UserTokenException.cs index e63305b1..ed0bae1a 100644 --- a/Timeline/Services/UserTokenException.cs +++ b/Timeline/Services/UserTokenException.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; namespace Timeline.Services { diff --git a/Timeline/Services/UserTokenManager.cs b/Timeline/Services/UserTokenManager.cs index a2c2980d..3e9ef3d4 100644 --- a/Timeline/Services/UserTokenManager.cs +++ b/Timeline/Services/UserTokenManager.cs @@ -62,7 +62,7 @@ namespace Timeline.Services throw new ArgumentNullException(nameof(password)); var user = await _userService.VerifyCredential(username, password); - var token = _userTokenService.GenerateToken(new UserTokenInfo { Id = user.Id, Version = user.Version, ExpireAt = expireAt }); + var token = _userTokenService.GenerateToken(new UserTokenInfo { Id = user.Id!.Value, Version = user.Version!.Value, ExpireAt = expireAt }); return new UserTokenCreateResult { Token = token, User = user }; } @@ -85,7 +85,7 @@ namespace Timeline.Services var user = await _userService.GetUserById(tokenInfo.Id); if (tokenInfo.Version < user.Version) - throw new UserTokenBadVersionException(token, tokenInfo.Version, user.Version); + throw new UserTokenBadVersionException(token, tokenInfo.Version, user.Version.Value); return user; } diff --git a/Timeline/Services/UsernameConfictException.cs b/Timeline/Services/UsernameConfictException.cs deleted file mode 100644 index fde1eda6..00000000 --- a/Timeline/Services/UsernameConfictException.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using Timeline.Helpers; - -namespace Timeline.Services -{ - /// - /// Thrown when the user already exists. - /// - [Serializable] - public class UsernameConfictException : Exception - { - public UsernameConfictException() : base(Resources.Services.Exception.UsernameConfictException) { } - public UsernameConfictException(string username) : base(Log.Format(Resources.Services.Exception.UsernameConfictException, ("Username", username))) { Username = username; } - public UsernameConfictException(string username, string message) : base(message) { Username = username; } - public UsernameConfictException(string message, Exception inner) : base(message, inner) { } - protected UsernameConfictException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - /// - /// The username that already exists. - /// - public string? Username { get; set; } - } -} diff --git a/Timeline/Startup.cs b/Timeline/Startup.cs index 379ce6ea..091a16e5 100644 --- a/Timeline/Startup.cs +++ b/Timeline/Startup.cs @@ -1,14 +1,11 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.HttpOverrides; -using Microsoft.AspNetCore.Localization; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using System; -using System.Collections.Generic; -using System.Globalization; using System.Text.Json.Serialization; using Timeline.Auth; using Timeline.Configs; @@ -89,7 +86,6 @@ namespace Timeline services.AddTransient(); services.AddTransient(); services.AddUserAvatarService(); - services.AddScoped(); services.AddScoped(); @@ -113,8 +109,6 @@ namespace Timeline options.UseMySql(databaseConfig.ConnectionString); }); } - - services.AddMemoryCache(); } @@ -128,19 +122,6 @@ namespace Timeline app.UseRouting(); - var supportedCultures = new List - { - new CultureInfo("en"), - new CultureInfo("zh") - }; - - app.UseRequestLocalization(new RequestLocalizationOptions - { - DefaultRequestCulture = new RequestCulture("en"), - SupportedCultures = supportedCultures, - SupportedUICultures = supportedCultures - }); - app.UseCors(); app.UseAuthentication(); diff --git a/Timeline/Timeline.csproj b/Timeline/Timeline.csproj index 195252d9..82b45094 100644 --- a/Timeline/Timeline.csproj +++ b/Timeline/Timeline.csproj @@ -82,10 +82,10 @@ True Common.resx - + True True - PasswordValidator.resx + NicknameValidator.resx True @@ -117,11 +117,6 @@ True UserDetailService.resx - - True - True - UserManager.resx - True True @@ -167,9 +162,9 @@ ResXFileCodeGenerator Common.Designer.cs - + ResXFileCodeGenerator - PasswordValidator.Designer.cs + NicknameValidator.Designer.cs ResXFileCodeGenerator @@ -195,10 +190,6 @@ ResXFileCodeGenerator UserDetailService.Designer.cs - - ResXFileCodeGenerator - UserManager.Designer.cs - ResXFileCodeGenerator UserService.Designer.cs -- cgit v1.2.3 From dd0097af5c4ccbe25a1faca2286d729c93fd4116 Mon Sep 17 00:00:00 2001 From: crupest Date: Wed, 29 Jan 2020 23:13:15 +0800 Subject: ... --- Timeline/Controllers/ControllerAuthExtensions.cs | 30 +++++++ Timeline/Controllers/UserController.cs | 99 +++++++++++++----------- Timeline/Models/Http/User.cs | 41 ---------- Timeline/Models/Http/UserController.cs | 27 +++++++ Timeline/Models/PutResult.cs | 17 ---- Timeline/Models/User.cs | 20 ----- Timeline/Models/UserRoleConvert.cs | 44 ----------- Timeline/Resources/Messages.Designer.cs | 36 +++++++++ Timeline/Resources/Messages.resx | 12 +++ Timeline/Services/User.cs | 49 ++++++++++++ Timeline/Services/UserRoleConvert.cs | 44 +++++++++++ Timeline/Services/UserService.cs | 1 - 12 files changed, 250 insertions(+), 170 deletions(-) create mode 100644 Timeline/Controllers/ControllerAuthExtensions.cs delete mode 100644 Timeline/Models/Http/User.cs create mode 100644 Timeline/Models/Http/UserController.cs delete mode 100644 Timeline/Models/PutResult.cs delete mode 100644 Timeline/Models/User.cs delete mode 100644 Timeline/Models/UserRoleConvert.cs create mode 100644 Timeline/Services/User.cs create mode 100644 Timeline/Services/UserRoleConvert.cs diff --git a/Timeline/Controllers/ControllerAuthExtensions.cs b/Timeline/Controllers/ControllerAuthExtensions.cs new file mode 100644 index 00000000..81fd2428 --- /dev/null +++ b/Timeline/Controllers/ControllerAuthExtensions.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Mvc; +using System.Security.Claims; +using Timeline.Auth; +using System; + +namespace Timeline.Controllers +{ + public static class ControllerAuthExtensions + { + public static bool IsAdministrator(this ControllerBase controller) + { + return controller.User != null && controller.User.IsAdministrator(); + } + + public static long GetUserId(this ControllerBase controller) + { + if (controller.User == null) + throw new InvalidOperationException("Failed to get user id because User is null."); + + var claim = controller.User.FindFirst(ClaimTypes.NameIdentifier); + if (claim == null) + throw new InvalidOperationException("Failed to get user id because User has no NameIdentifier claim."); + + if (long.TryParse(claim.Value, out var value)) + return value; + + throw new InvalidOperationException("Failed to get user id because NameIdentifier claim is not a number."); + } + } +} diff --git a/Timeline/Controllers/UserController.cs b/Timeline/Controllers/UserController.cs index 3305952a..4c585198 100644 --- a/Timeline/Controllers/UserController.cs +++ b/Timeline/Controllers/UserController.cs @@ -1,15 +1,16 @@ using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -using System; +using System.Linq; using System.Threading.Tasks; using Timeline.Auth; using Timeline.Helpers; -using Timeline.Models; using Timeline.Models.Http; using Timeline.Models.Validation; using Timeline.Services; using static Timeline.Resources.Controllers.UserController; +using static Timeline.Resources.Messages; namespace Timeline.Controllers { @@ -26,19 +27,20 @@ namespace Timeline.Controllers _userService = userService; } - [HttpGet("users"), AdminAuthorize] + [HttpGet("users")] public async Task> List() { - return Ok(await _userService.GetUsers()); + var users = await _userService.GetUsers(); + return Ok(users.Select(u => u.EraseSecretAndFinalFill(Url, this.IsAdministrator())).ToArray()); } - [HttpGet("users/{username}"), AdminAuthorize] + [HttpGet("users/{username}")] public async Task> Get([FromRoute][Username] string username) { try { var user = await _userService.GetUserByUsername(username); - return Ok(user); + return Ok(user.EraseSecretAndFinalFill(Url, this.IsAdministrator())); } catch (UserNotExistException e) { @@ -47,33 +49,53 @@ namespace Timeline.Controllers } } - [HttpPut("users/{username}"), AdminAuthorize] - public async Task> Put([FromBody] UserPutRequest request, [FromRoute][Username] string username) + [HttpPatch("users/{username}"), Authorize] + public async Task Patch([FromBody] UserPatchRequest body, [FromRoute][Username] string username) { - var result = await _userService.PutUser(username, request.Password, request.Administrator!.Value); - switch (result) + static User Convert(UserPatchRequest body) { - case PutResult.Create: - return CreatedAtAction("Get", new { username }, CommonPutResponse.Create()); - case PutResult.Modify: - return Ok(CommonPutResponse.Modify()); - default: - throw new Exception(ExceptionUnknownPutResult); + return new User + { + Username = body.Username, + Password = body.Password, + Administrator = body.Administrator, + Nickname = body.Nickname + }; } - } - [HttpPatch("users/{username}"), AdminAuthorize] - public async Task Patch([FromBody] UserPatchRequest request, [FromRoute][Username] string username) - { - try + if (this.IsAdministrator()) { - await _userService.PatchUser(username, request.Password, request.Administrator); - return Ok(); + try + { + await _userService.ModifyUser(username, Convert(body)); + return Ok(); + } + catch (UserNotExistException e) + { + _logger.LogInformation(e, Log.Format(LogPatchUserNotExist, ("Username", username))); + return NotFound(ErrorResponse.UserCommon.NotExist()); + } } - catch (UserNotExistException e) + else { - _logger.LogInformation(e, Log.Format(LogPatchUserNotExist, ("Username", username))); - return NotFound(ErrorResponse.UserCommon.NotExist()); + if (User.Identity.Name != username) + return StatusCode(StatusCodes.Status403Forbidden, + ErrorResponse.Common.CustomMessage_Forbid(Common_Forbid_NotSelf)); + + if (body.Username != null) + return StatusCode(StatusCodes.Status403Forbidden, + ErrorResponse.Common.CustomMessage_Forbid(UserController_Patch_Forbid_Username)); + + if (body.Password != null) + return StatusCode(StatusCodes.Status403Forbidden, + ErrorResponse.Common.CustomMessage_Forbid(UserController_Patch_Forbid_Password)); + + if (body.Administrator != null) + return StatusCode(StatusCodes.Status403Forbidden, + ErrorResponse.Common.CustomMessage_Forbid(UserController_Patch_Forbid_Administrator)); + + await _userService.ModifyUser(this.GetUserId(), Convert(body)); + return Ok(); } } @@ -91,27 +113,10 @@ namespace Timeline.Controllers } } - [HttpPost("userop/changeusername"), AdminAuthorize] - public async Task ChangeUsername([FromBody] ChangeUsernameRequest request) + [HttpPost("userop/create"), AdminAuthorize] + public async Task CreateUser([FromBody] User body) { - try - { - await _userService.ChangeUsername(request.OldUsername, request.NewUsername); - return Ok(); - } - catch (UserNotExistException e) - { - _logger.LogInformation(e, Log.Format(LogChangeUsernameNotExist, - ("Old Username", request.OldUsername), ("New Username", request.NewUsername))); - return BadRequest(ErrorResponse.UserCommon.NotExist()); - } - catch (ConfictException e) - { - _logger.LogInformation(e, Log.Format(LogChangeUsernameConflict, - ("Old Username", request.OldUsername), ("New Username", request.NewUsername))); - return BadRequest(ErrorResponse.UserController.ChangeUsername_Conflict()); - } - // there is no need to catch bad format exception because it is already checked in model validation. + } [HttpPost("userop/changepassword"), Authorize] @@ -119,7 +124,7 @@ namespace Timeline.Controllers { try { - await _userService.ChangePassword(User.Identity.Name!, request.OldPassword, request.NewPassword); + await _userService.ChangePassword(this.GetUserId(), request.OldPassword, request.NewPassword); return Ok(); } catch (BadPasswordException e) diff --git a/Timeline/Models/Http/User.cs b/Timeline/Models/Http/User.cs deleted file mode 100644 index b3812f48..00000000 --- a/Timeline/Models/Http/User.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; -using Timeline.Models.Validation; - -namespace Timeline.Models.Http -{ - [Obsolete("Remove this.")] - public class UserPutRequest - { - [Required] - public string Password { get; set; } = default!; - [Required] - public bool? Administrator { get; set; } - } - - [Obsolete("Remove this.")] - public class UserPatchRequest - { - public string? Password { get; set; } - public bool? Administrator { get; set; } - } - - public class ChangeUsernameRequest - { - [Required] - [Username] - public string OldUsername { get; set; } = default!; - - [Required] - [Username] - public string NewUsername { get; set; } = default!; - } - - public class ChangePasswordRequest - { - [Required] - public string OldPassword { get; set; } = default!; - [Required] - public string NewPassword { get; set; } = default!; - } -} diff --git a/Timeline/Models/Http/UserController.cs b/Timeline/Models/Http/UserController.cs new file mode 100644 index 00000000..229ca1e5 --- /dev/null +++ b/Timeline/Models/Http/UserController.cs @@ -0,0 +1,27 @@ +using System.ComponentModel.DataAnnotations; +using Timeline.Models.Validation; + +namespace Timeline.Models.Http +{ + public class UserPatchRequest + { + [Username] + public string? Username { get; set; } + + [MinLength(1)] + public string? Password { get; set; } + + [Nickname] + public string? Nickname { get; set; } + + public bool? Administrator { get; set; } + } + + public class ChangePasswordRequest + { + [Required(AllowEmptyStrings = false)] + public string OldPassword { get; set; } = default!; + [Required(AllowEmptyStrings = false)] + public string NewPassword { get; set; } = default!; + } +} diff --git a/Timeline/Models/PutResult.cs b/Timeline/Models/PutResult.cs deleted file mode 100644 index cecf86e6..00000000 --- a/Timeline/Models/PutResult.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Timeline.Models -{ - /// - /// Represents the result of a "put" operation. - /// - public enum PutResult - { - /// - /// Indicates the item did not exist and now is created. - /// - Create, - /// - /// Indicates the item exists already and is modified. - /// - Modify - } -} diff --git a/Timeline/Models/User.cs b/Timeline/Models/User.cs deleted file mode 100644 index 2cead892..00000000 --- a/Timeline/Models/User.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Timeline.Models.Validation; - -namespace Timeline.Models -{ - public class User - { - [Username] - public string? Username { get; set; } - public bool? Administrator { get; set; } - public string? Nickname { get; set; } - public string? AvatarUrl { get; set; } - - - #region secret - public long? Id { get; set; } - public string? Password { get; set; } - public long? Version { get; set; } - #endregion secret - } -} diff --git a/Timeline/Models/UserRoleConvert.cs b/Timeline/Models/UserRoleConvert.cs deleted file mode 100644 index ade9a799..00000000 --- a/Timeline/Models/UserRoleConvert.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Timeline.Entities; - -namespace Timeline.Models -{ - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "No need.")] - public static class UserRoleConvert - { - public const string UserRole = UserRoles.User; - public const string AdminRole = UserRoles.Admin; - - public static string[] ToArray(bool administrator) - { - return administrator ? new string[] { UserRole, AdminRole } : new string[] { UserRole }; - } - - public static string[] ToArray(string s) - { - return s.Split(',').ToArray(); - } - - public static bool ToBool(IReadOnlyCollection roles) - { - return roles.Contains(AdminRole); - } - - public static string ToString(IReadOnlyCollection roles) - { - return string.Join(',', roles); - } - - public static string ToString(bool administrator) - { - return administrator ? UserRole + "," + AdminRole : UserRole; - } - - public static bool ToBool(string s) - { - return s.Contains("admin", StringComparison.InvariantCulture); - } - } -} diff --git a/Timeline/Resources/Messages.Designer.cs b/Timeline/Resources/Messages.Designer.cs index 8c13374f..15101661 100644 --- a/Timeline/Resources/Messages.Designer.cs +++ b/Timeline/Resources/Messages.Designer.cs @@ -96,6 +96,15 @@ namespace Timeline.Resources { } } + /// + /// Looks up a localized string similar to You are not the resource owner.. + /// + internal static string Common_Forbid_NotSelf { + get { + return ResourceManager.GetString("Common_Forbid_NotSelf", resourceCulture); + } + } + /// /// Looks up a localized string similar to Header Content-Length is missing or of bad format.. /// @@ -266,5 +275,32 @@ namespace Timeline.Resources { return ResourceManager.GetString("UserController_ChangeUsername_Conflict", resourceCulture); } } + + /// + /// Looks up a localized string similar to You can't set permission unless you are administrator.. + /// + internal static string UserController_Patch_Forbid_Administrator { + get { + return ResourceManager.GetString("UserController_Patch_Forbid_Administrator", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You can't set password unless you are administrator. If you want to change password, use /userop/changepassword .. + /// + internal static string UserController_Patch_Forbid_Password { + get { + return ResourceManager.GetString("UserController_Patch_Forbid_Password", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You can't set username unless you are administrator.. + /// + internal static string UserController_Patch_Forbid_Username { + get { + return ResourceManager.GetString("UserController_Patch_Forbid_Username", resourceCulture); + } + } } } diff --git a/Timeline/Resources/Messages.resx b/Timeline/Resources/Messages.resx index c5228ed5..db56ed02 100644 --- a/Timeline/Resources/Messages.resx +++ b/Timeline/Resources/Messages.resx @@ -129,6 +129,9 @@ You have no permission to do the operation. + + You are not the resource owner. + Header Content-Length is missing or of bad format. @@ -186,4 +189,13 @@ The new username already exists. + + You can't set permission unless you are administrator. + + + You can't set password unless you are administrator. If you want to change password, use /userop/changepassword . + + + You can't set username unless you are administrator. + \ No newline at end of file diff --git a/Timeline/Services/User.cs b/Timeline/Services/User.cs new file mode 100644 index 00000000..f63a374e --- /dev/null +++ b/Timeline/Services/User.cs @@ -0,0 +1,49 @@ +using Microsoft.AspNetCore.Mvc; +using System; +using Timeline.Controllers; + +namespace Timeline.Services +{ + public class User + { + public string? Username { get; set; } + public string? Nickname { get; set; } + public string? AvatarUrl { get; set; } + + #region adminsecret + public bool? Administrator { get; set; } + #endregion adminsecret + + #region secret + public long? Id { get; set; } + public string? Password { get; set; } + public long? Version { get; set; } + #endregion secret + } + + public static class UserExtensions + { + public static User EraseSecretAndFinalFill(this User user, IUrlHelper urlHelper, bool adminstrator) + { + if (user == null) + throw new ArgumentNullException(nameof(user)); + + var result = new User + { + Username = user.Username, + Nickname = user.Nickname, + AvatarUrl = urlHelper.ActionLink(action: nameof(UserAvatarController.Get), controller: nameof(UserAvatarController), values: new + { + user.Username + }) + }; + + if (adminstrator) + { + result.Administrator = user.Administrator; + } + + return result; + } + } +} diff --git a/Timeline/Services/UserRoleConvert.cs b/Timeline/Services/UserRoleConvert.cs new file mode 100644 index 00000000..4fa4a7b8 --- /dev/null +++ b/Timeline/Services/UserRoleConvert.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Timeline.Entities; + +namespace Timeline.Services +{ + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "No need.")] + public static class UserRoleConvert + { + public const string UserRole = UserRoles.User; + public const string AdminRole = UserRoles.Admin; + + public static string[] ToArray(bool administrator) + { + return administrator ? new string[] { UserRole, AdminRole } : new string[] { UserRole }; + } + + public static string[] ToArray(string s) + { + return s.Split(',').ToArray(); + } + + public static bool ToBool(IReadOnlyCollection roles) + { + return roles.Contains(AdminRole); + } + + public static string ToString(IReadOnlyCollection roles) + { + return string.Join(',', roles); + } + + public static string ToString(bool administrator) + { + return administrator ? UserRole + "," + AdminRole : UserRole; + } + + public static bool ToBool(string s) + { + return s.Contains("admin", StringComparison.InvariantCulture); + } + } +} diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs index 616e70ba..ff2306c5 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Threading.Tasks; using Timeline.Entities; using Timeline.Helpers; -using Timeline.Models; using Timeline.Models.Validation; using static Timeline.Resources.Services.UserService; -- cgit v1.2.3 From 79ab2b304d93b1029515bd3f954db4e5a73f4168 Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 30 Jan 2020 20:26:52 +0800 Subject: ... --- Timeline.ErrorCodes/ErrorCodes.cs | 4 +- .../Controllers/PersonalTimelineControllerTest.cs | 388 ------------------- Timeline.Tests/Controllers/TokenControllerTest.cs | 135 ------- Timeline.Tests/Controllers/UserControllerTest.cs | 228 ----------- .../Controllers/UserDetailControllerTest.cs | 98 ----- Timeline.Tests/DatabaseTest.cs | 63 --- Timeline.Tests/GlobalSuppressions.cs | 2 + Timeline.Tests/Helpers/MockUser.cs | 24 -- Timeline.Tests/Helpers/PrincipalHelper.cs | 23 -- Timeline.Tests/Helpers/ResponseAssertions.cs | 14 +- Timeline.Tests/Helpers/TestApplication.cs | 15 +- Timeline.Tests/Helpers/TestClock.cs | 15 - Timeline.Tests/Helpers/TestDatabase.cs | 89 ----- Timeline.Tests/Helpers/UseCultureAttribute.cs | 94 ----- .../IntegratedTests/AuthorizationTest.cs | 4 +- Timeline.Tests/IntegratedTests/I18nTest.cs | 59 --- .../IntegratedTests/IntegratedTestBase.cs | 117 ++++-- .../IntegratedTests/PersonalTimelineTest.cs | 37 +- Timeline.Tests/IntegratedTests/TokenTest.cs | 18 +- Timeline.Tests/IntegratedTests/UserAvatarTest.cs | 45 ++- Timeline.Tests/IntegratedTests/UserDetailTest.cs | 154 -------- Timeline.Tests/IntegratedTests/UserTest.cs | 427 ++++++++++++++------- Timeline.Tests/Services/UserAvatarServiceTest.cs | 280 -------------- Timeline.Tests/Services/UserDetailServiceTest.cs | 107 ------ Timeline.Tests/Services/UserTokenManagerTest.cs | 163 -------- Timeline.Tests/Timeline.Tests.csproj | 1 - Timeline/Auth/MyAuthenticationHandler.cs | 1 - Timeline/Controllers/ControllerAuthExtensions.cs | 15 + Timeline/Controllers/PersonalTimelineController.cs | 111 +++--- .../Controllers/Testing/TestingI18nController.cs | 36 -- Timeline/Controllers/TokenController.cs | 18 +- Timeline/Controllers/UserAvatarController.cs | 89 +++-- Timeline/Controllers/UserController.cs | 58 +-- Timeline/Entities/DatabaseContext.cs | 2 - Timeline/Entities/TimelineEntity.cs | 2 +- Timeline/Filters/Header.cs | 2 - Timeline/Filters/Timeline.cs | 1 - Timeline/Filters/User.cs | 68 ---- Timeline/Formatters/StringInputFormatter.cs | 1 - Timeline/GlobalSuppressions.cs | 1 + Timeline/Helpers/InvalidModelResponseFactory.cs | 1 - .../Models/Converters/JsonDateTimeConverter.cs | 1 - Timeline/Models/Http/ErrorResponse.cs | 18 +- Timeline/Models/Http/Timeline.cs | 42 -- Timeline/Models/Http/TimelineCommon.cs | 44 +++ Timeline/Models/Http/TimelineController.cs | 20 + Timeline/Models/Http/Token.cs | 32 -- Timeline/Models/Http/TokenController.cs | 32 ++ Timeline/Models/Http/UserController.cs | 26 ++ Timeline/Models/Http/UserInfo.cs | 58 +++ Timeline/Models/Timeline.cs | 55 --- Timeline/Models/Validation/NicknameValidator.cs | 1 - Timeline/Models/Validation/UsernameValidator.cs | 1 - .../Testing/TestingI18nController.Designer.cs | 72 ---- .../Controllers/Testing/TestingI18nController.resx | 123 ------ .../Testing/TestingI18nController.zh.resx | 123 ------ Timeline/Resources/Messages.Designer.cs | 27 +- Timeline/Resources/Messages.resx | 9 +- Timeline/Resources/Services/Exception.Designer.cs | 49 +-- Timeline/Resources/Services/Exception.resx | 17 +- .../Resources/Services/TimelineService.Designer.cs | 81 ++++ Timeline/Resources/Services/TimelineService.resx | 126 ++++++ Timeline/Resources/Services/UserCache.Designer.cs | 99 ----- Timeline/Resources/Services/UserCache.resx | 132 ------- .../Services/UserDetailService.Designer.cs | 99 ----- Timeline/Resources/Services/UserDetailService.resx | 132 ------- Timeline/Services/ConfictException.cs | 21 - Timeline/Services/ConflictException.cs | 21 + Timeline/Services/DatabaseExtensions.cs | 36 -- Timeline/Services/TimelineAlreadyExistException.cs | 17 - .../TimelineMemberOperationUserException.cs | 37 -- .../Services/TimelineNameBadFormatException.cs | 21 - Timeline/Services/TimelineService.cs | 363 ++++++------------ Timeline/Services/User.cs | 33 +- Timeline/Services/UserAvatarService.cs | 48 +-- Timeline/Services/UserRoleConvert.cs | 1 - Timeline/Services/UserService.cs | 36 +- Timeline/Services/UserTokenService.cs | 1 - Timeline/Startup.cs | 12 +- Timeline/Timeline.csproj | 60 ++- 80 files changed, 1232 insertions(+), 3904 deletions(-) delete mode 100644 Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs delete mode 100644 Timeline.Tests/Controllers/TokenControllerTest.cs delete mode 100644 Timeline.Tests/Controllers/UserControllerTest.cs delete mode 100644 Timeline.Tests/Controllers/UserDetailControllerTest.cs delete mode 100644 Timeline.Tests/DatabaseTest.cs delete mode 100644 Timeline.Tests/Helpers/MockUser.cs delete mode 100644 Timeline.Tests/Helpers/PrincipalHelper.cs delete mode 100644 Timeline.Tests/Helpers/TestClock.cs delete mode 100644 Timeline.Tests/Helpers/TestDatabase.cs delete mode 100644 Timeline.Tests/Helpers/UseCultureAttribute.cs delete mode 100644 Timeline.Tests/IntegratedTests/I18nTest.cs delete mode 100644 Timeline.Tests/IntegratedTests/UserDetailTest.cs delete mode 100644 Timeline.Tests/Services/UserAvatarServiceTest.cs delete mode 100644 Timeline.Tests/Services/UserDetailServiceTest.cs delete mode 100644 Timeline.Tests/Services/UserTokenManagerTest.cs delete mode 100644 Timeline/Controllers/Testing/TestingI18nController.cs delete mode 100644 Timeline/Filters/User.cs delete mode 100644 Timeline/Models/Http/Timeline.cs create mode 100644 Timeline/Models/Http/TimelineCommon.cs create mode 100644 Timeline/Models/Http/TimelineController.cs delete mode 100644 Timeline/Models/Http/Token.cs create mode 100644 Timeline/Models/Http/TokenController.cs create mode 100644 Timeline/Models/Http/UserInfo.cs delete mode 100644 Timeline/Models/Timeline.cs delete mode 100644 Timeline/Resources/Controllers/Testing/TestingI18nController.Designer.cs delete mode 100644 Timeline/Resources/Controllers/Testing/TestingI18nController.resx delete mode 100644 Timeline/Resources/Controllers/Testing/TestingI18nController.zh.resx create mode 100644 Timeline/Resources/Services/TimelineService.Designer.cs create mode 100644 Timeline/Resources/Services/TimelineService.resx delete mode 100644 Timeline/Resources/Services/UserCache.Designer.cs delete mode 100644 Timeline/Resources/Services/UserCache.resx delete mode 100644 Timeline/Resources/Services/UserDetailService.Designer.cs delete mode 100644 Timeline/Resources/Services/UserDetailService.resx delete mode 100644 Timeline/Services/ConfictException.cs create mode 100644 Timeline/Services/ConflictException.cs delete mode 100644 Timeline/Services/DatabaseExtensions.cs delete mode 100644 Timeline/Services/TimelineAlreadyExistException.cs delete mode 100644 Timeline/Services/TimelineMemberOperationUserException.cs delete mode 100644 Timeline/Services/TimelineNameBadFormatException.cs diff --git a/Timeline.ErrorCodes/ErrorCodes.cs b/Timeline.ErrorCodes/ErrorCodes.cs index 730f42e0..ea74cf0e 100644 --- a/Timeline.ErrorCodes/ErrorCodes.cs +++ b/Timeline.ErrorCodes/ErrorCodes.cs @@ -45,7 +45,7 @@ public static class UserController { - public const int ChangeUsername_Conflict = 1_102_01_01; + public const int UsernameConflict = 1_102_01_01; public const int ChangePassword_BadOldPassword = 1_102_02_01; } @@ -58,7 +58,7 @@ public static class TimelineController { - public const int PostOperationDelete_NotExist = 1_104_01_01; + public const int MemberPut_NotExist = 1_104_01_01; } } } diff --git a/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs b/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs deleted file mode 100644 index bbc8ba75..00000000 --- a/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs +++ /dev/null @@ -1,388 +0,0 @@ -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.Reflection; -using System.Threading.Tasks; -using Timeline.Controllers; -using Timeline.Filters; -using Timeline.Models; -using Timeline.Models.Http; -using Timeline.Models.Validation; -using Timeline.Services; -using Timeline.Tests.Helpers; -using Xunit; - -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.PostListGet)); - m.Should().BeDecoratedWith() - .And.BeDecoratedWith(); - AssertUsernameParameter(m); - } - - { - var m = type.GetMethod(nameof(PersonalTimelineController.PostOperationCreate)); - m.Should().BeDecoratedWith() - .And.BeDecoratedWith() - .And.BeDecoratedWith(); - AssertUsernameParameter(m); - AssertBodyParamter(m); - } - - { - var m = type.GetMethod(nameof(PersonalTimelineController.PostOperationDelete)); - 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); - } - } - - 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 PostListGet_Forbid() - { - const string username = "username"; - SetUser(false); - _service.Setup(s => s.HasReadPermission(username, authUsername)).ReturnsAsync(false); - var result = (await _controller.PostListGet(username)).Result - .Should().BeAssignableTo() - .Which; - result.StatusCode.Should().Be(StatusCodes.Status403Forbidden); - result.Value.Should().BeAssignableTo() - .Which.Code.Should().Be(ErrorCodes.Common.Forbid); - _service.VerifyAll(); - } - - [Fact] - public async Task PostListGet_Admin_Success() - { - const string username = "username"; - SetUser(true); - _service.Setup(s => s.GetPosts(username)).ReturnsAsync(new List()); - (await _controller.PostListGet(username)).Value - .Should().BeAssignableTo>() - .Which.Should().NotBeNull().And.BeEmpty(); - _service.VerifyAll(); - } - - [Fact] - 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.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.Common.Forbid); - _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(); - } - - [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.Common.Forbid); - _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.TimelineController.PostOperationDelete_NotExist); - _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 = 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.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.UserCommon.NotExist); - _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.Tests/Controllers/TokenControllerTest.cs b/Timeline.Tests/Controllers/TokenControllerTest.cs deleted file mode 100644 index 43e1a413..00000000 --- a/Timeline.Tests/Controllers/TokenControllerTest.cs +++ /dev/null @@ -1,135 +0,0 @@ -using FluentAssertions; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Timeline.Controllers; -using Timeline.Models; -using Timeline.Models.Http; -using Timeline.Services; -using Timeline.Tests.Helpers; -using Xunit; - -namespace Timeline.Tests.Controllers -{ - public class TokenControllerTest : IDisposable - { - private readonly Mock _mockUserService = new Mock(); - private readonly TestClock _mockClock = new TestClock(); - - - private readonly TokenController _controller; - - public TokenControllerTest() - { - _controller = new TokenController(_mockUserService.Object, NullLogger.Instance, _mockClock); - } - - public void Dispose() - { - _controller.Dispose(); - } - - [Theory] - [InlineData(null)] - [InlineData(100)] - public async Task Create_Ok(int? expire) - { - var mockCurrentTime = DateTime.Now; - _mockClock.MockCurrentTime = mockCurrentTime; - var mockCreateResult = new UserTokenCreateResult - { - Token = "mocktokenaaaaa", - User = new Models.User - { - Id = 1, - Username = MockUser.User.Username, - Administrator = MockUser.User.Administrator, - Version = 1 - } - }; - _mockUserService.Setup(s => s.CreateToken("u", "p", expire == null ? null : (DateTime?)mockCurrentTime.AddDays(expire.Value))).ReturnsAsync(mockCreateResult); - var action = await _controller.Create(new CreateTokenRequest - { - Username = "u", - Password = "p", - Expire = expire - }); - action.Result.Should().BeAssignableTo() - .Which.Value.Should().BeEquivalentTo(new CreateTokenResponse - { - Token = mockCreateResult.Token, - User = MockUser.User.Info - }); - } - - [Fact] - public async Task Create_UserNotExist() - { - _mockUserService.Setup(s => s.CreateToken("u", "p", null)).ThrowsAsync(new UserNotExistException("u")); - var action = await _controller.Create(new CreateTokenRequest - { - Username = "u", - Password = "p", - Expire = null - }); - action.Result.Should().BeAssignableTo() - .Which.Value.Should().BeAssignableTo() - .Which.Code.Should().Be(ErrorCodes.TokenController.Create_BadCredential); - } - - [Fact] - public async Task Create_BadPassword() - { - _mockUserService.Setup(s => s.CreateToken("u", "p", null)).ThrowsAsync(new BadPasswordException("u")); - var action = await _controller.Create(new CreateTokenRequest - { - Username = "u", - Password = "p", - Expire = null - }); - action.Result.Should().BeAssignableTo() - .Which.Value.Should().BeAssignableTo() - .Which.Code.Should().Be(ErrorCodes.TokenController.Create_BadCredential); - } - - [Fact] - public async Task Verify_Ok() - { - const string token = "aaaaaaaaaaaaaa"; - _mockUserService.Setup(s => s.VerifyToken(token)).ReturnsAsync(new Models.User - { - Id = 1, - Username = MockUser.User.Username, - Administrator = MockUser.User.Administrator, - Version = 1 - }); - var action = await _controller.Verify(new VerifyTokenRequest { Token = token }); - action.Result.Should().BeAssignableTo() - .Which.Value.Should().BeAssignableTo() - .Which.User.Should().BeEquivalentTo(MockUser.User.Info); - } - - public static IEnumerable Verify_BadRequest_Data() - { - yield return new object[] { new UserTokenTimeExpireException(), ErrorCodes.TokenController.Verify_TimeExpired }; - yield return new object[] { new UserTokenBadVersionException(), ErrorCodes.TokenController.Verify_OldVersion }; - yield return new object[] { new UserTokenBadFormatException(), ErrorCodes.TokenController.Verify_BadFormat }; - yield return new object[] { new UserNotExistException(), ErrorCodes.TokenController.Verify_UserNotExist }; - } - - [Theory] - [MemberData(nameof(Verify_BadRequest_Data))] - public async Task Verify_BadRequest(Exception e, int code) - { - const string token = "aaaaaaaaaaaaaa"; - _mockUserService.Setup(s => s.VerifyToken(token)).ThrowsAsync(e); - var action = await _controller.Verify(new VerifyTokenRequest { Token = token }); - action.Result.Should().BeAssignableTo() - .Which.Value.Should().BeAssignableTo() - .Which.Code.Should().Be(code); - } - } -} diff --git a/Timeline.Tests/Controllers/UserControllerTest.cs b/Timeline.Tests/Controllers/UserControllerTest.cs deleted file mode 100644 index 3890712a..00000000 --- a/Timeline.Tests/Controllers/UserControllerTest.cs +++ /dev/null @@ -1,228 +0,0 @@ -using FluentAssertions; -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.Security.Claims; -using System.Threading.Tasks; -using Timeline.Controllers; -using Timeline.Models; -using Timeline.Models.Http; -using Timeline.Services; -using Timeline.Tests.Helpers; -using Xunit; - -namespace Timeline.Tests.Controllers -{ - public class UserControllerTest : IDisposable - { - private readonly Mock _mockUserService = new Mock(); - - private readonly UserController _controller; - - public UserControllerTest() - { - _controller = new UserController(NullLogger.Instance, _mockUserService.Object); - } - - public void Dispose() - { - _controller.Dispose(); - } - - [Fact] - public async Task GetList_Success() - { - var mockUserList = new Models.User[] { - new Models.User { Id = 1, Username = "aaa", Administrator = true, Version = 1 }, - new Models.User { Id = 2, Username = "bbb", Administrator = false, Version = 1 } - }; - _mockUserService.Setup(s => s.GetUsers()).ReturnsAsync(mockUserList); - var action = await _controller.List(); - action.Result.Should().BeAssignableTo() - .Which.Value.Should().BeEquivalentTo( - mockUserList.Select(u => new User { Username = u.Username, Administrator = u.Administrator }).ToArray()); - } - - [Fact] - public async Task Get_Success() - { - const string username = "aaa"; - _mockUserService.Setup(s => s.GetUserByUsername(username)).ReturnsAsync(new Models.User - { - Id = 1, - Username = MockUser.User.Username, - Administrator = MockUser.User.Administrator, - Version = 1 - }); - var action = await _controller.Get(username); - action.Result.Should().BeAssignableTo() - .Which.Value.Should().BeEquivalentTo(MockUser.User.Info); - } - - [Fact] - public async Task Get_NotFound() - { - const string username = "aaa"; - _mockUserService.Setup(s => s.GetUserByUsername(username)).ThrowsAsync(new UserNotExistException()); - var action = await _controller.Get(username); - action.Result.Should().BeAssignableTo() - .Which.Value.Should().BeAssignableTo() - .Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist); - } - - [Theory] - [InlineData(PutResult.Create, true)] - [InlineData(PutResult.Modify, false)] - public async Task Put_Success(PutResult result, bool create) - { - const string username = "aaa"; - const string password = "ppp"; - const bool administrator = true; - _mockUserService.Setup(s => s.PutUser(username, password, administrator)).ReturnsAsync(result); - var action = await _controller.Put(new UserPutRequest - { - Password = password, - Administrator = administrator - }, username); - var response = action.Result.Should().BeAssignableTo() - .Which.Value.Should().BeAssignableTo() - .Which; - response.Code.Should().Be(0); - response.Data.Create.Should().Be(create); - } - - [Fact] - public async Task Patch_Success() - { - const string username = "aaa"; - const string password = "ppp"; - const bool administrator = true; - _mockUserService.Setup(s => s.PatchUser(username, password, administrator)).Returns(Task.CompletedTask); - var action = await _controller.Patch(new UserPatchRequest - { - Password = password, - Administrator = administrator - }, username); - action.Should().BeAssignableTo(); - } - - [Fact] - public async Task Patch_NotExist() - { - const string username = "aaa"; - const string password = "ppp"; - const bool administrator = true; - _mockUserService.Setup(s => s.PatchUser(username, password, administrator)).ThrowsAsync(new UserNotExistException()); - var action = await _controller.Patch(new UserPatchRequest - { - Password = password, - Administrator = administrator - }, username); - action.Should().BeAssignableTo() - .Which.Value.Should().BeAssignableTo() - .Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist); - } - - [Fact] - public async Task Delete_Delete() - { - const string username = "aaa"; - _mockUserService.Setup(s => s.DeleteUser(username)).Returns(Task.CompletedTask); - var action = await _controller.Delete(username); - var body = action.Result.Should().BeAssignableTo() - .Which.Value.Should().BeAssignableTo() - .Which; - body.Code.Should().Be(0); - body.Data.Delete.Should().BeTrue(); - } - - [Fact] - public async Task Delete_NotExist() - { - const string username = "aaa"; - _mockUserService.Setup(s => s.DeleteUser(username)).ThrowsAsync(new UserNotExistException()); - var action = await _controller.Delete(username); - var body = action.Result.Should().BeAssignableTo() - .Which.Value.Should().BeAssignableTo() - .Which; - body.Code.Should().Be(0); - body.Data.Delete.Should().BeFalse(); - } - - [Fact] - public async Task Op_ChangeUsername_Success() - { - const string oldUsername = "aaa"; - const string newUsername = "bbb"; - _mockUserService.Setup(s => s.ChangeUsername(oldUsername, newUsername)).Returns(Task.CompletedTask); - var action = await _controller.ChangeUsername(new ChangeUsernameRequest { OldUsername = oldUsername, NewUsername = newUsername }); - action.Should().BeAssignableTo(); - } - - [Theory] - [InlineData(typeof(UserNotExistException), ErrorCodes.UserCommon.NotExist)] - [InlineData(typeof(ConfictException), ErrorCodes.UserController.ChangeUsername_Conflict)] - public async Task Op_ChangeUsername_Failure(Type exceptionType, int code) - { - const string oldUsername = "aaa"; - const string newUsername = "bbb"; - _mockUserService.Setup(s => s.ChangeUsername(oldUsername, newUsername)).ThrowsAsync(Activator.CreateInstance(exceptionType) as Exception); - var action = await _controller.ChangeUsername(new ChangeUsernameRequest { OldUsername = oldUsername, NewUsername = newUsername }); - action.Should().BeAssignableTo() - .Which.Value.Should().BeAssignableTo() - .Which.Code.Should().Be(code); - } - - [Fact] - public async Task Op_ChangePassword_Success() - { - const string username = "aaa"; - const string oldPassword = "aaa"; - const string newPassword = "bbb"; - _mockUserService.Setup(s => s.ChangePassword(username, oldPassword, newPassword)).Returns(Task.CompletedTask); - - _controller.ControllerContext = new ControllerContext() - { - HttpContext = new DefaultHttpContext() - { - User = new ClaimsPrincipal(new ClaimsIdentity(new Claim[] - { - new Claim(ClaimTypes.Name, username) - }, "TestAuthType")) - } - }; - - var action = await _controller.ChangePassword(new ChangePasswordRequest { OldPassword = oldPassword, NewPassword = newPassword }); - action.Should().BeAssignableTo(); - } - - [Fact] - public async Task Op_ChangePassword_BadPassword() - { - const string username = "aaa"; - const string oldPassword = "aaa"; - const string newPassword = "bbb"; - _mockUserService.Setup(s => s.ChangePassword(username, oldPassword, newPassword)).ThrowsAsync(new BadPasswordException()); - - _controller.ControllerContext = new ControllerContext() - { - HttpContext = new DefaultHttpContext() - { - User = new ClaimsPrincipal(new ClaimsIdentity(new Claim[] - { - new Claim(ClaimTypes.Name, username) - }, "TestAuthType")) - } - }; - - var action = await _controller.ChangePassword(new ChangePasswordRequest { OldPassword = oldPassword, NewPassword = newPassword }); - action.Should().BeAssignableTo() - .Which.Value.Should().BeAssignableTo() - .Which.Code.Should().Be(ErrorCodes.UserController.ChangePassword_BadOldPassword); - } - } -} diff --git a/Timeline.Tests/Controllers/UserDetailControllerTest.cs b/Timeline.Tests/Controllers/UserDetailControllerTest.cs deleted file mode 100644 index ffd88790..00000000 --- a/Timeline.Tests/Controllers/UserDetailControllerTest.cs +++ /dev/null @@ -1,98 +0,0 @@ -using FluentAssertions; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Moq; -using System; -using System.ComponentModel.DataAnnotations; -using System.Threading.Tasks; -using Timeline.Controllers; -using Timeline.Filters; -using Timeline.Models.Validation; -using Timeline.Services; -using Timeline.Tests.Helpers; -using Xunit; - -namespace Timeline.Tests.Controllers -{ - public class UserDetailControllerTest : IDisposable - { - private readonly Mock _mockUserDetailService; - private readonly UserDetailController _controller; - - public UserDetailControllerTest() - { - _mockUserDetailService = new Mock(); - _controller = new UserDetailController(_mockUserDetailService.Object); - } - - public void Dispose() - { - _controller.Dispose(); - } - - [Fact] - public void AttributeTest() - { - typeof(UserDetailController).Should().BeDecoratedWith(); - - var getNickname = typeof(UserDetailController).GetMethod(nameof(UserDetailController.GetNickname)); - getNickname.Should().BeDecoratedWith() - .And.BeDecoratedWith(); - getNickname.GetParameter("username").Should().BeDecoratedWith() - .And.BeDecoratedWith(); - - var putNickname = typeof(UserDetailController).GetMethod(nameof(UserDetailController.PutNickname)); - putNickname.Should().BeDecoratedWith() - .And.BeDecoratedWith() - .And.BeDecoratedWith() - .And.BeDecoratedWith(); - putNickname.GetParameter("username").Should().BeDecoratedWith() - .And.BeDecoratedWith(); - var stringLengthAttributeOnPutBody = putNickname.GetParameter("body").Should().BeDecoratedWith() - .And.BeDecoratedWith() - .Which; - stringLengthAttributeOnPutBody.MinimumLength.Should().Be(1); - stringLengthAttributeOnPutBody.MaximumLength.Should().Be(10); - - var deleteNickname = typeof(UserDetailController).GetMethod(nameof(UserDetailController.DeleteNickname)); - deleteNickname.Should().BeDecoratedWith() - .And.BeDecoratedWith() - .And.BeDecoratedWith() - .And.BeDecoratedWith(); - deleteNickname.GetParameter("username").Should().BeDecoratedWith() - .And.BeDecoratedWith(); - } - - [Fact] - public async Task GetNickname_ShouldWork() - { - const string username = "uuu"; - const string nickname = "nnn"; - _mockUserDetailService.Setup(s => s.GetNickname(username)).ReturnsAsync(nickname); - var actionResult = await _controller.GetNickname(username); - actionResult.Result.Should().BeAssignableTo(nickname); - _mockUserDetailService.VerifyAll(); - } - - [Fact] - public async Task PutNickname_ShouldWork() - { - const string username = "uuu"; - const string nickname = "nnn"; - _mockUserDetailService.Setup(s => s.SetNickname(username, nickname)).Returns(Task.CompletedTask); - var actionResult = await _controller.PutNickname(username, nickname); - actionResult.Should().BeAssignableTo(); - _mockUserDetailService.VerifyAll(); - } - - [Fact] - public async Task DeleteNickname_ShouldWork() - { - const string username = "uuu"; - _mockUserDetailService.Setup(s => s.SetNickname(username, null)).Returns(Task.CompletedTask); - var actionResult = await _controller.DeleteNickname(username); - actionResult.Should().BeAssignableTo(); - _mockUserDetailService.VerifyAll(); - } - } -} diff --git a/Timeline.Tests/DatabaseTest.cs b/Timeline.Tests/DatabaseTest.cs deleted file mode 100644 index a15823a9..00000000 --- a/Timeline.Tests/DatabaseTest.cs +++ /dev/null @@ -1,63 +0,0 @@ -using FluentAssertions; -using System; -using System.Linq; -using Timeline.Entities; -using Timeline.Tests.Helpers; -using Xunit; - -namespace Timeline.Tests -{ - public class DatabaseTest : IDisposable - { - private readonly TestDatabase _database; - private readonly DatabaseContext _context; - - public DatabaseTest() - { - _database = new TestDatabase(); - _context = _database.Context; - } - - public void Dispose() - { - _database.Dispose(); - } - - [Fact] - public void DeleteUserShouldAlsoDeleteAvatar() - { - var user = _context.Users.First(); - _context.UserAvatars.Count().Should().Be(0); - _context.UserAvatars.Add(new UserAvatarEntity - { - Data = null, - Type = null, - ETag = null, - LastModified = DateTime.Now, - UserId = user.Id - }); - _context.SaveChanges(); - _context.UserAvatars.Count().Should().Be(1); - _context.Users.Remove(user); - _context.SaveChanges(); - _context.UserAvatars.Count().Should().Be(0); - } - - [Fact] - public void DeleteUserShouldAlsoDeleteDetail() - { - var user = _context.Users.First(); - _context.UserDetails.Count().Should().Be(0); - _context.UserDetails.Add(new UserDetailEntity - { - Nickname = null, - UserId = user.Id - }); - _context.SaveChanges(); - _context.UserDetails.Count().Should().Be(1); - _context.Users.Remove(user); - _context.SaveChanges(); - _context.UserDetails.Count().Should().Be(0); - } - } -} diff --git a/Timeline.Tests/GlobalSuppressions.cs b/Timeline.Tests/GlobalSuppressions.cs index 1d1d294b..0f873033 100644 --- a/Timeline.Tests/GlobalSuppressions.cs +++ b/Timeline.Tests/GlobalSuppressions.cs @@ -12,3 +12,5 @@ [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA1816:Dispose methods should call SuppressFinalize", Justification = "Test classes do not need to implement it that way.")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2234:Pass system uri objects instead of strings", Justification = "I really don't understand this rule.")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "Tests do not need make strings resources.")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1054:Uri parameters should not be strings", Justification = "That's unnecessary.")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1056:Uri properties should not be strings", Justification = "That's unnecessary.")] diff --git a/Timeline.Tests/Helpers/MockUser.cs b/Timeline.Tests/Helpers/MockUser.cs deleted file mode 100644 index 49576842..00000000 --- a/Timeline.Tests/Helpers/MockUser.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Collections.Generic; -using Timeline.Models.Http; - -namespace Timeline.Tests.Helpers -{ - public class MockUser - { - public MockUser(string username, string password, bool administrator) - { - Info = new User { Username = username, Administrator = administrator }; - Password = password; - } - - public User 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 deleted file mode 100644 index 89f3f7b1..00000000 --- a/Timeline.Tests/Helpers/PrincipalHelper.cs +++ /dev/null @@ -1,23 +0,0 @@ -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/ResponseAssertions.cs b/Timeline.Tests/Helpers/ResponseAssertions.cs index be0043dc..f01a0677 100644 --- a/Timeline.Tests/Helpers/ResponseAssertions.cs +++ b/Timeline.Tests/Helpers/ResponseAssertions.cs @@ -125,6 +125,13 @@ namespace Timeline.Tests.Helpers return assertions.HaveJsonBody(because, becauseArgs); } + public static void HaveCommonBody(this HttpResponseMessageAssertions assertions, int code, string message = null, params object[] messageArgs) + { + message = string.IsNullOrEmpty(message) ? "" : ", " + string.Format(CultureInfo.CurrentCulture, message, messageArgs); + var body = assertions.HaveCommonBody("Response body should be CommonResponse{0}", message).Which; + body.Code.Should().Be(code, "Response body code is not the specified one{0}", message); + } + public static AndWhichConstraint> HaveCommonDataBody(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs) { return assertions.HaveJsonBody>(because, becauseArgs); @@ -148,13 +155,6 @@ namespace Timeline.Tests.Helpers body.Data.Delete.Should().Be(delete); } - public static void HaveCommonResponseBody(this HttpResponseMessageAssertions assertions, int code, string message = null, params object[] messageArgs) - { - message = string.IsNullOrEmpty(message) ? "" : ", " + string.Format(CultureInfo.CurrentCulture, message, messageArgs); - var body = assertions.HaveJsonBody("Response body should be CommonResponse{0}", message).Which; - body.Code.Should().Be(code, "Response body code is not the specified one{0}", message); - } - public static void BeInvalidModel(this HttpResponseMessageAssertions assertions, string message = null) { message = string.IsNullOrEmpty(message) ? "" : ", " + message; diff --git a/Timeline.Tests/Helpers/TestApplication.cs b/Timeline.Tests/Helpers/TestApplication.cs index d18f2848..14cafea3 100644 --- a/Timeline.Tests/Helpers/TestApplication.cs +++ b/Timeline.Tests/Helpers/TestApplication.cs @@ -1,4 +1,6 @@ using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using System; @@ -8,19 +10,21 @@ namespace Timeline.Tests.Helpers { public class TestApplication : IDisposable { - public TestDatabase Database { get; } = new TestDatabase(); + public SqliteConnection DatabaseConnection { get; } + public WebApplicationFactory Factory { get; } public TestApplication(WebApplicationFactory factory) { + DatabaseConnection = new SqliteConnection("Data Source=:memory:;"); + Factory = factory.WithWebHostBuilder(builder => { - builder.ConfigureServices(services => + builder.ConfigureTestServices(services => { - services.AddEntityFrameworkSqlite(); services.AddDbContext(options => { - options.UseSqlite(Database.Connection); + options.UseSqlite(DatabaseConnection); }); }); }); @@ -28,7 +32,8 @@ namespace Timeline.Tests.Helpers public void Dispose() { - Database.Dispose(); + DatabaseConnection.Close(); + DatabaseConnection.Dispose(); } } } diff --git a/Timeline.Tests/Helpers/TestClock.cs b/Timeline.Tests/Helpers/TestClock.cs deleted file mode 100644 index 12b320d3..00000000 --- a/Timeline.Tests/Helpers/TestClock.cs +++ /dev/null @@ -1,15 +0,0 @@ -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 deleted file mode 100644 index e29a71fa..00000000 --- a/Timeline.Tests/Helpers/TestDatabase.cs +++ /dev/null @@ -1,89 +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.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 UserEntity CreateEntityFromMock(MockUser user) - { - return new UserEntity - { - Username = user.Username, - Password = PasswordService.HashPassword(user.Password), - Roles = 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 DevelopmentDatabaseContext(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 deleted file mode 100644 index 017d77a8..00000000 --- a/Timeline.Tests/Helpers/UseCultureAttribute.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System; -using System.Globalization; -using System.Reflection; -using System.Threading; -using Xunit.Sdk; - - -namespace Timeline.Tests.Helpers -{ - // 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 - { - readonly Lazy culture; - readonly Lazy uiCulture; - - CultureInfo originalCulture; - CultureInfo originalUICulture; - - /// - /// 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) { } - - /// - /// 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)); - } - - /// - /// Gets the culture. - /// - public CultureInfo Culture { get { return culture.Value; } } - - /// - /// Gets the UI culture. - /// - public CultureInfo UICulture { get { return uiCulture.Value; } } - - /// - /// 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(); - } - } -} diff --git a/Timeline.Tests/IntegratedTests/AuthorizationTest.cs b/Timeline.Tests/IntegratedTests/AuthorizationTest.cs index 0bc094af..4aa6b3ae 100644 --- a/Timeline.Tests/IntegratedTests/AuthorizationTest.cs +++ b/Timeline.Tests/IntegratedTests/AuthorizationTest.cs @@ -22,7 +22,7 @@ namespace Timeline.Tests.IntegratedTests [Fact] public async Task UnauthenticationTest() { - using var client = await CreateClientWithNoAuth(); + using var client = await CreateDefaultClient(); var response = await client.GetAsync(AuthorizeUrl); response.Should().HaveStatusCode(HttpStatusCode.Unauthorized); } @@ -48,7 +48,7 @@ namespace Timeline.Tests.IntegratedTests [Fact] public async Task AdminAuthorizationTest() { - using var client = await CreateClientAsAdmin(); + using var client = await CreateClientAsAdministrator(); 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 deleted file mode 100644 index 855179af..00000000 --- a/Timeline.Tests/IntegratedTests/I18nTest.cs +++ /dev/null @@ -1,59 +0,0 @@ -using FluentAssertions; -using Microsoft.AspNetCore.Mvc.Testing; -using System; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Threading.Tasks; -using Timeline.Tests.Helpers; -using Xunit; - -namespace Timeline.Tests.IntegratedTests -{ - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1054:Uri parameters should not be strings")] - public class I18nTest : IntegratedTestBase - { - private readonly HttpClient _client; - - public I18nTest(WebApplicationFactory factory) - : base(factory) - { - _client = Factory.CreateDefaultClient(); - } - - protected override void OnDispose() - { - _client.Dispose(); - } - - private const string DirectUrl = "testing/i18n/direct"; - private const string LocalizerUrl = "testing/i18n/localizer"; - - [Theory] - [InlineData(DirectUrl)] - [InlineData(LocalizerUrl)] - public async Task DefaultShouldReturnEnglish(string url) - { - (await _client.GetStringAsync(url)).Should().ContainEquivalentOf("English"); - } - - [Theory] - [InlineData(DirectUrl, "en", true)] - [InlineData(LocalizerUrl, "en", true)] - [InlineData(DirectUrl, "en-US", true)] - [InlineData(LocalizerUrl, "en-US", true)] - [InlineData(DirectUrl, "zh", false)] - [InlineData(LocalizerUrl, "zh", false)] - public async Task ShouldWork(string url, string acceptLanguage, bool english) - { - var request = new HttpRequestMessage - { - Method = HttpMethod.Get, - RequestUri = new Uri(url, UriKind.RelativeOrAbsolute) - }; - request.Headers.AcceptLanguage.Add(new StringWithQualityHeaderValue(acceptLanguage)); - var body = await (await _client.SendAsync(request)).Content.ReadAsStringAsync(); - body.Should().ContainEquivalentOf(english ? "English" : "中文"); - request.Dispose(); - } - } -} diff --git a/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs b/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs index 242a452d..721a25af 100644 --- a/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs +++ b/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs @@ -1,36 +1,17 @@ -using Microsoft.AspNetCore.Mvc.Testing; +using AutoMapper; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection; using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; using Timeline.Models.Http; +using Timeline.Services; using Timeline.Tests.Helpers; 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 { @@ -38,14 +19,62 @@ namespace Timeline.Tests.IntegratedTests protected WebApplicationFactory Factory => TestApp.Factory; - public IntegratedTestBase(WebApplicationFactory factory) + public IntegratedTestBase(WebApplicationFactory factory) : this(factory, 1) + { + + } + + public IntegratedTestBase(WebApplicationFactory factory, int userCount) { + if (userCount < 0) + throw new ArgumentOutOfRangeException(nameof(userCount), userCount, "User count can't be negative."); + TestApp = new TestApplication(factory); + + using (var scope = Factory.Services.CreateScope()) + { + var users = new List() + { + new User + { + Username = "admin", + Password = "adminpw", + Administrator = true, + Nickname = "administrator" + } + }; + + for (int i = 1; i <= userCount; i++) + { + users.Add(new User + { + Username = $"user{i}", + Password = $"user{i}pw", + Administrator = false, + Nickname = $"imuser{i}" + }); + } + + var userInfoList = new List(); + var userInfoForAdminList = new List(); + + var userService = scope.ServiceProvider.GetRequiredService(); + var mapper = scope.ServiceProvider.GetRequiredService(); + + foreach (var user in users) + { + userService.CreateUser(user); + userInfoList.Add(mapper.Map(user)); + userInfoForAdminList.Add(mapper.Map(user)); + } + + UserInfoList = userInfoList; + UserInfoForAdminList = userInfoForAdminList; + } } protected virtual void OnDispose() { - } public void Dispose() @@ -54,14 +83,11 @@ namespace Timeline.Tests.IntegratedTests TestApp.Dispose(); } - protected void CreateExtraMockUsers(int count) - { - TestApp.Database.CreateExtraMockUsers(count); - } + public IReadOnlyList UserInfoList { get; } - protected IReadOnlyList ExtraMockUsers => TestApp.Database.ExtraMockUsers; + public IReadOnlyList UserInfoForAdminList { get; } - public Task CreateClientWithNoAuth() + public Task CreateDefaultClient() { return Task.FromResult(Factory.CreateDefaultClient()); } @@ -77,18 +103,25 @@ namespace Timeline.Tests.IntegratedTests return client; } - public Task CreateClientAs(MockUser user) + public Task CreateClientAs(int userNumber) { - if (user == null) - return CreateClientWithNoAuth(); - return CreateClientWithCredential(user.Username, user.Password); - } - - public Task CreateClientAs(AuthType authType) => CreateClientAs(authType.GetMockUser()); - + if (userNumber < 0) + throw new ArgumentOutOfRangeException(nameof(userNumber), "User number can't be negative."); - public Task CreateClientAsUser() => CreateClientAs(MockUser.User); - public Task CreateClientAsAdmin() => CreateClientAs(MockUser.Admin); + if (userNumber == 0) + return CreateClientWithCredential("admin", "adminpw"); + else + return CreateClientWithCredential($"user{userNumber}", $"user{userNumber}pw"); + } + public Task CreateClientAsAdministrator() + { + return CreateClientAs(0); + } + + public Task CreateClientAsUser() + { + return CreateClientAs(1); + } } } diff --git a/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs b/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs index 51e2d05e..14600659 100644 --- a/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs +++ b/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Threading.Tasks; -using Timeline.Models; using Timeline.Models.Http; using Timeline.Tests.Helpers; using Xunit; @@ -15,7 +14,7 @@ namespace Timeline.Tests.IntegratedTests public class PersonalTimelineTest : IntegratedTestBase { public PersonalTimelineTest(WebApplicationFactory factory) - : base(factory) + : base(factory, 3) { } @@ -23,11 +22,11 @@ namespace Timeline.Tests.IntegratedTests [Fact] public async Task TimelineGet_Should_Work() { - using var client = await CreateClientWithNoAuth(); - var res = await client.GetAsync("users/user/timeline"); + using var client = await CreateDefaultClient(); + var res = await client.GetAsync("users/user1/timeline"); var body = res.Should().HaveStatusCode(200) .And.HaveJsonBody().Which; - body.Owner.Should().Be("user"); + body.Owner.Should().BeEquivalentTo(UserInfoList[1]); body.Visibility.Should().Be(TimelineVisibility.Register); body.Description.Should().Be(""); body.Members.Should().NotBeNull().And.BeEmpty(); @@ -40,7 +39,7 @@ namespace Timeline.Tests.IntegratedTests async Task AssertDescription(string description) { - var res = await client.GetAsync("users/user/timeline"); + var res = await client.GetAsync("users/user1/timeline"); var body = res.Should().HaveStatusCode(200) .And.HaveJsonBody() .Which.Description.Should().Be(description); @@ -50,20 +49,20 @@ namespace Timeline.Tests.IntegratedTests await AssertDescription(""); { - var res = await client.PostAsJsonAsync("users/user/timeline/op/property", - new TimelinePropertyChangeRequest { Description = mockDescription }); + var res = await client.PatchAsJsonAsync("users/user1/timeline", + new TimelinePatchRequest { Description = mockDescription }); res.Should().HaveStatusCode(200); await AssertDescription(mockDescription); } { - var res = await client.PostAsJsonAsync("users/user/timeline/op/property", - new TimelinePropertyChangeRequest { Description = null }); + var res = await client.PatchAsJsonAsync("users/user1/timeline", + new TimelinePatchRequest { Description = null }); res.Should().HaveStatusCode(200); await AssertDescription(mockDescription); } { - var res = await client.PostAsJsonAsync("users/user/timeline/op/property", - new TimelinePropertyChangeRequest { Description = "" }); + var res = await client.PatchAsJsonAsync("users/user1/timeline", + new TimelinePatchRequest { Description = "" }); res.Should().HaveStatusCode(200); await AssertDescription(""); } @@ -141,13 +140,13 @@ namespace Timeline.Tests.IntegratedTests { var res = await client.PostAsJsonAsync("users/user/timeline/op/property", - new TimelinePropertyChangeRequest { Description = "hahaha" }); + new TimelinePatchRequest { Description = "hahaha" }); res.Should().HaveStatusCode(opPropertyUser); } { var res = await client.PostAsJsonAsync("users/admin/timeline/op/property", - new TimelinePropertyChangeRequest { Description = "hahaha" }); + new TimelinePatchRequest { Description = "hahaha" }); res.Should().HaveStatusCode(opPropertyAdmin); } @@ -193,7 +192,7 @@ namespace Timeline.Tests.IntegratedTests { using var client = await CreateClientAsUser(); var res = await client.PostAsJsonAsync("users/user/timeline/op/property", - new TimelinePropertyChangeRequest { Visibility = TimelineVisibility.Public }); + new TimelinePatchRequest { Visibility = TimelineVisibility.Public }); res.Should().HaveStatusCode(200); } { @@ -208,12 +207,12 @@ namespace Timeline.Tests.IntegratedTests using var client = await CreateClientAsAdmin(); { var res = await client.PostAsJsonAsync("users/user/timeline/op/property", - new TimelinePropertyChangeRequest { Visibility = TimelineVisibility.Private }); + new TimelinePatchRequest { Visibility = TimelineVisibility.Private }); res.Should().HaveStatusCode(200); } { var res = await client.PostAsJsonAsync("users/admin/timeline/op/property", - new TimelinePropertyChangeRequest { Visibility = TimelineVisibility.Private }); + new TimelinePatchRequest { Visibility = TimelineVisibility.Private }); res.Should().HaveStatusCode(200); } } @@ -331,7 +330,7 @@ namespace Timeline.Tests.IntegratedTests } { // self can delete self - var postId = await CreatePost(MockUser.User, "user"); + var postId = await CreatePost(MockUser.Ordinary, "user"); using var client = await CreateClientAsUser(); var res = await client.PostAsJsonAsync("users/user/timeline/postop/delete", new TimelinePostDeleteRequest { Id = postId }); @@ -339,7 +338,7 @@ namespace Timeline.Tests.IntegratedTests } { // admin can delete any - var postId = await CreatePost(MockUser.User, "user"); + var postId = await CreatePost(MockUser.Ordinary, "user"); using var client = await CreateClientAsAdmin(); var res = await client.PostAsJsonAsync("users/user/timeline/postop/delete", new TimelinePostDeleteRequest { Id = postId }); diff --git a/Timeline.Tests/IntegratedTests/TokenTest.cs b/Timeline.Tests/IntegratedTests/TokenTest.cs index ecd5d0b8..8ee19999 100644 --- a/Timeline.Tests/IntegratedTests/TokenTest.cs +++ b/Timeline.Tests/IntegratedTests/TokenTest.cs @@ -53,7 +53,7 @@ namespace Timeline.Tests.IntegratedTests public static IEnumerable CreateToken_UserCredential_Data() { yield return new[] { "usernotexist", "p" }; - yield return new[] { MockUser.User.Username, "???" }; + yield return new[] { MockUser.Ordinary.Username, "???" }; } [Theory] @@ -73,11 +73,11 @@ namespace Timeline.Tests.IntegratedTests { using var client = await CreateClientWithNoAuth(); var response = await client.PostAsJsonAsync(CreateTokenUrl, - new CreateTokenRequest { Username = MockUser.User.Username, Password = MockUser.User.Password }); + new CreateTokenRequest { Username = MockUser.Ordinary.Username, Password = MockUser.Ordinary.Password }); var body = response.Should().HaveStatusCode(200) .And.HaveJsonBody().Which; body.Token.Should().NotBeNullOrWhiteSpace(); - body.User.Should().BeEquivalentTo(MockUser.User.Info); + body.User.Should().BeEquivalentTo(MockUser.Ordinary.Info); } [Fact] @@ -103,13 +103,13 @@ namespace Timeline.Tests.IntegratedTests public async Task VerifyToken_OldVersion() { using var client = await CreateClientWithNoAuth(); - var token = (await CreateUserTokenAsync(client, MockUser.User.Username, MockUser.User.Password)).Token; + var token = (await CreateUserTokenAsync(client, MockUser.Ordinary.Username, MockUser.Ordinary.Password)).Token; using (var scope = Factory.Server.Host.Services.CreateScope()) // UserService is scoped. { // create a user for test var userService = scope.ServiceProvider.GetRequiredService(); - await userService.PatchUser(MockUser.User.Username, null, null); + await userService.PatchUser(MockUser.Ordinary.Username, null, null); } (await client.PostAsJsonAsync(VerifyTokenUrl, @@ -123,12 +123,12 @@ namespace Timeline.Tests.IntegratedTests public async Task VerifyToken_UserNotExist() { using var client = await CreateClientWithNoAuth(); - var token = (await CreateUserTokenAsync(client, MockUser.User.Username, MockUser.User.Password)).Token; + var token = (await CreateUserTokenAsync(client, MockUser.Ordinary.Username, MockUser.Ordinary.Password)).Token; using (var scope = Factory.Server.Host.Services.CreateScope()) // UserService is scoped. { var userService = scope.ServiceProvider.GetRequiredService(); - await userService.DeleteUser(MockUser.User.Username); + await userService.DeleteUser(MockUser.Ordinary.Username); } (await client.PostAsJsonAsync(VerifyTokenUrl, @@ -160,12 +160,12 @@ namespace Timeline.Tests.IntegratedTests public async Task VerifyToken_Success() { using var client = await CreateClientWithNoAuth(); - var createTokenResult = await CreateUserTokenAsync(client, MockUser.User.Username, MockUser.User.Password); + var createTokenResult = await CreateUserTokenAsync(client, MockUser.Ordinary.Username, MockUser.Ordinary.Password); var response = await client.PostAsJsonAsync(VerifyTokenUrl, new VerifyTokenRequest { Token = createTokenResult.Token }); response.Should().HaveStatusCode(200) .And.HaveJsonBody() - .Which.User.Should().BeEquivalentTo(MockUser.User.Info); + .Which.User.Should().BeEquivalentTo(MockUser.Ordinary.Info); } } } diff --git a/Timeline.Tests/IntegratedTests/UserAvatarTest.cs b/Timeline.Tests/IntegratedTests/UserAvatarTest.cs index a4e10634..989207e2 100644 --- a/Timeline.Tests/IntegratedTests/UserAvatarTest.cs +++ b/Timeline.Tests/IntegratedTests/UserAvatarTest.cs @@ -29,7 +29,6 @@ namespace Timeline.Tests.IntegratedTests } [Fact] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "HttpMessageRequest should be disposed ???")] public async Task Test() { Avatar mockAvatar = new Avatar @@ -61,7 +60,7 @@ namespace Timeline.Tests.IntegratedTests EntityTagHeaderValue eTag; { - var res = await client.GetAsync($"users/user/avatar"); + var res = await client.GetAsync($"users/user1/avatar"); res.Should().HaveStatusCode(200); res.Content.Headers.ContentType.MediaType.Should().Be("image/png"); var body = await res.Content.ReadAsByteArrayAsync(); @@ -78,7 +77,7 @@ namespace Timeline.Tests.IntegratedTests { var request = new HttpRequestMessage() { - RequestUri = new Uri(client.BaseAddress, "users/user/avatar"), + RequestUri = new Uri(client.BaseAddress, "users/user1/avatar"), Method = HttpMethod.Get, }; request.Headers.TryAddWithoutValidation("If-None-Match", "\"dsdfd"); @@ -90,7 +89,7 @@ namespace Timeline.Tests.IntegratedTests { var request = new HttpRequestMessage() { - RequestUri = new Uri(client.BaseAddress, "users/user/avatar"), + RequestUri = new Uri(client.BaseAddress, "users/user1/avatar"), Method = HttpMethod.Get, }; request.Headers.TryAddWithoutValidation("If-None-Match", "\"aaa\""); @@ -101,7 +100,7 @@ namespace Timeline.Tests.IntegratedTests { var request = new HttpRequestMessage() { - RequestUri = new Uri(client.BaseAddress, "users/user/avatar"), + RequestUri = new Uri(client.BaseAddress, "users/user1/avatar"), Method = HttpMethod.Get, }; request.Headers.Add("If-None-Match", eTag.ToString()); @@ -112,7 +111,7 @@ namespace Timeline.Tests.IntegratedTests { var content = new ByteArrayContent(new[] { (byte)0x00 }); content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); - var res = await client.PutAsync("users/user/avatar", content); + var res = await client.PutAsync("users/user1/avatar", content); res.Should().HaveStatusCode(HttpStatusCode.BadRequest) .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Common.Header.ContentLength_Missing); ; } @@ -120,7 +119,7 @@ namespace Timeline.Tests.IntegratedTests { var content = new ByteArrayContent(new[] { (byte)0x00 }); content.Headers.ContentLength = 1; - var res = await client.PutAsync("users/user/avatar", content); + var res = await client.PutAsync("users/user1/avatar", content); res.Should().HaveStatusCode(HttpStatusCode.BadRequest) .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Common.Header.ContentType_Missing); } @@ -129,13 +128,13 @@ namespace Timeline.Tests.IntegratedTests var content = new ByteArrayContent(new[] { (byte)0x00 }); content.Headers.ContentLength = 0; content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); - var res = await client.PutAsync("users/user/avatar", content); + var res = await client.PutAsync("users/user1/avatar", content); res.Should().HaveStatusCode(HttpStatusCode.BadRequest) .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Common.Header.ContentLength_Zero); } { - var res = await client.PutByteArrayAsync("users/user/avatar", new[] { (byte)0x00 }, "image/notaccept"); + var res = await client.PutByteArrayAsync("users/user1/avatar", new[] { (byte)0x00 }, "image/notaccept"); res.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); } @@ -143,7 +142,7 @@ namespace Timeline.Tests.IntegratedTests var content = new ByteArrayContent(new[] { (byte)0x00 }); content.Headers.ContentLength = 1000 * 1000 * 11; content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); - var res = await client.PutAsync("users/user/avatar", content); + var res = await client.PutAsync("users/user1/avatar", content); res.Should().HaveStatusCode(HttpStatusCode.BadRequest) .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Common.Content.TooBig); } @@ -152,7 +151,7 @@ namespace Timeline.Tests.IntegratedTests var content = new ByteArrayContent(new[] { (byte)0x00 }); content.Headers.ContentLength = 2; content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); - var res = await client.PutAsync("users/user/avatar", content); + var res = await client.PutAsync("users/user1/avatar", content); res.Should().HaveStatusCode(HttpStatusCode.BadRequest) .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Common.Content.UnmatchedLength_Smaller); } @@ -161,34 +160,34 @@ namespace Timeline.Tests.IntegratedTests var content = new ByteArrayContent(new[] { (byte)0x00, (byte)0x01 }); content.Headers.ContentLength = 1; content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); - var res = await client.PutAsync("users/user/avatar", content); + var res = await client.PutAsync("users/user1/avatar", content); res.Should().HaveStatusCode(HttpStatusCode.BadRequest) .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Common.Content.UnmatchedLength_Bigger); } { - var res = await client.PutByteArrayAsync("users/user/avatar", new[] { (byte)0x00 }, "image/png"); + var res = await client.PutByteArrayAsync("users/user1/avatar", new[] { (byte)0x00 }, "image/png"); res.Should().HaveStatusCode(HttpStatusCode.BadRequest) .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.UserAvatar.BadFormat_CantDecode); } { - var res = await client.PutByteArrayAsync("users/user/avatar", mockAvatar.Data, "image/jpeg"); + var res = await client.PutByteArrayAsync("users/user1/avatar", mockAvatar.Data, "image/jpeg"); res.Should().HaveStatusCode(HttpStatusCode.BadRequest) .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.UserAvatar.BadFormat_UnmatchedFormat); } { - var res = await client.PutByteArrayAsync("users/user/avatar", ImageHelper.CreatePngWithSize(100, 200), "image/png"); + var res = await client.PutByteArrayAsync("users/user1/avatar", ImageHelper.CreatePngWithSize(100, 200), "image/png"); res.Should().HaveStatusCode(HttpStatusCode.BadRequest) .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.UserAvatar.BadFormat_BadSize); } { - var res = await client.PutByteArrayAsync("users/user/avatar", mockAvatar.Data, mockAvatar.Type); + var res = await client.PutByteArrayAsync("users/user1/avatar", mockAvatar.Data, mockAvatar.Type); res.Should().HaveStatusCode(HttpStatusCode.OK); - var res2 = await client.GetAsync("users/user/avatar"); + var res2 = await client.GetAsync("users/user1/avatar"); res2.Should().HaveStatusCode(200); res2.Content.Headers.ContentType.MediaType.Should().Be(mockAvatar.Type); var body = await res2.Content.ReadAsByteArrayAsync(); @@ -204,7 +203,7 @@ namespace Timeline.Tests.IntegratedTests foreach ((var mimeType, var format) in formats) { - var res = await client.PutByteArrayAsync("users/user/avatar", ImageHelper.CreateImageWithSize(100, 100, format), mimeType); + var res = await client.PutByteArrayAsync("users/user1/avatar", ImageHelper.CreateImageWithSize(100, 100, format), mimeType); res.Should().HaveStatusCode(HttpStatusCode.OK); } @@ -222,22 +221,22 @@ namespace Timeline.Tests.IntegratedTests for (int i = 0; i < 2; i++) // double delete should work. { - var res = await client.DeleteAsync("users/user/avatar"); + var res = await client.DeleteAsync("users/user1/avatar"); res.Should().HaveStatusCode(200); await GetReturnDefault(); } } // Authorization check. - using (var client = await CreateClientAsAdmin()) + using (var client = await CreateClientAsAdministrator()) { { - var res = await client.PutByteArrayAsync("users/user/avatar", mockAvatar.Data, mockAvatar.Type); + var res = await client.PutByteArrayAsync("users/user1/avatar", mockAvatar.Data, mockAvatar.Type); res.Should().HaveStatusCode(HttpStatusCode.OK); } { - var res = await client.DeleteAsync("users/user/avatar"); + var res = await client.DeleteAsync("users/user1/avatar"); res.Should().HaveStatusCode(HttpStatusCode.OK); } @@ -256,7 +255,7 @@ namespace Timeline.Tests.IntegratedTests } // bad username check - using (var client = await CreateClientAsAdmin()) + using (var client = await CreateClientAsAdministrator()) { { var res = await client.GetAsync("users/u!ser/avatar"); diff --git a/Timeline.Tests/IntegratedTests/UserDetailTest.cs b/Timeline.Tests/IntegratedTests/UserDetailTest.cs deleted file mode 100644 index 3781a816..00000000 --- a/Timeline.Tests/IntegratedTests/UserDetailTest.cs +++ /dev/null @@ -1,154 +0,0 @@ -using FluentAssertions; -using Microsoft.AspNetCore.Mvc.Testing; -using System.Net; -using System.Net.Http.Headers; -using System.Net.Mime; -using System.Threading.Tasks; -using Timeline.Models.Http; -using Timeline.Tests.Helpers; -using Xunit; - -namespace Timeline.Tests.IntegratedTests -{ - public class UserDetailTest : IntegratedTestBase - { - public UserDetailTest(WebApplicationFactory factory) - : base(factory) - { - - } - - [Fact] - public async Task PermissionTest() - { - { // unauthorize - using var client = await CreateClientWithNoAuth(); - { // GET - var res = await client.GetAsync($"users/{MockUser.User.Username}/nickname"); - res.Should().HaveStatusCode(HttpStatusCode.OK); - } - { // PUT - var res = await client.PutStringAsync($"users/{MockUser.User.Username}/nickname", "aaa"); - res.Should().HaveStatusCode(HttpStatusCode.Unauthorized); - } - { // DELETE - var res = await client.DeleteAsync($"users/{MockUser.User.Username}/nickname"); - res.Should().HaveStatusCode(HttpStatusCode.Unauthorized); - } - } - { // user - using var client = await CreateClientAsUser(); - { // GET - var res = await client.GetAsync($"users/{MockUser.User.Username}/nickname"); - res.Should().HaveStatusCode(HttpStatusCode.OK); - } - { // PUT self - var res = await client.PutStringAsync($"users/{MockUser.User.Username}/nickname", "aaa"); - res.Should().HaveStatusCode(HttpStatusCode.OK); - } - { // PUT other - var res = await client.PutStringAsync($"users/{MockUser.Admin.Username}/nickname", "aaa"); - res.Should().HaveStatusCode(HttpStatusCode.Forbidden); - } - { // DELETE self - var res = await client.DeleteAsync($"users/{MockUser.User.Username}/nickname"); - res.Should().HaveStatusCode(HttpStatusCode.OK); - } - { // DELETE other - var res = await client.DeleteAsync($"users/{MockUser.Admin.Username}/nickname"); - res.Should().HaveStatusCode(HttpStatusCode.Forbidden); - } - } - { // user - using var client = await CreateClientAsAdmin(); - { // PUT other - var res = await client.PutStringAsync($"users/{MockUser.User.Username}/nickname", "aaa"); - res.Should().HaveStatusCode(HttpStatusCode.OK); - } - { // DELETE other - var res = await client.DeleteAsync($"users/{MockUser.User.Username}/nickname"); - res.Should().HaveStatusCode(HttpStatusCode.OK); - } - } - } - - [Fact] - public async Task FunctionTest() - { - var url = $"users/{MockUser.User.Username}/nickname"; - var userNotExistUrl = "users/usernotexist/nickname"; - { - using var client = await CreateClientAsUser(); - { - var res = await client.GetAsync(userNotExistUrl); - res.Should().HaveStatusCode(HttpStatusCode.NotFound) - .And.HaveCommonBody() - .Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist); - - } - { - var res = await client.GetAsync(url); - res.Should().HaveStatusCode(HttpStatusCode.OK); - res.Content.Headers.ContentType.Should().Be(new MediaTypeHeaderValue(MediaTypeNames.Text.Plain) { CharSet = "utf-8" }); - var body = await res.Content.ReadAsStringAsync(); - body.Should().Be(MockUser.User.Username); - } - { - var res = await client.PutStringAsync(url, ""); - res.Should().BeInvalidModel(); - } - { - var res = await client.PutStringAsync(url, new string('a', 11)); - res.Should().BeInvalidModel(); - } - var nickname1 = "nnn"; - var nickname2 = "nn2"; - { - var res = await client.PutStringAsync(url, nickname1); - res.Should().HaveStatusCode(HttpStatusCode.OK); - (await client.GetStringAsync(url)).Should().Be(nickname1); - } - { - var res = await client.PutStringAsync(url, nickname2); - res.Should().HaveStatusCode(HttpStatusCode.OK); - (await client.GetStringAsync(url)).Should().Be(nickname2); - } - { - var res = await client.DeleteAsync(url); - res.Should().HaveStatusCode(HttpStatusCode.OK); - (await client.GetStringAsync(url)).Should().Be(MockUser.User.Username); - } - { - var res = await client.DeleteAsync(url); - res.Should().HaveStatusCode(HttpStatusCode.OK); - } - } - { - using var client = await CreateClientAsAdmin(); - { - var res = await client.PutStringAsync(userNotExistUrl, "aaa"); - res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.HaveCommonBody() - .Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist); - } - { - var res = await client.DeleteAsync(userNotExistUrl); - res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.HaveCommonBody() - .Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist); - } - var nickname = "nnn"; - { - var res = await client.PutStringAsync(url, nickname); - res.Should().HaveStatusCode(HttpStatusCode.OK); - (await client.GetStringAsync(url)).Should().Be(nickname); - } - { - var res = await client.DeleteAsync(url); - res.Should().HaveStatusCode(HttpStatusCode.OK); - (await client.GetStringAsync(url)).Should().Be(MockUser.User.Username); - } - } - } - } -} diff --git a/Timeline.Tests/IntegratedTests/UserTest.cs b/Timeline.Tests/IntegratedTests/UserTest.cs index ea9f1177..4c2ccf7a 100644 --- a/Timeline.Tests/IntegratedTests/UserTest.cs +++ b/Timeline.Tests/IntegratedTests/UserTest.cs @@ -1,9 +1,9 @@ using FluentAssertions; using Microsoft.AspNetCore.Mvc.Testing; using System.Collections.Generic; +using System.Net; using System.Net.Http; using System.Threading.Tasks; -using Timeline.Models; using Timeline.Models.Http; using Timeline.Tests.Helpers; using Xunit; @@ -19,102 +19,144 @@ namespace Timeline.Tests.IntegratedTests } [Fact] - public async Task Get_List_Success() + public async Task GetList_NoAuth() { - using var client = await CreateClientAsAdmin(); - var res = await client.GetAsync("users"); + using var client = await CreateDefaultClient(); + var res = await client.GetAsync("/users"); res.Should().HaveStatusCode(200) - .And.HaveJsonBody() - .Which.Should().BeEquivalentTo(MockUser.UserInfoList); + .And.HaveJsonBody() + .Which.Should().BeEquivalentTo(UserInfoList); } [Fact] - public async Task Get_Single_Success() + public async Task GetList_User() { - using var client = await CreateClientAsAdmin(); - var res = await client.GetAsync("users/" + MockUser.User.Username); + using var client = await CreateClientAsUser(); + var res = await client.GetAsync("/users"); res.Should().HaveStatusCode(200) - .And.HaveJsonBody() - .Which.Should().BeEquivalentTo(MockUser.User.Info); + .And.HaveJsonBody() + .Which.Should().BeEquivalentTo(UserInfoList); } [Fact] - public async Task Get_InvalidModel() + public async Task GetList_Admin() { - using var client = await CreateClientAsAdmin(); - var res = await client.GetAsync("users/aaa!a"); - res.Should().BeInvalidModel(); + using var client = await CreateClientAsAdministrator(); + var res = await client.GetAsync("/users"); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Should().BeEquivalentTo(UserInfoForAdminList); } [Fact] - public async Task Get_Users_404() + public async Task Get_NoAuth() { - using var client = await CreateClientAsAdmin(); - var res = await client.GetAsync("users/usernotexist"); - res.Should().HaveStatusCode(404) - .And.HaveCommonBody() - .Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist); + using var client = await CreateDefaultClient(); + var res = await client.GetAsync($"/users/admin"); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Should().BeEquivalentTo(UserInfoList[0]); } - public static IEnumerable Put_InvalidModel_Data() + [Fact] + public async Task Get_User() { - yield return new object[] { "aaa", null, false }; - yield return new object[] { "aaa", "p", null }; - yield return new object[] { "aa!a", "p", false }; + using var client = await CreateClientAsUser(); + var res = await client.GetAsync($"/users/admin"); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Should().BeEquivalentTo(UserInfoList[0]); } - [Theory] - [MemberData(nameof(Put_InvalidModel_Data))] - public async Task Put_InvalidModel(string username, string password, bool? administrator) + [Fact] + public async Task Get_Admin() { - using var client = await CreateClientAsAdmin(); - (await client.PutAsJsonAsync("users/" + username, - new UserPutRequest { Password = password, Administrator = administrator })) - .Should().BeInvalidModel(); + using var client = await CreateClientAsAdministrator(); + var res = await client.GetAsync($"/users/user1"); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Should().BeEquivalentTo(UserInfoForAdminList[1]); } - private async Task CheckAdministrator(HttpClient client, string username, bool administrator) + [Fact] + public async Task Get_InvalidModel() { - var res = await client.GetAsync("users/" + username); - res.Should().HaveStatusCode(200) - .And.HaveJsonBody() - .Which.Administrator.Should().Be(administrator); + using var client = await CreateClientAsUser(); + var res = await client.GetAsync("/users/aaa!a"); + res.Should().BeInvalidModel(); } [Fact] - public async Task Put_Modiefied() + public async Task Get_404() { - using var client = await CreateClientAsAdmin(); - var res = await client.PutAsJsonAsync("users/" + MockUser.User.Username, new UserPutRequest + using var client = await CreateClientAsUser(); + var res = await client.GetAsync("/users/usernotexist"); + res.Should().HaveStatusCode(404) + .And.HaveCommonBody(ErrorCodes.UserCommon.NotExist); + } + + [Fact] + public async Task Patch_User() + { + using var client = await CreateClientAsUser(); { - Password = "password", - Administrator = false - }); - res.Should().BePut(false); - await CheckAdministrator(client, MockUser.User.Username, false); + var res = await client.PatchAsJsonAsync("/users/user1", + new UserPatchRequest { Nickname = "aaa" }); + res.Should().HaveStatusCode(200); + } + + { + var res = await client.GetAsync("/users/user1"); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Nickname.Should().Be("aaa"); + } } [Fact] - public async Task Put_Created() + public async Task Patch_Admin() { - using var client = await CreateClientAsAdmin(); - const string username = "puttest"; - const string url = "users/" + username; + using var client = await CreateClientAsAdministrator(); + using var userClient = await CreateClientAsUser(); + + { + var res = await client.PatchAsJsonAsync("/users/user1", + new UserPatchRequest + { + Username = "newuser", + Password = "newpw", + Administrator = true, + Nickname = "aaa" + }); + res.Should().HaveStatusCode(200); + } - var res = await client.PutAsJsonAsync(url, new UserPutRequest { - Password = "password", - Administrator = false - }); - res.Should().BePut(true); - await CheckAdministrator(client, username, false); + var res = await client.GetAsync("/users/newuser"); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which; + body.Administrator.Should().Be(true); + body.Nickname.Should().Be("aaa"); + } + + { + // Token should expire. + var res = await userClient.GetAsync("/users"); + res.Should().HaveStatusCode(HttpStatusCode.Unauthorized); + } + + { + // Check password. + (await CreateClientWithCredential("newuser", "newpw")).Dispose(); + } } [Fact] public async Task Patch_NotExist() { - using var client = await CreateClientAsAdmin(); - var res = await client.PatchAsJsonAsync("users/usernotexist", new UserPatchRequest { }); + using var client = await CreateClientAsAdministrator(); + var res = await client.PatchAsJsonAsync("/users/usernotexist", new UserPatchRequest { }); res.Should().HaveStatusCode(404) .And.HaveCommonBody() .Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist); @@ -123,114 +165,239 @@ namespace Timeline.Tests.IntegratedTests [Fact] public async Task Patch_InvalidModel() { - using var client = await CreateClientAsAdmin(); - var res = await client.PatchAsJsonAsync("users/aaa!a", new UserPatchRequest { }); + using var client = await CreateClientAsAdministrator(); + var res = await client.PatchAsJsonAsync("/users/aaa!a", new UserPatchRequest { }); + res.Should().BeInvalidModel(); + } + + public static IEnumerable Patch_InvalidModel_Body_Data() + { + yield return new[] { new UserPatchRequest { Username = "aaa!a" } }; + yield return new[] { new UserPatchRequest { Password = "" } }; + yield return new[] { new UserPatchRequest { Nickname = new string('a', 50) } }; + } + + [Theory] + [MemberData(nameof(Patch_InvalidModel_Body_Data))] + public async Task Patch_InvalidModel_Body(UserPatchRequest body) + { + using var client = await CreateClientAsAdministrator(); + var res = await client.PatchAsJsonAsync("/users/user1", body); res.Should().BeInvalidModel(); } [Fact] - public async Task Patch_Success() + public async Task Patch_UsernameConflict() { - using var client = await CreateClientAsAdmin(); - { - var res = await client.PatchAsJsonAsync("users/" + MockUser.User.Username, - new UserPatchRequest { Administrator = false }); - res.Should().HaveStatusCode(200); - await CheckAdministrator(client, MockUser.User.Username, false); - } + using var client = await CreateClientAsAdministrator(); + var res = await client.PatchAsJsonAsync("/users/user1", new UserPatchRequest { Username = "admin" }); + res.Should().HaveStatusCode(400) + .And.HaveCommonBody(ErrorCodes.UserController.UsernameConflict); } [Fact] - public async Task Delete_InvalidModel() + public async Task Patch_NoAuth_Unauthorized() { - using var client = await CreateClientAsAdmin(); - var url = "users/aaa!a"; - var res = await client.DeleteAsync(url); - res.Should().BeInvalidModel(); + using var client = await CreateClientAsUser(); + var res = await client.PatchAsJsonAsync("/users/user1", new UserPatchRequest { Nickname = "aaa" }); + res.Should().HaveStatusCode(HttpStatusCode.Unauthorized); + } + + [Fact] + public async Task Patch_User_Forbid() + { + using var client = await CreateClientAsUser(); + var res = await client.PatchAsJsonAsync("/users/admin", new UserPatchRequest { Nickname = "aaa" }); + res.Should().HaveStatusCode(HttpStatusCode.Forbidden); + } + + [Fact] + public async Task Patch_Username_Forbid() + { + using var client = await CreateClientAsUser(); + var res = await client.PatchAsJsonAsync("/users/user1", new UserPatchRequest { Username = "aaa" }); + res.Should().HaveStatusCode(HttpStatusCode.Forbidden); + } + + [Fact] + public async Task Patch_Password_Forbid() + { + using var client = await CreateClientAsUser(); + var res = await client.PatchAsJsonAsync("/users/user1", new UserPatchRequest { Password = "aaa" }); + res.Should().HaveStatusCode(HttpStatusCode.Forbidden); + } + + [Fact] + public async Task Patch_Administrator_Forbid() + { + using var client = await CreateClientAsUser(); + var res = await client.PatchAsJsonAsync("/users/user1", new UserPatchRequest { Administrator = true }); + res.Should().HaveStatusCode(HttpStatusCode.Forbidden); } [Fact] public async Task Delete_Deleted() { - using var client = await CreateClientAsAdmin(); - var url = "users/" + MockUser.User.Username; - var res = await client.DeleteAsync(url); - res.Should().BeDelete(true); + using var client = await CreateClientAsAdministrator(); + { + var res = await client.DeleteAsync("/users/user1"); + res.Should().BeDelete(true); + } - var res2 = await client.GetAsync(url); - res2.Should().HaveStatusCode(404); + { + var res = await client.GetAsync("/users/user1"); + res.Should().HaveStatusCode(404); + } } [Fact] public async Task Delete_NotExist() { - using var client = await CreateClientAsAdmin(); - var res = await client.DeleteAsync("users/usernotexist"); + using var client = await CreateClientAsAdministrator(); + var res = await client.DeleteAsync("/users/usernotexist"); res.Should().BeDelete(false); } - private const string changeUsernameUrl = "userop/changeusername"; + [Fact] + public async Task Delete_InvalidModel() + { + using var client = await CreateClientAsAdministrator(); + var res = await client.DeleteAsync("/users/aaa!a"); + res.Should().BeInvalidModel(); + } - public static IEnumerable Op_ChangeUsername_InvalidModel_Data() + [Fact] + public async Task Delete_NoAuth_Unauthorized() { - yield return new[] { null, "uuu" }; - yield return new[] { "uuu", null }; - yield return new[] { "a!a", "uuu" }; - yield return new[] { "uuu", "a!a" }; + using var client = await CreateDefaultClient(); + var res = await client.DeleteAsync("/users/aaa!a"); + res.Should().HaveStatusCode(HttpStatusCode.Unauthorized); } - [Theory] - [MemberData(nameof(Op_ChangeUsername_InvalidModel_Data))] - public async Task Op_ChangeUsername_InvalidModel(string oldUsername, string newUsername) + [Fact] + public async Task Delete_User_Forbid() { - using var client = await CreateClientAsAdmin(); - (await client.PostAsJsonAsync(changeUsernameUrl, - new ChangeUsernameRequest { OldUsername = oldUsername, NewUsername = newUsername })) - .Should().BeInvalidModel(); + using var client = await CreateClientAsUser(); + var res = await client.DeleteAsync("/users/aaa!a"); + res.Should().HaveStatusCode(HttpStatusCode.Forbidden); } + private const string createUserUrl = "/userop/createuser"; + [Fact] - public async Task Op_ChangeUsername_UserNotExist() + public async Task Op_CreateUser() { - using var client = await CreateClientAsAdmin(); - var res = await client.PostAsJsonAsync(changeUsernameUrl, - new ChangeUsernameRequest { OldUsername = "usernotexist", NewUsername = "newUsername" }); - res.Should().HaveStatusCode(400) - .And.HaveCommonBody() - .Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist); + using var client = await CreateClientAsAdministrator(); + { + var res = await client.PostAsJsonAsync(createUserUrl, new CreateUserRequest + { + Username = "aaa", + Password = "bbb", + Administrator = true, + Nickname = "ccc" + }); + res.Should().HaveStatusCode(200); + } + { + var res = await client.GetAsync("users/aaa"); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + body.Username.Should().Be("aaa"); + body.Nickname.Should().Be("ccc"); + body.Administrator.Should().BeTrue(); + } + { + // Test password. + (await CreateClientWithCredential("aaa", "bbb")).Dispose(); + } + } + + public static IEnumerable Op_CreateUser_InvalidModel_Data() + { + yield return new[] { new CreateUserRequest { Username = "aaa", Password = "bbb" } }; + yield return new[] { new CreateUserRequest { Username = "aaa", Administrator = true } }; + yield return new[] { new CreateUserRequest { Password = "bbb", Administrator = true } }; + yield return new[] { new CreateUserRequest { Username = "a!a", Password = "bbb", Administrator = true } }; + yield return new[] { new CreateUserRequest { Username = "aaa", Password = "", Administrator = true } }; + yield return new[] { new CreateUserRequest { Username = "aaa", Password = "bbb", Administrator = true, Nickname = new string('a', 40) } }; + } + + [Theory] + [MemberData(nameof(Op_CreateUser_InvalidModel_Data))] + public async Task Op_CreateUser_InvalidModel(CreateUserRequest body) + { + using var client = await CreateClientAsAdministrator(); + { + var res = await client.PostAsJsonAsync(createUserUrl, body); + res.Should().BeInvalidModel(); + } } [Fact] - public async Task Op_ChangeUsername_UserAlreadyExist() + public async Task Op_CreateUser_UsernameConflict() { - 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) - .And.HaveCommonBody() - .Which.Code.Should().Be(ErrorCodes.UserController.ChangeUsername_Conflict); + using var client = await CreateClientAsAdministrator(); + { + var res = await client.PostAsJsonAsync(createUserUrl, new CreateUserRequest + { + Username = "user1", + Password = "bbb", + Administrator = false + }); + res.Should().HaveStatusCode(400) + .And.HaveCommonBody(ErrorCodes.UserController.UsernameConflict); + } } - private async Task TestLogin(string username, string password) + [Fact] + public async Task Op_CreateUser_NoAuth_Unauthorized() { - using var client = await CreateClientWithNoAuth(); - var response = await client.PostAsJsonAsync("token/create", new CreateTokenRequest { Username = username, Password = password }); - response.Should().HaveStatusCode(200) - .And.HaveJsonBody(); + using var client = await CreateDefaultClient(); + { + var res = await client.PostAsJsonAsync(createUserUrl, new CreateUserRequest + { + Username = "aaa", + Password = "bbb", + Administrator = false + }); + res.Should().HaveStatusCode(HttpStatusCode.Unauthorized); + } } [Fact] - public async Task Op_ChangeUsername_Success() + public async Task Op_CreateUser_User_Forbid() { - 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 TestLogin(newUsername, MockUser.User.Password); + using var client = await CreateClientAsUser(); + { + var res = await client.PostAsJsonAsync(createUserUrl, new CreateUserRequest + { + Username = "aaa", + Password = "bbb", + Administrator = false + }); + res.Should().HaveStatusCode(HttpStatusCode.Forbidden); + } } - private const string changePasswordUrl = "userop/changepassword"; + private const string changePasswordUrl = "/userop/changepassword"; + + [Fact] + public async Task Op_ChangePassword() + { + using var client = await CreateClientAsUser(); + { + var res = await client.PostAsJsonAsync(changePasswordUrl, + new ChangePasswordRequest { OldPassword = "user1pw", NewPassword = "newpw" }); + res.Should().HaveStatusCode(200); + } + { + var res = await client.PatchAsJsonAsync("/users/user1", new UserPatchRequest { }); + res.Should().HaveStatusCode(HttpStatusCode.Unauthorized); + } + { + (await CreateClientWithCredential("user1", "newpw")).Dispose(); + } + } public static IEnumerable Op_ChangePassword_InvalidModel_Data() { @@ -243,9 +410,9 @@ namespace Timeline.Tests.IntegratedTests public async Task Op_ChangePassword_InvalidModel(string oldPassword, string newPassword) { using var client = await CreateClientAsUser(); - (await client.PostAsJsonAsync(changePasswordUrl, - new ChangePasswordRequest { OldPassword = oldPassword, NewPassword = newPassword })) - .Should().BeInvalidModel(); + var res = await client.PostAsJsonAsync(changePasswordUrl, + new ChangePasswordRequest { OldPassword = oldPassword, NewPassword = newPassword }); + res.Should().BeInvalidModel(); } [Fact] @@ -254,19 +421,15 @@ namespace Timeline.Tests.IntegratedTests using var client = await CreateClientAsUser(); var res = await client.PostAsJsonAsync(changePasswordUrl, new ChangePasswordRequest { OldPassword = "???", NewPassword = "???" }); res.Should().HaveStatusCode(400) - .And.HaveCommonBody() - .Which.Code.Should().Be(ErrorCodes.UserController.ChangePassword_BadOldPassword); + .And.HaveCommonBody(ErrorCodes.UserController.ChangePassword_BadOldPassword); } [Fact] - public async Task Op_ChangePassword_Success() + public async Task Op_ChangePassword_NoAuth_Unauthorized() { - 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 TestLogin(MockUser.User.Username, newPassword); + using var client = await CreateDefaultClient(); + var res = await client.PostAsJsonAsync(changePasswordUrl, new ChangePasswordRequest { OldPassword = "???", NewPassword = "???" }); + res.Should().HaveStatusCode(HttpStatusCode.Unauthorized); } } } diff --git a/Timeline.Tests/Services/UserAvatarServiceTest.cs b/Timeline.Tests/Services/UserAvatarServiceTest.cs deleted file mode 100644 index 2dca7ccf..00000000 --- a/Timeline.Tests/Services/UserAvatarServiceTest.cs +++ /dev/null @@ -1,280 +0,0 @@ -using FluentAssertions; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using SixLabors.ImageSharp.Formats.Png; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Timeline.Entities; -using Timeline.Services; -using Timeline.Tests.Helpers; -using Xunit; - -namespace Timeline.Tests.Services -{ - public class UserAvatarValidatorTest : IClassFixture - { - private readonly UserAvatarValidator _validator; - - public UserAvatarValidatorTest(UserAvatarValidator validator) - { - _validator = validator; - } - - [Fact] - public void CantDecode() - { - Avatar avatar = new Avatar - { - Data = Encoding.ASCII.GetBytes("This is not a image."), - Type = "image/png" - }; - _validator.Awaiting(v => v.Validate(avatar)) - .Should().Throw() - .Where(e => e.Avatar == avatar && e.Error == AvatarFormatException.ErrorReason.CantDecode); - } - - [Fact] - public void UnmatchedFormat() - { - Avatar avatar = new Avatar - { - Data = ImageHelper.CreatePngWithSize(100, 100), - Type = "image/jpeg" - }; - _validator.Awaiting(v => v.Validate(avatar)) - .Should().Throw() - .Where(e => e.Avatar == avatar && e.Error == AvatarFormatException.ErrorReason.UnmatchedFormat); - } - - [Fact] - public void BadSize() - { - Avatar avatar = new Avatar - { - Data = ImageHelper.CreatePngWithSize(100, 200), - Type = PngFormat.Instance.DefaultMimeType - }; - _validator.Awaiting(v => v.Validate(avatar)) - .Should().Throw() - .Where(e => e.Avatar == avatar && e.Error == AvatarFormatException.ErrorReason.BadSize); - } - - [Fact] - public void Success() - { - Avatar avatar = new Avatar - { - Data = ImageHelper.CreatePngWithSize(100, 100), - Type = PngFormat.Instance.DefaultMimeType - }; - _validator.Awaiting(v => v.Validate(avatar)) - .Should().NotThrow(); - } - } - - public class UserAvatarServiceTest : IDisposable - { - private UserAvatarEntity CreateMockAvatarEntity(string key) => new UserAvatarEntity - { - Type = $"image/test{key}", - Data = Encoding.ASCII.GetBytes($"mock{key}"), - ETag = $"etag{key}", - LastModified = DateTime.Now - }; - - private AvatarInfo CreateMockAvatarInfo(string key) => new AvatarInfo - { - Avatar = new Avatar - { - Type = $"image/test{key}", - Data = Encoding.ASCII.GetBytes($"mock{key}") - }, - LastModified = DateTime.Now - }; - - private Avatar CreateMockAvatar(string key) => new Avatar - { - Type = $"image/test{key}", - Data = Encoding.ASCII.GetBytes($"mock{key}") - }; - - private static Avatar ToAvatar(UserAvatarEntity entity) - { - return new Avatar - { - Data = entity.Data, - Type = entity.Type - }; - } - - private static AvatarInfo ToAvatarInfo(UserAvatarEntity entity) - { - return new AvatarInfo - { - Avatar = ToAvatar(entity), - LastModified = entity.LastModified - }; - } - - private readonly Mock _mockDefaultAvatarProvider; - private readonly Mock _mockValidator; - private readonly Mock _mockETagGenerator; - private readonly Mock _mockClock; - - private readonly TestDatabase _database; - - private readonly UserAvatarService _service; - - public UserAvatarServiceTest() - { - _mockDefaultAvatarProvider = new Mock(); - _mockValidator = new Mock(); - _mockETagGenerator = new Mock(); - _mockClock = new Mock(); - - _database = new TestDatabase(); - - _service = new UserAvatarService(NullLogger.Instance, _database.Context, _mockDefaultAvatarProvider.Object, _mockValidator.Object, _mockETagGenerator.Object, _mockClock.Object); - } - - public void Dispose() - { - _database.Dispose(); - } - - [Theory] - [InlineData(null, typeof(ArgumentNullException))] - [InlineData("", typeof(UsernameBadFormatException))] - [InlineData("a!a", typeof(UsernameBadFormatException))] - [InlineData("usernotexist", typeof(UserNotExistException))] - public async Task GetAvatarETag_ShouldThrow(string username, Type exceptionType) - { - await _service.Awaiting(s => s.GetAvatarETag(username)).Should().ThrowAsync(exceptionType); - } - - [Fact] - public async Task GetAvatarETag_ShouldReturn_Default() - { - const string etag = "aaaaaa"; - _mockDefaultAvatarProvider.Setup(p => p.GetDefaultAvatarETag()).ReturnsAsync(etag); - (await _service.GetAvatarETag(MockUser.User.Username)).Should().Be(etag); - } - - [Fact] - public async Task GetAvatarETag_ShouldReturn_Data() - { - string username = MockUser.User.Username; - var mockAvatarEntity = CreateMockAvatarEntity("aaa"); - { - var context = _database.Context; - var user = await context.Users.Where(u => u.Username == username).Include(u => u.Avatar).SingleAsync(); - user.Avatar = mockAvatarEntity; - await context.SaveChangesAsync(); - } - (await _service.GetAvatarETag(username)).Should().BeEquivalentTo(mockAvatarEntity.ETag); - } - - [Theory] - [InlineData(null, typeof(ArgumentNullException))] - [InlineData("", typeof(UsernameBadFormatException))] - [InlineData("a!a", typeof(UsernameBadFormatException))] - [InlineData("usernotexist", typeof(UserNotExistException))] - public async Task GetAvatar_ShouldThrow(string username, Type exceptionType) - { - await _service.Awaiting(s => s.GetAvatar(username)).Should().ThrowAsync(exceptionType); - - } - - [Fact] - public async Task GetAvatar_ShouldReturn_Default() - { - var mockAvatar = CreateMockAvatarInfo("aaa"); - _mockDefaultAvatarProvider.Setup(p => p.GetDefaultAvatar()).ReturnsAsync(mockAvatar); - string username = MockUser.User.Username; - (await _service.GetAvatar(username)).Should().BeEquivalentTo(mockAvatar); - } - - [Fact] - public async Task GetAvatar_ShouldReturn_Data() - { - string username = MockUser.User.Username; - var mockAvatarEntity = CreateMockAvatarEntity("aaa"); - { - var context = _database.Context; - var user = await context.Users.Where(u => u.Username == username).Include(u => u.Avatar).SingleAsync(); - user.Avatar = mockAvatarEntity; - await context.SaveChangesAsync(); - } - - (await _service.GetAvatar(username)).Should().BeEquivalentTo(ToAvatarInfo(mockAvatarEntity)); - } - - public static IEnumerable SetAvatar_ShouldThrow_Data() - { - yield return new object[] { null, null, typeof(ArgumentNullException) }; - yield return new object[] { "", null, typeof(UsernameBadFormatException) }; - yield return new object[] { "u!u", null, typeof(UsernameBadFormatException) }; - yield return new object[] { null, new Avatar { Type = null, Data = new[] { (byte)0x00 } }, typeof(ArgumentException) }; - yield return new object[] { null, new Avatar { Type = "", Data = new[] { (byte)0x00 } }, typeof(ArgumentException) }; - yield return new object[] { null, new Avatar { Type = "aaa", Data = null }, typeof(ArgumentException) }; - yield return new object[] { "usernotexist", null, typeof(UserNotExistException) }; - } - - [Theory] - [MemberData(nameof(SetAvatar_ShouldThrow_Data))] - public async Task SetAvatar_ShouldThrow(string username, Avatar avatar, Type exceptionType) - { - await _service.Awaiting(s => s.SetAvatar(username, avatar)).Should().ThrowAsync(exceptionType); - } - - [Fact] - public async Task SetAvatar_Should_Work() - { - string username = MockUser.User.Username; - - var user = await _database.Context.Users.Where(u => u.Username == username).Include(u => u.Avatar).SingleAsync(); - - var avatar1 = CreateMockAvatar("aaa"); - var avatar2 = CreateMockAvatar("bbb"); - - string etag1 = "etagaaa"; - string etag2 = "etagbbb"; - - DateTime dateTime1 = DateTime.Now.AddSeconds(2); - DateTime dateTime2 = DateTime.Now.AddSeconds(10); - DateTime dateTime3 = DateTime.Now.AddSeconds(20); - - // create - _mockETagGenerator.Setup(g => g.Generate(avatar1.Data)).ReturnsAsync(etag1); - _mockClock.Setup(c => c.GetCurrentTime()).Returns(dateTime1); - await _service.SetAvatar(username, avatar1); - user.Avatar.Should().NotBeNull(); - user.Avatar.Type.Should().Be(avatar1.Type); - user.Avatar.Data.Should().Equal(avatar1.Data); - user.Avatar.ETag.Should().Be(etag1); - user.Avatar.LastModified.Should().Be(dateTime1); - - // modify - _mockETagGenerator.Setup(g => g.Generate(avatar2.Data)).ReturnsAsync(etag2); - _mockClock.Setup(c => c.GetCurrentTime()).Returns(dateTime2); - await _service.SetAvatar(username, avatar2); - user.Avatar.Should().NotBeNull(); - user.Avatar.Type.Should().Be(avatar2.Type); - user.Avatar.Data.Should().Equal(avatar2.Data); - user.Avatar.ETag.Should().Be(etag2); - user.Avatar.LastModified.Should().Be(dateTime2); - - // delete - _mockClock.Setup(c => c.GetCurrentTime()).Returns(dateTime3); - await _service.SetAvatar(username, null); - user.Avatar.Type.Should().BeNull(); - user.Avatar.Data.Should().BeNull(); - user.Avatar.ETag.Should().BeNull(); - user.Avatar.LastModified.Should().Be(dateTime3); - } - } -} diff --git a/Timeline.Tests/Services/UserDetailServiceTest.cs b/Timeline.Tests/Services/UserDetailServiceTest.cs deleted file mode 100644 index dbff2705..00000000 --- a/Timeline.Tests/Services/UserDetailServiceTest.cs +++ /dev/null @@ -1,107 +0,0 @@ -using FluentAssertions; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging.Abstractions; -using System; -using System.Linq; -using System.Threading.Tasks; -using Timeline.Entities; -using Timeline.Services; -using Timeline.Tests.Helpers; -using Xunit; - -namespace Timeline.Tests.Services -{ - public class UserDetailServiceTest : IDisposable - { - private readonly TestDatabase _testDatabase; - - private readonly UserDetailService _service; - - public UserDetailServiceTest() - { - _testDatabase = new TestDatabase(); - _service = new UserDetailService(_testDatabase.Context, NullLogger.Instance); - } - - public void Dispose() - { - _testDatabase.Dispose(); - } - - [Theory] - [InlineData(null, typeof(ArgumentNullException))] - [InlineData("", typeof(UsernameBadFormatException))] - [InlineData("a!a", typeof(UsernameBadFormatException))] - [InlineData("usernotexist", typeof(UserNotExistException))] - public async Task GetNickname_ShouldThrow(string username, Type exceptionType) - { - await _service.Awaiting(s => s.GetNickname(username)).Should().ThrowAsync(exceptionType); - } - - [Fact] - public async Task GetNickname_ShouldReturnUsername() - { - var result = await _service.GetNickname(MockUser.User.Username); - result.Should().Be(MockUser.User.Username); - } - - [Fact] - public async Task GetNickname_ShouldReturnData() - { - const string nickname = "aaaaaa"; - { - var context = _testDatabase.Context; - var userId = (await context.Users.Where(u => u.Username == MockUser.User.Username).Select(u => new { u.Id }).SingleAsync()).Id; - context.UserDetails.Add(new UserDetailEntity - { - Nickname = nickname, - UserId = userId - }); - await context.SaveChangesAsync(); - } - var result = await _service.GetNickname(MockUser.User.Username); - result.Should().Be(nickname); - } - - [Theory] - [InlineData(null, typeof(ArgumentNullException))] - [InlineData("", typeof(UsernameBadFormatException))] - [InlineData("a!a", typeof(UsernameBadFormatException))] - [InlineData("usernotexist", typeof(UserNotExistException))] - public async Task SetNickname_ShouldThrow(string username, Type exceptionType) - { - await _service.Awaiting(s => s.SetNickname(username, null)).Should().ThrowAsync(exceptionType); - } - - [Fact] - public async Task SetNickname_ShouldThrow_ArgumentException() - { - await _service.Awaiting(s => s.SetNickname("uuu", new string('a', 50))).Should().ThrowAsync(); - } - - [Fact] - public async Task SetNickname_ShouldWork() - { - var username = MockUser.User.Username; - var user = await _testDatabase.Context.Users.Where(u => u.Username == username).Include(u => u.Detail).SingleAsync(); - - var nickname1 = "nickname1"; - var nickname2 = "nickname2"; - - await _service.SetNickname(username, null); - user.Detail.Should().BeNull(); - - await _service.SetNickname(username, nickname1); - user.Detail.Should().NotBeNull(); - user.Detail.Nickname.Should().Be(nickname1); - - await _service.SetNickname(username, nickname2); - user.Detail.Should().NotBeNull(); - user.Detail.Nickname.Should().Be(nickname2); - - await _service.SetNickname(username, null); - user.Detail.Should().NotBeNull(); - user.Detail.Nickname.Should().BeNull(); - } - } -} diff --git a/Timeline.Tests/Services/UserTokenManagerTest.cs b/Timeline.Tests/Services/UserTokenManagerTest.cs deleted file mode 100644 index e649fbab..00000000 --- a/Timeline.Tests/Services/UserTokenManagerTest.cs +++ /dev/null @@ -1,163 +0,0 @@ -using FluentAssertions; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using System; -using System.Threading.Tasks; -using Timeline.Models; -using Timeline.Services; -using Timeline.Tests.Helpers; -using Xunit; - -namespace Timeline.Tests.Services -{ - public class UserTokenManagerTest - { - private readonly UserTokenManager _service; - - private readonly Mock _mockUserService; - private readonly Mock _mockUserTokenService; - private readonly TestClock _mockClock; - - public UserTokenManagerTest() - { - _mockUserService = new Mock(); - _mockUserTokenService = new Mock(); - _mockClock = new TestClock(); - - _service = new UserTokenManager(NullLogger.Instance, _mockUserService.Object, _mockUserTokenService.Object, _mockClock); - } - - [Theory] - [InlineData(null, "aaa", "username")] - [InlineData("aaa", null, "password")] - public void CreateToken_NullArgument(string username, string password, string paramName) - { - _service.Invoking(s => s.CreateToken(username, password)).Should().Throw() - .Which.ParamName.Should().Be(paramName); - } - - [Theory] - [InlineData(typeof(UsernameBadFormatException))] - [InlineData(typeof(UserNotExistException))] - [InlineData(typeof(BadPasswordException))] - public async Task CreateToken_VerifyCredential_Throw(Type exceptionType) - { - const string username = "uuu"; - const string password = "ppp"; - _mockUserService.Setup(s => s.VerifyCredential(username, password)).ThrowsAsync((Exception)Activator.CreateInstance(exceptionType)); - await _service.Awaiting(s => s.CreateToken(username, password)).Should().ThrowAsync(exceptionType); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task CreateToken_Success(bool setExpireTime) - { - const string username = "uuu"; - const string password = "ppp"; - var mockExpireTime = setExpireTime ? (DateTime?)DateTime.Now : null; - var mockUserInfo = new User - { - Id = 1, - Username = username, - Administrator = false, - Version = 1 - }; - const string mockToken = "mocktokenaaaaaaa"; - - _mockUserService.Setup(s => s.VerifyCredential(username, password)).ReturnsAsync(mockUserInfo); - _mockUserTokenService.Setup(s => s.GenerateToken( - It.Is(userTokenInfo => - userTokenInfo.Id == mockUserInfo.Id && - userTokenInfo.Version == mockUserInfo.Version && - userTokenInfo.ExpireAt == mockExpireTime))).Returns(mockToken); - (await _service.CreateToken(username, password, mockExpireTime)) - .Should().BeEquivalentTo(new UserTokenCreateResult - { - Token = mockToken, - User = mockUserInfo - }); - } - - [Fact] - public void VerifyToken_NullArgument() - { - _service.Invoking(s => s.VerifyToken(null)).Should().ThrowAsync(); - } - - [Fact] - public async Task VerifyToken_BadFormat() - { - const string mockToken = "mocktokenaaaaaa"; - _mockUserTokenService.Setup(s => s.VerifyToken(mockToken)).Throws(new UserTokenBadFormatException()); - - await _service.Awaiting(s => s.VerifyToken(mockToken)).Should().ThrowAsync(); - } - - [Fact] - public async Task VerifyToken_TimeExpire() - { - const string mockToken = "mocktokenaaaaaa"; - var mockTime = DateTime.Now; - _mockClock.MockCurrentTime = mockTime; - var mockTokenInfo = new UserTokenInfo - { - Id = 1, - Version = 1, - ExpireAt = mockTime.AddDays(-1) - }; - _mockUserTokenService.Setup(s => s.VerifyToken(mockToken)).Returns(mockTokenInfo); - - await _service.Awaiting(s => s.VerifyToken(mockToken)).Should().ThrowAsync(); - } - - [Fact] - public async Task VerifyToken_BadVersion() - { - const string mockToken = "mocktokenaaaaaa"; - var mockTime = DateTime.Now; - _mockClock.MockCurrentTime = mockTime; - var mockTokenInfo = new UserTokenInfo - { - Id = 1, - Version = 1, - ExpireAt = mockTime.AddDays(1) - }; - _mockUserTokenService.Setup(s => s.VerifyToken(mockToken)).Returns(mockTokenInfo); - _mockUserService.Setup(s => s.GetUserById(1)).ReturnsAsync(new User - { - Id = 1, - Username = "aaa", - Administrator = false, - Version = 2 - }); - - await _service.Awaiting(s => s.VerifyToken(mockToken)).Should().ThrowAsync(); - } - - [Fact] - public async Task VerifyToken_Success() - { - const string mockToken = "mocktokenaaaaaa"; - var mockTime = DateTime.Now; - _mockClock.MockCurrentTime = mockTime; - var mockTokenInfo = new UserTokenInfo - { - Id = 1, - Version = 1, - ExpireAt = mockTime.AddDays(1) - }; - var mockUserInfo = new User - { - Id = 1, - Username = "aaa", - Administrator = false, - Version = 1 - }; - _mockUserTokenService.Setup(s => s.VerifyToken(mockToken)).Returns(mockTokenInfo); - _mockUserService.Setup(s => s.GetUserById(1)).ReturnsAsync(mockUserInfo); - - (await _service.VerifyToken(mockToken)).Should().BeEquivalentTo(mockUserInfo); - } - } -} diff --git a/Timeline.Tests/Timeline.Tests.csproj b/Timeline.Tests/Timeline.Tests.csproj index bde4f430..40e8cda2 100644 --- a/Timeline.Tests/Timeline.Tests.csproj +++ b/Timeline.Tests/Timeline.Tests.csproj @@ -18,7 +18,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/Timeline/Auth/MyAuthenticationHandler.cs b/Timeline/Auth/MyAuthenticationHandler.cs index e6b26c4b..3c97c329 100644 --- a/Timeline/Auth/MyAuthenticationHandler.cs +++ b/Timeline/Auth/MyAuthenticationHandler.cs @@ -8,7 +8,6 @@ using System.Linq; using System.Security.Claims; using System.Text.Encodings.Web; using System.Threading.Tasks; -using Timeline.Models; using Timeline.Services; using static Timeline.Resources.Authentication.AuthHandler; diff --git a/Timeline/Controllers/ControllerAuthExtensions.cs b/Timeline/Controllers/ControllerAuthExtensions.cs index 81fd2428..90da8a93 100644 --- a/Timeline/Controllers/ControllerAuthExtensions.cs +++ b/Timeline/Controllers/ControllerAuthExtensions.cs @@ -26,5 +26,20 @@ namespace Timeline.Controllers throw new InvalidOperationException("Failed to get user id because NameIdentifier claim is not a number."); } + + public static long? GetOptionalUserId(this ControllerBase controller) + { + if (controller.User == null) + return null; + + var claim = controller.User.FindFirst(ClaimTypes.NameIdentifier); + if (claim == null) + throw new InvalidOperationException("Failed to get user id because User has no NameIdentifier claim."); + + if (long.TryParse(claim.Value, out var value)) + return value; + + throw new InvalidOperationException("Failed to get user id because NameIdentifier claim is not a number."); + } } } diff --git a/Timeline/Controllers/PersonalTimelineController.cs b/Timeline/Controllers/PersonalTimelineController.cs index 2c70fad1..27618c41 100644 --- a/Timeline/Controllers/PersonalTimelineController.cs +++ b/Timeline/Controllers/PersonalTimelineController.cs @@ -4,45 +4,21 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using System.Collections.Generic; using System.Threading.Tasks; -using Timeline.Auth; using Timeline.Filters; -using Timeline.Models; using Timeline.Models.Http; using Timeline.Models.Validation; using Timeline.Services; -using static Timeline.Resources.Controllers.TimelineController; -using static Timeline.Resources.Messages; namespace Timeline.Controllers { [ApiController] + [CatchTimelineNotExistException] public class PersonalTimelineController : Controller { private readonly ILogger _logger; private readonly IPersonalTimelineService _service; - private bool IsAdmin() - { - if (User != null) - { - return User.IsAdministrator(); - } - return false; - } - - private string? GetAuthUsername() - { - if (User == null) - { - return null; - } - else - { - return User.Identity.Name; - } - } - public PersonalTimelineController(ILogger logger, IPersonalTimelineService service) { _logger = logger; @@ -50,17 +26,15 @@ namespace Timeline.Controllers } [HttpGet("users/{username}/timeline")] - [CatchTimelineNotExistException] public async Task> TimelineGet([FromRoute][Username] string username) { return await _service.GetTimeline(username); } [HttpGet("users/{username}/timeline/posts")] - [CatchTimelineNotExistException] public async Task>> PostListGet([FromRoute][Username] string username) { - if (!IsAdmin() && !await _service.HasReadPermission(username, GetAuthUsername())) + if (!this.IsAdministrator() && !await _service.HasReadPermission(username, this.GetOptionalUserId())) { return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } @@ -68,77 +42,88 @@ namespace Timeline.Controllers return await _service.GetPosts(username); } - [HttpPost("users/{username}/timeline/postop/create")] + [HttpPost("users/{username}/timeline/posts")] [Authorize] - [CatchTimelineNotExistException] - public async Task> PostOperationCreate([FromRoute][Username] string username, [FromBody] TimelinePostCreateRequest body) + public async Task> PostPost([FromRoute][Username] string username, [FromBody] TimelinePostCreateRequest body) { - if (!IsAdmin() && !await _service.IsMemberOf(username, GetAuthUsername()!)) + var id = this.GetUserId(); + if (!this.IsAdministrator() && !await _service.IsMemberOf(username, id)) { return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } - var res = await _service.CreatePost(username, User.Identity.Name!, body.Content, body.Time); + var res = await _service.CreatePost(username, id, body.Content, body.Time); return res; } - [HttpPost("users/{username}/timeline/postop/delete")] + [HttpDelete("users/{username}/timeline/posts/{id}")] [Authorize] - [CatchTimelineNotExistException] - public async Task PostOperationDelete([FromRoute][Username] string username, [FromBody] TimelinePostDeleteRequest body) + public async Task PostDelete([FromRoute][Username] string username, [FromRoute] long id) { try { - var postId = body.Id!.Value; - if (!IsAdmin() && !await _service.HasPostModifyPermission(username, postId, GetAuthUsername()!)) + if (!this.IsAdministrator() && !await _service.HasPostModifyPermission(username, id, this.GetUserId())) { return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } - await _service.DeletePost(username, postId); + await _service.DeletePost(username, id); + return Ok(CommonDeleteResponse.Delete()); } catch (TimelinePostNotExistException) { - return BadRequest(ErrorResponse.TimelineController.PostOperationDelete_NotExist()); + return Ok(CommonDeleteResponse.NotExist()); } - return Ok(); } - [HttpPost("users/{username}/timeline/op/property")] + [HttpPatch("users/{username}/timeline")] [Authorize] - [SelfOrAdmin] - [CatchTimelineNotExistException] - public async Task TimelineChangeProperty([FromRoute][Username] string username, [FromBody] TimelinePropertyChangeRequest body) + public async Task TimelinePatch([FromRoute][Username] string username, [FromBody] TimelinePatchRequest body) { + if (!this.IsAdministrator() && !(User.Identity.Name == username)) + { + return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); + } await _service.ChangeProperty(username, body); return Ok(); } - [HttpPost("users/{username}/timeline/op/member")] + [HttpPut("users/{username}/timeline/members/{member}")] [Authorize] - [SelfOrAdmin] - [CatchTimelineNotExistException] - public async Task TimelineChangeMember([FromRoute][Username] string username, [FromBody] TimelineMemberChangeRequest body) + public async Task TimelineMemberPut([FromRoute][Username] string username, [FromRoute][Username] string member) { + if (!this.IsAdministrator() && !(User.Identity.Name == username)) + { + return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); + } + try { - await _service.ChangeMember(username, body.Add, body.Remove); + await _service.ChangeMember(username, new List { member }, null); return Ok(); } - catch (TimelineMemberOperationUserException e) + catch (UserNotExistException) { - if (e.InnerException is UsernameBadFormatException) - { - return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel( - TimelineController_ChangeMember_UsernameBadFormat, e.Index, e.Operation)); - } - else if (e.InnerException is UserNotExistException) - { - return BadRequest(ErrorResponse.UserCommon.CustomMessage_NotExist( - TimelineController_ChangeMember_UserNotExist, e.Index, e.Operation)); - } + return BadRequest(ErrorResponse.TimelineController.MemberPut_NotExist()); + } + } + + [HttpDelete("users/{username}/timeline/members/{member}")] + [Authorize] + public async Task TimelineMemberDelete([FromRoute][Username] string username, [FromRoute][Username] string member) + { + if (!this.IsAdministrator() && !(User.Identity.Name == username)) + { + return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); + } - _logger.LogError(e, LogUnknownTimelineMemberOperationUserException); - throw; + try + { + await _service.ChangeMember(username, null, new List { member }); + return Ok(CommonDeleteResponse.Delete()); + } + catch (UserNotExistException) + { + return Ok(CommonDeleteResponse.NotExist()); } } } diff --git a/Timeline/Controllers/Testing/TestingI18nController.cs b/Timeline/Controllers/Testing/TestingI18nController.cs deleted file mode 100644 index febb56a5..00000000 --- a/Timeline/Controllers/Testing/TestingI18nController.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Localization; - -// For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860 - -namespace Timeline.Controllers.Testing -{ - [Route("testing/i18n")] - [ApiController] - public class TestingI18nController : Controller - { - private readonly IStringLocalizer _stringLocalizer; - - public TestingI18nController(IStringLocalizer stringLocalizer) - { - _stringLocalizer = stringLocalizer; - } - - [HttpGet("direct")] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static")] - public ActionResult Direct() - { - return Resources.Controllers.Testing.TestingI18nController.TestString; - } - - [HttpGet("localizer")] - public ActionResult Localizer() - { - return _stringLocalizer["TestString"].Value; - } - } -} diff --git a/Timeline/Controllers/TokenController.cs b/Timeline/Controllers/TokenController.cs index 9724c1a6..a7f5fbde 100644 --- a/Timeline/Controllers/TokenController.cs +++ b/Timeline/Controllers/TokenController.cs @@ -1,3 +1,4 @@ +using AutoMapper; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; @@ -5,7 +6,6 @@ using System; using System.Globalization; using System.Threading.Tasks; using Timeline.Helpers; -using Timeline.Models; using Timeline.Models.Http; using Timeline.Services; using static Timeline.Resources.Controllers.TokenController; @@ -20,20 +20,14 @@ namespace Timeline.Controllers private readonly ILogger _logger; private readonly IClock _clock; - private static Models.Http.User CreateUserFromUserInfo(Models.User userInfo) - { - return new Models.Http.User - { - Username = userInfo.Username, - Administrator = userInfo.Administrator - }; - } + private readonly IMapper _mapper; - public TokenController(IUserTokenManager userTokenManager, ILogger logger, IClock clock) + public TokenController(IUserTokenManager userTokenManager, ILogger logger, IClock clock, IMapper mapper) { _userTokenManager = userTokenManager; _logger = logger; _clock = clock; + _mapper = mapper; } [HttpPost("create")] @@ -65,7 +59,7 @@ namespace Timeline.Controllers return Ok(new CreateTokenResponse { Token = result.Token, - User = CreateUserFromUserInfo(result.User) + User = _mapper.Map(result.User) }); } catch (UserNotExistException e) @@ -100,7 +94,7 @@ namespace Timeline.Controllers ("Username", result.Username), ("Token", request.Token))); return Ok(new VerifyTokenResponse { - User = CreateUserFromUserInfo(result) + User = _mapper.Map(result) }); } catch (UserTokenTimeExpireException e) diff --git a/Timeline/Controllers/UserAvatarController.cs b/Timeline/Controllers/UserAvatarController.cs index 62f1d78c..ab0ad8e7 100644 --- a/Timeline/Controllers/UserAvatarController.cs +++ b/Timeline/Controllers/UserAvatarController.cs @@ -21,11 +21,13 @@ namespace Timeline.Controllers { private readonly ILogger _logger; + private readonly IUserService _userService; private readonly IUserAvatarService _service; - public UserAvatarController(ILogger logger, IUserAvatarService service) + public UserAvatarController(ILogger logger, IUserService userService, IUserAvatarService service) { _logger = logger; + _userService = userService; _service = service; } @@ -33,46 +35,50 @@ namespace Timeline.Controllers [ResponseCache(NoStore = false, Location = ResponseCacheLocation.None, Duration = 0)] public async Task Get([FromRoute][Username] string username) { - const string IfNonMatchHeaderKey = "If-None-Match"; - + long id; try { - var eTagValue = $"\"{await _service.GetAvatarETag(username)}\""; - var eTag = new EntityTagHeaderValue(eTagValue); - - if (Request.Headers.TryGetValue(IfNonMatchHeaderKey, out var value)) - { - if (!EntityTagHeaderValue.TryParseStrictList(value, out var eTagList)) - { - _logger.LogInformation(Log.Format(LogGetBadIfNoneMatch, - ("Username", username), ("If-None-Match", value))); - return BadRequest(ErrorResponse.Common.Header.IfNonMatch_BadFormat()); - } - - if (eTagList.FirstOrDefault(e => e.Equals(eTag)) != null) - { - Response.Headers.Add("ETag", eTagValue); - _logger.LogInformation(Log.Format(LogGetReturnNotModify, ("Username", username))); - return StatusCode(StatusCodes.Status304NotModified); - } - } - - var avatarInfo = await _service.GetAvatar(username); - var avatar = avatarInfo.Avatar; - - _logger.LogInformation(Log.Format(LogGetReturnData, ("Username", username))); - return File(avatar.Data, avatar.Type, new DateTimeOffset(avatarInfo.LastModified), eTag); + id = await _userService.GetUserIdByUsername(username); } catch (UserNotExistException e) { _logger.LogInformation(e, Log.Format(LogGetUserNotExist, ("Username", username))); return NotFound(ErrorResponse.UserCommon.NotExist()); } + + const string IfNonMatchHeaderKey = "If-None-Match"; + + var eTagValue = $"\"{await _service.GetAvatarETag(id)}\""; + var eTag = new EntityTagHeaderValue(eTagValue); + + if (Request.Headers.TryGetValue(IfNonMatchHeaderKey, out var value)) + { + if (!EntityTagHeaderValue.TryParseStrictList(value, out var eTagList)) + { + _logger.LogInformation(Log.Format(LogGetBadIfNoneMatch, + ("Username", username), ("If-None-Match", value))); + return BadRequest(ErrorResponse.Common.Header.IfNonMatch_BadFormat()); + } + + if (eTagList.FirstOrDefault(e => e.Equals(eTag)) != null) + { + Response.Headers.Add("ETag", eTagValue); + _logger.LogInformation(Log.Format(LogGetReturnNotModify, ("Username", username))); + return StatusCode(StatusCodes.Status304NotModified); + } + } + + var avatarInfo = await _service.GetAvatar(id); + var avatar = avatarInfo.Avatar; + + _logger.LogInformation(Log.Format(LogGetReturnData, ("Username", username))); + return File(avatar.Data, avatar.Type, new DateTimeOffset(avatarInfo.LastModified), eTag); + } [HttpPut("users/{username}/avatar")] [Authorize] - [RequireContentType, RequireContentLength] + [RequireContentLength] [Consumes("image/png", "image/jpeg", "image/gif", "image/webp")] public async Task Put([FromRoute][Username] string username) { @@ -87,6 +93,17 @@ namespace Timeline.Controllers return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } + long id; + try + { + id = await _userService.GetUserIdByUsername(username); + } + catch (UserNotExistException e) + { + _logger.LogInformation(e, Log.Format(LogPutUserNotExist, ("Username", username))); + return BadRequest(ErrorResponse.UserCommon.NotExist()); + } + try { var data = new byte[contentLength]; @@ -99,7 +116,7 @@ namespace Timeline.Controllers if (await Request.Body.ReadAsync(extraByte) != 0) return BadRequest(ErrorResponse.Common.Content.UnmatchedLength_Bigger()); - await _service.SetAvatar(username, new Avatar + await _service.SetAvatar(id, new Avatar { Data = data, Type = Request.ContentType @@ -109,11 +126,6 @@ namespace Timeline.Controllers ("Username", username), ("Mime Type", Request.ContentType))); return Ok(); } - catch (UserNotExistException e) - { - _logger.LogInformation(e, Log.Format(LogPutUserNotExist, ("Username", username))); - return BadRequest(ErrorResponse.UserCommon.NotExist()); - } catch (AvatarFormatException e) { _logger.LogInformation(e, Log.Format(LogPutUserBadFormat, ("Username", username))); @@ -139,16 +151,19 @@ namespace Timeline.Controllers return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } + long id; try { - await _service.SetAvatar(username, null); - return Ok(); + id = await _userService.GetUserIdByUsername(username); } catch (UserNotExistException e) { _logger.LogInformation(e, Log.Format(LogDeleteNotExist, ("Username", username))); return BadRequest(ErrorResponse.UserCommon.NotExist()); } + + await _service.SetAvatar(id, null); + return Ok(); } } } diff --git a/Timeline/Controllers/UserController.cs b/Timeline/Controllers/UserController.cs index 4c585198..400a518c 100644 --- a/Timeline/Controllers/UserController.cs +++ b/Timeline/Controllers/UserController.cs @@ -1,3 +1,4 @@ +using AutoMapper; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -17,30 +18,40 @@ namespace Timeline.Controllers [ApiController] public class UserController : Controller { - private readonly ILogger _logger; private readonly IUserService _userService; + private readonly IMapper _mapper; - public UserController(ILogger logger, IUserService userService) + public UserController(ILogger logger, IUserService userService, IMapper mapper) { _logger = logger; _userService = userService; + _mapper = mapper; + } + + private IUserInfo ConvertToUserInfo(User user, bool administrator) + { + if (administrator) + return _mapper.Map(user); + else + return _mapper.Map(user); } [HttpGet("users")] - public async Task> List() + public async Task> List() { var users = await _userService.GetUsers(); - return Ok(users.Select(u => u.EraseSecretAndFinalFill(Url, this.IsAdministrator())).ToArray()); + var administrator = this.IsAdministrator(); + return Ok(users.Select(u => ConvertToUserInfo(u, administrator)).ToArray()); } [HttpGet("users/{username}")] - public async Task> Get([FromRoute][Username] string username) + public async Task> Get([FromRoute][Username] string username) { try { var user = await _userService.GetUserByUsername(username); - return Ok(user.EraseSecretAndFinalFill(Url, this.IsAdministrator())); + return Ok(ConvertToUserInfo(user, this.IsAdministrator())); } catch (UserNotExistException e) { @@ -52,22 +63,11 @@ namespace Timeline.Controllers [HttpPatch("users/{username}"), Authorize] public async Task Patch([FromBody] UserPatchRequest body, [FromRoute][Username] string username) { - static User Convert(UserPatchRequest body) - { - return new User - { - Username = body.Username, - Password = body.Password, - Administrator = body.Administrator, - Nickname = body.Nickname - }; - } - if (this.IsAdministrator()) { try { - await _userService.ModifyUser(username, Convert(body)); + await _userService.ModifyUser(username, _mapper.Map(body)); return Ok(); } catch (UserNotExistException e) @@ -75,6 +75,10 @@ namespace Timeline.Controllers _logger.LogInformation(e, Log.Format(LogPatchUserNotExist, ("Username", username))); return NotFound(ErrorResponse.UserCommon.NotExist()); } + catch (ConflictException) + { + return BadRequest(ErrorResponse.UserController.UsernameConflict()); + } } else { @@ -94,7 +98,7 @@ namespace Timeline.Controllers return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.CustomMessage_Forbid(UserController_Patch_Forbid_Administrator)); - await _userService.ModifyUser(this.GetUserId(), Convert(body)); + await _userService.ModifyUser(this.GetUserId(), _mapper.Map(body)); return Ok(); } } @@ -113,10 +117,18 @@ namespace Timeline.Controllers } } - [HttpPost("userop/create"), AdminAuthorize] - public async Task CreateUser([FromBody] User body) + [HttpPost("userop/createuser"), AdminAuthorize] + public async Task CreateUser([FromBody] CreateUserRequest body) { - + try + { + await _userService.CreateUser(_mapper.Map(body)); + return Ok(); + } + catch (ConflictException) + { + return BadRequest(ErrorResponse.UserController.UsernameConflict()); + } } [HttpPost("userop/changepassword"), Authorize] @@ -133,7 +145,7 @@ namespace Timeline.Controllers ("Username", User.Identity.Name), ("Old Password", request.OldPassword))); return BadRequest(ErrorResponse.UserController.ChangePassword_BadOldPassword()); } - // User can't be non-existent or the token is bad. + // User can't be non-existent or the token is bad. } } } diff --git a/Timeline/Entities/DatabaseContext.cs b/Timeline/Entities/DatabaseContext.cs index ac4ad7b2..cac33379 100644 --- a/Timeline/Entities/DatabaseContext.cs +++ b/Timeline/Entities/DatabaseContext.cs @@ -10,7 +10,6 @@ namespace Timeline.Entities } - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")] protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity().Property(e => e.Version).HasDefaultValue(0); @@ -19,7 +18,6 @@ namespace Timeline.Entities public DbSet Users { get; set; } = default!; public DbSet UserAvatars { get; set; } = default!; - 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/Entities/TimelineEntity.cs b/Timeline/Entities/TimelineEntity.cs index 2bfd6107..c50fe6dd 100644 --- a/Timeline/Entities/TimelineEntity.cs +++ b/Timeline/Entities/TimelineEntity.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using Timeline.Models; +using Timeline.Models.Http; namespace Timeline.Entities { diff --git a/Timeline/Filters/Header.cs b/Timeline/Filters/Header.cs index 843a619d..0db11faf 100644 --- a/Timeline/Filters/Header.cs +++ b/Timeline/Filters/Header.cs @@ -6,7 +6,6 @@ namespace Timeline.Filters { public class RequireContentTypeAttribute : ActionFilterAttribute { - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")] public override void OnActionExecuting(ActionExecutingContext context) { if (context.HttpContext.Request.ContentType == null) @@ -31,7 +30,6 @@ namespace Timeline.Filters public bool RequireNonZero { get; set; } - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")] public override void OnActionExecuting(ActionExecutingContext context) { if (context.HttpContext.Request.ContentLength == null) diff --git a/Timeline/Filters/Timeline.cs b/Timeline/Filters/Timeline.cs index bc142db0..729dbec7 100644 --- a/Timeline/Filters/Timeline.cs +++ b/Timeline/Filters/Timeline.cs @@ -7,7 +7,6 @@ namespace Timeline.Filters { public class CatchTimelineNotExistExceptionAttribute : ExceptionFilterAttribute { - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")] public override void OnException(ExceptionContext context) { if (context.Exception is TimelineNotExistException e) diff --git a/Timeline/Filters/User.cs b/Timeline/Filters/User.cs deleted file mode 100644 index 12ed6155..00000000 --- a/Timeline/Filters/User.cs +++ /dev/null @@ -1,68 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using System; -using Timeline.Auth; -using Timeline.Models.Http; -using Timeline.Services; -using static Timeline.Resources.Filters; - -namespace Timeline.Filters -{ - public class SelfOrAdminAttribute : ActionFilterAttribute - { - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")] - public override void OnActionExecuting(ActionExecutingContext context) - { - var logger = context.HttpContext.RequestServices.GetRequiredService>(); - - var user = context.HttpContext.User; - - if (user == null) - { - logger.LogError(LogSelfOrAdminNoUser); - return; - } - - if (context.ModelState.TryGetValue("username", out var model)) - { - if (model.RawValue is string username) - { - if (!user.IsAdministrator() && user.Identity.Name != username) - { - context.Result = new ObjectResult(ErrorResponse.Common.Forbid()) - { StatusCode = StatusCodes.Status403Forbidden }; - } - } - else - { - logger.LogError(LogSelfOrAdminUsernameNotString); - } - } - else - { - logger.LogError(LogSelfOrAdminNoUsername); - } - } - } - - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] - public class CatchUserNotExistExceptionAttribute : ExceptionFilterAttribute - { - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "ASP.Net already checked.")] - public override void OnException(ExceptionContext context) - { - if (context.Exception is UserNotExistException) - { - var body = ErrorResponse.UserCommon.NotExist(); - - if (context.HttpContext.Request.Method == "GET") - context.Result = new NotFoundObjectResult(body); - else - context.Result = new BadRequestObjectResult(body); - } - } - } -} diff --git a/Timeline/Formatters/StringInputFormatter.cs b/Timeline/Formatters/StringInputFormatter.cs index 90847e36..b1924268 100644 --- a/Timeline/Formatters/StringInputFormatter.cs +++ b/Timeline/Formatters/StringInputFormatter.cs @@ -15,7 +15,6 @@ namespace Timeline.Formatters SupportedEncodings.Add(Encoding.UTF8); } - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")] public override async Task ReadRequestBodyAsync(InputFormatterContext context, Encoding effectiveEncoding) { var request = context.HttpContext.Request; diff --git a/Timeline/GlobalSuppressions.cs b/Timeline/GlobalSuppressions.cs index d27b3c16..2b0da576 100644 --- a/Timeline/GlobalSuppressions.cs +++ b/Timeline/GlobalSuppressions.cs @@ -11,3 +11,4 @@ [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1724:Type names should not match namespaces", Justification = "Generated error response identifiers.", Scope = "type", Target = "Timeline.Models.Http.ErrorResponse")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "Generated error response.", Scope = "type", Target = "Timeline.Models.Http.ErrorResponse")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1056:Uri properties should not be strings", Justification = "That's unnecessary.")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "Adundant")] diff --git a/Timeline/Helpers/InvalidModelResponseFactory.cs b/Timeline/Helpers/InvalidModelResponseFactory.cs index 71ee44a9..9b253e7d 100644 --- a/Timeline/Helpers/InvalidModelResponseFactory.cs +++ b/Timeline/Helpers/InvalidModelResponseFactory.cs @@ -6,7 +6,6 @@ namespace Timeline.Helpers { public static class InvalidModelResponseFactory { - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")] public static IActionResult Factory(ActionContext context) { var modelState = context.ModelState; diff --git a/Timeline/Models/Converters/JsonDateTimeConverter.cs b/Timeline/Models/Converters/JsonDateTimeConverter.cs index 69af53c1..ef129a01 100644 --- a/Timeline/Models/Converters/JsonDateTimeConverter.cs +++ b/Timeline/Models/Converters/JsonDateTimeConverter.cs @@ -6,7 +6,6 @@ using System.Text.Json.Serialization; namespace Timeline.Models.Converters { - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")] public class JsonDateTimeConverter : JsonConverter { public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) diff --git a/Timeline/Models/Http/ErrorResponse.cs b/Timeline/Models/Http/ErrorResponse.cs index 6a53e0c3..87516638 100644 --- a/Timeline/Models/Http/ErrorResponse.cs +++ b/Timeline/Models/Http/ErrorResponse.cs @@ -184,14 +184,14 @@ namespace Timeline.Models.Http public static class UserController { - public static CommonResponse ChangeUsername_Conflict(params object?[] formatArgs) + public static CommonResponse UsernameConflict(params object?[] formatArgs) { - return new CommonResponse(ErrorCodes.UserController.ChangeUsername_Conflict, string.Format(UserController_ChangeUsername_Conflict, formatArgs)); + return new CommonResponse(ErrorCodes.UserController.UsernameConflict, string.Format(UserController_UsernameConflict, formatArgs)); } - public static CommonResponse CustomMessage_ChangeUsername_Conflict(string message, params object?[] formatArgs) + public static CommonResponse CustomMessage_UsernameConflict(string message, params object?[] formatArgs) { - return new CommonResponse(ErrorCodes.UserController.ChangeUsername_Conflict, string.Format(message, formatArgs)); + return new CommonResponse(ErrorCodes.UserController.UsernameConflict, string.Format(message, formatArgs)); } public static CommonResponse ChangePassword_BadOldPassword(params object?[] formatArgs) @@ -244,18 +244,18 @@ namespace Timeline.Models.Http public static class TimelineController { - public static CommonResponse PostOperationDelete_NotExist(params object?[] formatArgs) + public static CommonResponse MemberPut_NotExist(params object?[] formatArgs) { - return new CommonResponse(ErrorCodes.TimelineController.PostOperationDelete_NotExist, string.Format(TimelineController_PostOperationDelete_NotExist, formatArgs)); + return new CommonResponse(ErrorCodes.TimelineController.MemberPut_NotExist, string.Format(TimelineController_MemberPut_NotExist, formatArgs)); } - public static CommonResponse CustomMessage_PostOperationDelete_NotExist(string message, params object?[] formatArgs) + public static CommonResponse CustomMessage_MemberPut_NotExist(string message, params object?[] formatArgs) { - return new CommonResponse(ErrorCodes.TimelineController.PostOperationDelete_NotExist, string.Format(message, formatArgs)); + return new CommonResponse(ErrorCodes.TimelineController.MemberPut_NotExist, string.Format(message, formatArgs)); } } } -} +} \ No newline at end of file diff --git a/Timeline/Models/Http/Timeline.cs b/Timeline/Models/Http/Timeline.cs deleted file mode 100644 index 3029434e..00000000 --- a/Timeline/Models/Http/Timeline.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; - -namespace Timeline.Models.Http -{ - public class TimelinePostCreateRequest - { - [Required(AllowEmptyStrings = true)] - public string Content { get; set; } = default!; - - public DateTime? Time { get; set; } - } - - public class TimelinePostCreateResponse - { - public long Id { get; set; } - - public DateTime Time { get; set; } - } - - public class TimelinePostDeleteRequest - { - [Required] - public long? Id { get; set; } - } - - public class TimelinePropertyChangeRequest - { - public string? Description { get; set; } - - public TimelineVisibility? Visibility { get; set; } - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "This is a DTO class.")] - public class TimelineMemberChangeRequest - { - public List? Add { get; set; } - - public List? Remove { get; set; } - } -} diff --git a/Timeline/Models/Http/TimelineCommon.cs b/Timeline/Models/Http/TimelineCommon.cs new file mode 100644 index 00000000..febb8186 --- /dev/null +++ b/Timeline/Models/Http/TimelineCommon.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; + +namespace Timeline.Models.Http +{ + 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; } + public string Content { get; set; } = default!; + public DateTime Time { get; set; } + public UserInfo Author { get; set; } = default!; + public DateTime LastUpdated { get; set; } = default!; + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "This is a DTO class.")] + public class BaseTimelineInfo + { + public string Description { get; set; } = default!; + public UserInfo Owner { get; set; } = default!; + public TimelineVisibility Visibility { get; set; } + public List Members { get; set; } = default!; + } + + public class TimelineInfo : BaseTimelineInfo + { + public string Name { get; set; } = default!; + } +} diff --git a/Timeline/Models/Http/TimelineController.cs b/Timeline/Models/Http/TimelineController.cs new file mode 100644 index 00000000..f9a4d3e5 --- /dev/null +++ b/Timeline/Models/Http/TimelineController.cs @@ -0,0 +1,20 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace Timeline.Models.Http +{ + public class TimelinePostCreateRequest + { + [Required(AllowEmptyStrings = true)] + public string Content { get; set; } = default!; + + public DateTime? Time { get; set; } + } + + public class TimelinePatchRequest + { + public string? Description { get; set; } + + public TimelineVisibility? Visibility { get; set; } + } +} diff --git a/Timeline/Models/Http/Token.cs b/Timeline/Models/Http/Token.cs deleted file mode 100644 index 0649f1d1..00000000 --- a/Timeline/Models/Http/Token.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace Timeline.Models.Http -{ - public class CreateTokenRequest - { - [Required] - public string Username { get; set; } = default!; - [Required] - public string Password { get; set; } = default!; - // in days, optional - [Range(1, 365)] - public int? Expire { get; set; } - } - - public class CreateTokenResponse - { - public string Token { get; set; } = default!; - public User User { get; set; } = default!; - } - - public class VerifyTokenRequest - { - [Required] - public string Token { get; set; } = default!; - } - - public class VerifyTokenResponse - { - public User User { get; set; } = default!; - } -} diff --git a/Timeline/Models/Http/TokenController.cs b/Timeline/Models/Http/TokenController.cs new file mode 100644 index 00000000..383b2965 --- /dev/null +++ b/Timeline/Models/Http/TokenController.cs @@ -0,0 +1,32 @@ +using System.ComponentModel.DataAnnotations; + +namespace Timeline.Models.Http +{ + public class CreateTokenRequest + { + [Required] + public string Username { get; set; } = default!; + [Required] + public string Password { get; set; } = default!; + // in days, optional + [Range(1, 365)] + public int? Expire { get; set; } + } + + public class CreateTokenResponse + { + public string Token { get; set; } = default!; + public UserInfoForAdmin User { get; set; } = default!; + } + + public class VerifyTokenRequest + { + [Required] + public string Token { get; set; } = default!; + } + + public class VerifyTokenResponse + { + public UserInfoForAdmin User { get; set; } = default!; + } +} diff --git a/Timeline/Models/Http/UserController.cs b/Timeline/Models/Http/UserController.cs index 229ca1e5..e4c95cbd 100644 --- a/Timeline/Models/Http/UserController.cs +++ b/Timeline/Models/Http/UserController.cs @@ -1,5 +1,7 @@ +using AutoMapper; using System.ComponentModel.DataAnnotations; using Timeline.Models.Validation; +using Timeline.Services; namespace Timeline.Models.Http { @@ -17,6 +19,21 @@ namespace Timeline.Models.Http public bool? Administrator { get; set; } } + public class CreateUserRequest + { + [Required, Username] + public string Username { get; set; } = default!; + + [Required, MinLength(1)] + public string Password { get; set; } = default!; + + [Required] + public bool? Administrator { get; set; } + + [Nickname] + public string? Nickname { get; set; } + } + public class ChangePasswordRequest { [Required(AllowEmptyStrings = false)] @@ -24,4 +41,13 @@ namespace Timeline.Models.Http [Required(AllowEmptyStrings = false)] public string NewPassword { get; set; } = default!; } + + public class UserControllerAutoMapperProfile : Profile + { + public UserControllerAutoMapperProfile() + { + CreateMap(MemberList.Source); + CreateMap(MemberList.Source); + } + } } diff --git a/Timeline/Models/Http/UserInfo.cs b/Timeline/Models/Http/UserInfo.cs new file mode 100644 index 00000000..6029b8aa --- /dev/null +++ b/Timeline/Models/Http/UserInfo.cs @@ -0,0 +1,58 @@ +using AutoMapper; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using Timeline.Controllers; +using Timeline.Services; + +namespace Timeline.Models.Http +{ + public interface IUserInfo + { + string Username { get; set; } + string Nickname { get; set; } + string AvatarUrl { get; set; } + } + + public class UserInfo : IUserInfo + { + public string Username { get; set; } = default!; + public string Nickname { get; set; } = default!; + public string AvatarUrl { get; set; } = default!; + } + + public class UserInfoForAdmin : IUserInfo + { + public string Username { get; set; } = default!; + public string Nickname { get; set; } = default!; + public string AvatarUrl { get; set; } = default!; + public bool Administrator { get; set; } + } + + public class UserInfoSetAvatarUrlAction : IMappingAction + { + private readonly IActionContextAccessor _actionContextAccessor; + private readonly IUrlHelperFactory _urlHelperFactory; + + public UserInfoSetAvatarUrlAction(IActionContextAccessor actionContextAccessor, IUrlHelperFactory urlHelperFactory) + { + _actionContextAccessor = actionContextAccessor; + _urlHelperFactory = urlHelperFactory; + } + + public void Process(object source, IUserInfo destination, ResolutionContext context) + { + var urlHelper = _urlHelperFactory.GetUrlHelper(_actionContextAccessor.ActionContext); + destination.AvatarUrl = urlHelper.ActionLink(nameof(UserAvatarController.Get), nameof(UserAvatarController), new { destination.Username }); + } + } + + public class UserInfoAutoMapperProfile : Profile + { + public UserInfoAutoMapperProfile() + { + CreateMap().AfterMap(); + CreateMap().AfterMap(); + } + } +} diff --git a/Timeline/Models/Timeline.cs b/Timeline/Models/Timeline.cs deleted file mode 100644 index 752c698d..00000000 --- a/Timeline/Models/Timeline.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Collections.Generic; - -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; } - - public string? Content { get; set; } - - public DateTime Time { get; set; } - - /// - /// The username of the author. - /// - public string Author { get; set; } = default!; - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "This is a DTO class.")] - public class BaseTimelineInfo - { - public string? Description { get; set; } - - /// - /// The username of the owner. - /// - public string Owner { get; set; } = default!; - - public TimelineVisibility Visibility { get; set; } - - public List Members { get; set; } = default!; - } - - public class TimelineInfo : BaseTimelineInfo - { - public string Name { get; set; } = default!; - } -} diff --git a/Timeline/Models/Validation/NicknameValidator.cs b/Timeline/Models/Validation/NicknameValidator.cs index f6626a2a..53a2916b 100644 --- a/Timeline/Models/Validation/NicknameValidator.cs +++ b/Timeline/Models/Validation/NicknameValidator.cs @@ -5,7 +5,6 @@ namespace Timeline.Models.Validation { public class NicknameValidator : Validator { - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "Already checked in base.")] protected override (bool, string) DoValidate(string value) { if (value.Length > 10) diff --git a/Timeline/Models/Validation/UsernameValidator.cs b/Timeline/Models/Validation/UsernameValidator.cs index fc6cdf37..d8f3bdc0 100644 --- a/Timeline/Models/Validation/UsernameValidator.cs +++ b/Timeline/Models/Validation/UsernameValidator.cs @@ -8,7 +8,6 @@ namespace Timeline.Models.Validation { public const int MaxLength = 26; - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "Already checked in base class.")] protected override (bool, string) DoValidate(string value) { if (value.Length == 0) diff --git a/Timeline/Resources/Controllers/Testing/TestingI18nController.Designer.cs b/Timeline/Resources/Controllers/Testing/TestingI18nController.Designer.cs deleted file mode 100644 index e015c5fc..00000000 --- a/Timeline/Resources/Controllers/Testing/TestingI18nController.Designer.cs +++ /dev/null @@ -1,72 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Timeline.Resources.Controllers.Testing { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class TestingI18nController { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal TestingI18nController() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Controllers.Testing.TestingI18nController", typeof(TestingI18nController).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to English test string.. - /// - internal static string TestString { - get { - return ResourceManager.GetString("TestString", resourceCulture); - } - } - } -} diff --git a/Timeline/Resources/Controllers/Testing/TestingI18nController.resx b/Timeline/Resources/Controllers/Testing/TestingI18nController.resx deleted file mode 100644 index 57dfd5b9..00000000 --- a/Timeline/Resources/Controllers/Testing/TestingI18nController.resx +++ /dev/null @@ -1,123 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - English test string. - - \ No newline at end of file diff --git a/Timeline/Resources/Controllers/Testing/TestingI18nController.zh.resx b/Timeline/Resources/Controllers/Testing/TestingI18nController.zh.resx deleted file mode 100644 index 6931cdf6..00000000 --- a/Timeline/Resources/Controllers/Testing/TestingI18nController.zh.resx +++ /dev/null @@ -1,123 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - 中文测试字符串。 - - \ No newline at end of file diff --git a/Timeline/Resources/Messages.Designer.cs b/Timeline/Resources/Messages.Designer.cs index 15101661..332c8817 100644 --- a/Timeline/Resources/Messages.Designer.cs +++ b/Timeline/Resources/Messages.Designer.cs @@ -168,6 +168,15 @@ namespace Timeline.Resources { } } + /// + /// Looks up a localized string similar to The user to set as member does not exist.. + /// + internal static string TimelineController_MemberPut_NotExist { + get { + return ResourceManager.GetString("TimelineController_MemberPut_NotExist", resourceCulture); + } + } + /// /// Looks up a localized string similar to The post to delete does not exist.. /// @@ -267,15 +276,6 @@ namespace Timeline.Resources { } } - /// - /// Looks up a localized string similar to The new username already exists.. - /// - internal static string UserController_ChangeUsername_Conflict { - get { - return ResourceManager.GetString("UserController_ChangeUsername_Conflict", resourceCulture); - } - } - /// /// Looks up a localized string similar to You can't set permission unless you are administrator.. /// @@ -302,5 +302,14 @@ namespace Timeline.Resources { return ResourceManager.GetString("UserController_Patch_Forbid_Username", resourceCulture); } } + + /// + /// Looks up a localized string similar to A user with given username already exists.. + /// + internal static string UserController_UsernameConflict { + get { + return ResourceManager.GetString("UserController_UsernameConflict", resourceCulture); + } + } } } diff --git a/Timeline/Resources/Messages.resx b/Timeline/Resources/Messages.resx index db56ed02..cb6c3891 100644 --- a/Timeline/Resources/Messages.resx +++ b/Timeline/Resources/Messages.resx @@ -153,6 +153,9 @@ The {0}-st user to do operation {1} on does not exist. + + The user to set as member does not exist. + The post to delete does not exist. @@ -186,9 +189,6 @@ Old password is wrong. - - The new username already exists. - You can't set permission unless you are administrator. @@ -198,4 +198,7 @@ You can't set username unless you are administrator. + + A user with given username already exists. + \ No newline at end of file diff --git a/Timeline/Resources/Services/Exception.Designer.cs b/Timeline/Resources/Services/Exception.Designer.cs index cada1788..e6806873 100644 --- a/Timeline/Resources/Services/Exception.Designer.cs +++ b/Timeline/Resources/Services/Exception.Designer.cs @@ -117,9 +117,9 @@ namespace Timeline.Resources.Services { /// /// Looks up a localized string similar to A present resource conflicts with the given resource.. /// - internal static string ConfictException { + internal static string ConflictException { get { - return ResourceManager.GetString("ConfictException", resourceCulture); + return ResourceManager.GetString("ConflictException", resourceCulture); } } @@ -258,42 +258,6 @@ namespace Timeline.Resources.Services { } } - /// - /// Looks up a localized string similar to The timeline with that name already exists.. - /// - internal static string TimelineAlreadyExistException { - get { - return ResourceManager.GetString("TimelineAlreadyExistException", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to An exception happened when add or remove member on timeline.. - /// - internal static string TimelineMemberOperationException { - get { - return ResourceManager.GetString("TimelineMemberOperationException", resourceCulture); - } - } - - /// - /// 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 { - return ResourceManager.GetString("TimelineMemberOperationExceptionDetail", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to 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.. - /// - internal static string TimelineNameBadFormatException { - get { - return ResourceManager.GetString("TimelineNameBadFormatException", resourceCulture); - } - } - /// /// Looks up a localized string similar to Timeline does not exist. If this is a personal timeline, it means the user does not exist and inner exception should be a UserNotExistException.. /// @@ -312,15 +276,6 @@ namespace Timeline.Resources.Services { } } - /// - /// Looks up a localized string similar to The use is not a member of the timeline.. - /// - internal static string TimelineUserNotMemberException { - get { - return ResourceManager.GetString("TimelineUserNotMemberException", resourceCulture); - } - } - /// /// Looks up a localized string similar to The user does not exist.. /// diff --git a/Timeline/Resources/Services/Exception.resx b/Timeline/Resources/Services/Exception.resx index 2cb0f11a..11ae5f27 100644 --- a/Timeline/Resources/Services/Exception.resx +++ b/Timeline/Resources/Services/Exception.resx @@ -135,7 +135,7 @@ The password is wrong. - + A present resource conflicts with the given resource. @@ -183,27 +183,12 @@ Password is of bad format. - - The timeline with that name already exists. - - - An exception happened when add or remove 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. - Timeline does not exist. If this is a personal timeline, it means the user does not exist and inner exception should be a UserNotExistException. The timeline post does not exist. You can't do operation on it. - - The use is not a member of the timeline. - The user does not exist. diff --git a/Timeline/Resources/Services/TimelineService.Designer.cs b/Timeline/Resources/Services/TimelineService.Designer.cs new file mode 100644 index 00000000..8212c252 --- /dev/null +++ b/Timeline/Resources/Services/TimelineService.Designer.cs @@ -0,0 +1,81 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Resources.Services { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class TimelineService { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal TimelineService() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Services.TimelineService", typeof(TimelineService).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to The number {0} username is invalid.. + /// + internal static string ExceptionChangeMemberUsernameBadFormat { + get { + return ResourceManager.GetString("ExceptionChangeMemberUsernameBadFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The owner username of personal timeline is of bad format.. + /// + internal static string ExceptionFindTimelineUsernameBadFormat { + get { + return ResourceManager.GetString("ExceptionFindTimelineUsernameBadFormat", resourceCulture); + } + } + } +} diff --git a/Timeline/Resources/Services/TimelineService.resx b/Timeline/Resources/Services/TimelineService.resx new file mode 100644 index 00000000..0429a2f8 --- /dev/null +++ b/Timeline/Resources/Services/TimelineService.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The number {0} username is invalid. + + + The owner username of personal timeline is of bad format. + + \ No newline at end of file diff --git a/Timeline/Resources/Services/UserCache.Designer.cs b/Timeline/Resources/Services/UserCache.Designer.cs deleted file mode 100644 index 28a74a6c..00000000 --- a/Timeline/Resources/Services/UserCache.Designer.cs +++ /dev/null @@ -1,99 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Timeline.Resources.Services { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class UserCache { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal UserCache() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Services.UserCache", typeof(UserCache).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to Found user info from cache. Entry: {0} .. - /// - internal static string LogGetCacheExist { - get { - return ResourceManager.GetString("LogGetCacheExist", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to User info not exist in cache. Id: {0} .. - /// - internal static string LogGetCacheNotExist { - get { - return ResourceManager.GetString("LogGetCacheNotExist", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to User info remove in cache. Id: {0} .. - /// - internal static string LogRemoveCache { - get { - return ResourceManager.GetString("LogRemoveCache", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to User info set in cache. Entry: {0} .. - /// - internal static string LogSetCache { - get { - return ResourceManager.GetString("LogSetCache", resourceCulture); - } - } - } -} diff --git a/Timeline/Resources/Services/UserCache.resx b/Timeline/Resources/Services/UserCache.resx deleted file mode 100644 index 1102108b..00000000 --- a/Timeline/Resources/Services/UserCache.resx +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Found user info from cache. Entry: {0} . - - - User info not exist in cache. Id: {0} . - - - User info remove in cache. Id: {0} . - - - User info set in cache. Entry: {0} . - - \ No newline at end of file diff --git a/Timeline/Resources/Services/UserDetailService.Designer.cs b/Timeline/Resources/Services/UserDetailService.Designer.cs deleted file mode 100644 index 2f586b36..00000000 --- a/Timeline/Resources/Services/UserDetailService.Designer.cs +++ /dev/null @@ -1,99 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Timeline.Resources.Services { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class UserDetailService { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal UserDetailService() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Services.UserDetailService", typeof(UserDetailService).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to Length of nickname can't be bigger than 10.. - /// - internal static string ExceptionNicknameTooLong { - get { - return ResourceManager.GetString("ExceptionNicknameTooLong", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A user_details entity has been created. User id is {0}. Nickname is {1}.. - /// - internal static string LogEntityNicknameCreate { - get { - return ResourceManager.GetString("LogEntityNicknameCreate", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Nickname of a user_details entity has been updated to a new value. User id is {0}. New value is {1}.. - /// - internal static string LogEntityNicknameSetNotNull { - get { - return ResourceManager.GetString("LogEntityNicknameSetNotNull", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Nickname of a user_details entity has been updated to null. User id is {0}.. - /// - internal static string LogEntityNicknameSetToNull { - get { - return ResourceManager.GetString("LogEntityNicknameSetToNull", resourceCulture); - } - } - } -} diff --git a/Timeline/Resources/Services/UserDetailService.resx b/Timeline/Resources/Services/UserDetailService.resx deleted file mode 100644 index ea32aeda..00000000 --- a/Timeline/Resources/Services/UserDetailService.resx +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Length of nickname can't be bigger than 10. - - - A user_details entity has been created. User id is {0}. Nickname is {1}. - - - Nickname of a user_details entity has been updated to a new value. User id is {0}. New value is {1}. - - - Nickname of a user_details entity has been updated to null. User id is {0}. - - \ No newline at end of file diff --git a/Timeline/Services/ConfictException.cs b/Timeline/Services/ConfictException.cs deleted file mode 100644 index dcd77366..00000000 --- a/Timeline/Services/ConfictException.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; - -namespace Timeline.Services -{ - /// - /// Thrown when a resource already exists and conflicts with the given resource. - /// - /// - /// For example a username already exists and conflicts with the given username. - /// - [Serializable] - public class ConfictException : Exception - { - public ConfictException() : base(Resources.Services.Exception.ConfictException) { } - public ConfictException(string message) : base(message) { } - public ConfictException(string message, Exception inner) : base(message, inner) { } - protected ConfictException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - } -} diff --git a/Timeline/Services/ConflictException.cs b/Timeline/Services/ConflictException.cs new file mode 100644 index 00000000..6ede183a --- /dev/null +++ b/Timeline/Services/ConflictException.cs @@ -0,0 +1,21 @@ +using System; + +namespace Timeline.Services +{ + /// + /// Thrown when a resource already exists and conflicts with the given resource. + /// + /// + /// For example a username already exists and conflicts with the given username. + /// + [Serializable] + public class ConflictException : Exception + { + public ConflictException() : base(Resources.Services.Exception.ConflictException) { } + public ConflictException(string message) : base(message) { } + public ConflictException(string message, Exception inner) : base(message, inner) { } + protected ConflictException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + } +} diff --git a/Timeline/Services/DatabaseExtensions.cs b/Timeline/Services/DatabaseExtensions.cs deleted file mode 100644 index e77dd01a..00000000 --- a/Timeline/Services/DatabaseExtensions.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using System; -using System.Linq; -using System.Threading.Tasks; -using Timeline.Entities; -using Timeline.Models.Validation; - -namespace Timeline.Services -{ - internal static class DatabaseExtensions - { - private static readonly UsernameValidator usernameValidator = new UsernameValidator(); - - /// - /// Check the existence and get the id of the user. - /// - /// The username of the user. - /// The user id. - /// Thrown if is null. - /// Thrown if is of bad format. - /// Thrown if user does not exist. - internal static async Task CheckAndGetUser(DbSet userDbSet, string? username) - { - if (username == null) - throw new ArgumentNullException(nameof(username)); - var (result, message) = usernameValidator.Validate(username); - if (!result) - throw new UsernameBadFormatException(username, message); - - var userId = await userDbSet.Where(u => u.Username == username).Select(u => u.Id).SingleOrDefaultAsync(); - if (userId == 0) - throw new UserNotExistException(username); - return userId; - } - } -} diff --git a/Timeline/Services/TimelineAlreadyExistException.cs b/Timeline/Services/TimelineAlreadyExistException.cs deleted file mode 100644 index c2dea1f9..00000000 --- a/Timeline/Services/TimelineAlreadyExistException.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace Timeline.Services -{ - [Serializable] - public class TimelineAlreadyExistException : Exception - { - public TimelineAlreadyExistException() : base(Resources.Services.Exception.TimelineAlreadyExistException) { } - public TimelineAlreadyExistException(string name) : base(Resources.Services.Exception.TimelineAlreadyExistException) { Name = name; } - public TimelineAlreadyExistException(string name, Exception inner) : base(Resources.Services.Exception.TimelineAlreadyExistException, inner) { Name = name; } - protected TimelineAlreadyExistException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - public string? Name { get; set; } - } -} diff --git a/Timeline/Services/TimelineMemberOperationUserException.cs b/Timeline/Services/TimelineMemberOperationUserException.cs deleted file mode 100644 index 543ee160..00000000 --- a/Timeline/Services/TimelineMemberOperationUserException.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Globalization; - -namespace Timeline.Services -{ - [Serializable] - public class TimelineMemberOperationUserException : Exception - { - public enum MemberOperation - { - Add, - Remove - } - - public TimelineMemberOperationUserException() : base(Resources.Services.Exception.TimelineMemberOperationException) { } - public TimelineMemberOperationUserException(string message) : base(message) { } - public TimelineMemberOperationUserException(string message, Exception inner) : base(message, inner) { } - protected TimelineMemberOperationUserException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - public TimelineMemberOperationUserException(int index, MemberOperation operation, string username, Exception inner) - : base(MakeMessage(operation, index), inner) { Operation = operation; Index = index; Username = username; } - - private static string MakeMessage(MemberOperation operation, int index) => string.Format(CultureInfo.CurrentCulture, - Resources.Services.Exception.TimelineMemberOperationExceptionDetail, operation, index); - - public MemberOperation? Operation { get; set; } - - /// - /// The index of the member on which the operation failed. - /// - public int? Index { get; set; } - - public string? Username { get; set; } - } -} diff --git a/Timeline/Services/TimelineNameBadFormatException.cs b/Timeline/Services/TimelineNameBadFormatException.cs deleted file mode 100644 index 5120a175..00000000 --- a/Timeline/Services/TimelineNameBadFormatException.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; - -namespace Timeline.Services -{ - [Serializable] - public class TimelineNameBadFormatException : Exception - { - public TimelineNameBadFormatException() - : base(Resources.Services.Exception.TimelineNameBadFormatException) { } - public TimelineNameBadFormatException(string name) - : base(Resources.Services.Exception.TimelineNameBadFormatException) { Name = name; } - public TimelineNameBadFormatException(string name, Exception inner) - : base(Resources.Services.Exception.TimelineNameBadFormatException, inner) { Name = name; } - - protected TimelineNameBadFormatException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - public string? Name { get; set; } - } -} diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs index f43d2de5..89936aa2 100644 --- a/Timeline/Services/TimelineService.cs +++ b/Timeline/Services/TimelineService.cs @@ -1,13 +1,15 @@ -using Microsoft.EntityFrameworkCore; +using AutoMapper; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Threading.Tasks; using Timeline.Entities; -using Timeline.Models; using Timeline.Models.Http; using Timeline.Models.Validation; +using static Timeline.Resources.Services.TimelineService; namespace Timeline.Services { @@ -28,12 +30,7 @@ namespace Timeline.Services /// Username or the timeline name. See remarks of . /// A list of all posts. /// 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 is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service). /// /// Thrown when timeline does not exist. /// For normal timeline, it means the name does not exist. @@ -46,26 +43,20 @@ namespace Timeline.Services /// Create a new post in timeline. /// /// Username or the timeline name. See remarks of . - /// The author's username. + /// The author's id. /// The content. /// The time of the post. If null, then use current time. /// The info of the created post. - /// Thrown when or or 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 or is null. + /// Thrown when is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service). /// /// 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 . /// - /// Thrown if is of bad format. - /// Thrown if does not exist. - Task CreatePost(string name, string author, string content, DateTime? time); + /// Thrown if user with does not exist. + Task CreatePost(string name, long authorId, string content, DateTime? time); /// /// Delete a post @@ -73,12 +64,7 @@ namespace Timeline.Services /// Username or the timeline name. See remarks of . /// The id of the post to delete. /// Thrown when or 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 is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service). /// /// Thrown when timeline does not exist. /// For normal timeline, it means the name does not exist. @@ -100,19 +86,14 @@ namespace Timeline.Services /// Username or the timeline name. See remarks of . /// The new properties. Null member means not to change. /// Thrown when or 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 is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service). /// /// 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 . /// - Task ChangeProperty(string name, TimelinePropertyChangeRequest newProperties); + Task ChangeProperty(string name, TimelinePatchRequest newProperties); /// /// Remove members to a timeline. @@ -121,24 +102,16 @@ namespace Timeline.Services /// A list of usernames of members to add. May be null. /// A list of usernames of members to remove. May be null. /// 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 is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service). + /// Thrown when names in or is not a valid username. /// /// 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 . /// - /// - /// Thrown when an exception occurs on the user list. - /// The inner exception is - /// when one of the username is invalid. - /// The inner exception is - /// when one of the user to change does not exist. + /// + /// Thrown when one of the user to change does not exist. /// /// /// Operating on a username that is of bad format or does not exist always throws. @@ -153,42 +126,30 @@ namespace Timeline.Services /// Verify whether a visitor has the permission to read a timeline. /// /// Username or the timeline name. See remarks of . - /// The user to check on. Null means visitor without account. + /// The id 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. - /// 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 is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service). /// /// 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 . /// - /// - /// Thrown when is of bad format. - /// - /// - /// Thrown when does not exist. - /// - Task HasReadPermission(string name, string? username); + /// + /// This method does not check whether visitor is administrator. + /// Return false if user with visitor id does not exist. + /// + Task HasReadPermission(string name, long? visitorId); /// /// Verify whether a user has the permission to modify a post. /// /// Username or the timeline name. See remarks of . - /// The user to check on. + /// The id of the user to check on. /// True if can modify, false if can't modify. - /// Thrown when or 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 is null. + /// Thrown when is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service). /// /// Thrown when timeline does not exist. /// For normal timeline, it means the name does not exist. @@ -198,47 +159,32 @@ namespace Timeline.Services /// /// Thrown when the post with given id does not exist or is deleted already. /// - /// - /// Thrown when is of bad format. - /// - /// - /// Thrown when does not exist. - /// /// /// This method does not check whether the user is administrator. /// It only checks whether he is the author of the post or the owner of the timeline. + /// Return false when user with modifier id does not exist. /// - Task HasPostModifyPermission(string name, long id, string username); + Task HasPostModifyPermission(string name, long id, long modifierId); /// /// Verify whether a user is member of a timeline. /// /// Username or the timeline name. See remarks of . - /// The user to check on. + /// The id of user to check on. /// True if it is a member, false if not. - /// Thrown when or 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 is null. + /// Thrown when is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service). /// /// 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 . /// - /// - /// Thrown when is not a valid username. - /// - /// - /// Thrown when user does not exist. - /// /// /// Timeline owner is also considered as a member. + /// Return false when user with user id does not exist. /// - Task IsMemberOf(string name, string username); + Task IsMemberOf(string name, long userId); } /// @@ -252,7 +198,7 @@ namespace Timeline.Services /// The name of the timeline. /// The timeline info. /// Thrown when is null. - /// + /// /// Thrown when timeline name is invalid. Currently it means it is an empty string. /// /// @@ -264,20 +210,12 @@ namespace Timeline.Services /// Create a timeline. /// /// The name of the timeline. - /// The owner of the timeline. + /// The id of owner of the timeline. /// Thrown when or is null. - /// - /// Thrown when timeline name is invalid. Currently it means it is an empty string. - /// - /// - /// Thrown when the timeline already exists. - /// - /// - /// Thrown when the username of the owner is not valid. - /// - /// - /// Thrown when the owner user does not exist. - Task CreateTimeline(string name, string owner); + /// Thrown when timeline name is invalid. Currently it means it is an empty string. + /// Thrown when the timeline already exists. + /// Thrown when the owner user does not exist. + Task CreateTimeline(string name, long owner); } public interface IPersonalTimelineService : IBaseTimelineService @@ -290,8 +228,8 @@ namespace Timeline.Services /// /// Thrown when is null. /// - /// - /// Thrown when is of bad format. Inner exception MUST be . + /// + /// Thrown when is of bad format. /// /// /// Thrown when the user does not exist. Inner exception MUST be . @@ -301,10 +239,12 @@ namespace Timeline.Services public abstract class BaseTimelineService : IBaseTimelineService { - protected BaseTimelineService(ILoggerFactory loggerFactory, DatabaseContext database, IClock clock) + protected BaseTimelineService(ILoggerFactory loggerFactory, DatabaseContext database, IUserService userService, IMapper mapper, IClock clock) { Clock = clock; Database = database; + UserService = userService; + Mapper = mapper; } protected IClock Clock { get; } @@ -313,6 +253,10 @@ namespace Timeline.Services protected DatabaseContext Database { get; } + protected IUserService UserService { get; } + + protected IMapper Mapper { get; } + /// /// Find the timeline id by the name. /// For details, see remarks. @@ -320,12 +264,7 @@ namespace Timeline.Services /// 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 is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service). /// /// Thrown when timeline does not exist. /// For normal timeline, it means the name does not exist. @@ -347,66 +286,60 @@ namespace Timeline.Services if (name == null) throw new ArgumentNullException(nameof(name)); + var timelineId = await FindTimelineId(name); var postEntities = await Database.TimelinePosts.OrderBy(p => p.Time).Where(p => p.TimelineId == timelineId && p.Content != null).ToListAsync(); + var posts = new List(); foreach (var entity in postEntities) { - posts.Add(new TimelinePostInfo + if (entity.Content != null) // otherwise it is deleted { - Id = entity.Id, - Content = entity.Content, - Author = (await Database.Users.Where(u => u.Id == entity.AuthorId).Select(u => new { u.Username }).SingleAsync()).Name, - Time = entity.Time - }); + var author = Mapper.Map(UserService.GetUserById(entity.AuthorId)); + posts.Add(new TimelinePostInfo + { + Id = entity.Id, + Content = entity.Content, + Author = author, + Time = entity.Time, + LastUpdated = entity.LastUpdated + }); + } } return posts; } - public async Task CreatePost(string name, string author, string content, DateTime? time) + public async Task CreatePost(string name, long authorId, 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.Username == author).Select(u => new { u.Id }).SingleOrDefault(); - if (authorEntity == null) - { - throw new UserNotExistException(author); - } - var authorId = authorEntity.Id; + var author = Mapper.Map(await UserService.GetUserById(authorId)); var currentTime = Clock.GetCurrentTime(); + var finalTime = time ?? currentTime; var postEntity = new TimelinePostEntity { Content = content, AuthorId = authorId, TimelineId = timelineId, - Time = time ?? currentTime, + Time = finalTime, LastUpdated = currentTime }; - Database.TimelinePosts.Add(postEntity); await Database.SaveChangesAsync(); - return new TimelinePostCreateResponse + return new TimelinePostInfo { Id = postEntity.Id, - Time = postEntity.Time + Content = content, + Author = author, + Time = finalTime, + LastUpdated = currentTime }; } @@ -426,7 +359,7 @@ namespace Timeline.Services await Database.SaveChangesAsync(); } - public async Task ChangeProperty(string name, TimelinePropertyChangeRequest newProperties) + public async Task ChangeProperty(string name, TimelinePatchRequest newProperties) { if (name == null) throw new ArgumentNullException(nameof(name)); @@ -455,27 +388,23 @@ namespace Timeline.Services 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) + List? RemoveDuplicateAndCheckFormat(IList? list, string paramName) { if (list != null) { - Dictionary result = new Dictionary(); + List result = new List(); var count = list.Count; for (var index = 0; index < count; index++) { var username = list[index]; - if (result.ContainsKey(username)) + if (result.Contains(username)) { continue; } var (validationResult, message) = UsernameValidator.Validate(username); if (!validationResult) - throw new TimelineMemberOperationUserException( - index, operation, username, - new UsernameBadFormatException(username, message)); - result.Add(username, index); + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, ExceptionChangeMemberUsernameBadFormat, index), nameof(paramName)); + result.Add(username); } return result; } @@ -484,13 +413,13 @@ namespace Timeline.Services return null; } } - var simplifiedAdd = RemoveDuplicateAndCheckFormat(add, TimelineMemberOperationUserException.MemberOperation.Add); - var simplifiedRemove = RemoveDuplicateAndCheckFormat(remove, TimelineMemberOperationUserException.MemberOperation.Remove); + var simplifiedAdd = RemoveDuplicateAndCheckFormat(add, nameof(add)); + var simplifiedRemove = RemoveDuplicateAndCheckFormat(remove, nameof(remove)); // remove those both in add and remove if (simplifiedAdd != null && simplifiedRemove != null) { - var usersToClean = simplifiedRemove.Keys.Where(u => simplifiedAdd.ContainsKey(u)); + var usersToClean = simplifiedRemove.Where(u => simplifiedAdd.Contains(u)).ToList(); foreach (var u in usersToClean) { simplifiedAdd.Remove(u); @@ -500,26 +429,20 @@ namespace Timeline.Services var timelineId = await FindTimelineId(name); - async Task?> CheckExistenceAndGetId(Dictionary? map, TimelineMemberOperationUserException.MemberOperation operation) + async Task?> CheckExistenceAndGetId(List? list) { - if (map == null) + if (list == null) return null; List result = new List(); - foreach (var (username, index) in map) + foreach (var username in list) { - var user = await Database.Users.Where(u => u.Username == username).Select(u => new { u.Id }).SingleOrDefaultAsync(); - if (user == null) - { - throw new TimelineMemberOperationUserException(index, operation, username, - new UserNotExistException(username)); - } - result.Add(user.Id); + result.Add(await UserService.GetUserIdByUsername(username)); } return result; } - var userIdsAdd = await CheckExistenceAndGetId(simplifiedAdd, TimelineMemberOperationUserException.MemberOperation.Add); - var userIdsRemove = await CheckExistenceAndGetId(simplifiedRemove, TimelineMemberOperationUserException.MemberOperation.Remove); + var userIdsAdd = await CheckExistenceAndGetId(simplifiedAdd); + var userIdsRemove = await CheckExistenceAndGetId(simplifiedRemove); if (userIdsAdd != null) { @@ -536,30 +459,11 @@ namespace Timeline.Services await Database.SaveChangesAsync(); } - public async Task HasReadPermission(string name, string? username) + public async Task HasReadPermission(string name, long? visitorId) { 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.Username == 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(); @@ -567,43 +471,24 @@ namespace Timeline.Services if (timelineEntity.Visibility == TimelineVisibility.Public) return true; - if (timelineEntity.Visibility == TimelineVisibility.Register && username != null) + if (timelineEntity.Visibility == TimelineVisibility.Register && visitorId != null) return true; - if (userId == null) + if (visitorId == null) { return false; } else { - var memberEntity = await Database.TimelineMembers.Where(m => m.UserId == userId && m.TimelineId == timelineId).SingleOrDefaultAsync(); + var memberEntity = await Database.TimelineMembers.Where(m => m.UserId == visitorId && m.TimelineId == timelineId).SingleOrDefaultAsync(); return memberEntity != null; } } - public async Task HasPostModifyPermission(string name, long id, string username) + public async Task HasPostModifyPermission(string name, long id, long modifierId) { 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.Username == username).Select(u => new { u.Id }).SingleOrDefaultAsync(); - - if (user == null) - { - throw new UserNotExistException(username); - } - - var userId = user.Id; var timelineId = await FindTimelineId(name); @@ -614,32 +499,13 @@ namespace Timeline.Services if (postEntity == null) throw new TimelinePostNotExistException(id); - return timelineEntity.OwnerId == userId || postEntity.AuthorId == userId; + return timelineEntity.OwnerId == modifierId || postEntity.AuthorId == modifierId; } - public async Task IsMemberOf(string name, string username) + public async Task IsMemberOf(string name, long userId) { 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.Username == username).Select(u => new { u.Id }).SingleOrDefaultAsync(); - - if (user == null) - { - throw new UserNotExistException(username); - } - - var userId = user.Id; var timelineId = await FindTimelineId(name); @@ -648,38 +514,33 @@ namespace Timeline.Services if (userId == timelineEntity.OwnerId) return true; - var timelineMemberEntity = await Database.TimelineMembers.Where(m => m.TimelineId == timelineId && m.UserId == userId).SingleOrDefaultAsync(); - - return timelineMemberEntity != null; + return await Database.TimelineMembers.AnyAsync(m => m.TimelineId == timelineId && m.UserId == userId); } } public class PersonalTimelineService : BaseTimelineService, IPersonalTimelineService { - public PersonalTimelineService(ILoggerFactory loggerFactory, DatabaseContext database, IClock clock) - : base(loggerFactory, database, clock) + public PersonalTimelineService(ILoggerFactory loggerFactory, DatabaseContext database, IUserService userService, IMapper mapper, IClock clock) + : base(loggerFactory, database, userService, mapper, clock) { } protected override async Task FindTimelineId(string name) { + long userId; + try { - var (result, message) = UsernameValidator.Validate(name); - if (!result) - { - throw new TimelineNameBadFormatException(name, new UsernameBadFormatException(name, message)); - } + userId = await UserService.GetUserIdByUsername(name); } - - var userEntity = await Database.Users.Where(u => u.Username == name).Select(u => new { u.Id }).SingleOrDefaultAsync(); - - if (userEntity == null) + catch (ArgumentException e) { - throw new TimelineNotExistException(name, new UserNotExistException(name)); + throw new ArgumentException(ExceptionFindTimelineUsernameBadFormat, nameof(name), e); + } + catch (UserNotExistException e) + { + throw new TimelineNotExistException(name, e); } - - var userId = userEntity.Id; var timelineEntity = await Database.Timelines.Where(t => t.OwnerId == userId && t.Name == null).Select(t => new { t.Id }).SingleOrDefaultAsync(); @@ -715,16 +576,20 @@ namespace Timeline.Services var timelineMemberEntities = await Database.TimelineMembers.Where(m => m.TimelineId == timelineId).Select(m => new { m.UserId }).ToListAsync(); - var memberUsernameTasks = timelineMemberEntities.Select(m => Database.Users.Where(u => u.Id == m.UserId).Select(u => u.Username).SingleAsync()).ToArray(); + var owner = Mapper.Map(await UserService.GetUserById(timelineEntity.OwnerId)); - var memberUsernames = await Task.WhenAll(memberUsernameTasks); + var members = new List(); + foreach (var memberEntity in timelineMemberEntities) + { + members.Add(Mapper.Map(await UserService.GetUserById(memberEntity.UserId))); + } return new BaseTimelineInfo { Description = timelineEntity.Description ?? "", - Owner = username, + Owner = owner, Visibility = timelineEntity.Visibility, - Members = memberUsernames.ToList() + Members = members }; } diff --git a/Timeline/Services/User.cs b/Timeline/Services/User.cs index f63a374e..09a472e5 100644 --- a/Timeline/Services/User.cs +++ b/Timeline/Services/User.cs @@ -1,14 +1,9 @@ -using Microsoft.AspNetCore.Mvc; -using System; -using Timeline.Controllers; - -namespace Timeline.Services +namespace Timeline.Services { public class User { public string? Username { get; set; } public string? Nickname { get; set; } - public string? AvatarUrl { get; set; } #region adminsecret public bool? Administrator { get; set; } @@ -20,30 +15,4 @@ namespace Timeline.Services public long? Version { get; set; } #endregion secret } - - public static class UserExtensions - { - public static User EraseSecretAndFinalFill(this User user, IUrlHelper urlHelper, bool adminstrator) - { - if (user == null) - throw new ArgumentNullException(nameof(user)); - - var result = new User - { - Username = user.Username, - Nickname = user.Nickname, - AvatarUrl = urlHelper.ActionLink(action: nameof(UserAvatarController.Get), controller: nameof(UserAvatarController), values: new - { - user.Username - }) - }; - - if (adminstrator) - { - result.Administrator = user.Administrator; - } - - return result; - } - } } diff --git a/Timeline/Services/UserAvatarService.cs b/Timeline/Services/UserAvatarService.cs index ac7dd857..39b408e6 100644 --- a/Timeline/Services/UserAvatarService.cs +++ b/Timeline/Services/UserAvatarService.cs @@ -11,7 +11,6 @@ using System.Linq; using System.Threading.Tasks; using Timeline.Entities; using Timeline.Helpers; -using Timeline.Models.Validation; namespace Timeline.Services { @@ -61,36 +60,27 @@ namespace Timeline.Services public interface IUserAvatarService { /// - /// Get the etag of a user's avatar. + /// Get the etag of a user's avatar. Warning: This method does not check the user existence. /// - /// The username of the user to get avatar etag of. + /// The id of the user to get avatar etag of. /// The etag. - /// Thrown if is null. - /// Thrown if the is of bad format. - /// Thrown if the user does not exist. - Task GetAvatarETag(string username); + Task GetAvatarETag(long id); /// - /// Get avatar of a user. If the user has no avatar set, a default one is returned. + /// Get avatar of a user. If the user has no avatar set, a default one is returned. Warning: This method does not check the user existence. /// - /// The username of the user to get avatar of. + /// The id of the user to get avatar of. /// The avatar info. - /// Thrown if is null. - /// Thrown if the is of bad format. - /// Thrown if the user does not exist. - Task GetAvatar(string username); + Task GetAvatar(long id); /// - /// Set avatar for a user. + /// Set avatar for a user. Warning: This method does not check the user existence. /// - /// The username of the user to set avatar for. + /// The id of the user to set avatar for. /// The avatar. Can be null to delete the saved avatar. - /// Throw if is null. /// Thrown if any field in is null when is not null. - /// Thrown if the is of bad format. - /// Thrown if the user does not exist. /// Thrown if avatar is of bad format. - Task SetAvatar(string username, Avatar? avatar); + Task SetAvatar(long id, Avatar? avatar); } // TODO! : Make this configurable. @@ -104,7 +94,6 @@ namespace Timeline.Services private DateTime _cacheLastModified; private string _cacheETag = default!; - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "DI.")] public DefaultUserAvatarProvider(IWebHostEnvironment environment, IETagGenerator eTagGenerator) { _avatarPath = Path.Combine(environment.ContentRootPath, "default-avatar.png"); @@ -195,22 +184,18 @@ namespace Timeline.Services _clock = clock; } - public async Task GetAvatarETag(string username) + public async Task GetAvatarETag(long id) { - var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username); - - var eTag = (await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.ETag }).SingleOrDefaultAsync())?.ETag; + var eTag = (await _database.UserAvatars.Where(a => a.UserId == id).Select(a => new { a.ETag }).SingleOrDefaultAsync())?.ETag; if (eTag == null) return await _defaultUserAvatarProvider.GetDefaultAvatarETag(); else return eTag; } - public async Task GetAvatar(string username) + public async Task GetAvatar(long id) { - var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username); - - var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.Type, a.Data, a.LastModified }).SingleOrDefaultAsync(); + var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == id).Select(a => new { a.Type, a.Data, a.LastModified }).SingleOrDefaultAsync(); if (avatarEntity != null) { @@ -240,7 +225,7 @@ namespace Timeline.Services return defaultAvatar; } - public async Task SetAvatar(string username, Avatar? avatar) + public async Task SetAvatar(long id, Avatar? avatar) { if (avatar != null) { @@ -250,8 +235,7 @@ namespace Timeline.Services throw new ArgumentException(Resources.Services.UserAvatarService.ExceptionAvatarTypeNullOrEmpty, nameof(avatar)); } - var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username); - var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == userId).SingleOrDefaultAsync(); + var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == id).SingleOrDefaultAsync(); if (avatar == null) { @@ -281,7 +265,7 @@ namespace Timeline.Services avatarEntity.Data = avatar.Data; avatarEntity.ETag = await _eTagGenerator.Generate(avatar.Data); avatarEntity.LastModified = _clock.GetCurrentTime(); - avatarEntity.UserId = userId; + avatarEntity.UserId = id; if (create) { _database.UserAvatars.Add(avatarEntity); diff --git a/Timeline/Services/UserRoleConvert.cs b/Timeline/Services/UserRoleConvert.cs index 4fa4a7b8..f27ee1bb 100644 --- a/Timeline/Services/UserRoleConvert.cs +++ b/Timeline/Services/UserRoleConvert.cs @@ -5,7 +5,6 @@ using Timeline.Entities; namespace Timeline.Services { - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "No need.")] public static class UserRoleConvert { public const string UserRole = UserRoles.User; diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs index ff2306c5..1197bb73 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -43,6 +43,16 @@ namespace Timeline.Services /// Thrown when the user with given username does not exist. Task GetUserByUsername(string username); + /// + /// Get the user id of given username. + /// + /// Username of the user. + /// The id of the user. + /// Thrown when is null. + /// Thrown when is of bad format. + /// Thrown when the user with given username does not exist. + Task GetUserIdByUsername(string username); + /// /// List all users. /// @@ -57,7 +67,7 @@ namespace Timeline.Services /// The id of the new user. /// Thrown when is null. /// Thrown when some fields in is bad. - /// Thrown when a user with given username already exists. + /// Thrown when a user with given username already exists. /// /// must not be null and must be a valid username. /// must not be null or empty. @@ -78,13 +88,12 @@ namespace Timeline.Services /// Only , , and will be used. /// If null, then not change. /// Other fields are ignored. - /// After modified, even if nothing is changed, version will increase. + /// Version will increase if password is changed. /// /// must be a valid username if set. /// can't be empty if set. /// must be a valid nickname if set. /// - /// Note: Whether is set or not, version will increase and not set to the specified value if there is one. /// /// Task ModifyUser(long id, User? info); @@ -97,6 +106,7 @@ namespace Timeline.Services /// Thrown when is null. /// Thrown when is of bad format or some fields in is bad. /// Thrown when user with given id does not exist. + /// Thrown when user with the newusername already exist. /// /// Only , and will be used. /// If null, then not change. @@ -184,7 +194,7 @@ namespace Timeline.Services private static void ThrowUsernameConflict() { - throw new ConfictException(ExceptionUsernameConflict); + throw new ConflictException(ExceptionUsernameConflict); } private static User CreateUserFromEntity(UserEntity entity) @@ -245,6 +255,21 @@ namespace Timeline.Services return CreateUserFromEntity(entity); } + public async Task GetUserIdByUsername(string username) + { + if (username == null) + throw new ArgumentNullException(nameof(username)); + + CheckUsernameFormat(username, nameof(username)); + + var entity = await _databaseContext.Users.Where(user => user.Username == username).Select(u => new { u.Id }).SingleOrDefaultAsync(); + + if (entity == null) + throw new UserNotExistException(username); + + return entity.Id; + } + public async Task GetUsers() { var entities = await _databaseContext.Users.ToArrayAsync(); @@ -325,6 +350,7 @@ namespace Timeline.Services if (password != null) { entity.Password = _passwordService.HashPassword(password); + entity.Version += 1; } var administrator = info.Administrator; @@ -339,8 +365,6 @@ namespace Timeline.Services entity.Nickname = nickname; } } - - entity.Version += 1; } diff --git a/Timeline/Services/UserTokenService.cs b/Timeline/Services/UserTokenService.cs index c246fdff..cf7286f4 100644 --- a/Timeline/Services/UserTokenService.cs +++ b/Timeline/Services/UserTokenService.cs @@ -49,7 +49,6 @@ namespace Timeline.Services private readonly JwtSecurityTokenHandler _tokenHandler = new JwtSecurityTokenHandler(); private SymmetricSecurityKey _tokenSecurityKey; - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "")] public JwtUserTokenService(IOptionsMonitor jwtConfig, IClock clock) { _jwtConfig = jwtConfig; diff --git a/Timeline/Startup.cs b/Timeline/Startup.cs index 091a16e5..998b5c44 100644 --- a/Timeline/Startup.cs +++ b/Timeline/Startup.cs @@ -1,3 +1,4 @@ +using AutoMapper; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.HttpOverrides; @@ -47,7 +48,6 @@ namespace Timeline }); services.Configure(Configuration.GetSection(nameof(JwtConfig))); - var jwtConfig = Configuration.GetSection(nameof(JwtConfig)).Get(); services.AddAuthentication(AuthenticationConstants.Scheme) .AddScheme(AuthenticationConstants.Scheme, AuthenticationConstants.DisplayName, o => { }); services.AddAuthorization(); @@ -75,16 +75,14 @@ namespace Timeline }); } - services.AddLocalization(options => - { - options.ResourcesPath = "Resources"; - }); + services.AddAutoMapper(GetType().Assembly); + + services.AddTransient(); + services.AddTransient(); services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddTransient(); - services.AddTransient(); services.AddUserAvatarService(); services.AddScoped(); diff --git a/Timeline/Timeline.csproj b/Timeline/Timeline.csproj index 82b45094..25d73068 100644 --- a/Timeline/Timeline.csproj +++ b/Timeline/Timeline.csproj @@ -15,6 +15,8 @@ + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -26,14 +28,14 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + @@ -42,11 +44,6 @@ True AuthHandler.resx - - True - True - TestingI18nController.resx - True True @@ -73,9 +70,9 @@ Filters.resx - True - True - Messages.resx + True + True + Messages.resx True @@ -83,9 +80,9 @@ Common.resx - True - True - NicknameValidator.resx + True + True + NicknameValidator.resx True @@ -102,20 +99,15 @@ True Exception.resx - - True - True - UserAvatarService.resx - - + True True - UserCache.resx + TimelineService.resx - + True True - UserDetailService.resx + UserAvatarService.resx True @@ -129,10 +121,6 @@ ResXFileCodeGenerator AuthHandler.Designer.cs - - ResXFileCodeGenerator - TestingI18nController.Designer.cs - ResXFileCodeGenerator TimelineController.Designer.cs @@ -155,16 +143,16 @@ Filters.Designer.cs - ResXFileCodeGenerator - Messages.Designer.cs + ResXFileCodeGenerator + Messages.Designer.cs ResXFileCodeGenerator Common.Designer.cs - ResXFileCodeGenerator - NicknameValidator.Designer.cs + ResXFileCodeGenerator + NicknameValidator.Designer.cs ResXFileCodeGenerator @@ -178,17 +166,13 @@ ResXFileCodeGenerator Exception.Designer.cs - - ResXFileCodeGenerator - UserAvatarService.Designer.cs - - + ResXFileCodeGenerator - UserCache.Designer.cs + TimelineService.Designer.cs - + ResXFileCodeGenerator - UserDetailService.Designer.cs + UserAvatarService.Designer.cs ResXFileCodeGenerator -- cgit v1.2.3 From d988669de355df12d3be3c658e8617c275fe70dd Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 30 Jan 2020 21:15:18 +0800 Subject: ... --- .../IntegratedTests/IntegratedTestBase.cs | 3 +- .../IntegratedTests/PersonalTimelineTest.cs | 262 +++++++++------------ Timeline.Tests/IntegratedTests/TokenTest.cs | 36 +-- Timeline.Tests/UsernameValidatorUnitTest.cs | 1 - 4 files changed, 130 insertions(+), 172 deletions(-) diff --git a/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs b/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs index 721a25af..af3e0c2f 100644 --- a/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs +++ b/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs @@ -106,8 +106,7 @@ namespace Timeline.Tests.IntegratedTests public Task CreateClientAs(int userNumber) { if (userNumber < 0) - throw new ArgumentOutOfRangeException(nameof(userNumber), "User number can't be negative."); - + return CreateDefaultClient(); if (userNumber == 0) return CreateClientWithCredential("admin", "adminpw"); else diff --git a/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs b/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs index 14600659..5c472e52 100644 --- a/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs +++ b/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs @@ -71,11 +71,10 @@ 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"; + const string getUrl = "users/user1/timeline"; using var client = await CreateClientAsUser(); - async Task AssertMembers(IList members) + async Task AssertMembers(IList members) { var res = await client.GetAsync(getUrl); res.Should().HaveStatusCode(200) @@ -93,90 +92,85 @@ namespace Timeline.Tests.IntegratedTests await AssertEmptyMembers(); { - var res = await client.PostAsJsonAsync(changeUrl, - new TimelineMemberChangeRequest { Add = new List { "admin", "usernotexist" } }); + var res = await client.PutAsync("/users/user1/timeline/members/usernotexist", null); res.Should().HaveStatusCode(400) - .And.HaveCommonBody() - .Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist); - } - { - var res = await client.PostAsJsonAsync(changeUrl, - new TimelineMemberChangeRequest { Remove = new List { "admin", "usernotexist" } }); - res.Should().HaveStatusCode(400) - .And.HaveCommonBody() - .Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist); + .And.HaveCommonBody(ErrorCodes.TimelineController.MemberPut_NotExist); } + await AssertEmptyMembers(); { - var res = await client.PostAsJsonAsync(changeUrl, - new TimelineMemberChangeRequest { Add = new List { "admin" }, Remove = new List { "admin" } }); + var res = await client.PutAsync("/users/user1/timeline/members/user2", null); res.Should().HaveStatusCode(200); - await AssertEmptyMembers(); } + await AssertMembers(new List { UserInfoList[2] }); { - 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.DeleteAsync("/users/users1/timeline/members/users2"); + res.Should().BeDelete(true); + } + await AssertEmptyMembers(); { - var res = await client.PostAsJsonAsync(changeUrl, - new TimelineMemberChangeRequest { Remove = new List { "admin" } }); - res.Should().HaveStatusCode(200); - await AssertEmptyMembers(); + var res = await client.DeleteAsync("/users/users1/timeline/members/users2"); + res.Should().BeDelete(false); } + await AssertEmptyMembers(); } [Theory] - [InlineData(AuthType.None, 200, 401, 401, 401, 401)] - [InlineData(AuthType.User, 200, 200, 403, 200, 403)] - [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) + [InlineData(-1, 200, 401, 401, 401, 401)] + [InlineData(1, 200, 200, 403, 200, 403)] + [InlineData(0, 200, 200, 200, 200, 200)] + public async Task Permission_Timeline(int userNumber, int get, int opPatchUser, int opPatchAdmin, int opMemberUser, int opMemberAdmin) { - using var client = await CreateClientAs(authType); + using var client = await CreateClientAs(userNumber); { - var res = await client.GetAsync("users/user/timeline"); + var res = await client.GetAsync("users/user1/timeline"); res.Should().HaveStatusCode(get); } { - var res = await client.PostAsJsonAsync("users/user/timeline/op/property", - new TimelinePatchRequest { Description = "hahaha" }); - res.Should().HaveStatusCode(opPropertyUser); + var res = await client.PatchAsJsonAsync("users/user1/timeline", new TimelinePatchRequest { Description = "hahaha" }); + res.Should().HaveStatusCode(opPatchUser); } { - var res = await client.PostAsJsonAsync("users/admin/timeline/op/property", - new TimelinePatchRequest { Description = "hahaha" }); - res.Should().HaveStatusCode(opPropertyAdmin); + var res = await client.PatchAsJsonAsync("users/admin/timeline", new TimelinePatchRequest { Description = "hahaha" }); + res.Should().HaveStatusCode(opPatchAdmin); } { - var res = await client.PostAsJsonAsync("users/user/timeline/op/member", - new TimelineMemberChangeRequest { Add = new List { "admin" } }); + var res = await client.PutAsync("users/user1/timeline/member/user2", null); + res.Should().HaveStatusCode(opMemberUser); + } + + { + var res = await client.DeleteAsync("users/user1/timeline/member/user2"); res.Should().HaveStatusCode(opMemberUser); } { - var res = await client.PostAsJsonAsync("users/admin/timeline/op/member", - new TimelineMemberChangeRequest { Add = new List { "user" } }); + var res = await client.PutAsync("users/admin/timeline/member/user2", null); + res.Should().HaveStatusCode(opMemberAdmin); + } + + { + var res = await client.DeleteAsync("users/admin/timeline/member/user2"); res.Should().HaveStatusCode(opMemberAdmin); } } [Fact] - public async Task Permission_GetPost() + public async Task Visibility_Test() { - const string userUrl = "users/user/timeline/posts"; + const string userUrl = "users/user1/timeline/posts"; const string adminUrl = "users/admin/timeline/posts"; { using var client = await CreateClientAsUser(); - var res = await client.PostAsync("users/user/timeline/op/property", + var res = await client.PatchAsync("users/user1/timeline", new StringContent(@"{""visibility"":""abcdefg""}", System.Text.Encoding.UTF8, System.Net.Mime.MediaTypeNames.Application.Json)); res.Should().BeInvalidModel(); } { // default visibility is registered { - using var client = await CreateClientWithNoAuth(); + using var client = await CreateDefaultClient(); var res = await client.GetAsync(userUrl); res.Should().HaveStatusCode(403); } @@ -191,12 +185,12 @@ namespace Timeline.Tests.IntegratedTests { // change visibility to public { using var client = await CreateClientAsUser(); - var res = await client.PostAsJsonAsync("users/user/timeline/op/property", + var res = await client.PatchAsJsonAsync("users/user1/timeline", new TimelinePatchRequest { Visibility = TimelineVisibility.Public }); res.Should().HaveStatusCode(200); } { - using var client = await CreateClientWithNoAuth(); + using var client = await CreateDefaultClient(); var res = await client.GetAsync(userUrl); res.Should().HaveStatusCode(200); } @@ -204,20 +198,20 @@ namespace Timeline.Tests.IntegratedTests { // change visibility to private { - using var client = await CreateClientAsAdmin(); + using var client = await CreateClientAsAdministrator(); { - var res = await client.PostAsJsonAsync("users/user/timeline/op/property", + var res = await client.PatchAsJsonAsync("users/user1/timeline", new TimelinePatchRequest { Visibility = TimelineVisibility.Private }); res.Should().HaveStatusCode(200); } { - var res = await client.PostAsJsonAsync("users/admin/timeline/op/property", + var res = await client.PatchAsJsonAsync("users/admin/timeline", new TimelinePatchRequest { Visibility = TimelineVisibility.Private }); res.Should().HaveStatusCode(200); } } { - using var client = await CreateClientWithNoAuth(); + using var client = await CreateDefaultClient(); var res = await client.GetAsync(userUrl); res.Should().HaveStatusCode(403); } @@ -227,14 +221,13 @@ namespace Timeline.Tests.IntegratedTests res.Should().HaveStatusCode(403); } { // admin can read user's - using var client = await CreateClientAsAdmin(); + using var client = await CreateClientAsAdministrator(); var res = await client.GetAsync(userUrl); res.Should().HaveStatusCode(200); } { // add member - using var client = await CreateClientAsAdmin(); - var res = await client.PostAsJsonAsync("users/admin/timeline/op/member", - new TimelineMemberChangeRequest { Add = new List { "user" } }); + using var client = await CreateClientAsAdministrator(); + var res = await client.PutAsync("/users/admin/timeline/members/user1", null); res.Should().HaveStatusCode(200); } { // now user can read admin's @@ -249,19 +242,16 @@ namespace Timeline.Tests.IntegratedTests [Fact] public async Task Permission_Post_Create() { - CreateExtraMockUsers(1); - using (var client = await CreateClientAsUser()) { - var res = await client.PostAsJsonAsync("users/user/timeline/op/member", - new TimelineMemberChangeRequest { Add = new List { "user0" } }); + var res = await client.PutAsync("users/user/timeline/members/user2", null); res.Should().HaveStatusCode(200); } - using (var client = await CreateClientWithNoAuth()) + using (var client = await CreateDefaultClient()) { { // no auth should get 401 - var res = await client.PostAsJsonAsync("users/user/timeline/postop/create", + var res = await client.PostAsJsonAsync("users/user1/timeline/posts", new TimelinePostCreateRequest { Content = "aaa" }); res.Should().HaveStatusCode(401); } @@ -270,30 +260,30 @@ namespace Timeline.Tests.IntegratedTests using (var client = await CreateClientAsUser()) { { // post self's - var res = await client.PostAsJsonAsync("users/user/timeline/postop/create", + var res = await client.PostAsJsonAsync("users/user1/timeline/posts", new TimelinePostCreateRequest { Content = "aaa" }); res.Should().HaveStatusCode(200); } { // post other not as a member should get 403 - var res = await client.PostAsJsonAsync("users/admin/timeline/postop/create", + var res = await client.PostAsJsonAsync("users/admin/timeline/posts", new TimelinePostCreateRequest { Content = "aaa" }); res.Should().HaveStatusCode(403); } } - using (var client = await CreateClientAsAdmin()) + using (var client = await CreateClientAsAdministrator()) { { // post as admin - var res = await client.PostAsJsonAsync("users/user/timeline/postop/create", + var res = await client.PostAsJsonAsync("users/user1/timeline/posts", new TimelinePostCreateRequest { Content = "aaa" }); res.Should().HaveStatusCode(200); } } - using (var client = await CreateClientAs(ExtraMockUsers[0])) + using (var client = await CreateClientAs(2)) { { // post as member - var res = await client.PostAsJsonAsync("users/user/timeline/postop/create", + var res = await client.PostAsJsonAsync("users/user1/timeline/postop/create", new TimelinePostCreateRequest { Content = "aaa" }); res.Should().HaveStatusCode(200); } @@ -303,69 +293,66 @@ namespace Timeline.Tests.IntegratedTests [Fact] public async Task Permission_Post_Delete() { - CreateExtraMockUsers(2); - - async Task CreatePost(MockUser auth, string timeline) + async Task CreatePost(int userNumber) { - using var client = await CreateClientAs(auth); - var res = await client.PostAsJsonAsync($"users/{timeline}/timeline/postop/create", + using var client = await CreateClientAs(userNumber); + var res = await client.PostAsJsonAsync($"users/user1/timeline/postop/create", new TimelinePostCreateRequest { Content = "aaa" }); return res.Should().HaveStatusCode(200) - .And.HaveJsonBody() + .And.HaveJsonBody() .Which.Id; } using (var client = await CreateClientAsUser()) { - var res = await client.PostAsJsonAsync("users/user/timeline/op/member", - new TimelineMemberChangeRequest { Add = new List { "user0", "user1" } }); - res.Should().HaveStatusCode(200); + { + var res = await client.PutAsync("users/user1/timeline/members/user2", null); + res.Should().HaveStatusCode(200); + } + { + var res = await client.PutAsync("users/user1/timeline/members/user3", null); + res.Should().HaveStatusCode(200); + } } { // no auth should get 401 - using var client = await CreateClientWithNoAuth(); - var res = await client.PostAsJsonAsync("users/user/timeline/postop/delete", - new TimelinePostDeleteRequest { Id = 12 }); + using var client = await CreateDefaultClient(); + var res = await client.DeleteAsync("users/user1/timeline/posts/12"); res.Should().HaveStatusCode(401); } { // self can delete self - var postId = await CreatePost(MockUser.Ordinary, "user"); + var postId = await CreatePost(1); using var client = await CreateClientAsUser(); - var res = await client.PostAsJsonAsync("users/user/timeline/postop/delete", - new TimelinePostDeleteRequest { Id = postId }); + var res = await client.DeleteAsync($"users/user1/timeline/posts/{postId}"); res.Should().HaveStatusCode(200); } { // admin can delete any - var postId = await CreatePost(MockUser.Ordinary, "user"); - using var client = await CreateClientAsAdmin(); - var res = await client.PostAsJsonAsync("users/user/timeline/postop/delete", - new TimelinePostDeleteRequest { Id = postId }); + var postId = await CreatePost(1); + using var client = await CreateClientAsAdministrator(); + var res = await client.DeleteAsync($"users/user1/timeline/posts/{postId}"); res.Should().HaveStatusCode(200); } { // owner can delete other - var postId = await CreatePost(ExtraMockUsers[0], "user"); + var postId = await CreatePost(2); using var client = await CreateClientAsUser(); - var res = await client.PostAsJsonAsync("users/user/timeline/postop/delete", - new TimelinePostDeleteRequest { Id = postId }); + var res = await client.DeleteAsync($"users/user1/timeline/posts/{postId}"); res.Should().HaveStatusCode(200); } { // author can delete self - var postId = await CreatePost(ExtraMockUsers[0], "user"); - using var client = await CreateClientAs(ExtraMockUsers[0]); - var res = await client.PostAsJsonAsync("users/user/timeline/postop/delete", - new TimelinePostDeleteRequest { Id = postId }); + var postId = await CreatePost(2); + using var client = await CreateClientAs(2); + var res = await client.DeleteAsync($"users/user1/timeline/posts/{postId}"); res.Should().HaveStatusCode(200); } { // otherwise is forbidden - var postId = await CreatePost(ExtraMockUsers[0], "user"); - using var client = await CreateClientAs(ExtraMockUsers[1]); - var res = await client.PostAsJsonAsync("users/user/timeline/postop/delete", - new TimelinePostDeleteRequest { Id = postId }); + var postId = await CreatePost(2); + using var client = await CreateClientAs(3); + var res = await client.DeleteAsync($"users/user1/timeline/posts/{postId}"); res.Should().HaveStatusCode(403); } } @@ -376,96 +363,69 @@ namespace Timeline.Tests.IntegratedTests { using var client = await CreateClientAsUser(); { - var res = await client.GetAsync("users/user/timeline/posts"); + var res = await client.GetAsync("users/user1/timeline/posts"); res.Should().HaveStatusCode(200) .And.HaveJsonBody() .Which.Should().NotBeNull().And.BeEmpty(); } { - var res = await client.PostAsJsonAsync("users/user/timeline/postop/create", + var res = await client.PostAsJsonAsync("users/user1/timeline/posts", new TimelinePostCreateRequest { Content = null }); res.Should().BeInvalidModel(); } const string mockContent = "aaa"; - TimelinePostCreateResponse createRes; + TimelinePostInfo createRes; { - var res = await client.PostAsJsonAsync("users/user/timeline/postop/create", + var res = await client.PostAsJsonAsync("users/user1/timeline/posts", new TimelinePostCreateRequest { Content = mockContent }); var body = res.Should().HaveStatusCode(200) - .And.HaveJsonBody() + .And.HaveJsonBody() .Which; body.Should().NotBeNull(); + body.Content.Should().Be(mockContent); + body.Author.Should().Be(UserInfoList[1]); createRes = body; } { - var res = await client.GetAsync("users/user/timeline/posts"); + var res = await client.GetAsync("users/user1/timeline/posts"); res.Should().HaveStatusCode(200) .And.HaveJsonBody() - .Which.Should().NotBeNull().And.BeEquivalentTo( - new TimelinePostInfo - { - Id = createRes.Id, - Author = "user", - Content = mockContent, - Time = createRes.Time - }); + .Which.Should().NotBeNull().And.BeEquivalentTo(createRes); } const string mockContent2 = "bbb"; var mockTime2 = DateTime.Now.AddDays(-1); - TimelinePostCreateResponse createRes2; + TimelinePostInfo createRes2; { - var res = await client.PostAsJsonAsync("users/user/timeline/postop/create", + var res = await client.PostAsJsonAsync("users/user1/timeline/posts", new TimelinePostCreateRequest { Content = mockContent2, Time = mockTime2 }); var body = res.Should().HaveStatusCode(200) - .And.HaveJsonBody() + .And.HaveJsonBody() .Which; - body.Should().NotBeNull(); + body.Should().NotBeNull(); + body.Content.Should().Be(mockContent); + body.Author.Should().Be(UserInfoList[1]); + body.Time.Should().Be(mockTime2); createRes2 = body; } { - var res = await client.GetAsync("users/user/timeline/posts"); + var res = await client.GetAsync("users/user1/timeline/posts"); res.Should().HaveStatusCode(200) .And.HaveJsonBody() - .Which.Should().NotBeNull().And.BeEquivalentTo( - new TimelinePostInfo - { - Id = createRes.Id, - Author = "user", - Content = mockContent, - Time = createRes.Time - }, - new TimelinePostInfo - { - Id = createRes2.Id, - Author = "user", - Content = mockContent2, - Time = createRes2.Time - }); + .Which.Should().NotBeNull().And.BeEquivalentTo(createRes, createRes2); } { - var res = await client.PostAsJsonAsync("users/user/timeline/postop/delete", - new TimelinePostDeleteRequest { Id = createRes.Id }); - res.Should().HaveStatusCode(200); + var res = await client.DeleteAsync($"users/user1/timeline/posts/{createRes.Id}"); + res.Should().BeDelete(true); } { - var res = await client.PostAsJsonAsync("users/user/timeline/postop/delete", - new TimelinePostDeleteRequest { Id = 30000 }); - res.Should().HaveStatusCode(400) - .And.HaveCommonBody() - .Which.Code.Should().Be(ErrorCodes.TimelineController.PostOperationDelete_NotExist); + var res = await client.DeleteAsync("users/user1/timeline/posts/30000"); + res.Should().BeDelete(false); } { - var res = await client.GetAsync("users/user/timeline/posts"); + var res = await client.GetAsync("users/user1/timeline/posts"); res.Should().HaveStatusCode(200) .And.HaveJsonBody() - .Which.Should().NotBeNull().And.BeEquivalentTo( - new TimelinePostInfo - { - Id = createRes2.Id, - Author = "user", - Content = mockContent2, - Time = createRes2.Time - }); + .Which.Should().NotBeNull().And.BeEquivalentTo(createRes2); } } } @@ -477,10 +437,10 @@ namespace Timeline.Tests.IntegratedTests async Task CreatePost(DateTime time) { - var res = await client.PostAsJsonAsync("users/user/timeline/postop/create", + var res = await client.PostAsJsonAsync("users/user1/timeline/posts", new TimelinePostCreateRequest { Content = "aaa", Time = time }); return res.Should().HaveStatusCode(200) - .And.HaveJsonBody() + .And.HaveJsonBody() .Which.Id; } diff --git a/Timeline.Tests/IntegratedTests/TokenTest.cs b/Timeline.Tests/IntegratedTests/TokenTest.cs index 8ee19999..ec7514ff 100644 --- a/Timeline.Tests/IntegratedTests/TokenTest.cs +++ b/Timeline.Tests/IntegratedTests/TokenTest.cs @@ -41,7 +41,7 @@ namespace Timeline.Tests.IntegratedTests [MemberData(nameof(CreateToken_InvalidModel_Data))] public async Task CreateToken_InvalidModel(string username, string password, int expire) { - using var client = await CreateClientWithNoAuth(); + using var client = await CreateDefaultClient(); (await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest { Username = username, @@ -53,14 +53,14 @@ namespace Timeline.Tests.IntegratedTests public static IEnumerable CreateToken_UserCredential_Data() { yield return new[] { "usernotexist", "p" }; - yield return new[] { MockUser.Ordinary.Username, "???" }; + yield return new[] { "user1", "???" }; } [Theory] [MemberData(nameof(CreateToken_UserCredential_Data))] public async void CreateToken_UserCredential(string username, string password) { - using var client = await CreateClientWithNoAuth(); + using var client = await CreateDefaultClient(); var response = await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest { Username = username, Password = password }); response.Should().HaveStatusCode(400) @@ -71,19 +71,19 @@ namespace Timeline.Tests.IntegratedTests [Fact] public async Task CreateToken_Success() { - using var client = await CreateClientWithNoAuth(); + using var client = await CreateDefaultClient(); var response = await client.PostAsJsonAsync(CreateTokenUrl, - new CreateTokenRequest { Username = MockUser.Ordinary.Username, Password = MockUser.Ordinary.Password }); + new CreateTokenRequest { Username = "user1", Password = "user1pw" }); var body = response.Should().HaveStatusCode(200) .And.HaveJsonBody().Which; body.Token.Should().NotBeNullOrWhiteSpace(); - body.User.Should().BeEquivalentTo(MockUser.Ordinary.Info); + body.User.Should().BeEquivalentTo(UserInfoForAdminList[1]); } [Fact] public async Task VerifyToken_InvalidModel() { - using var client = await CreateClientWithNoAuth(); + using var client = await CreateDefaultClient(); (await client.PostAsJsonAsync(VerifyTokenUrl, new VerifyTokenRequest { Token = null })).Should().BeInvalidModel(); } @@ -91,7 +91,7 @@ namespace Timeline.Tests.IntegratedTests [Fact] public async Task VerifyToken_BadFormat() { - using var client = await CreateClientWithNoAuth(); + using var client = await CreateDefaultClient(); var response = await client.PostAsJsonAsync(VerifyTokenUrl, new VerifyTokenRequest { Token = "bad token hahaha" }); response.Should().HaveStatusCode(400) @@ -102,14 +102,14 @@ namespace Timeline.Tests.IntegratedTests [Fact] public async Task VerifyToken_OldVersion() { - using var client = await CreateClientWithNoAuth(); - var token = (await CreateUserTokenAsync(client, MockUser.Ordinary.Username, MockUser.Ordinary.Password)).Token; + using var client = await CreateDefaultClient(); + var token = (await CreateUserTokenAsync(client, "user1", "user1pw")).Token; - using (var scope = Factory.Server.Host.Services.CreateScope()) // UserService is scoped. + using (var scope = Factory.Services.CreateScope()) // UserService is scoped. { // create a user for test var userService = scope.ServiceProvider.GetRequiredService(); - await userService.PatchUser(MockUser.Ordinary.Username, null, null); + await userService.ModifyUser("user1", new User { Password = "user1pw" }); } (await client.PostAsJsonAsync(VerifyTokenUrl, @@ -122,13 +122,13 @@ namespace Timeline.Tests.IntegratedTests [Fact] public async Task VerifyToken_UserNotExist() { - using var client = await CreateClientWithNoAuth(); - var token = (await CreateUserTokenAsync(client, MockUser.Ordinary.Username, MockUser.Ordinary.Password)).Token; + using var client = await CreateDefaultClient(); + var token = (await CreateUserTokenAsync(client, "user1", "user1pw")).Token; using (var scope = Factory.Server.Host.Services.CreateScope()) // UserService is scoped. { var userService = scope.ServiceProvider.GetRequiredService(); - await userService.DeleteUser(MockUser.Ordinary.Username); + await userService.DeleteUser("user1"); } (await client.PostAsJsonAsync(VerifyTokenUrl, @@ -159,13 +159,13 @@ namespace Timeline.Tests.IntegratedTests [Fact] public async Task VerifyToken_Success() { - using var client = await CreateClientWithNoAuth(); - var createTokenResult = await CreateUserTokenAsync(client, MockUser.Ordinary.Username, MockUser.Ordinary.Password); + using var client = await CreateDefaultClient(); + var createTokenResult = await CreateUserTokenAsync(client, "user1", "user1pw"); var response = await client.PostAsJsonAsync(VerifyTokenUrl, new VerifyTokenRequest { Token = createTokenResult.Token }); response.Should().HaveStatusCode(200) .And.HaveJsonBody() - .Which.User.Should().BeEquivalentTo(MockUser.Ordinary.Info); + .Which.User.Should().BeEquivalentTo(UserInfoForAdminList[1]); } } } diff --git a/Timeline.Tests/UsernameValidatorUnitTest.cs b/Timeline.Tests/UsernameValidatorUnitTest.cs index e0f4633f..1a09d477 100644 --- a/Timeline.Tests/UsernameValidatorUnitTest.cs +++ b/Timeline.Tests/UsernameValidatorUnitTest.cs @@ -5,7 +5,6 @@ using Xunit; namespace Timeline.Tests { - [UseCulture("en")] public class UsernameValidatorUnitTest : IClassFixture { private readonly UsernameValidator _validator; -- cgit v1.2.3 From 5aaa4f95e6bdd46e6740c1ecbbd46bdf415eedd2 Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 30 Jan 2020 23:49:02 +0800 Subject: Finish reafctor, TODO: Database migration. --- Timeline.Tests/Helpers/TestApplication.cs | 12 +++++++++- .../IntegratedTests/IntegratedTestBase.cs | 2 +- .../IntegratedTests/PersonalTimelineTest.cs | 28 +++++++++++----------- Timeline.Tests/IntegratedTests/UserAvatarTest.cs | 2 +- Timeline.Tests/IntegratedTests/UserTest.cs | 8 +++---- Timeline.Tests/UsernameValidatorUnitTest.cs | 8 +------ Timeline/Controllers/ControllerAuthExtensions.cs | 2 +- Timeline/Controllers/UserAvatarController.cs | 2 +- Timeline/Controllers/UserController.cs | 18 +++++++------- Timeline/Entities/UserEntity.cs | 2 +- Timeline/Models/Http/UserInfo.cs | 21 +++++++++++----- Timeline/Models/Validation/NicknameValidator.cs | 2 +- Timeline/Services/TimelineService.cs | 2 +- Timeline/Services/UserService.cs | 2 ++ 14 files changed, 64 insertions(+), 47 deletions(-) diff --git a/Timeline.Tests/Helpers/TestApplication.cs b/Timeline.Tests/Helpers/TestApplication.cs index 14cafea3..bc5deeec 100644 --- a/Timeline.Tests/Helpers/TestApplication.cs +++ b/Timeline.Tests/Helpers/TestApplication.cs @@ -17,10 +17,20 @@ namespace Timeline.Tests.Helpers public TestApplication(WebApplicationFactory factory) { DatabaseConnection = new SqliteConnection("Data Source=:memory:;"); + DatabaseConnection.Open(); + + var options = new DbContextOptionsBuilder() + .UseSqlite(DatabaseConnection) + .Options; + + using (var context = new DevelopmentDatabaseContext(options)) + { + context.Database.EnsureCreated(); + } Factory = factory.WithWebHostBuilder(builder => { - builder.ConfigureTestServices(services => + builder.ConfigureServices(services => { services.AddDbContext(options => { diff --git a/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs b/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs index af3e0c2f..242e96cd 100644 --- a/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs +++ b/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs @@ -63,7 +63,7 @@ namespace Timeline.Tests.IntegratedTests foreach (var user in users) { - userService.CreateUser(user); + userService.CreateUser(user).Wait(); userInfoList.Add(mapper.Map(user)); userInfoForAdminList.Add(mapper.Map(user)); } diff --git a/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs b/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs index 5c472e52..d787d87d 100644 --- a/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs +++ b/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs @@ -103,12 +103,12 @@ namespace Timeline.Tests.IntegratedTests } await AssertMembers(new List { UserInfoList[2] }); { - var res = await client.DeleteAsync("/users/users1/timeline/members/users2"); + var res = await client.DeleteAsync("/users/user1/timeline/members/user2"); res.Should().BeDelete(true); } await AssertEmptyMembers(); { - var res = await client.DeleteAsync("/users/users1/timeline/members/users2"); + var res = await client.DeleteAsync("/users/user1/timeline/members/users2"); res.Should().BeDelete(false); } await AssertEmptyMembers(); @@ -137,22 +137,22 @@ namespace Timeline.Tests.IntegratedTests } { - var res = await client.PutAsync("users/user1/timeline/member/user2", null); + var res = await client.PutAsync("users/user1/timeline/members/user2", null); res.Should().HaveStatusCode(opMemberUser); } { - var res = await client.DeleteAsync("users/user1/timeline/member/user2"); + var res = await client.DeleteAsync("users/user1/timeline/members/user2"); res.Should().HaveStatusCode(opMemberUser); } { - var res = await client.PutAsync("users/admin/timeline/member/user2", null); + var res = await client.PutAsync("users/admin/timeline/members/user2", null); res.Should().HaveStatusCode(opMemberAdmin); } { - var res = await client.DeleteAsync("users/admin/timeline/member/user2"); + var res = await client.DeleteAsync("users/admin/timeline/members/user2"); res.Should().HaveStatusCode(opMemberAdmin); } } @@ -244,7 +244,7 @@ namespace Timeline.Tests.IntegratedTests { using (var client = await CreateClientAsUser()) { - var res = await client.PutAsync("users/user/timeline/members/user2", null); + var res = await client.PutAsync("users/user1/timeline/members/user2", null); res.Should().HaveStatusCode(200); } @@ -283,7 +283,7 @@ namespace Timeline.Tests.IntegratedTests using (var client = await CreateClientAs(2)) { { // post as member - var res = await client.PostAsJsonAsync("users/user1/timeline/postop/create", + var res = await client.PostAsJsonAsync("users/user1/timeline/posts", new TimelinePostCreateRequest { Content = "aaa" }); res.Should().HaveStatusCode(200); } @@ -296,7 +296,7 @@ namespace Timeline.Tests.IntegratedTests async Task CreatePost(int userNumber) { using var client = await CreateClientAs(userNumber); - var res = await client.PostAsJsonAsync($"users/user1/timeline/postop/create", + var res = await client.PostAsJsonAsync($"users/user1/timeline/posts", new TimelinePostCreateRequest { Content = "aaa" }); return res.Should().HaveStatusCode(200) .And.HaveJsonBody() @@ -383,7 +383,7 @@ namespace Timeline.Tests.IntegratedTests .Which; body.Should().NotBeNull(); body.Content.Should().Be(mockContent); - body.Author.Should().Be(UserInfoList[1]); + body.Author.Should().BeEquivalentTo(UserInfoList[1]); createRes = body; } { @@ -402,9 +402,9 @@ namespace Timeline.Tests.IntegratedTests .And.HaveJsonBody() .Which; body.Should().NotBeNull(); - body.Content.Should().Be(mockContent); - body.Author.Should().Be(UserInfoList[1]); - body.Time.Should().Be(mockTime2); + body.Content.Should().Be(mockContent2); + body.Author.Should().BeEquivalentTo(UserInfoList[1]); + body.Time.Should().BeCloseTo(mockTime2, 1000); createRes2 = body; } { @@ -450,7 +450,7 @@ namespace Timeline.Tests.IntegratedTests var id2 = await CreatePost(now); { - var res = await client.GetAsync("users/user/timeline/posts"); + var res = await client.GetAsync("users/user1/timeline/posts"); res.Should().HaveStatusCode(200) .And.HaveJsonBody() .Which.Select(p => p.Id).Should().Equal(id1, id2, id0); diff --git a/Timeline.Tests/IntegratedTests/UserAvatarTest.cs b/Timeline.Tests/IntegratedTests/UserAvatarTest.cs index 989207e2..67c2dd9a 100644 --- a/Timeline.Tests/IntegratedTests/UserAvatarTest.cs +++ b/Timeline.Tests/IntegratedTests/UserAvatarTest.cs @@ -49,7 +49,7 @@ namespace Timeline.Tests.IntegratedTests 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") + async Task GetReturnDefault(string username = "user1") { var res = await client.GetAsync($"users/{username}/avatar"); res.Should().HaveStatusCode(200); diff --git a/Timeline.Tests/IntegratedTests/UserTest.cs b/Timeline.Tests/IntegratedTests/UserTest.cs index 4c2ccf7a..1b9733ff 100644 --- a/Timeline.Tests/IntegratedTests/UserTest.cs +++ b/Timeline.Tests/IntegratedTests/UserTest.cs @@ -44,7 +44,7 @@ namespace Timeline.Tests.IntegratedTests using var client = await CreateClientAsAdministrator(); var res = await client.GetAsync("/users"); res.Should().HaveStatusCode(200) - .And.HaveJsonBody() + .And.HaveJsonBody() .Which.Should().BeEquivalentTo(UserInfoForAdminList); } @@ -74,7 +74,7 @@ namespace Timeline.Tests.IntegratedTests using var client = await CreateClientAsAdministrator(); var res = await client.GetAsync($"/users/user1"); res.Should().HaveStatusCode(200) - .And.HaveJsonBody() + .And.HaveJsonBody() .Which.Should().BeEquivalentTo(UserInfoForAdminList[1]); } @@ -142,7 +142,7 @@ namespace Timeline.Tests.IntegratedTests { // Token should expire. - var res = await userClient.GetAsync("/users"); + var res = await userClient.GetAsync("/testing/auth/Authorize"); res.Should().HaveStatusCode(HttpStatusCode.Unauthorized); } @@ -198,7 +198,7 @@ namespace Timeline.Tests.IntegratedTests [Fact] public async Task Patch_NoAuth_Unauthorized() { - using var client = await CreateClientAsUser(); + using var client = await CreateDefaultClient(); var res = await client.PatchAsJsonAsync("/users/user1", new UserPatchRequest { Nickname = "aaa" }); res.Should().HaveStatusCode(HttpStatusCode.Unauthorized); } diff --git a/Timeline.Tests/UsernameValidatorUnitTest.cs b/Timeline.Tests/UsernameValidatorUnitTest.cs index 1a09d477..0f844452 100644 --- a/Timeline.Tests/UsernameValidatorUnitTest.cs +++ b/Timeline.Tests/UsernameValidatorUnitTest.cs @@ -21,12 +21,6 @@ namespace Timeline.Tests return message; } - [Fact] - public void Null() - { - FailAndMessage(null).Should().ContainEquivalentOf("null"); - } - [Fact] public void NotString() { @@ -58,6 +52,7 @@ namespace Timeline.Tests } [Theory] + [InlineData(null)] [InlineData("abc")] [InlineData("-abc")] [InlineData("_abc")] @@ -68,7 +63,6 @@ namespace Timeline.Tests [InlineData("a-b_c")] public void Success(string value) { - var (result, _) = _validator.Validate(value); result.Should().BeTrue(); } diff --git a/Timeline/Controllers/ControllerAuthExtensions.cs b/Timeline/Controllers/ControllerAuthExtensions.cs index 90da8a93..34fd4d99 100644 --- a/Timeline/Controllers/ControllerAuthExtensions.cs +++ b/Timeline/Controllers/ControllerAuthExtensions.cs @@ -34,7 +34,7 @@ namespace Timeline.Controllers var claim = controller.User.FindFirst(ClaimTypes.NameIdentifier); if (claim == null) - throw new InvalidOperationException("Failed to get user id because User has no NameIdentifier claim."); + return null; if (long.TryParse(claim.Value, out var value)) return value; diff --git a/Timeline/Controllers/UserAvatarController.cs b/Timeline/Controllers/UserAvatarController.cs index ab0ad8e7..2dd279a8 100644 --- a/Timeline/Controllers/UserAvatarController.cs +++ b/Timeline/Controllers/UserAvatarController.cs @@ -78,7 +78,7 @@ namespace Timeline.Controllers [HttpPut("users/{username}/avatar")] [Authorize] - [RequireContentLength] + [RequireContentType, RequireContentLength] [Consumes("image/png", "image/jpeg", "image/gif", "image/webp")] public async Task Put([FromRoute][Username] string username) { diff --git a/Timeline/Controllers/UserController.cs b/Timeline/Controllers/UserController.cs index 400a518c..fa73c6f9 100644 --- a/Timeline/Controllers/UserController.cs +++ b/Timeline/Controllers/UserController.cs @@ -42,7 +42,13 @@ namespace Timeline.Controllers { var users = await _userService.GetUsers(); var administrator = this.IsAdministrator(); - return Ok(users.Select(u => ConvertToUserInfo(u, administrator)).ToArray()); + // Note: the (object) explicit conversion. If not convert, + // then result is a IUserInfo array and JsonSerializer will + // treat all element as IUserInfo and deserialize only properties + // in IUserInfo. So we convert it to object to make an object + // array so that JsonSerializer use the runtime type. + var result = users.Select(u => (object)ConvertToUserInfo(u, administrator)).ToArray(); + return Ok(result); } [HttpGet("users/{username}")] @@ -106,15 +112,11 @@ namespace Timeline.Controllers [HttpDelete("users/{username}"), AdminAuthorize] public async Task> Delete([FromRoute][Username] string username) { - try - { - await _userService.DeleteUser(username); + var delete = await _userService.DeleteUser(username); + if (delete) return Ok(CommonDeleteResponse.Delete()); - } - catch (UserNotExistException) - { + else return Ok(CommonDeleteResponse.NotExist()); - } } [HttpPost("userop/createuser"), AdminAuthorize] diff --git a/Timeline/Entities/UserEntity.cs b/Timeline/Entities/UserEntity.cs index dae6979f..946c3fa2 100644 --- a/Timeline/Entities/UserEntity.cs +++ b/Timeline/Entities/UserEntity.cs @@ -29,7 +29,7 @@ namespace Timeline.Entities [Column("version"), Required] public long Version { get; set; } - [Column("nickname"), MaxLength(40)] + [Column("nickname"), MaxLength(100)] public string? Nickname { get; set; } public UserAvatarEntity? Avatar { get; set; } diff --git a/Timeline/Models/Http/UserInfo.cs b/Timeline/Models/Http/UserInfo.cs index 6029b8aa..62d989a2 100644 --- a/Timeline/Models/Http/UserInfo.cs +++ b/Timeline/Models/Http/UserInfo.cs @@ -29,21 +29,30 @@ namespace Timeline.Models.Http public bool Administrator { get; set; } } - public class UserInfoSetAvatarUrlAction : IMappingAction + public class UserInfoAvatarUrlValueResolver : IValueResolver { private readonly IActionContextAccessor _actionContextAccessor; private readonly IUrlHelperFactory _urlHelperFactory; - public UserInfoSetAvatarUrlAction(IActionContextAccessor actionContextAccessor, IUrlHelperFactory urlHelperFactory) + public UserInfoAvatarUrlValueResolver() + { + } + + public UserInfoAvatarUrlValueResolver(IActionContextAccessor actionContextAccessor, IUrlHelperFactory urlHelperFactory) { _actionContextAccessor = actionContextAccessor; _urlHelperFactory = urlHelperFactory; } - public void Process(object source, IUserInfo destination, ResolutionContext context) + public string Resolve(User source, IUserInfo destination, string destMember, ResolutionContext context) { + if (_actionContextAccessor == null) + { + return $"/users/{destination.Username}/avatar"; + } + var urlHelper = _urlHelperFactory.GetUrlHelper(_actionContextAccessor.ActionContext); - destination.AvatarUrl = urlHelper.ActionLink(nameof(UserAvatarController.Get), nameof(UserAvatarController), new { destination.Username }); + return urlHelper.ActionLink(nameof(UserAvatarController.Get), nameof(UserAvatarController), new { destination.Username }); } } @@ -51,8 +60,8 @@ namespace Timeline.Models.Http { public UserInfoAutoMapperProfile() { - CreateMap().AfterMap(); - CreateMap().AfterMap(); + CreateMap().ForMember(u => u.AvatarUrl, opt => opt.MapFrom()); + CreateMap().ForMember(u => u.AvatarUrl, opt => opt.MapFrom()); } } } diff --git a/Timeline/Models/Validation/NicknameValidator.cs b/Timeline/Models/Validation/NicknameValidator.cs index 53a2916b..1d6ab163 100644 --- a/Timeline/Models/Validation/NicknameValidator.cs +++ b/Timeline/Models/Validation/NicknameValidator.cs @@ -7,7 +7,7 @@ namespace Timeline.Models.Validation { protected override (bool, string) DoValidate(string value) { - if (value.Length > 10) + if (value.Length > 25) return (false, MessageTooLong); return (true, GetSuccessMessage()); diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs index 89936aa2..16402f3e 100644 --- a/Timeline/Services/TimelineService.cs +++ b/Timeline/Services/TimelineService.cs @@ -295,7 +295,7 @@ namespace Timeline.Services { if (entity.Content != null) // otherwise it is deleted { - var author = Mapper.Map(UserService.GetUserById(entity.AuthorId)); + var author = Mapper.Map(await UserService.GetUserById(entity.AuthorId)); posts.Add(new TimelinePostInfo { Id = entity.Id, diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs index 1197bb73..d2dc969e 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -300,12 +300,14 @@ namespace Timeline.Services var administrator = info.Administrator ?? false; var password = info.Password; + var nickname = info.Nickname; var newEntity = new UserEntity { Username = username, Password = _passwordService.HashPassword(password), Roles = UserRoleConvert.ToString(administrator), + Nickname = nickname, Version = 1 }; _databaseContext.Users.Add(newEntity); -- cgit v1.2.3 From 038e8dcf461d4d4ebd51c8fdf7680497869f691c Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 31 Jan 2020 00:10:23 +0800 Subject: ... --- .../IntegratedTests/PersonalTimelineTest.cs | 75 +++++++++++- Timeline.Tests/IntegratedTests/UserAvatarTest.cs | 18 +-- Timeline/Controllers/ControllerAuthExtensions.cs | 15 +-- Timeline/Filters/Timeline.cs | 2 +- Timeline/Models/Http/UserInfo.cs | 8 +- .../ControllerAuthExtensions.Designer.cs | 81 +++++++++++++ .../Controllers/ControllerAuthExtensions.resx | 126 +++++++++++++++++++++ Timeline/Services/TimelineService.cs | 3 + Timeline/Timeline.csproj | 9 ++ 9 files changed, 311 insertions(+), 26 deletions(-) create mode 100644 Timeline/Resources/Controllers/ControllerAuthExtensions.Designer.cs create mode 100644 Timeline/Resources/Controllers/ControllerAuthExtensions.resx diff --git a/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs b/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs index d787d87d..dacfea62 100644 --- a/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs +++ b/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs @@ -30,6 +30,74 @@ namespace Timeline.Tests.IntegratedTests body.Visibility.Should().Be(TimelineVisibility.Register); body.Description.Should().Be(""); body.Members.Should().NotBeNull().And.BeEmpty(); + } + + [Fact] + public async Task InvalidModel_BadUsername() + { + using var client = await CreateClientAsAdministrator(); + { + var res = await client.GetAsync("users/user!!!/timeline"); + res.Should().BeInvalidModel(); + } + { + var res = await client.PatchAsJsonAsync("users/user!!!/timeline", new TimelinePatchRequest { }); + res.Should().BeInvalidModel(); + } + { + var res = await client.PutAsync("users/user!!!/timeline/members/user1", null); + res.Should().BeInvalidModel(); + } + { + var res = await client.DeleteAsync("users/user!!!/timeline/members/user1"); + res.Should().BeInvalidModel(); + } + { + var res = await client.GetAsync("users/user!!!/timeline/posts"); + res.Should().BeInvalidModel(); + } + { + var res = await client.PostAsJsonAsync("users/user!!!/timeline/posts", new TimelinePostCreateRequest { Content = "aaa" }); + res.Should().BeInvalidModel(); + } + { + var res = await client.DeleteAsync("users/user!!!/timeline/posts/123"); + res.Should().BeInvalidModel(); + } + } + + [Fact] + public async Task NotFound() + { + using var client = await CreateClientAsAdministrator(); + { + var res = await client.GetAsync("users/usernotexist/timeline"); + res.Should().HaveStatusCode(404).And.HaveCommonBody(ErrorCodes.UserCommon.NotExist); + } + { + var res = await client.PatchAsJsonAsync("users/usernotexist/timeline", new TimelinePatchRequest { }); + res.Should().HaveStatusCode(404).And.HaveCommonBody(ErrorCodes.UserCommon.NotExist); + } + { + var res = await client.PutAsync("users/usernotexist/timeline/members/user1", null); + res.Should().HaveStatusCode(404).And.HaveCommonBody(ErrorCodes.UserCommon.NotExist); + } + { + var res = await client.DeleteAsync("users/usernotexist/timeline/members/user1"); + res.Should().HaveStatusCode(404).And.HaveCommonBody(ErrorCodes.UserCommon.NotExist); + } + { + var res = await client.GetAsync("users/usernotexist/timeline/posts"); + res.Should().HaveStatusCode(404).And.HaveCommonBody(ErrorCodes.UserCommon.NotExist); + } + { + var res = await client.PostAsJsonAsync("users/usernotexist/timeline/posts", new TimelinePostCreateRequest { Content = "aaa" }); + res.Should().HaveStatusCode(404).And.HaveCommonBody(ErrorCodes.UserCommon.NotExist); + } + { + var res = await client.DeleteAsync("users/usernotexist/timeline/posts/123"); + res.Should().HaveStatusCode(404).And.HaveCommonBody(ErrorCodes.UserCommon.NotExist); + } } [Fact] @@ -162,10 +230,11 @@ namespace Timeline.Tests.IntegratedTests { const string userUrl = "users/user1/timeline/posts"; const string adminUrl = "users/admin/timeline/posts"; - { + { + using var client = await CreateClientAsUser(); - var res = await client.PatchAsync("users/user1/timeline", - new StringContent(@"{""visibility"":""abcdefg""}", System.Text.Encoding.UTF8, System.Net.Mime.MediaTypeNames.Application.Json)); + using var content = new StringContent(@"{""visibility"":""abcdefg""}", System.Text.Encoding.UTF8, System.Net.Mime.MediaTypeNames.Application.Json); + var res = await client.PatchAsync("users/user1/timeline", content); res.Should().BeInvalidModel(); } { // default visibility is registered diff --git a/Timeline.Tests/IntegratedTests/UserAvatarTest.cs b/Timeline.Tests/IntegratedTests/UserAvatarTest.cs index 67c2dd9a..fa0120f1 100644 --- a/Timeline.Tests/IntegratedTests/UserAvatarTest.cs +++ b/Timeline.Tests/IntegratedTests/UserAvatarTest.cs @@ -75,7 +75,7 @@ namespace Timeline.Tests.IntegratedTests await GetReturnDefault("admin"); { - var request = new HttpRequestMessage() + using var request = new HttpRequestMessage() { RequestUri = new Uri(client.BaseAddress, "users/user1/avatar"), Method = HttpMethod.Get, @@ -87,7 +87,7 @@ namespace Timeline.Tests.IntegratedTests } { - var request = new HttpRequestMessage() + using var request = new HttpRequestMessage() { RequestUri = new Uri(client.BaseAddress, "users/user1/avatar"), Method = HttpMethod.Get, @@ -98,7 +98,7 @@ namespace Timeline.Tests.IntegratedTests } { - var request = new HttpRequestMessage() + using var request = new HttpRequestMessage() { RequestUri = new Uri(client.BaseAddress, "users/user1/avatar"), Method = HttpMethod.Get, @@ -109,7 +109,7 @@ namespace Timeline.Tests.IntegratedTests } { - var content = new ByteArrayContent(new[] { (byte)0x00 }); + using var content = new ByteArrayContent(new[] { (byte)0x00 }); content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); var res = await client.PutAsync("users/user1/avatar", content); res.Should().HaveStatusCode(HttpStatusCode.BadRequest) @@ -117,7 +117,7 @@ namespace Timeline.Tests.IntegratedTests } { - var content = new ByteArrayContent(new[] { (byte)0x00 }); + using var content = new ByteArrayContent(new[] { (byte)0x00 }); content.Headers.ContentLength = 1; var res = await client.PutAsync("users/user1/avatar", content); res.Should().HaveStatusCode(HttpStatusCode.BadRequest) @@ -125,7 +125,7 @@ namespace Timeline.Tests.IntegratedTests } { - var content = new ByteArrayContent(new[] { (byte)0x00 }); + using var content = new ByteArrayContent(new[] { (byte)0x00 }); content.Headers.ContentLength = 0; content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); var res = await client.PutAsync("users/user1/avatar", content); @@ -139,7 +139,7 @@ namespace Timeline.Tests.IntegratedTests } { - var content = new ByteArrayContent(new[] { (byte)0x00 }); + using var content = new ByteArrayContent(new[] { (byte)0x00 }); content.Headers.ContentLength = 1000 * 1000 * 11; content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); var res = await client.PutAsync("users/user1/avatar", content); @@ -148,7 +148,7 @@ namespace Timeline.Tests.IntegratedTests } { - var content = new ByteArrayContent(new[] { (byte)0x00 }); + using var content = new ByteArrayContent(new[] { (byte)0x00 }); content.Headers.ContentLength = 2; content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); var res = await client.PutAsync("users/user1/avatar", content); @@ -157,7 +157,7 @@ namespace Timeline.Tests.IntegratedTests } { - var content = new ByteArrayContent(new[] { (byte)0x00, (byte)0x01 }); + using var content = new ByteArrayContent(new[] { (byte)0x00, (byte)0x01 }); content.Headers.ContentLength = 1; content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); var res = await client.PutAsync("users/user1/avatar", content); diff --git a/Timeline/Controllers/ControllerAuthExtensions.cs b/Timeline/Controllers/ControllerAuthExtensions.cs index 34fd4d99..00a65454 100644 --- a/Timeline/Controllers/ControllerAuthExtensions.cs +++ b/Timeline/Controllers/ControllerAuthExtensions.cs @@ -1,7 +1,8 @@ using Microsoft.AspNetCore.Mvc; +using System; using System.Security.Claims; using Timeline.Auth; -using System; +using static Timeline.Resources.Controllers.ControllerAuthExtensions; namespace Timeline.Controllers { @@ -14,24 +15,18 @@ namespace Timeline.Controllers public static long GetUserId(this ControllerBase controller) { - if (controller.User == null) - throw new InvalidOperationException("Failed to get user id because User is null."); - var claim = controller.User.FindFirst(ClaimTypes.NameIdentifier); if (claim == null) - throw new InvalidOperationException("Failed to get user id because User has no NameIdentifier claim."); + throw new InvalidOperationException(ExceptionNoUserIdentifierClaim); if (long.TryParse(claim.Value, out var value)) return value; - throw new InvalidOperationException("Failed to get user id because NameIdentifier claim is not a number."); + throw new InvalidOperationException(ExceptionUserIdentifierClaimBadFormat); } public static long? GetOptionalUserId(this ControllerBase controller) { - if (controller.User == null) - return null; - var claim = controller.User.FindFirst(ClaimTypes.NameIdentifier); if (claim == null) return null; @@ -39,7 +34,7 @@ namespace Timeline.Controllers if (long.TryParse(claim.Value, out var value)) return value; - throw new InvalidOperationException("Failed to get user id because NameIdentifier claim is not a number."); + throw new InvalidOperationException(ExceptionUserIdentifierClaimBadFormat); } } } diff --git a/Timeline/Filters/Timeline.cs b/Timeline/Filters/Timeline.cs index 729dbec7..ed78e645 100644 --- a/Timeline/Filters/Timeline.cs +++ b/Timeline/Filters/Timeline.cs @@ -13,7 +13,7 @@ namespace Timeline.Filters { if (e.InnerException is UserNotExistException) { - context.Result = new BadRequestObjectResult(ErrorResponse.UserCommon.NotExist()); + context.Result = new NotFoundObjectResult(ErrorResponse.UserCommon.NotExist()); } else { diff --git a/Timeline/Models/Http/UserInfo.cs b/Timeline/Models/Http/UserInfo.cs index 62d989a2..07ac0aad 100644 --- a/Timeline/Models/Http/UserInfo.cs +++ b/Timeline/Models/Http/UserInfo.cs @@ -31,11 +31,13 @@ namespace Timeline.Models.Http public class UserInfoAvatarUrlValueResolver : IValueResolver { - private readonly IActionContextAccessor _actionContextAccessor; - private readonly IUrlHelperFactory _urlHelperFactory; + private readonly IActionContextAccessor? _actionContextAccessor; + private readonly IUrlHelperFactory? _urlHelperFactory; public UserInfoAvatarUrlValueResolver() { + _actionContextAccessor = null; + _urlHelperFactory = null; } public UserInfoAvatarUrlValueResolver(IActionContextAccessor actionContextAccessor, IUrlHelperFactory urlHelperFactory) @@ -51,7 +53,7 @@ namespace Timeline.Models.Http return $"/users/{destination.Username}/avatar"; } - var urlHelper = _urlHelperFactory.GetUrlHelper(_actionContextAccessor.ActionContext); + var urlHelper = _urlHelperFactory!.GetUrlHelper(_actionContextAccessor.ActionContext); return urlHelper.ActionLink(nameof(UserAvatarController.Get), nameof(UserAvatarController), new { destination.Username }); } } diff --git a/Timeline/Resources/Controllers/ControllerAuthExtensions.Designer.cs b/Timeline/Resources/Controllers/ControllerAuthExtensions.Designer.cs new file mode 100644 index 00000000..70a1d605 --- /dev/null +++ b/Timeline/Resources/Controllers/ControllerAuthExtensions.Designer.cs @@ -0,0 +1,81 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Resources.Controllers { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class ControllerAuthExtensions { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal ControllerAuthExtensions() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Controllers.ControllerAuthExtensions", typeof(ControllerAuthExtensions).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Failed to get user id because User has no NameIdentifier claim.. + /// + internal static string ExceptionNoUserIdentifierClaim { + get { + return ResourceManager.GetString("ExceptionNoUserIdentifierClaim", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failed to get user id because NameIdentifier claim is not a number.. + /// + internal static string ExceptionUserIdentifierClaimBadFormat { + get { + return ResourceManager.GetString("ExceptionUserIdentifierClaimBadFormat", resourceCulture); + } + } + } +} diff --git a/Timeline/Resources/Controllers/ControllerAuthExtensions.resx b/Timeline/Resources/Controllers/ControllerAuthExtensions.resx new file mode 100644 index 00000000..03e6d95a --- /dev/null +++ b/Timeline/Resources/Controllers/ControllerAuthExtensions.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Failed to get user id because User has no NameIdentifier claim. + + + Failed to get user id because NameIdentifier claim is not a number. + + \ No newline at end of file diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs index 16402f3e..85445973 100644 --- a/Timeline/Services/TimelineService.cs +++ b/Timeline/Services/TimelineService.cs @@ -348,6 +348,9 @@ namespace Timeline.Services if (name == null) throw new ArgumentNullException(nameof(name)); + // Currently we don't use the result. But we need to check the timeline. + var _ = await FindTimelineId(name); + var post = await Database.TimelinePosts.Where(p => p.Id == id).SingleOrDefaultAsync(); if (post == null) diff --git a/Timeline/Timeline.csproj b/Timeline/Timeline.csproj index 25d73068..1a3a07cd 100644 --- a/Timeline/Timeline.csproj +++ b/Timeline/Timeline.csproj @@ -44,6 +44,11 @@ True AuthHandler.resx + + True + True + ControllerAuthExtensions.resx + True True @@ -121,6 +126,10 @@ ResXFileCodeGenerator AuthHandler.Designer.cs + + ResXFileCodeGenerator + ControllerAuthExtensions.Designer.cs + ResXFileCodeGenerator TimelineController.Designer.cs -- cgit v1.2.3 From 45a6ca3a189a81060ae44d0764248547fe2b686c Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 31 Jan 2020 15:21:58 +0800 Subject: Combine two user info types. --- .../IntegratedTests/IntegratedTestBase.cs | 15 ++++---- .../IntegratedTests/PersonalTimelineTest.cs | 8 ++-- Timeline.Tests/IntegratedTests/TokenTest.cs | 4 +- Timeline.Tests/IntegratedTests/UserTest.cs | 20 +++++----- Timeline/Controllers/TokenController.cs | 4 +- Timeline/Controllers/UserController.cs | 22 +++-------- Timeline/Models/Http/TokenController.cs | 4 +- Timeline/Models/Http/UserInfo.cs | 45 ++++++++++------------ 8 files changed, 54 insertions(+), 68 deletions(-) diff --git a/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs b/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs index 242e96cd..59af5eab 100644 --- a/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs +++ b/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs @@ -15,6 +15,12 @@ namespace Timeline.Tests.IntegratedTests public abstract class IntegratedTestBase : IClassFixture>, IDisposable { + static IntegratedTestBase() + { + FluentAssertions.AssertionOptions.AssertEquivalencyUsing(options => + options.Excluding(m => m.RuntimeType == typeof(UserInfo) && m.SelectedMemberPath == "_links")); + } + protected TestApplication TestApp { get; } protected WebApplicationFactory Factory => TestApp.Factory; @@ -56,7 +62,6 @@ namespace Timeline.Tests.IntegratedTests } var userInfoList = new List(); - var userInfoForAdminList = new List(); var userService = scope.ServiceProvider.GetRequiredService(); var mapper = scope.ServiceProvider.GetRequiredService(); @@ -65,11 +70,9 @@ namespace Timeline.Tests.IntegratedTests { userService.CreateUser(user).Wait(); userInfoList.Add(mapper.Map(user)); - userInfoForAdminList.Add(mapper.Map(user)); } - UserInfoList = userInfoList; - UserInfoForAdminList = userInfoForAdminList; + UserInfos = userInfoList; } } @@ -83,9 +86,7 @@ namespace Timeline.Tests.IntegratedTests TestApp.Dispose(); } - public IReadOnlyList UserInfoList { get; } - - public IReadOnlyList UserInfoForAdminList { get; } + public IReadOnlyList UserInfos { get; } public Task CreateDefaultClient() { diff --git a/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs b/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs index dacfea62..f3d6b172 100644 --- a/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs +++ b/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs @@ -26,7 +26,7 @@ namespace Timeline.Tests.IntegratedTests var res = await client.GetAsync("users/user1/timeline"); var body = res.Should().HaveStatusCode(200) .And.HaveJsonBody().Which; - body.Owner.Should().BeEquivalentTo(UserInfoList[1]); + body.Owner.Should().BeEquivalentTo(UserInfos[1]); body.Visibility.Should().Be(TimelineVisibility.Register); body.Description.Should().Be(""); body.Members.Should().NotBeNull().And.BeEmpty(); @@ -169,7 +169,7 @@ namespace Timeline.Tests.IntegratedTests var res = await client.PutAsync("/users/user1/timeline/members/user2", null); res.Should().HaveStatusCode(200); } - await AssertMembers(new List { UserInfoList[2] }); + await AssertMembers(new List { UserInfos[2] }); { var res = await client.DeleteAsync("/users/user1/timeline/members/user2"); res.Should().BeDelete(true); @@ -452,7 +452,7 @@ namespace Timeline.Tests.IntegratedTests .Which; body.Should().NotBeNull(); body.Content.Should().Be(mockContent); - body.Author.Should().BeEquivalentTo(UserInfoList[1]); + body.Author.Should().BeEquivalentTo(UserInfos[1]); createRes = body; } { @@ -472,7 +472,7 @@ namespace Timeline.Tests.IntegratedTests .Which; body.Should().NotBeNull(); body.Content.Should().Be(mockContent2); - body.Author.Should().BeEquivalentTo(UserInfoList[1]); + body.Author.Should().BeEquivalentTo(UserInfos[1]); body.Time.Should().BeCloseTo(mockTime2, 1000); createRes2 = body; } diff --git a/Timeline.Tests/IntegratedTests/TokenTest.cs b/Timeline.Tests/IntegratedTests/TokenTest.cs index ec7514ff..928d546c 100644 --- a/Timeline.Tests/IntegratedTests/TokenTest.cs +++ b/Timeline.Tests/IntegratedTests/TokenTest.cs @@ -77,7 +77,7 @@ namespace Timeline.Tests.IntegratedTests var body = response.Should().HaveStatusCode(200) .And.HaveJsonBody().Which; body.Token.Should().NotBeNullOrWhiteSpace(); - body.User.Should().BeEquivalentTo(UserInfoForAdminList[1]); + body.User.Should().BeEquivalentTo(UserInfos[1]); } [Fact] @@ -165,7 +165,7 @@ namespace Timeline.Tests.IntegratedTests new VerifyTokenRequest { Token = createTokenResult.Token }); response.Should().HaveStatusCode(200) .And.HaveJsonBody() - .Which.User.Should().BeEquivalentTo(UserInfoForAdminList[1]); + .Which.User.Should().BeEquivalentTo(UserInfos[1]); } } } diff --git a/Timeline.Tests/IntegratedTests/UserTest.cs b/Timeline.Tests/IntegratedTests/UserTest.cs index 1b9733ff..bbeb6ad6 100644 --- a/Timeline.Tests/IntegratedTests/UserTest.cs +++ b/Timeline.Tests/IntegratedTests/UserTest.cs @@ -25,7 +25,7 @@ namespace Timeline.Tests.IntegratedTests var res = await client.GetAsync("/users"); res.Should().HaveStatusCode(200) .And.HaveJsonBody() - .Which.Should().BeEquivalentTo(UserInfoList); + .Which.Should().BeEquivalentTo(UserInfos); } [Fact] @@ -35,7 +35,7 @@ namespace Timeline.Tests.IntegratedTests var res = await client.GetAsync("/users"); res.Should().HaveStatusCode(200) .And.HaveJsonBody() - .Which.Should().BeEquivalentTo(UserInfoList); + .Which.Should().BeEquivalentTo(UserInfos); } [Fact] @@ -44,8 +44,8 @@ namespace Timeline.Tests.IntegratedTests using var client = await CreateClientAsAdministrator(); var res = await client.GetAsync("/users"); res.Should().HaveStatusCode(200) - .And.HaveJsonBody() - .Which.Should().BeEquivalentTo(UserInfoForAdminList); + .And.HaveJsonBody() + .Which.Should().BeEquivalentTo(UserInfos); } [Fact] @@ -55,7 +55,7 @@ namespace Timeline.Tests.IntegratedTests var res = await client.GetAsync($"/users/admin"); res.Should().HaveStatusCode(200) .And.HaveJsonBody() - .Which.Should().BeEquivalentTo(UserInfoList[0]); + .Which.Should().BeEquivalentTo(UserInfos[0]); } [Fact] @@ -65,7 +65,7 @@ namespace Timeline.Tests.IntegratedTests var res = await client.GetAsync($"/users/admin"); res.Should().HaveStatusCode(200) .And.HaveJsonBody() - .Which.Should().BeEquivalentTo(UserInfoList[0]); + .Which.Should().BeEquivalentTo(UserInfos[0]); } [Fact] @@ -74,8 +74,8 @@ namespace Timeline.Tests.IntegratedTests using var client = await CreateClientAsAdministrator(); var res = await client.GetAsync($"/users/user1"); res.Should().HaveStatusCode(200) - .And.HaveJsonBody() - .Which.Should().BeEquivalentTo(UserInfoForAdminList[1]); + .And.HaveJsonBody() + .Which.Should().BeEquivalentTo(UserInfos[1]); } [Fact] @@ -134,7 +134,7 @@ namespace Timeline.Tests.IntegratedTests { var res = await client.GetAsync("/users/newuser"); var body = res.Should().HaveStatusCode(200) - .And.HaveJsonBody() + .And.HaveJsonBody() .Which; body.Administrator.Should().Be(true); body.Nickname.Should().Be("aaa"); @@ -301,7 +301,7 @@ namespace Timeline.Tests.IntegratedTests { var res = await client.GetAsync("users/aaa"); var body = res.Should().HaveStatusCode(200) - .And.HaveJsonBody().Which; + .And.HaveJsonBody().Which; body.Username.Should().Be("aaa"); body.Nickname.Should().Be("ccc"); body.Administrator.Should().BeTrue(); diff --git a/Timeline/Controllers/TokenController.cs b/Timeline/Controllers/TokenController.cs index a7f5fbde..1fb0b17a 100644 --- a/Timeline/Controllers/TokenController.cs +++ b/Timeline/Controllers/TokenController.cs @@ -59,7 +59,7 @@ namespace Timeline.Controllers return Ok(new CreateTokenResponse { Token = result.Token, - User = _mapper.Map(result.User) + User = _mapper.Map(result.User) }); } catch (UserNotExistException e) @@ -94,7 +94,7 @@ namespace Timeline.Controllers ("Username", result.Username), ("Token", request.Token))); return Ok(new VerifyTokenResponse { - User = _mapper.Map(result) + User = _mapper.Map(result) }); } catch (UserTokenTimeExpireException e) diff --git a/Timeline/Controllers/UserController.cs b/Timeline/Controllers/UserController.cs index fa73c6f9..4572296b 100644 --- a/Timeline/Controllers/UserController.cs +++ b/Timeline/Controllers/UserController.cs @@ -29,35 +29,23 @@ namespace Timeline.Controllers _mapper = mapper; } - private IUserInfo ConvertToUserInfo(User user, bool administrator) - { - if (administrator) - return _mapper.Map(user); - else - return _mapper.Map(user); - } + private UserInfo ConvertToUserInfo(User user) => _mapper.Map(user); [HttpGet("users")] - public async Task> List() + public async Task> List() { var users = await _userService.GetUsers(); - var administrator = this.IsAdministrator(); - // Note: the (object) explicit conversion. If not convert, - // then result is a IUserInfo array and JsonSerializer will - // treat all element as IUserInfo and deserialize only properties - // in IUserInfo. So we convert it to object to make an object - // array so that JsonSerializer use the runtime type. - var result = users.Select(u => (object)ConvertToUserInfo(u, administrator)).ToArray(); + var result = users.Select(u => ConvertToUserInfo(u)).ToArray(); return Ok(result); } [HttpGet("users/{username}")] - public async Task> Get([FromRoute][Username] string username) + public async Task> Get([FromRoute][Username] string username) { try { var user = await _userService.GetUserByUsername(username); - return Ok(ConvertToUserInfo(user, this.IsAdministrator())); + return Ok(ConvertToUserInfo(user)); } catch (UserNotExistException e) { diff --git a/Timeline/Models/Http/TokenController.cs b/Timeline/Models/Http/TokenController.cs index 383b2965..ea8b59ed 100644 --- a/Timeline/Models/Http/TokenController.cs +++ b/Timeline/Models/Http/TokenController.cs @@ -16,7 +16,7 @@ namespace Timeline.Models.Http public class CreateTokenResponse { public string Token { get; set; } = default!; - public UserInfoForAdmin User { get; set; } = default!; + public UserInfo User { get; set; } = default!; } public class VerifyTokenRequest @@ -27,6 +27,6 @@ namespace Timeline.Models.Http public class VerifyTokenResponse { - public UserInfoForAdmin User { get; set; } = default!; + public UserInfo User { get; set; } = default!; } } diff --git a/Timeline/Models/Http/UserInfo.cs b/Timeline/Models/Http/UserInfo.cs index 07ac0aad..5890a7a6 100644 --- a/Timeline/Models/Http/UserInfo.cs +++ b/Timeline/Models/Http/UserInfo.cs @@ -7,54 +7,52 @@ using Timeline.Services; namespace Timeline.Models.Http { - public interface IUserInfo - { - string Username { get; set; } - string Nickname { get; set; } - string AvatarUrl { get; set; } - } - - public class UserInfo : IUserInfo + public class UserInfo { public string Username { get; set; } = default!; public string Nickname { get; set; } = default!; - public string AvatarUrl { get; set; } = default!; + public bool? Administrator { get; set; } = default!; +#pragma warning disable CA1707 + public UserInfoLinks? _links { get; set; } +#pragma warning restore CA1707 } - public class UserInfoForAdmin : IUserInfo + public class UserInfoLinks { - public string Username { get; set; } = default!; - public string Nickname { get; set; } = default!; - public string AvatarUrl { get; set; } = default!; - public bool Administrator { get; set; } + public string Avatar { get; set; } = default!; + public string Timeline { get; set; } = default!; } - public class UserInfoAvatarUrlValueResolver : IValueResolver + public class UserInfoLinksValueResolver : IValueResolver { private readonly IActionContextAccessor? _actionContextAccessor; private readonly IUrlHelperFactory? _urlHelperFactory; - public UserInfoAvatarUrlValueResolver() + public UserInfoLinksValueResolver() { _actionContextAccessor = null; _urlHelperFactory = null; } - public UserInfoAvatarUrlValueResolver(IActionContextAccessor actionContextAccessor, IUrlHelperFactory urlHelperFactory) + public UserInfoLinksValueResolver(IActionContextAccessor actionContextAccessor, IUrlHelperFactory urlHelperFactory) { _actionContextAccessor = actionContextAccessor; _urlHelperFactory = urlHelperFactory; } - public string Resolve(User source, IUserInfo destination, string destMember, ResolutionContext context) + public UserInfoLinks? Resolve(User source, UserInfo destination, UserInfoLinks? destMember, ResolutionContext context) { - if (_actionContextAccessor == null) + if (_actionContextAccessor == null || _urlHelperFactory == null) { - return $"/users/{destination.Username}/avatar"; + return null; } - var urlHelper = _urlHelperFactory!.GetUrlHelper(_actionContextAccessor.ActionContext); - return urlHelper.ActionLink(nameof(UserAvatarController.Get), nameof(UserAvatarController), new { destination.Username }); + var urlHelper = _urlHelperFactory.GetUrlHelper(_actionContextAccessor.ActionContext); + return new UserInfoLinks + { + Avatar = urlHelper.ActionLink(nameof(UserAvatarController.Get), nameof(UserAvatarController), new { destination.Username }), + Timeline = urlHelper.ActionLink(nameof(PersonalTimelineController.TimelineGet), nameof(PersonalTimelineController), new { destination.Username }) + }; } } @@ -62,8 +60,7 @@ namespace Timeline.Models.Http { public UserInfoAutoMapperProfile() { - CreateMap().ForMember(u => u.AvatarUrl, opt => opt.MapFrom()); - CreateMap().ForMember(u => u.AvatarUrl, opt => opt.MapFrom()); + CreateMap().ForMember(u => u._links, opt => opt.MapFrom()); } } } -- cgit v1.2.3 From a7fd42ddee50a8066a083c57b7940e4f9896dcb7 Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 31 Jan 2020 20:25:06 +0800 Subject: Fix a bug in url generation and add development database migration. --- .../20200131100517_RefactorUser.Designer.cs | 243 +++++++++++++++++++++ .../20200131100517_RefactorUser.cs | 136 ++++++++++++ .../DevelopmentDatabaseContextModelSnapshot.cs | 92 +++----- Timeline/Models/Http/UserInfo.cs | 7 +- Timeline/Startup.cs | 4 + 5 files changed, 420 insertions(+), 62 deletions(-) create mode 100644 Timeline/Migrations/DevelopmentDatabase/20200131100517_RefactorUser.Designer.cs create mode 100644 Timeline/Migrations/DevelopmentDatabase/20200131100517_RefactorUser.cs diff --git a/Timeline/Migrations/DevelopmentDatabase/20200131100517_RefactorUser.Designer.cs b/Timeline/Migrations/DevelopmentDatabase/20200131100517_RefactorUser.Designer.cs new file mode 100644 index 00000000..13e322c8 --- /dev/null +++ b/Timeline/Migrations/DevelopmentDatabase/20200131100517_RefactorUser.Designer.cs @@ -0,0 +1,243 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Timeline.Entities; + +namespace Timeline.Migrations.DevelopmentDatabase +{ + [DbContext(typeof(DevelopmentDatabaseContext))] + [Migration("20200131100517_RefactorUser")] + partial class RefactorUser + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.1"); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("CreateTime") + .HasColumnName("create_time") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnName("description") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnName("name") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnName("owner") + .HasColumnType("INTEGER"); + + b.Property("Visibility") + .HasColumnName("visibility") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("timelines"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("TimelineId") + .HasColumnName("timeline") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnName("user") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TimelineId"); + + b.HasIndex("UserId"); + + b.ToTable("timeline_members"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnName("author") + .HasColumnType("INTEGER"); + + b.Property("Content") + .HasColumnName("content") + .HasColumnType("TEXT"); + + b.Property("LastUpdated") + .HasColumnName("last_updated") + .HasColumnType("TEXT"); + + b.Property("Time") + .HasColumnName("time") + .HasColumnType("TEXT"); + + b.Property("TimelineId") + .HasColumnName("timeline") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("TimelineId"); + + b.ToTable("timeline_posts"); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Data") + .HasColumnName("data") + .HasColumnType("BLOB"); + + b.Property("ETag") + .HasColumnName("etag") + .HasColumnType("TEXT") + .HasMaxLength(30); + + b.Property("LastModified") + .HasColumnName("last_modified") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnName("type") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnName("user") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("user_avatars"); + }); + + modelBuilder.Entity("Timeline.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Nickname") + .HasColumnName("nickname") + .HasColumnType("TEXT") + .HasMaxLength(100); + + b.Property("Password") + .IsRequired() + .HasColumnName("password") + .HasColumnType("TEXT"); + + b.Property("Roles") + .IsRequired() + .HasColumnName("roles") + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasColumnName("username") + .HasColumnType("TEXT") + .HasMaxLength(26); + + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnName("version") + .HasColumnType("INTEGER") + .HasDefaultValue(0L); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("users"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Owner") + .WithMany("Timelines") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Members") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithMany("TimelinesJoined") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Author") + .WithMany("TimelinePosts") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Posts") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithOne("Avatar") + .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Timeline/Migrations/DevelopmentDatabase/20200131100517_RefactorUser.cs b/Timeline/Migrations/DevelopmentDatabase/20200131100517_RefactorUser.cs new file mode 100644 index 00000000..e6f38506 --- /dev/null +++ b/Timeline/Migrations/DevelopmentDatabase/20200131100517_RefactorUser.cs @@ -0,0 +1,136 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Timeline.Migrations.DevelopmentDatabase +{ + public partial class RefactorUser : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn(name: "name", table: "users", newName: "username"); + migrationBuilder.RenameIndex(name: "IX_users_name", table: "users", newName: "IX_users_username"); + + migrationBuilder.AddColumn( + name: "nickname", + table: "users", + maxLength: 100, + nullable: true); + + migrationBuilder.Sql(@" +UPDATE users + SET nickname = ( + SELECT nickname + FROM user_details + WHERE user_details.UserId = users.id + ); + "); + + /* + migrationBuilder.RenameColumn(name: "UserId", table: "user_avatars", newName: "user"); + + migrationBuilder.DropForeignKey( + name: "FK_user_avatars_users_UserId", + table: "user_avatars"); + + migrationBuilder.AddForeignKey( + name: "FK_user_avatars_users_user", + table: "user_avatars", + column: "user", + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.RenameIndex( + name: "IX_user_avatars_UserId", + table: "user_avatars", + newName: "IX_user_avatars_user"); + */ + + migrationBuilder.Sql(@" +CREATE TABLE user_avatars_backup ( + id INTEGER NOT NULL + CONSTRAINT PK_user_avatars PRIMARY KEY AUTOINCREMENT, + data BLOB, + type TEXT, + etag TEXT, + last_modified TEXT NOT NULL, + user INTEGER NOT NULL, + CONSTRAINT FK_user_avatars_users_user FOREIGN KEY ( + user + ) + REFERENCES users (id) ON DELETE CASCADE +); + +INSERT INTO user_avatars_backup (id, data, type, etag, last_modified, user) + SELECT id, data, type, etag, last_modified, UserId FROM user_avatars; + +DROP TABLE user_avatars; + +ALTER TABLE user_avatars_backup + RENAME TO user_avatars; + +CREATE UNIQUE INDEX IX_user_avatars_user ON user_avatars (user); + "); + + // migrationBuilder.DropTable(name: "user_details"); + + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@" +CREATE TABLE user_avatars_backup ( + id INTEGER NOT NULL + CONSTRAINT PK_user_avatars PRIMARY KEY AUTOINCREMENT, + data BLOB, + type TEXT, + etag TEXT, + last_modified TEXT NOT NULL, + UserId INTEGER NOT NULL, + CONSTRAINT FK_user_avatars_users_UserId FOREIGN KEY ( + user + ) + REFERENCES users (id) ON DELETE CASCADE +); + +INSERT INTO user_avatars_backup (id, data, type, etag, last_modified, UserId) + SELECT id, data, type, etag, last_modified, user FROM user_avatars; + +DROP TABLE user_avatars; + +ALTER TABLE user_avatars_backup + RENAME TO user_avatars; + +CREATE UNIQUE INDEX IX_user_avatars_UserId ON user_avatars (UserId); + "); + + migrationBuilder.Sql(@" +CREATE TABLE users_backup ( + id INTEGER NOT NULL + CONSTRAINT PK_users PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + password TEXT NOT NULL, + roles TEXT NOT NULL, + version INTEGER NOT NULL + DEFAULT 0 +); + +INSERT INTO users_backup (id, name, password, roles, version) + SELECT id, username, password, roles, version FROM users; + +DROP TABLE users; + +ALTER TABLE users_backup + RENAME TO users; + +CREATE UNIQUE INDEX IX_users_name ON users (name); + "); + + migrationBuilder.RenameColumn(name: "user", table: "user_avatars", newName: "UserId"); + + migrationBuilder.RenameIndex( + name: "IX_user_avatars_user", + table: "user_avatars", + newName: "IX_user_avatars_UserId"); + } + } +} diff --git a/Timeline/Migrations/DevelopmentDatabase/DevelopmentDatabaseContextModelSnapshot.cs b/Timeline/Migrations/DevelopmentDatabase/DevelopmentDatabaseContextModelSnapshot.cs index 6fbaea5f..5da49dbe 100644 --- a/Timeline/Migrations/DevelopmentDatabase/DevelopmentDatabaseContextModelSnapshot.cs +++ b/Timeline/Migrations/DevelopmentDatabase/DevelopmentDatabaseContextModelSnapshot.cs @@ -14,7 +14,7 @@ namespace Timeline.Migrations.DevelopmentDatabase { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "3.1.0"); + .HasAnnotation("ProductVersion", "3.1.1"); modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => { @@ -110,44 +110,7 @@ namespace Timeline.Migrations.DevelopmentDatabase b.ToTable("timeline_posts"); }); - modelBuilder.Entity("Timeline.Entities.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("EncryptedPassword") - .IsRequired() - .HasColumnName("password") - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasColumnName("name") - .HasColumnType("TEXT") - .HasMaxLength(26); - - b.Property("RoleString") - .IsRequired() - .HasColumnName("roles") - .HasColumnType("TEXT"); - - b.Property("Version") - .ValueGeneratedOnAdd() - .HasColumnName("version") - .HasColumnType("INTEGER") - .HasDefaultValue(0L); - - b.HasKey("Id"); - - b.HasIndex("Name") - .IsUnique(); - - b.ToTable("users"); - }); - - modelBuilder.Entity("Timeline.Entities.UserAvatar", b => + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -172,6 +135,7 @@ namespace Timeline.Migrations.DevelopmentDatabase .HasColumnType("TEXT"); b.Property("UserId") + .HasColumnName("user") .HasColumnType("INTEGER"); b.HasKey("Id"); @@ -182,7 +146,7 @@ namespace Timeline.Migrations.DevelopmentDatabase b.ToTable("user_avatars"); }); - modelBuilder.Entity("Timeline.Entities.UserDetail", b => + modelBuilder.Entity("Timeline.Entities.UserEntity", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -192,22 +156,41 @@ namespace Timeline.Migrations.DevelopmentDatabase b.Property("Nickname") .HasColumnName("nickname") .HasColumnType("TEXT") + .HasMaxLength(100); + + b.Property("Password") + .IsRequired() + .HasColumnName("password") + .HasColumnType("TEXT"); + + b.Property("Roles") + .IsRequired() + .HasColumnName("roles") + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasColumnName("username") + .HasColumnType("TEXT") .HasMaxLength(26); - b.Property("UserId") - .HasColumnType("INTEGER"); + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnName("version") + .HasColumnType("INTEGER") + .HasDefaultValue(0L); b.HasKey("Id"); - b.HasIndex("UserId") + b.HasIndex("Username") .IsUnique(); - b.ToTable("user_details"); + b.ToTable("users"); }); modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => { - b.HasOne("Timeline.Entities.User", "Owner") + b.HasOne("Timeline.Entities.UserEntity", "Owner") .WithMany("Timelines") .HasForeignKey("OwnerId") .OnDelete(DeleteBehavior.Cascade) @@ -222,7 +205,7 @@ namespace Timeline.Migrations.DevelopmentDatabase .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Timeline.Entities.User", "User") + b.HasOne("Timeline.Entities.UserEntity", "User") .WithMany("TimelinesJoined") .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -231,7 +214,7 @@ namespace Timeline.Migrations.DevelopmentDatabase modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => { - b.HasOne("Timeline.Entities.User", "Author") + b.HasOne("Timeline.Entities.UserEntity", "Author") .WithMany("TimelinePosts") .HasForeignKey("AuthorId") .OnDelete(DeleteBehavior.Cascade) @@ -244,20 +227,11 @@ namespace Timeline.Migrations.DevelopmentDatabase .IsRequired(); }); - modelBuilder.Entity("Timeline.Entities.UserAvatar", b => + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => { - b.HasOne("Timeline.Entities.User", null) + b.HasOne("Timeline.Entities.UserEntity", "User") .WithOne("Avatar") - .HasForeignKey("Timeline.Entities.UserAvatar", "UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.UserDetail", b => - { - b.HasOne("Timeline.Entities.User", null) - .WithOne("Detail") - .HasForeignKey("Timeline.Entities.UserDetail", "UserId") + .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); diff --git a/Timeline/Models/Http/UserInfo.cs b/Timeline/Models/Http/UserInfo.cs index 5890a7a6..fee53ade 100644 --- a/Timeline/Models/Http/UserInfo.cs +++ b/Timeline/Models/Http/UserInfo.cs @@ -48,11 +48,12 @@ namespace Timeline.Models.Http } var urlHelper = _urlHelperFactory.GetUrlHelper(_actionContextAccessor.ActionContext); - return new UserInfoLinks + var result = new UserInfoLinks { - Avatar = urlHelper.ActionLink(nameof(UserAvatarController.Get), nameof(UserAvatarController), new { destination.Username }), - Timeline = urlHelper.ActionLink(nameof(PersonalTimelineController.TimelineGet), nameof(PersonalTimelineController), new { destination.Username }) + Avatar = urlHelper.ActionLink(nameof(UserAvatarController.Get), nameof(UserAvatarController)[0..^nameof(Controller).Length], new { destination.Username }), + Timeline = urlHelper.ActionLink(nameof(PersonalTimelineController.TimelineGet), nameof(PersonalTimelineController)[0..^nameof(Controller).Length], new { destination.Username }) }; + return result; } } diff --git a/Timeline/Startup.cs b/Timeline/Startup.cs index 998b5c44..14ee37e3 100644 --- a/Timeline/Startup.cs +++ b/Timeline/Startup.cs @@ -2,9 +2,11 @@ using AutoMapper; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using System; using System.Text.Json.Serialization; @@ -87,6 +89,8 @@ namespace Timeline services.AddScoped(); + services.TryAddSingleton(); + var databaseConfig = Configuration.GetSection(nameof(DatabaseConfig)).Get(); if (databaseConfig.UseDevelopment) -- cgit v1.2.3 From 3749a642306b19c84f324b0e94c4d62d8ec60332 Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 31 Jan 2020 21:57:09 +0800 Subject: Fix test bugs in user info mapper. Make create user action return created user info. --- Timeline.Tests/IntegratedTests/IntegratedTestBase.cs | 3 +-- Timeline.Tests/IntegratedTests/UserTest.cs | 6 +++++- Timeline/Controllers/UserController.cs | 6 +++--- Timeline/Models/Http/UserInfo.cs | 14 +++----------- Timeline/Services/UserService.cs | 8 ++++---- 5 files changed, 16 insertions(+), 21 deletions(-) diff --git a/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs b/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs index 59af5eab..dfde2ea5 100644 --- a/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs +++ b/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs @@ -12,13 +12,12 @@ using Xunit; namespace Timeline.Tests.IntegratedTests { - public abstract class IntegratedTestBase : IClassFixture>, IDisposable { static IntegratedTestBase() { FluentAssertions.AssertionOptions.AssertEquivalencyUsing(options => - options.Excluding(m => m.RuntimeType == typeof(UserInfo) && m.SelectedMemberPath == "_links")); + options.Excluding(m => m.RuntimeType == typeof(UserInfoLinks))); } protected TestApplication TestApp { get; } diff --git a/Timeline.Tests/IntegratedTests/UserTest.cs b/Timeline.Tests/IntegratedTests/UserTest.cs index bbeb6ad6..f863eb6c 100644 --- a/Timeline.Tests/IntegratedTests/UserTest.cs +++ b/Timeline.Tests/IntegratedTests/UserTest.cs @@ -296,7 +296,11 @@ namespace Timeline.Tests.IntegratedTests Administrator = true, Nickname = "ccc" }); - res.Should().HaveStatusCode(200); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + body.Username.Should().Be("aaa"); + body.Nickname.Should().Be("ccc"); + body.Administrator.Should().BeTrue(); } { var res = await client.GetAsync("users/aaa"); diff --git a/Timeline/Controllers/UserController.cs b/Timeline/Controllers/UserController.cs index 4572296b..26e63f63 100644 --- a/Timeline/Controllers/UserController.cs +++ b/Timeline/Controllers/UserController.cs @@ -108,12 +108,12 @@ namespace Timeline.Controllers } [HttpPost("userop/createuser"), AdminAuthorize] - public async Task CreateUser([FromBody] CreateUserRequest body) + public async Task> CreateUser([FromBody] CreateUserRequest body) { try { - await _userService.CreateUser(_mapper.Map(body)); - return Ok(); + var user = await _userService.CreateUser(_mapper.Map(body)); + return Ok(ConvertToUserInfo(user)); } catch (ConflictException) { diff --git a/Timeline/Models/Http/UserInfo.cs b/Timeline/Models/Http/UserInfo.cs index fee53ade..0d1d702b 100644 --- a/Timeline/Models/Http/UserInfo.cs +++ b/Timeline/Models/Http/UserInfo.cs @@ -25,14 +25,8 @@ namespace Timeline.Models.Http public class UserInfoLinksValueResolver : IValueResolver { - private readonly IActionContextAccessor? _actionContextAccessor; - private readonly IUrlHelperFactory? _urlHelperFactory; - - public UserInfoLinksValueResolver() - { - _actionContextAccessor = null; - _urlHelperFactory = null; - } + private readonly IActionContextAccessor _actionContextAccessor; + private readonly IUrlHelperFactory _urlHelperFactory; public UserInfoLinksValueResolver(IActionContextAccessor actionContextAccessor, IUrlHelperFactory urlHelperFactory) { @@ -42,10 +36,8 @@ namespace Timeline.Models.Http public UserInfoLinks? Resolve(User source, UserInfo destination, UserInfoLinks? destMember, ResolutionContext context) { - if (_actionContextAccessor == null || _urlHelperFactory == null) - { + if (_actionContextAccessor.ActionContext == null) return null; - } var urlHelper = _urlHelperFactory.GetUrlHelper(_actionContextAccessor.ActionContext); var result = new UserInfoLinks diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs index d2dc969e..93d92740 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -64,7 +64,7 @@ namespace Timeline.Services /// /// The info of new user. /// The password, can't be null or empty. - /// The id of the new user. + /// The the new user. /// Thrown when is null. /// Thrown when some fields in is bad. /// Thrown when a user with given username already exists. @@ -75,7 +75,7 @@ namespace Timeline.Services /// must be a valid nickname if set. It is empty by default. /// Other fields are ignored. /// - Task CreateUser(User info); + Task CreateUser(User info); /// /// Modify a user's info. @@ -276,7 +276,7 @@ namespace Timeline.Services return entities.Select(user => CreateUserFromEntity(user)).ToArray(); } - public async Task CreateUser(User info) + public async Task CreateUser(User info) { if (info == null) throw new ArgumentNullException(nameof(info)); @@ -316,7 +316,7 @@ namespace Timeline.Services _logger.LogInformation(Log.Format(LogDatabaseCreate, ("Id", newEntity.Id), ("Username", username), ("Administrator", administrator))); - return newEntity.Id; + return CreateUserFromEntity(newEntity); } private void ValidateModifyUserInfo(User? info) -- cgit v1.2.3 From 69a5cc2a6f3282de4859d7c083bd67dfa17226fb Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 31 Jan 2020 22:46:17 +0800 Subject: Make all patch return the new entity. --- .../IntegratedTests/PersonalTimelineTest.cs | 9 ++++-- Timeline.Tests/IntegratedTests/UserTest.cs | 10 +++++-- Timeline/Controllers/PersonalTimelineController.cs | 5 ++-- Timeline/Controllers/UserController.cs | 10 +++---- Timeline/Services/TimelineService.cs | 32 ++++++++++++---------- Timeline/Services/UserService.cs | 14 +++++++--- 6 files changed, 49 insertions(+), 31 deletions(-) diff --git a/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs b/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs index f3d6b172..81446fd8 100644 --- a/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs +++ b/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs @@ -119,19 +119,22 @@ namespace Timeline.Tests.IntegratedTests { var res = await client.PatchAsJsonAsync("users/user1/timeline", new TimelinePatchRequest { Description = mockDescription }); - res.Should().HaveStatusCode(200); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which.Description.Should().Be(mockDescription); await AssertDescription(mockDescription); } { var res = await client.PatchAsJsonAsync("users/user1/timeline", new TimelinePatchRequest { Description = null }); - res.Should().HaveStatusCode(200); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which.Description.Should().Be(mockDescription); await AssertDescription(mockDescription); } { var res = await client.PatchAsJsonAsync("users/user1/timeline", new TimelinePatchRequest { Description = "" }); - res.Should().HaveStatusCode(200); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which.Description.Should().Be(""); await AssertDescription(""); } } diff --git a/Timeline.Tests/IntegratedTests/UserTest.cs b/Timeline.Tests/IntegratedTests/UserTest.cs index f863eb6c..8ce76299 100644 --- a/Timeline.Tests/IntegratedTests/UserTest.cs +++ b/Timeline.Tests/IntegratedTests/UserTest.cs @@ -102,7 +102,9 @@ namespace Timeline.Tests.IntegratedTests { var res = await client.PatchAsJsonAsync("/users/user1", new UserPatchRequest { Nickname = "aaa" }); - res.Should().HaveStatusCode(200); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Nickname.Should().Be("aaa"); } { @@ -128,7 +130,11 @@ namespace Timeline.Tests.IntegratedTests Administrator = true, Nickname = "aaa" }); - res.Should().HaveStatusCode(200); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which; + body.Administrator.Should().Be(true); + body.Nickname.Should().Be("aaa"); } { diff --git a/Timeline/Controllers/PersonalTimelineController.cs b/Timeline/Controllers/PersonalTimelineController.cs index 27618c41..11353bb5 100644 --- a/Timeline/Controllers/PersonalTimelineController.cs +++ b/Timeline/Controllers/PersonalTimelineController.cs @@ -77,14 +77,15 @@ namespace Timeline.Controllers [HttpPatch("users/{username}/timeline")] [Authorize] - public async Task TimelinePatch([FromRoute][Username] string username, [FromBody] TimelinePatchRequest body) + public async Task> TimelinePatch([FromRoute][Username] string username, [FromBody] TimelinePatchRequest body) { if (!this.IsAdministrator() && !(User.Identity.Name == username)) { return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } await _service.ChangeProperty(username, body); - return Ok(); + var timeline = await _service.GetTimeline(username); + return Ok(timeline); } [HttpPut("users/{username}/timeline/members/{member}")] diff --git a/Timeline/Controllers/UserController.cs b/Timeline/Controllers/UserController.cs index 26e63f63..a3e8d816 100644 --- a/Timeline/Controllers/UserController.cs +++ b/Timeline/Controllers/UserController.cs @@ -55,14 +55,14 @@ namespace Timeline.Controllers } [HttpPatch("users/{username}"), Authorize] - public async Task Patch([FromBody] UserPatchRequest body, [FromRoute][Username] string username) + public async Task> Patch([FromBody] UserPatchRequest body, [FromRoute][Username] string username) { if (this.IsAdministrator()) { try { - await _userService.ModifyUser(username, _mapper.Map(body)); - return Ok(); + var user = await _userService.ModifyUser(username, _mapper.Map(body)); + return Ok(ConvertToUserInfo(user)); } catch (UserNotExistException e) { @@ -92,8 +92,8 @@ namespace Timeline.Controllers return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.CustomMessage_Forbid(UserController_Patch_Forbid_Administrator)); - await _userService.ModifyUser(this.GetUserId(), _mapper.Map(body)); - return Ok(); + var user = await _userService.ModifyUser(this.GetUserId(), _mapper.Map(body)); + return Ok(ConvertToUserInfo(user)); } } diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs index 85445973..0ea68265 100644 --- a/Timeline/Services/TimelineService.cs +++ b/Timeline/Services/TimelineService.cs @@ -80,21 +80,6 @@ namespace Timeline.Services /// Task DeletePost(string name, long id); - /// - /// Set the properties of a timeline. - /// - /// Username or the timeline name. See remarks of . - /// The new properties. Null member means not to change. - /// Thrown when or is null. - /// Thrown when is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service). - /// - /// 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 . - /// - Task ChangeProperty(string name, TimelinePatchRequest newProperties); - /// /// Remove members to a timeline. /// @@ -235,6 +220,23 @@ namespace Timeline.Services /// Thrown when the user does not exist. Inner exception MUST be . /// Task GetTimeline(string username); + + /// + /// Set the properties of a timeline. + /// + /// Username or the timeline name. See remarks of . + /// The new properties. Null member means not to change. + /// + /// Thrown when is null. + /// + /// + /// Thrown when is of bad format. + /// + /// + /// Thrown when the user does not exist. Inner exception MUST be . + /// + Task ChangeProperty(string name, TimelinePatchRequest newProperties); + } public abstract class BaseTimelineService : IBaseTimelineService diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs index 93d92740..7dc7159d 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -82,6 +82,7 @@ namespace Timeline.Services /// /// The id of the user. /// The new info. May be null. + /// The new user info. /// Thrown when some fields in is bad. /// Thrown when user with given id does not exist. /// @@ -96,13 +97,14 @@ namespace Timeline.Services /// /// /// - Task ModifyUser(long id, User? info); + Task ModifyUser(long id, User? info); /// /// Modify a user's info. /// /// The username of the user. /// The new info. May be null. + /// The new user info. /// Thrown when is null. /// Thrown when is of bad format or some fields in is bad. /// Thrown when user with given id does not exist. @@ -120,7 +122,7 @@ namespace Timeline.Services /// Note: Whether is set or not, version will increase and not set to the specified value if there is one. /// /// - Task ModifyUser(string username, User? info); + Task ModifyUser(string username, User? info); /// /// Delete a user of given id. @@ -370,7 +372,7 @@ namespace Timeline.Services } - public async Task ModifyUser(long id, User? info) + public async Task ModifyUser(long id, User? info) { ValidateModifyUserInfo(info); @@ -382,9 +384,11 @@ namespace Timeline.Services await _databaseContext.SaveChangesAsync(); _logger.LogInformation(LogDatabaseUpdate, ("Id", id)); + + return CreateUserFromEntity(entity); } - public async Task ModifyUser(string username, User? info) + public async Task ModifyUser(string username, User? info) { if (username == null) throw new ArgumentNullException(nameof(username)); @@ -400,6 +404,8 @@ namespace Timeline.Services await _databaseContext.SaveChangesAsync(); _logger.LogInformation(LogDatabaseUpdate, ("Username", username)); + + return CreateUserFromEntity(entity); } public async Task DeleteUser(long id) -- cgit v1.2.3 From bcb0a2361467614531a337282da1fd23996924f1 Mon Sep 17 00:00:00 2001 From: crupest Date: Sat, 1 Feb 2020 00:25:41 +0800 Subject: ... --- .../20200131100517_RefactorUser.cs | 7 - .../20200131152033_RefactorUser.Designer.cs | 244 +++++++++++++++++++++ .../20200131152033_RefactorUser.cs | 55 +++++ .../ProductionDatabaseContextModelSnapshot.cs | 92 +++----- Timeline/Startup.cs | 5 +- 5 files changed, 336 insertions(+), 67 deletions(-) create mode 100644 Timeline/Migrations/ProductionDatabase/20200131152033_RefactorUser.Designer.cs create mode 100644 Timeline/Migrations/ProductionDatabase/20200131152033_RefactorUser.cs diff --git a/Timeline/Migrations/DevelopmentDatabase/20200131100517_RefactorUser.cs b/Timeline/Migrations/DevelopmentDatabase/20200131100517_RefactorUser.cs index e6f38506..ade65eb1 100644 --- a/Timeline/Migrations/DevelopmentDatabase/20200131100517_RefactorUser.cs +++ b/Timeline/Migrations/DevelopmentDatabase/20200131100517_RefactorUser.cs @@ -124,13 +124,6 @@ ALTER TABLE users_backup CREATE UNIQUE INDEX IX_users_name ON users (name); "); - - migrationBuilder.RenameColumn(name: "user", table: "user_avatars", newName: "UserId"); - - migrationBuilder.RenameIndex( - name: "IX_user_avatars_user", - table: "user_avatars", - newName: "IX_user_avatars_UserId"); } } } diff --git a/Timeline/Migrations/ProductionDatabase/20200131152033_RefactorUser.Designer.cs b/Timeline/Migrations/ProductionDatabase/20200131152033_RefactorUser.Designer.cs new file mode 100644 index 00000000..bb0bb5af --- /dev/null +++ b/Timeline/Migrations/ProductionDatabase/20200131152033_RefactorUser.Designer.cs @@ -0,0 +1,244 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Timeline.Entities; + +namespace Timeline.Migrations.ProductionDatabase +{ + [DbContext(typeof(ProductionDatabaseContext))] + [Migration("20200131152033_RefactorUser")] + partial class RefactorUser + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.1") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("bigint"); + + b.Property("CreateTime") + .HasColumnName("create_time") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnName("description") + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("Name") + .HasColumnName("name") + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("OwnerId") + .HasColumnName("owner") + .HasColumnType("bigint"); + + b.Property("Visibility") + .HasColumnName("visibility") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("timelines"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("bigint"); + + b.Property("TimelineId") + .HasColumnName("timeline") + .HasColumnType("bigint"); + + b.Property("UserId") + .HasColumnName("user") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("TimelineId"); + + b.HasIndex("UserId"); + + b.ToTable("timeline_members"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("bigint"); + + b.Property("AuthorId") + .HasColumnName("author") + .HasColumnType("bigint"); + + b.Property("Content") + .HasColumnName("content") + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("LastUpdated") + .HasColumnName("last_updated") + .HasColumnType("datetime(6)"); + + b.Property("Time") + .HasColumnName("time") + .HasColumnType("datetime(6)"); + + b.Property("TimelineId") + .HasColumnName("timeline") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("TimelineId"); + + b.ToTable("timeline_posts"); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("bigint"); + + b.Property("Data") + .HasColumnName("data") + .HasColumnType("longblob"); + + b.Property("ETag") + .HasColumnName("etag") + .HasColumnType("varchar(30) CHARACTER SET utf8mb4") + .HasMaxLength(30); + + b.Property("LastModified") + .HasColumnName("last_modified") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnName("type") + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("UserId") + .HasColumnName("user") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("user_avatars"); + }); + + modelBuilder.Entity("Timeline.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("bigint"); + + b.Property("Nickname") + .HasColumnName("nickname") + .HasColumnType("varchar(100) CHARACTER SET utf8mb4") + .HasMaxLength(100); + + b.Property("Password") + .IsRequired() + .HasColumnName("password") + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("Roles") + .IsRequired() + .HasColumnName("roles") + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("Username") + .IsRequired() + .HasColumnName("username") + .HasColumnType("varchar(26) CHARACTER SET utf8mb4") + .HasMaxLength(26); + + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnName("version") + .HasColumnType("bigint") + .HasDefaultValue(0L); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("users"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Owner") + .WithMany("Timelines") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Members") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithMany("TimelinesJoined") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Author") + .WithMany("TimelinePosts") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Posts") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithOne("Avatar") + .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Timeline/Migrations/ProductionDatabase/20200131152033_RefactorUser.cs b/Timeline/Migrations/ProductionDatabase/20200131152033_RefactorUser.cs new file mode 100644 index 00000000..a0ca5212 --- /dev/null +++ b/Timeline/Migrations/ProductionDatabase/20200131152033_RefactorUser.cs @@ -0,0 +1,55 @@ +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Timeline.Migrations.ProductionDatabase +{ + public partial class RefactorUser : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@" +START TRANSACTION; + +ALTER TABLE `users` + CHANGE COLUMN `name` `username` varchar (26) NOT NULL, + RENAME INDEX IX_users_name TO IX_users_username, + ADD `nickname` varchar(100) CHARACTER SET utf8mb4 NULL; + +UPDATE users + SET nickname = ( + SELECT nickname + FROM user_details + WHERE user_details.UserId = users.id + ); + +ALTER TABLE `user_avatars` + CHANGE COLUMN `UserId` `user` bigint (20) NOT NULL, + RENAME INDEX IX_user_avatars_UserId TO IX_user_avatars_user, + DROP FOREIGN KEY FK_user_avatars_users_UserId, + ADD CONSTRAINT FK_user_avatars_users_user FOREIGN KEY (`user`) REFERENCES `users` (`id`) ON DELETE CASCADE; + +COMMIT; + "); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@" +START TRANSACTION; + +ALTER TABLE `users` + CHANGE COLUMN `username` `name` varchar (26) NOT NULL, + RENAME INDEX IX_users_username TO IX_users_name, + DROP COLUMN `nickname`; + +ALTER TABLE `user_avatars` + CHANGE COLUMN `user` `UserId` bigint (20) NOT NULL, + RENAME INDEX IX_user_avatars_user TO IX_user_avatars_UserId, + DROP FOREIGN KEY FK_user_avatars_users_user, + ADD CONSTRAINT FK_user_avatars_users_UserId FOREIGN KEY (`UserId`) REFERENCES `users` (`id`) ON DELETE CASCADE; + +COMMIT; + "); + } + } +} diff --git a/Timeline/Migrations/ProductionDatabase/ProductionDatabaseContextModelSnapshot.cs b/Timeline/Migrations/ProductionDatabase/ProductionDatabaseContextModelSnapshot.cs index 67ef52a4..bfc9b768 100644 --- a/Timeline/Migrations/ProductionDatabase/ProductionDatabaseContextModelSnapshot.cs +++ b/Timeline/Migrations/ProductionDatabase/ProductionDatabaseContextModelSnapshot.cs @@ -14,7 +14,7 @@ namespace Timeline.Migrations.ProductionDatabase { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "3.1.0") + .HasAnnotation("ProductVersion", "3.1.1") .HasAnnotation("Relational:MaxIdentifierLength", 64); modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => @@ -111,44 +111,7 @@ namespace Timeline.Migrations.ProductionDatabase b.ToTable("timeline_posts"); }); - modelBuilder.Entity("Timeline.Entities.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("bigint"); - - b.Property("EncryptedPassword") - .IsRequired() - .HasColumnName("password") - .HasColumnType("longtext CHARACTER SET utf8mb4"); - - b.Property("Name") - .IsRequired() - .HasColumnName("name") - .HasColumnType("varchar(26) CHARACTER SET utf8mb4") - .HasMaxLength(26); - - b.Property("RoleString") - .IsRequired() - .HasColumnName("roles") - .HasColumnType("longtext CHARACTER SET utf8mb4"); - - b.Property("Version") - .ValueGeneratedOnAdd() - .HasColumnName("version") - .HasColumnType("bigint") - .HasDefaultValue(0L); - - b.HasKey("Id"); - - b.HasIndex("Name") - .IsUnique(); - - b.ToTable("users"); - }); - - modelBuilder.Entity("Timeline.Entities.UserAvatar", b => + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -173,6 +136,7 @@ namespace Timeline.Migrations.ProductionDatabase .HasColumnType("longtext CHARACTER SET utf8mb4"); b.Property("UserId") + .HasColumnName("user") .HasColumnType("bigint"); b.HasKey("Id"); @@ -183,7 +147,7 @@ namespace Timeline.Migrations.ProductionDatabase b.ToTable("user_avatars"); }); - modelBuilder.Entity("Timeline.Entities.UserDetail", b => + modelBuilder.Entity("Timeline.Entities.UserEntity", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -192,23 +156,42 @@ namespace Timeline.Migrations.ProductionDatabase b.Property("Nickname") .HasColumnName("nickname") + .HasColumnType("varchar(100) CHARACTER SET utf8mb4") + .HasMaxLength(100); + + b.Property("Password") + .IsRequired() + .HasColumnName("password") + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("Roles") + .IsRequired() + .HasColumnName("roles") + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("Username") + .IsRequired() + .HasColumnName("username") .HasColumnType("varchar(26) CHARACTER SET utf8mb4") .HasMaxLength(26); - b.Property("UserId") - .HasColumnType("bigint"); + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnName("version") + .HasColumnType("bigint") + .HasDefaultValue(0L); b.HasKey("Id"); - b.HasIndex("UserId") + b.HasIndex("Username") .IsUnique(); - b.ToTable("user_details"); + b.ToTable("users"); }); modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => { - b.HasOne("Timeline.Entities.User", "Owner") + b.HasOne("Timeline.Entities.UserEntity", "Owner") .WithMany("Timelines") .HasForeignKey("OwnerId") .OnDelete(DeleteBehavior.Cascade) @@ -223,7 +206,7 @@ namespace Timeline.Migrations.ProductionDatabase .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Timeline.Entities.User", "User") + b.HasOne("Timeline.Entities.UserEntity", "User") .WithMany("TimelinesJoined") .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -232,7 +215,7 @@ namespace Timeline.Migrations.ProductionDatabase modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => { - b.HasOne("Timeline.Entities.User", "Author") + b.HasOne("Timeline.Entities.UserEntity", "Author") .WithMany("TimelinePosts") .HasForeignKey("AuthorId") .OnDelete(DeleteBehavior.Cascade) @@ -245,20 +228,11 @@ namespace Timeline.Migrations.ProductionDatabase .IsRequired(); }); - modelBuilder.Entity("Timeline.Entities.UserAvatar", b => + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => { - b.HasOne("Timeline.Entities.User", null) + b.HasOne("Timeline.Entities.UserEntity", "User") .WithOne("Avatar") - .HasForeignKey("Timeline.Entities.UserAvatar", "UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.UserDetail", b => - { - b.HasOne("Timeline.Entities.User", null) - .WithOne("Detail") - .HasForeignKey("Timeline.Entities.UserDetail", "UserId") + .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); diff --git a/Timeline/Startup.cs b/Timeline/Startup.cs index 14ee37e3..86349a27 100644 --- a/Timeline/Startup.cs +++ b/Timeline/Startup.cs @@ -8,6 +8,8 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; +using Pomelo.EntityFrameworkCore.MySql.Infrastructure; +using Pomelo.EntityFrameworkCore.MySql.Storage; using System; using System.Text.Json.Serialization; using Timeline.Auth; @@ -108,7 +110,8 @@ namespace Timeline { if (databaseConfig.ConnectionString == null) throw new InvalidOperationException("DatabaseConfig.ConnectionString is not set. Please set it as a mysql connection string."); - options.UseMySql(databaseConfig.ConnectionString); + options.UseMySql(databaseConfig.ConnectionString, + mySqlOptions => mySqlOptions.ServerVersion(new ServerVersion(new Version(5, 7), ServerType.MySql))); }); } } -- cgit v1.2.3