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 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