From fa2a3282c51d831b25f374803301e75eac15d11c Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Thu, 17 Oct 2019 20:46:57 +0800 Subject: ... --- Timeline.Tests/Mock/Services/TestClock.cs | 10 ---------- 1 file changed, 10 deletions(-) (limited to 'Timeline.Tests/Mock/Services') diff --git a/Timeline.Tests/Mock/Services/TestClock.cs b/Timeline.Tests/Mock/Services/TestClock.cs index 0082171e..6671395a 100644 --- a/Timeline.Tests/Mock/Services/TestClock.cs +++ b/Timeline.Tests/Mock/Services/TestClock.cs @@ -1,5 +1,3 @@ -using Microsoft.AspNetCore.Mvc.Testing; -using Microsoft.Extensions.DependencyInjection; using System; using Timeline.Services; @@ -14,12 +12,4 @@ namespace Timeline.Tests.Mock.Services return MockCurrentTime.GetValueOrDefault(DateTime.Now); } } - - public static class TestClockWebApplicationFactoryExtensions - { - public static TestClock GetTestClock(this WebApplicationFactory factory) where T : class - { - return factory.Server.Host.Services.GetRequiredService() as TestClock; - } - } } -- cgit v1.2.3 From c9cc1e18fb36df25ad28778c26e4b2bd88b6a96d Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Sun, 20 Oct 2019 16:24:11 +0800 Subject: ... --- Timeline.Tests/Controllers/TokenControllerTest.cs | 13 +- Timeline.Tests/GlobalSuppressions.cs | 9 ++ .../Mock/Services/MockStringLocalizer.cs | 31 +++++ Timeline.Tests/Timeline.Tests.csproj | 6 +- Timeline/Controllers/TokenController.cs | 34 +++-- .../Controllers/TokenController.en.Designer.cs | 72 ++++++++++ .../Resources/Controllers/TokenController.en.resx | 153 +++++++++++++++++++++ Timeline/Startup.cs | 5 + Timeline/Timeline.csproj | 15 ++ 9 files changed, 318 insertions(+), 20 deletions(-) create mode 100644 Timeline.Tests/GlobalSuppressions.cs create mode 100644 Timeline.Tests/Mock/Services/MockStringLocalizer.cs create mode 100644 Timeline/Resources/Controllers/TokenController.en.Designer.cs create mode 100644 Timeline/Resources/Controllers/TokenController.en.resx (limited to 'Timeline.Tests/Mock/Services') diff --git a/Timeline.Tests/Controllers/TokenControllerTest.cs b/Timeline.Tests/Controllers/TokenControllerTest.cs index 8b1cf071..86a241e5 100644 --- a/Timeline.Tests/Controllers/TokenControllerTest.cs +++ b/Timeline.Tests/Controllers/TokenControllerTest.cs @@ -15,7 +15,7 @@ using static Timeline.ErrorCodes.Http.Token; namespace Timeline.Tests.Controllers { - public class TokenControllerTest + public class TokenControllerTest : IDisposable { private readonly Mock _mockUserService = new Mock(); private readonly TestClock _mockClock = new TestClock(); @@ -24,7 +24,14 @@ namespace Timeline.Tests.Controllers public TokenControllerTest() { - _controller = new TokenController(_mockUserService.Object, NullLogger.Instance, _mockClock); + _controller = new TokenController(_mockUserService.Object, + NullLogger.Instance, _mockClock, + new MockStringLocalizer()); + } + + public void Dispose() + { + _controller.Dispose(); } [Theory] @@ -110,7 +117,5 @@ namespace Timeline.Tests.Controllers .Which.Value.Should().BeAssignableTo() .Which.Code.Should().Be(code); } - - // TODO! Verify unit tests } } diff --git a/Timeline.Tests/GlobalSuppressions.cs b/Timeline.Tests/GlobalSuppressions.cs new file mode 100644 index 00000000..6562efbb --- /dev/null +++ b/Timeline.Tests/GlobalSuppressions.cs @@ -0,0 +1,9 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "This is not a UI application.")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Tests name have underscores.")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1063:Implement IDisposable Correctly", Justification = "Test classes do not need to implement it that way.")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA1816:Dispose methods should call SuppressFinalize", Justification = "Test classes do not need to implement it that way.")] diff --git a/Timeline.Tests/Mock/Services/MockStringLocalizer.cs b/Timeline.Tests/Mock/Services/MockStringLocalizer.cs new file mode 100644 index 00000000..7729d56c --- /dev/null +++ b/Timeline.Tests/Mock/Services/MockStringLocalizer.cs @@ -0,0 +1,31 @@ +using Microsoft.Extensions.Localization; +using System.Collections.Generic; +using System.Globalization; + +namespace Timeline.Tests.Mock.Services +{ + public class MockStringLocalizer : IStringLocalizer + { + private const string mockKey = "MOCK_KEY"; + private const string mockString = "THIS IS A MOCK LOCALIZED STRING."; + + public LocalizedString this[string name] => new LocalizedString(name, mockString); + + public LocalizedString this[string name, params object[] arguments] => new LocalizedString(name, mockString); + + public IEnumerable GetAllStrings(bool includeParentCultures) + { + yield return new LocalizedString(mockKey, mockString); + } + + public IStringLocalizer WithCulture(CultureInfo culture) + { + return this; + } + } + + public class MockStringLocalizer : MockStringLocalizer, IStringLocalizer + { + + } +} diff --git a/Timeline.Tests/Timeline.Tests.csproj b/Timeline.Tests/Timeline.Tests.csproj index 3f88f174..a611dfd3 100644 --- a/Timeline.Tests/Timeline.Tests.csproj +++ b/Timeline.Tests/Timeline.Tests.csproj @@ -15,9 +15,13 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - + all diff --git a/Timeline/Controllers/TokenController.cs b/Timeline/Controllers/TokenController.cs index ce5786ca..eba69319 100644 --- a/Timeline/Controllers/TokenController.cs +++ b/Timeline/Controllers/TokenController.cs @@ -7,6 +7,8 @@ using System.Threading.Tasks; using Timeline.Models.Http; using Timeline.Services; using Timeline.Helpers; +using Microsoft.Extensions.Localization; +using System.Globalization; namespace Timeline { @@ -42,12 +44,14 @@ namespace Timeline.Controllers private readonly IUserService _userService; private readonly ILogger _logger; private readonly IClock _clock; + private readonly IStringLocalizer _localizer; - public TokenController(IUserService userService, ILogger logger, IClock clock) + public TokenController(IUserService userService, ILogger logger, IClock clock, IStringLocalizer localizer) { _userService = userService; _logger = logger; _clock = clock; + _localizer = localizer; } [HttpPost("create")] @@ -56,7 +60,7 @@ namespace Timeline.Controllers { void LogFailure(string reason, Exception? e = null) { - _logger.LogInformation(e, Log.Format("Attemp to login failed.", + _logger.LogInformation(e, Log.Format(_localizer["LogCreateFailure"], ("Reason", reason), ("Username", request.Username), ("Password", request.Password), @@ -72,9 +76,9 @@ namespace Timeline.Controllers var result = await _userService.CreateToken(request.Username, request.Password, expireTime); - _logger.LogInformation(Log.Format("Attemp to login succeeded.", + _logger.LogInformation(Log.Format(_localizer["LogCreateSuccess"], ("Username", request.Username), - ("Expire At", expireTime?.ToString() ?? "default") + ("Expire At", expireTime?.ToString(CultureInfo.CurrentUICulture.DateTimeFormat) ?? "default") )); return Ok(new CreateTokenResponse { @@ -84,15 +88,15 @@ namespace Timeline.Controllers } catch (UserNotExistException e) { - LogFailure("User does not exist.", e); + LogFailure(_localizer["LogUserNotExist"], e); return BadRequest(new CommonResponse(ErrorCodes.Http.Token.Create.BadCredential, - "Bad username or password.")); + _localizer["ErrorBadCredential"])); } catch (BadPasswordException e) { - LogFailure("Password is wrong.", e); + LogFailure(_localizer["LogBadPassword"], e); return BadRequest(new CommonResponse(ErrorCodes.Http.Token.Create.BadCredential, - "Bad username or password.")); + _localizer["ErrorBadCredential"])); } } @@ -102,17 +106,17 @@ namespace Timeline.Controllers { void LogFailure(string reason, Exception? e = null, params (string, object?)[] otherProperties) { - var properties = new (string, object)[2 + otherProperties.Length]; + var properties = new (string, object?)[2 + otherProperties.Length]; properties[0] = ("Reason", reason); properties[1] = ("Token", request.Token); otherProperties.CopyTo(properties, 2); - _logger.LogInformation(e, Log.Format("Token verification failed.", properties)); + _logger.LogInformation(e, Log.Format(_localizer["LogVerifyFailure"], properties)); } try { var result = await _userService.VerifyToken(request.Token); - _logger.LogInformation(Log.Format("Token verification succeeded.", + _logger.LogInformation(Log.Format(_localizer["LogVerifySuccess"], ("Username", result.Username), ("Token", request.Token))); return Ok(new VerifyTokenResponse { @@ -123,27 +127,27 @@ namespace Timeline.Controllers { if (e.ErrorCode == JwtTokenVerifyException.ErrorCodes.Expired) { - const string message = "Token is expired."; + string message = _localizer["ErrorVerifyExpire"]; var innerException = e.InnerException as SecurityTokenExpiredException; LogFailure(message, e, ("Expires", innerException?.Expires), ("Current Time", _clock.GetCurrentTime())); return BadRequest(new CommonResponse(ErrorCodes.Http.Token.Verify.Expired, message)); } else { - const string message = "Token is of bad format."; + string message = _localizer["ErrorVerifyBadFormat"]; LogFailure(message, e); return BadRequest(new CommonResponse(ErrorCodes.Http.Token.Verify.BadFormat, message)); } } catch (UserNotExistException e) { - const string message = "User does not exist. Administrator might have deleted this user."; + string message = _localizer["ErrorVerifyUserNotExist"]; LogFailure(message, e); return BadRequest(new CommonResponse(ErrorCodes.Http.Token.Verify.UserNotExist, message)); } catch (BadTokenVersionException e) { - const string message = "Token has an old version."; + string message = _localizer["ErrorVerifyOldVersion"]; LogFailure(message, e, ("Token Version", e.TokenVersion), ("Required Version", e.RequiredVersion)); return BadRequest(new CommonResponse(ErrorCodes.Http.Token.Verify.OldVersion, message)); } diff --git a/Timeline/Resources/Controllers/TokenController.en.Designer.cs b/Timeline/Resources/Controllers/TokenController.en.Designer.cs new file mode 100644 index 00000000..64326860 --- /dev/null +++ b/Timeline/Resources/Controllers/TokenController.en.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.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()] + public class TokenController { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal TokenController() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public 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.TokenController", typeof(TokenController).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)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to A user succeeded to create a token.. + /// + public static string LogCreateSuccess { + get { + return ResourceManager.GetString("LogCreateSuccess", resourceCulture); + } + } + } +} diff --git a/Timeline/Resources/Controllers/TokenController.en.resx b/Timeline/Resources/Controllers/TokenController.en.resx new file mode 100644 index 00000000..7309ea6a --- /dev/null +++ b/Timeline/Resources/Controllers/TokenController.en.resx @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + 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. + + + A user failed to create a token. + + + A user succeeded to create a token. + + + The user does not exist. + + + A token failed to be verified. + + + A token succeeded to be verified. + + \ No newline at end of file diff --git a/Timeline/Startup.cs b/Timeline/Startup.cs index ddbe0840..72c9bf32 100644 --- a/Timeline/Startup.cs +++ b/Timeline/Startup.cs @@ -50,6 +50,11 @@ namespace Timeline ); }); + services.AddLocalization(options => + { + options.ResourcesPath = "Resources"; + }); + services.AddScoped(); services.AddScoped(); services.AddTransient(); diff --git a/Timeline/Timeline.csproj b/Timeline/Timeline.csproj index c634563a..01207ae2 100644 --- a/Timeline/Timeline.csproj +++ b/Timeline/Timeline.csproj @@ -26,4 +26,19 @@ + + + + True + True + TokenController.en.resx + + + + + + PublicResXFileCodeGenerator + TokenController.en.Designer.cs + + -- cgit v1.2.3 From 5e64e3385ae8eb9b877c032418da9e5086d50a06 Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Mon, 21 Oct 2019 13:41:46 +0800 Subject: ... --- Timeline.Tests/Controllers/TokenControllerTest.cs | 13 +-- Timeline.Tests/Controllers/UserControllerTest.cs | 109 +++++++++++++++++++++ Timeline.Tests/GlobalSuppressions.cs | 5 + .../Helpers/AssertionResponseExtensions.cs | 46 +++------ Timeline.Tests/Helpers/HttpClientExtensions.cs | 13 +++ Timeline.Tests/Helpers/ImageHelper.cs | 24 ++--- Timeline.Tests/IntegratedTests/UserUnitTest.cs | 22 ++--- .../Mock/Services/MockStringLocalizer.cs | 31 ------ .../Mock/Services/TestStringLocalizerFactory.cs | 25 +++++ Timeline/Controllers/TokenController.cs | 4 +- Timeline/Controllers/UserController.cs | 39 ++++---- Timeline/Models/Http/Common.cs | 42 ++++++-- .../Resources/Controllers/UserController.en.resx | 9 ++ Timeline/Resources/Controllers/UserController.resx | 15 +++ .../Resources/Controllers/UserController.zh.resx | 9 ++ 15 files changed, 281 insertions(+), 125 deletions(-) create mode 100644 Timeline.Tests/Controllers/UserControllerTest.cs delete mode 100644 Timeline.Tests/Mock/Services/MockStringLocalizer.cs create mode 100644 Timeline.Tests/Mock/Services/TestStringLocalizerFactory.cs (limited to 'Timeline.Tests/Mock/Services') diff --git a/Timeline.Tests/Controllers/TokenControllerTest.cs b/Timeline.Tests/Controllers/TokenControllerTest.cs index 86a241e5..71520e77 100644 --- a/Timeline.Tests/Controllers/TokenControllerTest.cs +++ b/Timeline.Tests/Controllers/TokenControllerTest.cs @@ -20,13 +20,14 @@ namespace Timeline.Tests.Controllers 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, - new MockStringLocalizer()); + TestStringLocalizerFactory.Create().Create()); } public void Dispose() @@ -53,7 +54,7 @@ namespace Timeline.Tests.Controllers Password = "p", Expire = expire }); - action.Should().BeAssignableTo() + action.Result.Should().BeAssignableTo() .Which.Value.Should().BeEquivalentTo(createResult); } @@ -67,7 +68,7 @@ namespace Timeline.Tests.Controllers Password = "p", Expire = null }); - action.Should().BeAssignableTo() + action.Result.Should().BeAssignableTo() .Which.Value.Should().BeAssignableTo() .Which.Code.Should().Be(Create.BadCredential); } @@ -82,7 +83,7 @@ namespace Timeline.Tests.Controllers Password = "p", Expire = null }); - action.Should().BeAssignableTo() + action.Result.Should().BeAssignableTo() .Which.Value.Should().BeAssignableTo() .Which.Code.Should().Be(Create.BadCredential); } @@ -93,7 +94,7 @@ namespace Timeline.Tests.Controllers const string token = "aaaaaaaaaaaaaa"; _mockUserService.Setup(s => s.VerifyToken(token)).ReturnsAsync(MockUser.User.Info); var action = await _controller.Verify(new VerifyTokenRequest { Token = token }); - action.Should().BeAssignableTo() + action.Result.Should().BeAssignableTo() .Which.Value.Should().BeAssignableTo() .Which.User.Should().BeEquivalentTo(MockUser.User.Info); } @@ -113,7 +114,7 @@ namespace Timeline.Tests.Controllers const string token = "aaaaaaaaaaaaaa"; _mockUserService.Setup(s => s.VerifyToken(token)).ThrowsAsync(e); var action = await _controller.Verify(new VerifyTokenRequest { Token = token }); - action.Should().BeAssignableTo() + 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 new file mode 100644 index 00000000..9fec477f --- /dev/null +++ b/Timeline.Tests/Controllers/UserControllerTest.cs @@ -0,0 +1,109 @@ +using FluentAssertions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using System; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Controllers; +using Timeline.Models; +using Timeline.Models.Http; +using Timeline.Services; +using Timeline.Tests.Helpers; +using Timeline.Tests.Mock.Data; +using Timeline.Tests.Mock.Services; +using Xunit; +using static Timeline.ErrorCodes.Http.User; + +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, + TestStringLocalizerFactory.Create()); + } + + public void Dispose() + { + _controller.Dispose(); + } + + [Fact] + public async Task GetList_Success() + { + var array = MockUser.UserInfoList.ToArray(); + _mockUserService.Setup(s => s.ListUsers()).ReturnsAsync(array); + var action = await _controller.List(); + action.Result.Should().BeAssignableTo() + .Which.Value.Should().BeEquivalentTo(array); + } + + [Fact] + public async Task Get_Success() + { + const string username = "aaa"; + _mockUserService.Setup(s => s.GetUser(username)).ReturnsAsync(MockUser.User.Info); + 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.GetUser(username)).Returns(Task.FromResult(null)); + var action = await _controller.Get(username); + action.Result.Should().BeAssignableTo() + .Which.Value.Should().BeAssignableTo() + .Which.Code.Should().Be(Get.NotExist); + } + + [Theory] + [InlineData(PutResult.Created, true)] + [InlineData(PutResult.Modified, 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 Put_BadUsername() + { + const string username = "aaa"; + const string password = "ppp"; + const bool administrator = true; + _mockUserService.Setup(s => s.PutUser(username, password, administrator)).ThrowsAsync(new UsernameBadFormatException()); + var action = await _controller.Put(new UserPutRequest + { + Password = password, + Administrator = administrator + }, username); + action.Result.Should().BeAssignableTo() + .Which.Value.Should().BeAssignableTo() + .Which.Code.Should().Be(Put.BadUsername); + } + + //TODO! Complete this. + } +} diff --git a/Timeline.Tests/GlobalSuppressions.cs b/Timeline.Tests/GlobalSuppressions.cs index 6562efbb..2191a5c4 100644 --- a/Timeline.Tests/GlobalSuppressions.cs +++ b/Timeline.Tests/GlobalSuppressions.cs @@ -5,5 +5,10 @@ [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "This is not a UI application.")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Tests name have underscores.")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1034:Nested types should not be visible", Justification = "Test classes can be nested.")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "This is redundant.")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1063:Implement IDisposable Correctly", Justification = "Test classes do not need to implement it that way.")] [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.")] + diff --git a/Timeline.Tests/Helpers/AssertionResponseExtensions.cs b/Timeline.Tests/Helpers/AssertionResponseExtensions.cs index 5ce025ee..08f10b2b 100644 --- a/Timeline.Tests/Helpers/AssertionResponseExtensions.cs +++ b/Timeline.Tests/Helpers/AssertionResponseExtensions.cs @@ -82,22 +82,14 @@ namespace Timeline.Tests.Helpers { body = Subject.Content.ReadAsStringAsync().Result; } - catch (Exception e) + catch (AggregateException e) { - a.FailWith("Expected response body of {context:HttpResponseMessage} to be json string{reason}, but failed to read it or it was not a string. Exception is {0}.", e); + a.FailWith("Expected response body of {context:HttpResponseMessage} to be json string{reason}, but failed to read it or it was not a string. Exception is {0}.", e.InnerExceptions); return new AndWhichConstraint(Subject, null); } - try - { - var result = JsonConvert.DeserializeObject(body); - return new AndWhichConstraint(Subject, result); - } - catch (Exception e) - { - a.FailWith("Expected response body of {context:HttpResponseMessage} to be able to convert to {0} instance{reason}, but failed. Exception is {1}.", typeof(T).FullName, e); - return new AndWhichConstraint(Subject, null); - } + var result = JsonConvert.DeserializeObject(body); + return new AndWhichConstraint(Subject, result); } } @@ -118,28 +110,22 @@ namespace Timeline.Tests.Helpers return assertions.HaveJsonBody>(because, becauseArgs); } - public static void BePutCreate(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs) - { - assertions.HaveStatusCode(201, because, becauseArgs) - .And.Should().HaveCommonDataBody(because, becauseArgs).Which.Should().BeEquivalentTo(CommonPutResponse.Create(), because, becauseArgs); - } - - public static void BePutModify(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs) - { - assertions.HaveStatusCode(200, because, becauseArgs) - .And.Should().HaveCommonDataBody(because, becauseArgs).Which.Should().BeEquivalentTo(CommonPutResponse.Modify(), because, becauseArgs); - } - - public static void BeDeleteDelete(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs) + public static void BePut(this HttpResponseMessageAssertions assertions, bool create, string because = "", params object[] becauseArgs) { - assertions.HaveStatusCode(200, because, becauseArgs) - .And.Should().HaveCommonDataBody(because, becauseArgs).Which.Should().BeEquivalentTo(CommonDeleteResponse.Delete(), because, becauseArgs); + var body = assertions.HaveStatusCode(create ? 201 : 200, because, becauseArgs) + .And.Should().HaveJsonBody(because, becauseArgs) + .Which; + body.Code.Should().Be(0); + body.Data.Create.Should().Be(create); } - public static void BeDeleteNotExist(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs) + public static void BeDelete(this HttpResponseMessageAssertions assertions, bool delete, string because = "", params object[] becauseArgs) { - assertions.HaveStatusCode(200, because, becauseArgs) - .And.Should().HaveCommonDataBody(because, becauseArgs).Which.Should().BeEquivalentTo(CommonDeleteResponse.NotExist(), because, becauseArgs); + var body = assertions.HaveStatusCode(200, because, becauseArgs) + .And.Should().HaveJsonBody(because, becauseArgs) + .Which; + body.Code.Should().Be(0); + body.Data.Delete.Should().Be(delete); } public static void BeInvalidModel(this HttpResponseMessageAssertions assertions, string message = null) diff --git a/Timeline.Tests/Helpers/HttpClientExtensions.cs b/Timeline.Tests/Helpers/HttpClientExtensions.cs index e3beea1d..38641f90 100644 --- a/Timeline.Tests/Helpers/HttpClientExtensions.cs +++ b/Timeline.Tests/Helpers/HttpClientExtensions.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using System; using System.Net.Http; using System.Net.Http.Headers; using System.Net.Mime; @@ -10,12 +11,24 @@ namespace Timeline.Tests.Helpers public static class HttpClientExtensions { public static Task PatchAsJsonAsync(this HttpClient client, string url, T body) + { + return client.PatchAsJsonAsync(new Uri(url, UriKind.RelativeOrAbsolute), body); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope")] + public static Task PatchAsJsonAsync(this HttpClient client, Uri url, T body) { return client.PatchAsync(url, new StringContent( JsonConvert.SerializeObject(body), Encoding.UTF8, MediaTypeNames.Application.Json)); } public static Task PutByteArrayAsync(this HttpClient client, string url, byte[] body, string mimeType) + { + return client.PutByteArrayAsync(new Uri(url, UriKind.RelativeOrAbsolute), body, mimeType); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope")] + public static Task PutByteArrayAsync(this HttpClient client, Uri url, byte[] body, string mimeType) { var content = new ByteArrayContent(body); content.Headers.ContentLength = body.Length; diff --git a/Timeline.Tests/Helpers/ImageHelper.cs b/Timeline.Tests/Helpers/ImageHelper.cs index 2a2f3870..9bed0917 100644 --- a/Timeline.Tests/Helpers/ImageHelper.cs +++ b/Timeline.Tests/Helpers/ImageHelper.cs @@ -9,26 +9,18 @@ namespace Timeline.Tests.Helpers { public static byte[] CreatePngWithSize(int width, int height) { - using (var image = new Image(width, height)) - { - using (var stream = new MemoryStream()) - { - image.SaveAsPng(stream); - return stream.ToArray(); - } - } + using var image = new Image(width, height); + using var stream = new MemoryStream(); + image.SaveAsPng(stream); + return stream.ToArray(); } public static byte[] CreateImageWithSize(int width, int height, IImageFormat format) { - using (var image = new Image(width, height)) - { - using (var stream = new MemoryStream()) - { - image.Save(stream, format); - return stream.ToArray(); - } - } + using var image = new Image(width, height); + using var stream = new MemoryStream(); + image.Save(stream, format); + return stream.ToArray(); } } } diff --git a/Timeline.Tests/IntegratedTests/UserUnitTest.cs b/Timeline.Tests/IntegratedTests/UserUnitTest.cs index b2aab24c..47a8699c 100644 --- a/Timeline.Tests/IntegratedTests/UserUnitTest.cs +++ b/Timeline.Tests/IntegratedTests/UserUnitTest.cs @@ -4,13 +4,13 @@ using System; using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; -using Timeline.Controllers; using Timeline.Models; using Timeline.Models.Http; using Timeline.Tests.Helpers; using Timeline.Tests.Helpers.Authentication; using Timeline.Tests.Mock.Data; using Xunit; +using static Timeline.ErrorCodes.Http.User; namespace Timeline.Tests.IntegratedTests { @@ -57,7 +57,7 @@ namespace Timeline.Tests.IntegratedTests var res = await client.GetAsync("users/usernotexist"); res.Should().HaveStatusCode(404) .And.Should().HaveCommonBody() - .Which.Code.Should().Be(UserController.ErrorCodes.Get_NotExist); + .Which.Code.Should().Be(Get.NotExist); } public static IEnumerable Put_InvalidModel_Data() @@ -88,7 +88,7 @@ namespace Timeline.Tests.IntegratedTests }); res.Should().HaveStatusCode(400) .And.Should().HaveCommonBody() - .Which.Code.Should().Be(UserController.ErrorCodes.Put_BadUsername); + .Which.Code.Should().Be(Put.BadUsername); } private async Task CheckAdministrator(HttpClient client, string username, bool administrator) @@ -108,7 +108,7 @@ namespace Timeline.Tests.IntegratedTests Password = "password", Administrator = false }); - res.Should().BePutModify(); + res.Should().BePut(false); await CheckAdministrator(client, MockUser.User.Username, false); } @@ -124,7 +124,7 @@ namespace Timeline.Tests.IntegratedTests Password = "password", Administrator = false }); - res.Should().BePutCreate(); + res.Should().BePut(true); await CheckAdministrator(client, username, false); } @@ -135,7 +135,7 @@ namespace Timeline.Tests.IntegratedTests var res = await client.PatchAsJsonAsync("users/usernotexist", new UserPatchRequest { }); res.Should().HaveStatusCode(404) .And.Should().HaveCommonBody() - .Which.Code.Should().Be(UserController.ErrorCodes.Patch_NotExist); + .Which.Code.Should().Be(Patch.NotExist); } [Fact] @@ -156,7 +156,7 @@ namespace Timeline.Tests.IntegratedTests using var client = await _factory.CreateClientAsAdmin(); var url = "users/" + MockUser.User.Username; var res = await client.DeleteAsync(url); - res.Should().BeDeleteDelete(); + res.Should().BeDelete(true); var res2 = await client.GetAsync(url); res2.Should().HaveStatusCode(404); @@ -167,7 +167,7 @@ namespace Timeline.Tests.IntegratedTests { using var client = await _factory.CreateClientAsAdmin(); var res = await client.DeleteAsync("users/usernotexist"); - res.Should().BeDeleteNotExist(); + res.Should().BeDelete(false); } @@ -214,7 +214,7 @@ namespace Timeline.Tests.IntegratedTests new ChangeUsernameRequest { OldUsername = "usernotexist", NewUsername = "newUsername" }); res.Should().HaveStatusCode(400) .And.Should().HaveCommonBody() - .Which.Code.Should().Be(UserController.ErrorCodes.ChangeUsername_NotExist); + .Which.Code.Should().Be(Op.ChangeUsername.NotExist); } [Fact] @@ -225,7 +225,7 @@ namespace Timeline.Tests.IntegratedTests new ChangeUsernameRequest { OldUsername = MockUser.User.Username, NewUsername = MockUser.Admin.Username }); res.Should().HaveStatusCode(400) .And.Should().HaveCommonBody() - .Which.Code.Should().Be(UserController.ErrorCodes.ChangeUsername_AlreadyExist); + .Which.Code.Should().Be(Op.ChangeUsername.AlreadyExist); } [Fact] @@ -282,7 +282,7 @@ namespace Timeline.Tests.IntegratedTests var res = await client.PostAsJsonAsync(url, new ChangePasswordRequest { OldPassword = "???", NewPassword = "???" }); res.Should().HaveStatusCode(400) .And.Should().HaveCommonBody() - .Which.Code.Should().Be(UserController.ErrorCodes.ChangePassword_BadOldPassword); + .Which.Code.Should().Be(Op.ChangePassword.BadOldPassword); } [Fact] diff --git a/Timeline.Tests/Mock/Services/MockStringLocalizer.cs b/Timeline.Tests/Mock/Services/MockStringLocalizer.cs deleted file mode 100644 index 7729d56c..00000000 --- a/Timeline.Tests/Mock/Services/MockStringLocalizer.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Microsoft.Extensions.Localization; -using System.Collections.Generic; -using System.Globalization; - -namespace Timeline.Tests.Mock.Services -{ - public class MockStringLocalizer : IStringLocalizer - { - private const string mockKey = "MOCK_KEY"; - private const string mockString = "THIS IS A MOCK LOCALIZED STRING."; - - public LocalizedString this[string name] => new LocalizedString(name, mockString); - - public LocalizedString this[string name, params object[] arguments] => new LocalizedString(name, mockString); - - public IEnumerable GetAllStrings(bool includeParentCultures) - { - yield return new LocalizedString(mockKey, mockString); - } - - public IStringLocalizer WithCulture(CultureInfo culture) - { - return this; - } - } - - public class MockStringLocalizer : MockStringLocalizer, IStringLocalizer - { - - } -} diff --git a/Timeline.Tests/Mock/Services/TestStringLocalizerFactory.cs b/Timeline.Tests/Mock/Services/TestStringLocalizerFactory.cs new file mode 100644 index 00000000..4084dd8f --- /dev/null +++ b/Timeline.Tests/Mock/Services/TestStringLocalizerFactory.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; + +namespace Timeline.Tests.Mock.Services +{ + internal static class TestStringLocalizerFactory + { + internal static IStringLocalizerFactory Create() + { + return new ResourceManagerStringLocalizerFactory( + Options.Create(new LocalizationOptions() + { + ResourcesPath = "Resource" + }), + NullLoggerFactory.Instance + ); + } + + internal static IStringLocalizer Create(this IStringLocalizerFactory factory) + { + return new StringLocalizer(factory); + } + } +} diff --git a/Timeline/Controllers/TokenController.cs b/Timeline/Controllers/TokenController.cs index d708127a..cf32a562 100644 --- a/Timeline/Controllers/TokenController.cs +++ b/Timeline/Controllers/TokenController.cs @@ -56,7 +56,7 @@ namespace Timeline.Controllers [HttpPost("create")] [AllowAnonymous] - public async Task Create([FromBody] CreateTokenRequest request) + public async Task> Create([FromBody] CreateTokenRequest request) { void LogFailure(string reason, Exception? e = null) { @@ -102,7 +102,7 @@ namespace Timeline.Controllers [HttpPost("verify")] [AllowAnonymous] - public async Task Verify([FromBody] VerifyTokenRequest request) + public async Task> Verify([FromBody] VerifyTokenRequest request) { void LogFailure(string reason, Exception? e = null, params (string, object?)[] otherProperties) { diff --git a/Timeline/Controllers/UserController.cs b/Timeline/Controllers/UserController.cs index b01d06fb..6afc890c 100644 --- a/Timeline/Controllers/UserController.cs +++ b/Timeline/Controllers/UserController.cs @@ -77,7 +77,7 @@ namespace Timeline.Controllers } [HttpGet("users/{username}"), AdminAuthorize] - public async Task Get([FromRoute] string username) + public async Task> Get([FromRoute] string username) { var user = await _userService.GetUser(username); if (user == null) @@ -89,7 +89,7 @@ namespace Timeline.Controllers } [HttpPut("users/{username}"), AdminAuthorize] - public async Task Put([FromBody] UserPutRequest request, [FromRoute] string username) + public async Task> Put([FromBody] UserPutRequest request, [FromRoute] string username) { try { @@ -114,7 +114,7 @@ namespace Timeline.Controllers } [HttpPatch("users/{username}"), AdminAuthorize] - public async Task Patch([FromBody] UserPatchRequest request, [FromRoute] string username) + public async Task Patch([FromBody] UserPatchRequest request, [FromRoute] string username) { try { @@ -129,7 +129,7 @@ namespace Timeline.Controllers } [HttpDelete("users/{username}"), AdminAuthorize] - public async Task Delete([FromRoute] string username) + public async Task> Delete([FromRoute] string username) { try { @@ -145,44 +145,45 @@ namespace Timeline.Controllers } [HttpPost("userop/changeusername"), AdminAuthorize] - public async Task ChangeUsername([FromBody] ChangeUsernameRequest request) + public async Task ChangeUsername([FromBody] ChangeUsernameRequest request) { try { await _userService.ChangeUsername(request.OldUsername, request.NewUsername); - _logger.LogInformation(FormatLogMessage("A user changed username.", - Pair("Old Username", request.OldUsername), Pair("New Username", request.NewUsername))); + _logger.LogInformation(Log.Format(_localizer["LogChangeUsernameSuccess"], + ("Old Username", request.OldUsername), ("New Username", request.NewUsername))); return Ok(); } catch (UserNotExistException e) { - _logger.LogInformation(e, FormatLogMessage("Attempt to change a non-existent user's username failed.", - Pair("Old Username", request.OldUsername), Pair("New Username", request.NewUsername))); - return BadRequest(new CommonResponse(ErrorCodes.ChangeUsername_NotExist, $"The user {request.OldUsername} does not exist.")); + _logger.LogInformation(e, Log.Format(_localizer["LogChangeUsernameNotExist"], + ("Old Username", request.OldUsername), ("New Username", request.NewUsername))); + return BadRequest(new CommonResponse(ErrorCodes.Http.User.Op.ChangeUsername.NotExist, _localizer["ErrorChangeUsernameNotExist", request.OldUsername])); } catch (UserAlreadyExistException e) { - _logger.LogInformation(e, FormatLogMessage("Attempt to change a user's username to a existent one failed.", - Pair("Old Username", request.OldUsername), Pair("New Username", request.NewUsername))); - return BadRequest(new CommonResponse(ErrorCodes.ChangeUsername_AlreadyExist, $"The user {request.NewUsername} already exists.")); + _logger.LogInformation(e, Log.Format(_localizer["LogChangeUsernameAlreadyExist"], + ("Old Username", request.OldUsername), ("New Username", request.NewUsername))); + return BadRequest(new CommonResponse(ErrorCodes.Http.User.Op.ChangeUsername.AlreadyExist, _localizer["ErrorChangeUsernameAlreadyExist"])); } // there is no need to catch bad format exception because it is already checked in model validation. } [HttpPost("userop/changepassword"), Authorize] - public async Task ChangePassword([FromBody] ChangePasswordRequest request) + public async Task ChangePassword([FromBody] ChangePasswordRequest request) { try { - await _userService.ChangePassword(User.Identity.Name, request.OldPassword, request.NewPassword); - _logger.LogInformation(FormatLogMessage("A user changed password.", Pair("Username", User.Identity.Name))); + await _userService.ChangePassword(User.Identity.Name!, request.OldPassword, request.NewPassword); + _logger.LogInformation(Log.Format(_localizer["LogChangePasswordSuccess"], ("Username", User.Identity.Name))); return Ok(); } catch (BadPasswordException e) { - _logger.LogInformation(e, FormatLogMessage("A user attempt to change password but old password is wrong.", - Pair("Username", User.Identity.Name), Pair("Old Password", request.OldPassword))); - return BadRequest(new CommonResponse(ErrorCodes.ChangePassword_BadOldPassword, "Old password is wrong.")); + _logger.LogInformation(e, Log.Format(_localizer["LogChangePasswordBadPassword"], + ("Username", User.Identity.Name), ("Old Password", request.OldPassword))); + return BadRequest(new CommonResponse(ErrorCodes.Http.User.Op.ChangePassword.BadOldPassword, + _localizer["ErrorChangePasswordBadPassword"])); } // User can't be non-existent or the token is bad. } diff --git a/Timeline/Models/Http/Common.cs b/Timeline/Models/Http/Common.cs index 2735e43c..130439d3 100644 --- a/Timeline/Models/Http/Common.cs +++ b/Timeline/Models/Http/Common.cs @@ -61,7 +61,7 @@ namespace Timeline.Models.Http public T Data { get; set; } = default!; } - public static class CommonPutResponse + public class CommonPutResponse : CommonDataResponse { public class ResponseData { @@ -73,21 +73,32 @@ namespace Timeline.Models.Http public bool Create { get; set; } } - internal static CommonDataResponse Create(IStringLocalizerFactory localizerFactory) + public CommonPutResponse() + { + + } + + public CommonPutResponse(int code, string message, bool create) + : base(code, message, new ResponseData(create)) + { + + } + + internal static CommonPutResponse Create(IStringLocalizerFactory localizerFactory) { var localizer = localizerFactory.Create("Http.Common"); - return new CommonDataResponse(0, localizer["ResponsePutCreate"], new ResponseData(true)); + return new CommonPutResponse(0, localizer["ResponsePutCreate"], true); } - internal static CommonDataResponse Modify(IStringLocalizerFactory localizerFactory) + internal static CommonPutResponse Modify(IStringLocalizerFactory localizerFactory) { var localizer = localizerFactory.Create("Http.Common"); - return new CommonDataResponse(0, localizer["ResponsePutModify"], new ResponseData(false)); + return new CommonPutResponse(0, localizer["ResponsePutModify"], false); } } - public static class CommonDeleteResponse + public class CommonDeleteResponse : CommonDataResponse { public class ResponseData { @@ -99,16 +110,27 @@ namespace Timeline.Models.Http public bool Delete { get; set; } } - internal static CommonDataResponse Delete(IStringLocalizerFactory localizerFactory) + public CommonDeleteResponse() + { + + } + + public CommonDeleteResponse(int code, string message, bool delete) + : base(code, message, new ResponseData(delete)) + { + + } + + internal static CommonDeleteResponse Delete(IStringLocalizerFactory localizerFactory) { var localizer = localizerFactory.Create("Http.Common"); - return new CommonDataResponse(0, localizer["ResponseDeleteDelete"], new ResponseData(true)); + return new CommonDeleteResponse(0, localizer["ResponseDeleteDelete"], true); } - internal static CommonDataResponse NotExist(IStringLocalizerFactory localizerFactory) + internal static CommonDeleteResponse NotExist(IStringLocalizerFactory localizerFactory) { var localizer = localizerFactory.Create("Http.Common"); - return new CommonDataResponse(0, localizer["ResponseDeleteNotExist"], new ResponseData(false)); + return new CommonDeleteResponse(0, localizer["ResponseDeleteNotExist"], false); } } } diff --git a/Timeline/Resources/Controllers/UserController.en.resx b/Timeline/Resources/Controllers/UserController.en.resx index f0fb372a..0bd1dfe3 100644 --- a/Timeline/Resources/Controllers/UserController.en.resx +++ b/Timeline/Resources/Controllers/UserController.en.resx @@ -117,6 +117,15 @@ 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. diff --git a/Timeline/Resources/Controllers/UserController.resx b/Timeline/Resources/Controllers/UserController.resx index 901f8aab..d720d1c1 100644 --- a/Timeline/Resources/Controllers/UserController.resx +++ b/Timeline/Resources/Controllers/UserController.resx @@ -117,6 +117,21 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + 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. diff --git a/Timeline/Resources/Controllers/UserController.zh.resx b/Timeline/Resources/Controllers/UserController.zh.resx index 519f08f6..3556083e 100644 --- a/Timeline/Resources/Controllers/UserController.zh.resx +++ b/Timeline/Resources/Controllers/UserController.zh.resx @@ -117,6 +117,15 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + 旧密码错误。 + + + 新用户名{0}已经存在。 + + + 旧用户名{0}不存在。 + 用户不存在。 -- cgit v1.2.3