From fa2a3282c51d831b25f374803301e75eac15d11c Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Thu, 17 Oct 2019 20:46:57 +0800 Subject: ... --- Timeline/Models/Http/Common.cs | 74 +++++++++++++++++++++++++++--------------- 1 file changed, 47 insertions(+), 27 deletions(-) (limited to 'Timeline/Models/Http/Common.cs') diff --git a/Timeline/Models/Http/Common.cs b/Timeline/Models/Http/Common.cs index a72f187c..af185e85 100644 --- a/Timeline/Models/Http/Common.cs +++ b/Timeline/Models/Http/Common.cs @@ -2,43 +2,29 @@ namespace Timeline.Models.Http { public class CommonResponse { - public static class ErrorCodes - { - /// - /// Used when the model is invaid. - /// For example a required field is null. - /// - public const int InvalidModel = -100; - - public const int Header_Missing_ContentType = -111; - public const int Header_Missing_ContentLength = -112; - public const int Header_Zero_ContentLength = -113; - public const int Header_BadFormat_IfNonMatch = -114; - } - public static CommonResponse InvalidModel(string message) { - return new CommonResponse(ErrorCodes.InvalidModel, message); + return new CommonResponse(ErrorCodes.Http.Common.InvalidModel, message); } public static CommonResponse MissingContentType() { - return new CommonResponse(ErrorCodes.Header_Missing_ContentType, "Header Content-Type is required."); + return new CommonResponse(ErrorCodes.Http.Common.Header.Missing_ContentType, "Header Content-Type is required."); } public static CommonResponse MissingContentLength() { - return new CommonResponse(ErrorCodes.Header_Missing_ContentLength, "Header Content-Length is missing or of bad format."); + return new CommonResponse(ErrorCodes.Http.Common.Header.Missing_ContentLength, "Header Content-Length is missing or of bad format."); } public static CommonResponse ZeroContentLength() { - return new CommonResponse(ErrorCodes.Header_Zero_ContentLength, "Header Content-Length must not be 0."); + return new CommonResponse(ErrorCodes.Http.Common.Header.Zero_ContentLength, "Header Content-Length must not be 0."); } public static CommonResponse BadIfNonMatch() { - return new CommonResponse(ErrorCodes.Header_BadFormat_IfNonMatch, "Header If-Non-Match is of bad format."); + return new CommonResponse(ErrorCodes.Http.Common.Header.BadFormat_IfNonMatch, "Header If-Non-Match is of bad format."); } public CommonResponse() @@ -56,21 +42,55 @@ namespace Timeline.Models.Http public string Message { get; set; } } + public class CommonDataResponse : CommonResponse + { + public CommonDataResponse() + { + + } + + public CommonDataResponse(int code, string message, T data) + : base(code, message) + { + Data = data; + } + + public T Data { get; set; } + } + public static class CommonPutResponse { - public const int CreatedCode = 0; - public const int ModifiedCode = 1; + public class ResponseData + { + public ResponseData(bool create) + { + Create = create; + } - public static CommonResponse Created { get; } = new CommonResponse(CreatedCode, "A new item is created."); - public static CommonResponse Modified { get; } = new CommonResponse(ModifiedCode, "An existent item is modified."); + public bool Create { get; set; } + } + + public static CommonDataResponse Create() => + new CommonDataResponse(0, "A new item is created.", new ResponseData(true)); + public static CommonDataResponse Modify() => + new CommonDataResponse(0, "An existent item is modified.", new ResponseData(false)); } public static class CommonDeleteResponse { - public const int DeletedCode = 0; - public const int NotExistsCode = 1; + public class ResponseData + { + public ResponseData(bool delete) + { + Delete = delete; + } + + public bool Delete { get; set; } + } - public static CommonResponse Deleted { get; } = new CommonResponse(DeletedCode, "An existent item is deleted."); - public static CommonResponse NotExists { get; } = new CommonResponse(NotExistsCode, "The item does not exist."); + public static CommonDataResponse Delete() => + new CommonDataResponse(0, "An existent item is deleted.", new ResponseData(true)); + public static CommonDataResponse NotExist() => + new CommonDataResponse(0, "The item does not exist.", new ResponseData(false)); } } -- cgit v1.2.3 From a268999b8c975588c01b345829edfc0099af6f93 Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Sat, 19 Oct 2019 17:33:38 +0800 Subject: ... --- Timeline.Tests/Controllers/TokenControllerTest.cs | 11 + .../Helpers/AssertionResponseExtensions.cs | 95 +++---- .../Authentication/AuthenticationExtensions.cs | 2 +- Timeline.Tests/Helpers/HttpClientExtensions.cs | 4 +- Timeline.Tests/Helpers/InvalidModelTestHelpers.cs | 22 -- Timeline.Tests/IntegratedTests/TokenUnitTest.cs | 183 +++++++------ Timeline.Tests/IntegratedTests/UserAvatarTests.cs | 46 ++-- Timeline.Tests/IntegratedTests/UserDetailTest.cs | 28 +- Timeline.Tests/IntegratedTests/UserUnitTest.cs | 288 ++++++++++----------- Timeline/GlobalSuppressions.cs | 9 + Timeline/Models/Http/Common.cs | 2 +- Timeline/Program.cs | 2 +- Timeline/Services/UserService.cs | 61 ++++- Timeline/Startup.cs | 5 +- Timeline/Timeline.csproj | 7 +- 15 files changed, 392 insertions(+), 373 deletions(-) delete mode 100644 Timeline.Tests/Helpers/InvalidModelTestHelpers.cs create mode 100644 Timeline/GlobalSuppressions.cs (limited to 'Timeline/Models/Http/Common.cs') diff --git a/Timeline.Tests/Controllers/TokenControllerTest.cs b/Timeline.Tests/Controllers/TokenControllerTest.cs index fff7c020..60ba75dc 100644 --- a/Timeline.Tests/Controllers/TokenControllerTest.cs +++ b/Timeline.Tests/Controllers/TokenControllerTest.cs @@ -79,6 +79,17 @@ namespace Timeline.Tests.Controllers .Which.Code.Should().Be(Create.BadCredential); } + [Fact] + public async Task Verify_Ok() + { + 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() + .Which.Value.Should().BeAssignableTo() + .Which.User.Should().BeEquivalentTo(MockUser.User.Info); + } + // TODO! Verify unit tests } } diff --git a/Timeline.Tests/Helpers/AssertionResponseExtensions.cs b/Timeline.Tests/Helpers/AssertionResponseExtensions.cs index c7ebdb7a..5ce025ee 100644 --- a/Timeline.Tests/Helpers/AssertionResponseExtensions.cs +++ b/Timeline.Tests/Helpers/AssertionResponseExtensions.cs @@ -6,6 +6,7 @@ using Newtonsoft.Json; using System; using System.Net; using System.Net.Http; +using System.Text; using Timeline.Models.Http; namespace Timeline.Tests.Helpers @@ -23,8 +24,25 @@ namespace Timeline.Tests.Helpers string padding = new string('\t', context.Depth); var res = (HttpResponseMessage)value; - var body = res.Content.ReadAsStringAsync().Result; - return $"{newline}{padding} Status Code: {res.StatusCode} ; Body: {body.Substring(0, Math.Min(body.Length, 20))} ;"; + + var builder = new StringBuilder(); + builder.Append($"{newline}{padding} Status Code: {res.StatusCode} ; Body: "); + + try + { + var body = res.Content.ReadAsStringAsync().Result; + if (body.Length > 40) + { + body = body[0..40] + " ..."; + } + builder.Append(body); + } + catch (AggregateException) + { + builder.Append("NOT A STRING."); + } + + return builder.ToString(); } } @@ -43,15 +61,20 @@ namespace Timeline.Tests.Helpers protected override string Identifier => "HttpResponseMessage"; + public AndConstraint HaveStatusCode(int expected, string because = "", params object[] becauseArgs) + { + return HaveStatusCode((HttpStatusCode)expected, because, becauseArgs); + } + public AndConstraint HaveStatusCode(HttpStatusCode expected, string because = "", params object[] becauseArgs) { Execute.Assertion.BecauseOf(because, becauseArgs) .ForCondition(Subject.StatusCode == expected) - .FailWith("Expected status code of {context:HttpResponseMessage} to be {0}{reason}, but found {1}.\nResponse is {2}.", expected, Subject.StatusCode, Subject); + .FailWith("Expected status code of {context:HttpResponseMessage} to be {0}{reason}, but found {1}.", expected, Subject.StatusCode); return new AndConstraint(Subject); } - public AndWhichConstraint HaveBodyAsJson(string because = "", params object[] becauseArgs) + public AndWhichConstraint HaveJsonBody(string because = "", params object[] becauseArgs) { var a = Execute.Assertion.BecauseOf(because, becauseArgs); string body; @@ -61,7 +84,7 @@ namespace Timeline.Tests.Helpers } catch (Exception e) { - a.FailWith("Failed to read response body of {context:HttpResponseMessage}{reason}.\nException 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); return new AndWhichConstraint(Subject, null); } @@ -72,7 +95,7 @@ namespace Timeline.Tests.Helpers } catch (Exception e) { - a.FailWith("Failed to convert response body of {context:HttpResponseMessage} to {0}{reason}.\nResponse is {1}.\nException is {2}.", typeof(T).FullName, Subject, 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); } } @@ -85,64 +108,48 @@ namespace Timeline.Tests.Helpers return new HttpResponseMessageAssertions(instance); } - public static AndConstraint HaveStatusCodeOk(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs) - { - return assertions.HaveStatusCode(HttpStatusCode.OK, because, becauseArgs); - } - - public static AndConstraint HaveStatusCodeCreated(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs) - { - return assertions.HaveStatusCode(HttpStatusCode.Created, because, becauseArgs); - } - - public static AndConstraint HaveStatusCodeBadRequest(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs) - { - return assertions.HaveStatusCode(HttpStatusCode.BadRequest, because, becauseArgs); - } - - public static AndConstraint HaveStatusCodeNotFound(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs) - { - return assertions.HaveStatusCode(HttpStatusCode.NotFound, because, becauseArgs); - } - - public static AndWhichConstraint HaveBodyAsCommonResponse(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs) - { - return assertions.HaveBodyAsJson(because, becauseArgs); - } - - public static AndWhichConstraint> HaveBodyAsCommonDataResponse(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs) - { - return assertions.HaveBodyAsJson>(because, becauseArgs); - } - - public static void HaveBodyAsCommonResponseWithCode(this HttpResponseMessageAssertions assertions, int expected, string because = "", params object[] becauseArgs) + public static AndWhichConstraint HaveCommonBody(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs) { - assertions.HaveBodyAsCommonResponse(because, becauseArgs).Which.Code.Should().Be(expected, because, becauseArgs); + return assertions.HaveJsonBody(because, becauseArgs); } - public static void HaveBodyAsCommonDataResponseWithCode(this HttpResponseMessageAssertions assertions, int expected, string because = "", params object[] becauseArgs) + public static AndWhichConstraint> HaveCommonDataBody(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs) { - assertions.HaveBodyAsCommonDataResponse(because, becauseArgs).Which.Code.Should().Be(expected, because, becauseArgs); + return assertions.HaveJsonBody>(because, becauseArgs); } public static void BePutCreate(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs) { - assertions.HaveStatusCodeCreated(because, becauseArgs).And.Should().HaveBodyAsCommonDataResponse(because, becauseArgs).Which.Should().BeEquivalentTo(CommonPutResponse.Create(), because, 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.HaveStatusCodeOk(because, becauseArgs).And.Should().HaveBodyAsCommonDataResponse(because, becauseArgs).Which.Should().BeEquivalentTo(CommonPutResponse.Modify(), because, 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) { - assertions.HaveStatusCodeOk(because, becauseArgs).And.Should().HaveBodyAsCommonDataResponse(because, becauseArgs).Which.Should().BeEquivalentTo(CommonDeleteResponse.Delete(), because, becauseArgs); + assertions.HaveStatusCode(200, because, becauseArgs) + .And.Should().HaveCommonDataBody(because, becauseArgs).Which.Should().BeEquivalentTo(CommonDeleteResponse.Delete(), because, becauseArgs); } public static void BeDeleteNotExist(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs) { - assertions.HaveStatusCodeOk(because, becauseArgs).And.Should().HaveBodyAsCommonDataResponse(because, becauseArgs).Which.Should().BeEquivalentTo(CommonDeleteResponse.NotExist(), because, becauseArgs); + assertions.HaveStatusCode(200, because, becauseArgs) + .And.Should().HaveCommonDataBody(because, becauseArgs).Which.Should().BeEquivalentTo(CommonDeleteResponse.NotExist(), because, becauseArgs); + } + + 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.Should().HaveCommonBody("Invalid Model Error must have CommonResponse body{0}", message) + .Which.Code.Should().Be(ErrorCodes.Http.Common.InvalidModel, + "Invalid Model Error must have code {0} in body{1}", + ErrorCodes.Http.Common.InvalidModel, message); } } } diff --git a/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs b/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs index d068a08a..34d7e460 100644 --- a/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs +++ b/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs @@ -14,7 +14,7 @@ namespace Timeline.Tests.Helpers.Authentication public static async Task CreateUserTokenAsync(this HttpClient client, string username, string password, int? expireOffset = null) { var response = await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest { Username = username, Password = password, Expire = expireOffset }); - response.Should().HaveStatusCodeOk(); + response.Should().HaveStatusCode(200); var result = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); return result; } diff --git a/Timeline.Tests/Helpers/HttpClientExtensions.cs b/Timeline.Tests/Helpers/HttpClientExtensions.cs index b9204fcc..e3beea1d 100644 --- a/Timeline.Tests/Helpers/HttpClientExtensions.cs +++ b/Timeline.Tests/Helpers/HttpClientExtensions.cs @@ -1,6 +1,7 @@ using Newtonsoft.Json; using System.Net.Http; using System.Net.Http.Headers; +using System.Net.Mime; using System.Text; using System.Threading.Tasks; @@ -10,7 +11,8 @@ namespace Timeline.Tests.Helpers { public static Task PatchAsJsonAsync(this HttpClient client, string url, T body) { - return client.PatchAsync(url, new StringContent(JsonConvert.SerializeObject(body), Encoding.UTF8, "application/json")); + 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) diff --git a/Timeline.Tests/Helpers/InvalidModelTestHelpers.cs b/Timeline.Tests/Helpers/InvalidModelTestHelpers.cs deleted file mode 100644 index 4a445ca4..00000000 --- a/Timeline.Tests/Helpers/InvalidModelTestHelpers.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Net.Http; -using System.Threading.Tasks; - -namespace Timeline.Tests.Helpers -{ - public static class InvalidModelTestHelpers - { - public static async Task TestPostInvalidModel(HttpClient client, string url, T body) - { - var response = await client.PostAsJsonAsync(url, body); - response.Should().HaveStatusCodeBadRequest() - .And.Should().HaveBodyAsCommonResponseWithCode(ErrorCodes.Http.Common.InvalidModel); - } - - public static async Task TestPutInvalidModel(HttpClient client, string url, T body) - { - var response = await client.PutAsJsonAsync(url, body); - response.Should().HaveStatusCodeBadRequest() - .And.Should().HaveBodyAsCommonResponseWithCode(ErrorCodes.Http.Common.InvalidModel); - } - } -} diff --git a/Timeline.Tests/IntegratedTests/TokenUnitTest.cs b/Timeline.Tests/IntegratedTests/TokenUnitTest.cs index 05e2b3e5..6a60a1a3 100644 --- a/Timeline.Tests/IntegratedTests/TokenUnitTest.cs +++ b/Timeline.Tests/IntegratedTests/TokenUnitTest.cs @@ -2,6 +2,7 @@ using FluentAssertions; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.DependencyInjection; using System; +using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; using Timeline.Models.Http; @@ -12,7 +13,7 @@ using Timeline.Tests.Mock.Data; using Xunit; using static Timeline.ErrorCodes.Http.Token; -namespace Timeline.Tests +namespace Timeline.Tests.IntegratedTests { public class TokenUnitTest : IClassFixture>, IDisposable { @@ -32,126 +33,118 @@ namespace Timeline.Tests { _testApp.Dispose(); } - [Fact] - public async Task CreateToken_InvalidModel() + + public static IEnumerable CreateToken_InvalidModel_Data() { - using var client = _factory.CreateDefaultClient(); - // missing username - await InvalidModelTestHelpers.TestPostInvalidModel(client, CreateTokenUrl, - new CreateTokenRequest { Username = null, Password = "user" }); - // missing password - await InvalidModelTestHelpers.TestPostInvalidModel(client, CreateTokenUrl, - new CreateTokenRequest { Username = "user", Password = null }); - // bad expire offset - await InvalidModelTestHelpers.TestPostInvalidModel(client, CreateTokenUrl, - new CreateTokenRequest - { - Username = "user", - Password = "password", - Expire = 1000 - }); + yield return new[] { null, "p", null }; + yield return new[] { "u", null, null }; + yield return new object[] { "u", "p", 2000 }; + yield return new object[] { "u", "p", -1 }; } - [Fact] - public async void CreateToken_UserNotExist() + [Theory] + [MemberData(nameof(CreateToken_InvalidModel_Data))] + public async Task CreateToken_InvalidModel(string username, string password, int expire) { using var client = _factory.CreateDefaultClient(); - var response = await client.PostAsJsonAsync(CreateTokenUrl, - new CreateTokenRequest { Username = "usernotexist", Password = "???" }); - response.Should().HaveStatusCodeBadRequest() - .And.Should().HaveBodyAsCommonResponseWithCode(Create.BadCredential); + (await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest + { + Username = username, + Password = password, + Expire = expire + })).Should().BeInvalidModel(); } - [Fact] - public async void CreateToken_BadPassword() + public static IEnumerable CreateToken_UserCredential_Data() { - using (var client = _factory.CreateDefaultClient()) - { - var response = await client.PostAsJsonAsync(CreateTokenUrl, - new CreateTokenRequest { Username = MockUser.User.Username, Password = "???" }); - response.Should().HaveStatusCodeBadRequest() - .And.Should().HaveBodyAsCommonResponseWithCode(Create.BadCredential); - } + yield return new[] { "usernotexist", "p" }; + yield return new[] { MockUser.User.Username, "???" }; + } + + [Theory] + [MemberData(nameof(CreateToken_UserCredential_Data))] + public async void CreateToken_UserCredential(string username, string password) + { + using var client = _factory.CreateDefaultClient(); + var response = await client.PostAsJsonAsync(CreateTokenUrl, + new CreateTokenRequest { Username = username, Password = password }); + response.Should().HaveStatusCode(400) + .And.Should().HaveCommonBody() + .Which.Code.Should().Be(Create.BadCredential); } [Fact] - public async void CreateToken_Success() + public async Task CreateToken_Success() { - using (var client = _factory.CreateDefaultClient()) - { - var response = await client.PostAsJsonAsync(CreateTokenUrl, - new CreateTokenRequest { Username = MockUser.User.Username, Password = MockUser.User.Password }); - var body = response.Should().HaveStatusCodeOk() - .And.Should().HaveBodyAsJson().Which; - body.Token.Should().NotBeNullOrWhiteSpace(); - body.User.Should().BeEquivalentTo(MockUser.User.Info); - } + using var client = _factory.CreateDefaultClient(); + var response = await client.PostAsJsonAsync(CreateTokenUrl, + new CreateTokenRequest { Username = MockUser.User.Username, Password = MockUser.User.Password }); + var body = response.Should().HaveStatusCode(200) + .And.Should().HaveJsonBody().Which; + body.Token.Should().NotBeNullOrWhiteSpace(); + body.User.Should().BeEquivalentTo(MockUser.User.Info); } [Fact] - public async void VerifyToken_InvalidModel() + public async Task VerifyToken_InvalidModel() { - using (var client = _factory.CreateDefaultClient()) - { - // missing token - await InvalidModelTestHelpers.TestPostInvalidModel(client, VerifyTokenUrl, - new VerifyTokenRequest { Token = null }); - } + using var client = _factory.CreateDefaultClient(); + (await client.PostAsJsonAsync(VerifyTokenUrl, + new VerifyTokenRequest { Token = null })).Should().BeInvalidModel(); } [Fact] - public async void VerifyToken_BadToken() + public async Task VerifyToken_BadToken() { - using (var client = _factory.CreateDefaultClient()) - { - var response = await client.PostAsJsonAsync(VerifyTokenUrl, new VerifyTokenRequest { Token = "bad token hahaha" }); - response.Should().HaveStatusCodeBadRequest() - .And.Should().HaveBodyAsCommonResponseWithCode(Verify.BadFormat); - } + using var client = _factory.CreateDefaultClient(); + var response = await client.PostAsJsonAsync(VerifyTokenUrl, + new VerifyTokenRequest { Token = "bad token hahaha" }); + response.Should().HaveStatusCode(400) + .And.Should().HaveCommonBody() + .Which.Code.Should().Be(Verify.BadFormat); } [Fact] - public async void VerifyToken_BadVersion() + public async Task VerifyToken_BadVersion() { - using (var client = _factory.CreateDefaultClient()) + using var client = _factory.CreateDefaultClient(); + var token = (await client.CreateUserTokenAsync(MockUser.User.Username, MockUser.User.Password)).Token; + + using (var scope = _factory.Server.Host.Services.CreateScope()) // UserService is scoped. { - var token = (await client.CreateUserTokenAsync(MockUser.User.Username, MockUser.User.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); - } - - var response = await client.PostAsJsonAsync(VerifyTokenUrl, new VerifyTokenRequest { Token = token }); - response.Should().HaveStatusCodeBadRequest() - .And.Should().HaveBodyAsCommonResponseWithCode(Verify.OldVersion); + // create a user for test + var userService = scope.ServiceProvider.GetRequiredService(); + await userService.PatchUser(MockUser.User.Username, null, null); } + + (await client.PostAsJsonAsync(VerifyTokenUrl, + new VerifyTokenRequest { Token = token })) + .Should().HaveStatusCode(400) + .And.Should().HaveCommonBody() + .Which.Code.Should().Be(Verify.OldVersion); } [Fact] - public async void VerifyToken_UserNotExist() + public async Task VerifyToken_UserNotExist() { - using (var client = _factory.CreateDefaultClient()) + using var client = _factory.CreateDefaultClient(); + var token = (await client.CreateUserTokenAsync(MockUser.User.Username, MockUser.User.Password)).Token; + + using (var scope = _factory.Server.Host.Services.CreateScope()) // UserService is scoped. { - var token = (await client.CreateUserTokenAsync(MockUser.User.Username, MockUser.User.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.DeleteUser(MockUser.User.Username); - } - - var response = await client.PostAsJsonAsync(VerifyTokenUrl, new VerifyTokenRequest { Token = token }); - response.Should().HaveStatusCodeBadRequest() - .And.Should().HaveBodyAsCommonResponseWithCode(Verify.UserNotExist); + var userService = scope.ServiceProvider.GetRequiredService(); + await userService.DeleteUser(MockUser.User.Username); } + + (await client.PostAsJsonAsync(VerifyTokenUrl, + new VerifyTokenRequest { Token = token })) + .Should().HaveStatusCode(400) + .And.Should().HaveCommonBody() + .Which.Code.Should().Be(Verify.UserNotExist); } //[Fact] - //public async void VerifyToken_Expired() + //public async Task VerifyToken_Expired() //{ // using (var client = _factory.CreateDefaultClient()) // { @@ -169,17 +162,15 @@ namespace Timeline.Tests //} [Fact] - public async void VerifyToken_Success() + public async Task VerifyToken_Success() { - using (var client = _factory.CreateDefaultClient()) - { - var createTokenResult = await client.CreateUserTokenAsync(MockUser.User.Username, MockUser.User.Password); - var response = await client.PostAsJsonAsync(VerifyTokenUrl, - new VerifyTokenRequest { Token = createTokenResult.Token }); - response.Should().HaveStatusCodeOk() - .And.Should().HaveBodyAsJson() - .Which.User.Should().BeEquivalentTo(MockUser.User.Info); - } + using var client = _factory.CreateDefaultClient(); + var createTokenResult = await client.CreateUserTokenAsync(MockUser.User.Username, MockUser.User.Password); + var response = await client.PostAsJsonAsync(VerifyTokenUrl, + new VerifyTokenRequest { Token = createTokenResult.Token }); + response.Should().HaveStatusCode(200) + .And.Should().HaveJsonBody() + .Which.User.Should().BeEquivalentTo(MockUser.User.Info); } } } diff --git a/Timeline.Tests/IntegratedTests/UserAvatarTests.cs b/Timeline.Tests/IntegratedTests/UserAvatarTests.cs index 439e8d9b..ad0e4221 100644 --- a/Timeline.Tests/IntegratedTests/UserAvatarTests.cs +++ b/Timeline.Tests/IntegratedTests/UserAvatarTests.cs @@ -50,8 +50,9 @@ namespace Timeline.Tests.IntegratedTests { { var res = await client.GetAsync("users/usernotexist/avatar"); - res.Should().HaveStatusCodeNotFound() - .And.Should().HaveBodyAsCommonResponseWithCode(UserAvatarController.ErrorCodes.Get_UserNotExist); + res.Should().HaveStatusCode(404) + .And.Should().HaveCommonBody() + .Which.Code.Should().Be(UserAvatarController.ErrorCodes.Get_UserNotExist); } var env = _factory.Server.Host.Services.GetRequiredService(); @@ -60,7 +61,7 @@ namespace Timeline.Tests.IntegratedTests async Task GetReturnDefault(string username = "user") { var res = await client.GetAsync($"users/{username}/avatar"); - res.Should().HaveStatusCodeOk(); + res.Should().HaveStatusCode(200); res.Content.Headers.ContentType.MediaType.Should().Be("image/png"); var body = await res.Content.ReadAsByteArrayAsync(); body.Should().Equal(defaultAvatarData); @@ -69,7 +70,7 @@ namespace Timeline.Tests.IntegratedTests EntityTagHeaderValue eTag; { var res = await client.GetAsync($"users/user/avatar"); - res.Should().HaveStatusCodeOk(); + res.Should().HaveStatusCode(200); res.Content.Headers.ContentType.MediaType.Should().Be("image/png"); var body = await res.Content.ReadAsByteArrayAsync(); body.Should().Equal(defaultAvatarData); @@ -91,7 +92,7 @@ namespace Timeline.Tests.IntegratedTests request.Headers.TryAddWithoutValidation("If-None-Match", "\"dsdfd"); var res = await client.SendAsync(request); res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.Should().HaveBodyAsCommonResponseWithCode(ErrorCodes.Http.Common.Header.BadFormat_IfNonMatch); + .And.Should().HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Http.Common.Header.BadFormat_IfNonMatch); } { @@ -121,7 +122,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.Should().HaveBodyAsCommonResponseWithCode(ErrorCodes.Http.Common.Header.Missing_ContentLength); + .And.Should().HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Http.Common.Header.Missing_ContentLength); } { @@ -129,7 +130,7 @@ namespace Timeline.Tests.IntegratedTests content.Headers.ContentLength = 1; var res = await client.PutAsync("users/user/avatar", content); res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.Should().HaveBodyAsCommonResponseWithCode(ErrorCodes.Http.Common.Header.Missing_ContentType); + .And.Should().HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Http.Common.Header.Missing_ContentType); } { @@ -138,7 +139,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.Should().HaveBodyAsCommonResponseWithCode(ErrorCodes.Http.Common.Header.Zero_ContentLength); + .And.Should().HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Http.Common.Header.Zero_ContentLength); } { @@ -152,7 +153,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.Should().HaveBodyAsCommonResponseWithCode(UserAvatarController.ErrorCodes.Put_Content_TooBig); + .And.Should().HaveCommonBody().Which.Code.Should().Be(UserAvatarController.ErrorCodes.Put_Content_TooBig); } { @@ -161,7 +162,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.Should().HaveBodyAsCommonResponseWithCode(UserAvatarController.ErrorCodes.Put_Content_UnmatchedLength_Less); + .And.Should().HaveCommonBody().Which.Code.Should().Be(UserAvatarController.ErrorCodes.Put_Content_UnmatchedLength_Less); } { @@ -170,25 +171,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.Should().HaveBodyAsCommonResponseWithCode(UserAvatarController.ErrorCodes.Put_Content_UnmatchedLength_Bigger); + .And.Should().HaveCommonBody().Which.Code.Should().Be(UserAvatarController.ErrorCodes.Put_Content_UnmatchedLength_Bigger); } { var res = await client.PutByteArrayAsync("users/user/avatar", new[] { (byte)0x00 }, "image/png"); res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.Should().HaveBodyAsCommonResponseWithCode(UserAvatarController.ErrorCodes.Put_BadFormat_CantDecode); + .And.Should().HaveCommonBody().Which.Code.Should().Be(UserAvatarController.ErrorCodes.Put_BadFormat_CantDecode); } { var res = await client.PutByteArrayAsync("users/user/avatar", mockAvatar.Data, "image/jpeg"); res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.Should().HaveBodyAsCommonResponseWithCode(UserAvatarController.ErrorCodes.Put_BadFormat_UnmatchedFormat); + .And.Should().HaveCommonBody().Which.Code.Should().Be(UserAvatarController.ErrorCodes.Put_BadFormat_UnmatchedFormat); } { var res = await client.PutByteArrayAsync("users/user/avatar", ImageHelper.CreatePngWithSize(100, 200), "image/png"); res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.Should().HaveBodyAsCommonResponseWithCode(UserAvatarController.ErrorCodes.Put_BadFormat_BadSize); + .And.Should().HaveCommonBody().Which.Code.Should().Be(UserAvatarController.ErrorCodes.Put_BadFormat_BadSize); } { @@ -196,7 +197,7 @@ namespace Timeline.Tests.IntegratedTests res.Should().HaveStatusCode(HttpStatusCode.OK); var res2 = await client.GetAsync("users/user/avatar"); - res2.Should().HaveStatusCodeOk(); + res2.Should().HaveStatusCode(200); res2.Content.Headers.ContentType.MediaType.Should().Be(mockAvatar.Type); var body = await res2.Content.ReadAsByteArrayAsync(); body.Should().Equal(mockAvatar.Data); @@ -218,19 +219,19 @@ namespace Timeline.Tests.IntegratedTests { var res = await client.PutByteArrayAsync("users/admin/avatar", new[] { (byte)0x00 }, "image/png"); res.Should().HaveStatusCode(HttpStatusCode.Forbidden) - .And.Should().HaveBodyAsCommonResponseWithCode(UserAvatarController.ErrorCodes.Put_Forbid); + .And.Should().HaveCommonBody().Which.Code.Should().Be(UserAvatarController.ErrorCodes.Put_Forbid); } { var res = await client.DeleteAsync("users/admin/avatar"); res.Should().HaveStatusCode(HttpStatusCode.Forbidden) - .And.Should().HaveBodyAsCommonResponseWithCode(UserAvatarController.ErrorCodes.Delete_Forbid); + .And.Should().HaveCommonBody().Which.Code.Should().Be(UserAvatarController.ErrorCodes.Delete_Forbid); } for (int i = 0; i < 2; i++) // double delete should work. { var res = await client.DeleteAsync("users/user/avatar"); - res.Should().HaveStatusCodeOk(); + res.Should().HaveStatusCode(200); await GetReturnDefault(); } } @@ -250,14 +251,15 @@ namespace Timeline.Tests.IntegratedTests { var res = await client.PutByteArrayAsync("users/usernotexist/avatar", new[] { (byte)0x00 }, "image/png"); - res.Should().HaveStatusCodeBadRequest() - .And.Should().HaveBodyAsCommonResponseWithCode(UserAvatarController.ErrorCodes.Put_UserNotExist); + res.Should().HaveStatusCode(400) + .And.Should().HaveCommonBody() + .Which.Code.Should().Be(UserAvatarController.ErrorCodes.Put_UserNotExist); } { var res = await client.DeleteAsync("users/usernotexist/avatar"); - res.Should().HaveStatusCodeBadRequest() - .And.Should().HaveBodyAsCommonResponseWithCode(UserAvatarController.ErrorCodes.Delete_UserNotExist); + res.Should().HaveStatusCode(400) + .And.Should().HaveCommonBody().Which.Code.Should().Be(UserAvatarController.ErrorCodes.Delete_UserNotExist); } } } diff --git a/Timeline.Tests/IntegratedTests/UserDetailTest.cs b/Timeline.Tests/IntegratedTests/UserDetailTest.cs index d1a67d9d..4d268efa 100644 --- a/Timeline.Tests/IntegratedTests/UserDetailTest.cs +++ b/Timeline.Tests/IntegratedTests/UserDetailTest.cs @@ -35,21 +35,21 @@ namespace Timeline.Tests.IntegratedTests { { var res = await client.GetAsync($"users/usernotexist/nickname"); - res.Should().HaveStatusCodeNotFound() - .And.Should().HaveBodyAsCommonResponseWithCode(UserDetailController.ErrorCodes.GetNickname_UserNotExist); + res.Should().HaveStatusCode(404) + .And.Should().HaveCommonBody().Which.Code.Should().Be(UserDetailController.ErrorCodes.GetNickname_UserNotExist); } { var res = await client.GetAsync($"users/usernotexist/details"); - res.Should().HaveStatusCodeNotFound() - .And.Should().HaveBodyAsCommonResponseWithCode(UserDetailController.ErrorCodes.Get_UserNotExist); + res.Should().HaveStatusCode(404) + .And.Should().HaveCommonBody().Which.Code.Should().Be(UserDetailController.ErrorCodes.Get_UserNotExist); } async Task GetAndTest(UserDetail d) { var res = await client.GetAsync($"users/{MockUser.User.Username}/details"); - res.Should().HaveStatusCodeOk() - .And.Should().HaveBodyAsJson() + res.Should().HaveStatusCode(200) + .And.Should().HaveJsonBody() .Which.Should().BeEquivalentTo(d); } @@ -58,7 +58,7 @@ namespace Timeline.Tests.IntegratedTests { var res = await client.PatchAsJsonAsync($"users/{MockUser.Admin.Username}/details", new UserDetail()); res.Should().HaveStatusCode(HttpStatusCode.Forbidden) - .And.Should().HaveBodyAsCommonResponseWithCode(UserDetailController.ErrorCodes.Patch_Forbid); + .And.Should().HaveCommonBody().Which.Code.Should().Be(UserDetailController.ErrorCodes.Patch_Forbid); } { @@ -70,7 +70,7 @@ namespace Timeline.Tests.IntegratedTests PhoneNumber = "aaaaaaaa" }); var body = res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.Should().HaveBodyAsCommonResponse().Which; + .And.Should().HaveCommonBody().Which; body.Code.Should().Be(ErrorCodes.Http.Common.InvalidModel); foreach (var key in new string[] { "nickname", "qq", "email", "phonenumber" }) { @@ -89,13 +89,13 @@ namespace Timeline.Tests.IntegratedTests { var res = await client.PatchAsJsonAsync($"users/{MockUser.User.Username}/details", detail); - res.Should().HaveStatusCodeOk(); + res.Should().HaveStatusCode(200); await GetAndTest(detail); } { var res = await client.GetAsync($"users/{MockUser.User.Username}/nickname"); - res.Should().HaveStatusCodeOk().And.Should().HaveBodyAsJson() + res.Should().HaveStatusCode(200).And.Should().HaveJsonBody() .Which.Should().BeEquivalentTo(new UserDetail { Nickname = detail.Nickname @@ -111,7 +111,7 @@ namespace Timeline.Tests.IntegratedTests { var res = await client.PatchAsJsonAsync($"users/{MockUser.User.Username}/details", detail2); - res.Should().HaveStatusCodeOk(); + res.Should().HaveStatusCode(200); await GetAndTest(new UserDetail { Nickname = detail.Nickname, @@ -131,13 +131,13 @@ namespace Timeline.Tests.IntegratedTests { { var res = await client.PatchAsJsonAsync($"users/{MockUser.User.Username}/details", new UserDetail()); - res.Should().HaveStatusCodeOk(); + res.Should().HaveStatusCode(200); } { var res = await client.PatchAsJsonAsync($"users/usernotexist/details", new UserDetail()); - res.Should().HaveStatusCodeNotFound() - .And.Should().HaveBodyAsCommonResponseWithCode(UserDetailController.ErrorCodes.Patch_UserNotExist); + res.Should().HaveStatusCode(404) + .And.Should().HaveCommonBody().Which.Code.Should().Be(UserDetailController.ErrorCodes.Patch_UserNotExist); } } } diff --git a/Timeline.Tests/IntegratedTests/UserUnitTest.cs b/Timeline.Tests/IntegratedTests/UserUnitTest.cs index d228c563..b2aab24c 100644 --- a/Timeline.Tests/IntegratedTests/UserUnitTest.cs +++ b/Timeline.Tests/IntegratedTests/UserUnitTest.cs @@ -1,6 +1,7 @@ using FluentAssertions; using Microsoft.AspNetCore.Mvc.Testing; using System; +using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; using Timeline.Controllers; @@ -11,7 +12,7 @@ using Timeline.Tests.Helpers.Authentication; using Timeline.Tests.Mock.Data; using Xunit; -namespace Timeline.Tests +namespace Timeline.Tests.IntegratedTests { public class UserUnitTest : IClassFixture>, IDisposable { @@ -32,157 +33,141 @@ namespace Timeline.Tests [Fact] public async Task Get_Users_List() { - using (var client = await _factory.CreateClientAsAdmin()) - { - var res = await client.GetAsync("users"); - res.Should().HaveStatusCodeOk().And.Should().HaveBodyAsJson() - .Which.Should().BeEquivalentTo(MockUser.UserInfoList); - } + using var client = await _factory.CreateClientAsAdmin(); + var res = await client.GetAsync("users"); + res.Should().HaveStatusCode(200) + .And.Should().HaveJsonBody() + .Which.Should().BeEquivalentTo(MockUser.UserInfoList); } [Fact] public async Task Get_Users_User() { - using (var client = await _factory.CreateClientAsAdmin()) - { - var res = await client.GetAsync("users/" + MockUser.User.Username); - res.Should().HaveStatusCodeOk() - .And.Should().HaveBodyAsJson() - .Which.Should().BeEquivalentTo(MockUser.User.Info); - } + using var client = await _factory.CreateClientAsAdmin(); + var res = await client.GetAsync("users/" + MockUser.User.Username); + res.Should().HaveStatusCode(200) + .And.Should().HaveJsonBody() + .Which.Should().BeEquivalentTo(MockUser.User.Info); } [Fact] public async Task Get_Users_404() { - using (var client = await _factory.CreateClientAsAdmin()) - { - var res = await client.GetAsync("users/usernotexist"); - res.Should().HaveStatusCodeNotFound() - .And.Should().HaveBodyAsCommonResponseWithCode(UserController.ErrorCodes.Get_NotExist); - } + using var client = await _factory.CreateClientAsAdmin(); + var res = await client.GetAsync("users/usernotexist"); + res.Should().HaveStatusCode(404) + .And.Should().HaveCommonBody() + .Which.Code.Should().Be(UserController.ErrorCodes.Get_NotExist); } - [Fact] - public async Task Put_InvalidModel() + public static IEnumerable Put_InvalidModel_Data() { - using (var client = await _factory.CreateClientAsAdmin()) - { - const string url = "users/aaaaaaaa"; - // missing password - await InvalidModelTestHelpers.TestPutInvalidModel(client, url, new UserPutRequest { Password = null, Administrator = false }); - // missing administrator - await InvalidModelTestHelpers.TestPutInvalidModel(client, url, new UserPutRequest { Password = "???", Administrator = null }); - } + yield return new object[] { null, false }; + yield return new object[] { "p", null }; + } + + [Theory] + [MemberData(nameof(Put_InvalidModel_Data))] + public async Task Put_InvalidModel(string password, bool? administrator) + { + using var client = await _factory.CreateClientAsAdmin(); + const string url = "users/aaaaaaaa"; + (await client.PutAsJsonAsync(url, + new UserPutRequest { Password = password, Administrator = administrator })) + .Should().BeInvalidModel(); } [Fact] public async Task Put_BadUsername() { - using (var client = await _factory.CreateClientAsAdmin()) + using var client = await _factory.CreateClientAsAdmin(); + var res = await client.PutAsJsonAsync("users/dsf fddf", new UserPutRequest { - var res = await client.PutAsJsonAsync("users/dsf fddf", new UserPutRequest - { - Password = "???", - Administrator = false - }); - res.Should().HaveStatusCodeBadRequest() - .And.Should().HaveBodyAsCommonResponseWithCode(UserController.ErrorCodes.Put_BadUsername); - } + Password = "???", + Administrator = false + }); + res.Should().HaveStatusCode(400) + .And.Should().HaveCommonBody() + .Which.Code.Should().Be(UserController.ErrorCodes.Put_BadUsername); } private async Task CheckAdministrator(HttpClient client, string username, bool administrator) { var res = await client.GetAsync("users/" + username); - res.Should().HaveStatusCodeOk() - .And.Should().HaveBodyAsJson() + res.Should().HaveStatusCode(200) + .And.Should().HaveJsonBody() .Which.Administrator.Should().Be(administrator); } [Fact] public async Task Put_Modiefied() { - using (var client = await _factory.CreateClientAsAdmin()) + using var client = await _factory.CreateClientAsAdmin(); + var res = await client.PutAsJsonAsync("users/" + MockUser.User.Username, new UserPutRequest { - var res = await client.PutAsJsonAsync("users/" + MockUser.User.Username, new UserPutRequest - { - Password = "password", - Administrator = false - }); - res.Should().BePutModify(); - await CheckAdministrator(client, MockUser.User.Username, false); - } + Password = "password", + Administrator = false + }); + res.Should().BePutModify(); + await CheckAdministrator(client, MockUser.User.Username, false); } [Fact] public async Task Put_Created() { - using (var client = await _factory.CreateClientAsAdmin()) + using var client = await _factory.CreateClientAsAdmin(); + const string username = "puttest"; + const string url = "users/" + username; + + var res = await client.PutAsJsonAsync(url, new UserPutRequest { - const string username = "puttest"; - const string url = "users/" + username; - - var res = await client.PutAsJsonAsync(url, new UserPutRequest - { - Password = "password", - Administrator = false - }); - res.Should().BePutCreate(); - await CheckAdministrator(client, username, false); - } + Password = "password", + Administrator = false + }); + res.Should().BePutCreate(); + await CheckAdministrator(client, username, false); } [Fact] public async Task Patch_NotExist() { - using (var client = await _factory.CreateClientAsAdmin()) - { - var res = await client.PatchAsJsonAsync("users/usernotexist", new UserPatchRequest { }); - res.Should().HaveStatusCodeNotFound() - .And.Should().HaveBodyAsCommonResponseWithCode(UserController.ErrorCodes.Patch_NotExist); - } + using var client = await _factory.CreateClientAsAdmin(); + var res = await client.PatchAsJsonAsync("users/usernotexist", new UserPatchRequest { }); + res.Should().HaveStatusCode(404) + .And.Should().HaveCommonBody() + .Which.Code.Should().Be(UserController.ErrorCodes.Patch_NotExist); } [Fact] public async Task Patch_Success() { - using (var client = await _factory.CreateClientAsAdmin()) + using var client = await _factory.CreateClientAsAdmin(); { - { - var res = await client.PatchAsJsonAsync("users/" + MockUser.User.Username, - new UserPatchRequest { Administrator = false }); - res.Should().HaveStatusCodeOk(); - await CheckAdministrator(client, MockUser.User.Username, false); - } + var res = await client.PatchAsJsonAsync("users/" + MockUser.User.Username, + new UserPatchRequest { Administrator = false }); + res.Should().HaveStatusCode(200); + await CheckAdministrator(client, MockUser.User.Username, false); } } [Fact] public async Task Delete_Deleted() { - using (var client = await _factory.CreateClientAsAdmin()) - { - { - var url = "users/" + MockUser.User.Username; - var res = await client.DeleteAsync(url); - res.Should().BeDeleteDelete(); - - var res2 = await client.GetAsync(url); - res2.Should().HaveStatusCodeNotFound(); - } - } + using var client = await _factory.CreateClientAsAdmin(); + var url = "users/" + MockUser.User.Username; + var res = await client.DeleteAsync(url); + res.Should().BeDeleteDelete(); + + var res2 = await client.GetAsync(url); + res2.Should().HaveStatusCode(404); } [Fact] public async Task Delete_NotExist() { - using (var client = await _factory.CreateClientAsAdmin()) - { - { - var res = await client.DeleteAsync("users/usernotexist"); - res.Should().BeDeleteNotExist(); - } - } + using var client = await _factory.CreateClientAsAdmin(); + var res = await client.DeleteAsync("users/usernotexist"); + res.Should().BeDeleteNotExist(); } @@ -204,58 +189,54 @@ namespace Timeline.Tests _testApp.Dispose(); } - [Fact] - public async Task InvalidModel() + public static IEnumerable InvalidModel_Data() + { + yield return new[] { null, "uuu" }; + yield return new[] { "uuu", null }; + yield return new[] { "uuu", "???" }; + } + + [Theory] + [MemberData(nameof(InvalidModel_Data))] + public async Task InvalidModel(string oldUsername, string newUsername) { - using (var client = await _factory.CreateClientAsAdmin()) - { - // missing old username - await InvalidModelTestHelpers.TestPostInvalidModel(client, url, - new ChangeUsernameRequest { OldUsername = null, NewUsername = "hhh" }); - // missing new username - await InvalidModelTestHelpers.TestPostInvalidModel(client, url, - new ChangeUsernameRequest { OldUsername = "hhh", NewUsername = null }); - // bad username - await InvalidModelTestHelpers.TestPostInvalidModel(client, url, - new ChangeUsernameRequest { OldUsername = "hhh", NewUsername = "???" }); - } + using var client = await _factory.CreateClientAsAdmin(); + (await client.PostAsJsonAsync(url, + new ChangeUsernameRequest { OldUsername = oldUsername, NewUsername = newUsername })) + .Should().BeInvalidModel(); } [Fact] public async Task UserNotExist() { - using (var client = await _factory.CreateClientAsAdmin()) - { - var res = await client.PostAsJsonAsync(url, - new ChangeUsernameRequest { OldUsername = "usernotexist", NewUsername = "newUsername" }); - res.Should().HaveStatusCodeBadRequest() - .And.Should().HaveBodyAsCommonResponseWithCode(UserController.ErrorCodes.ChangeUsername_NotExist); - } + using var client = await _factory.CreateClientAsAdmin(); + var res = await client.PostAsJsonAsync(url, + new ChangeUsernameRequest { OldUsername = "usernotexist", NewUsername = "newUsername" }); + res.Should().HaveStatusCode(400) + .And.Should().HaveCommonBody() + .Which.Code.Should().Be(UserController.ErrorCodes.ChangeUsername_NotExist); } [Fact] public async Task UserAlreadyExist() { - using (var client = await _factory.CreateClientAsAdmin()) - { - var res = await client.PostAsJsonAsync(url, - new ChangeUsernameRequest { OldUsername = MockUser.User.Username, NewUsername = MockUser.Admin.Username }); - res.Should().HaveStatusCodeBadRequest() - .And.Should().HaveBodyAsCommonResponseWithCode(UserController.ErrorCodes.ChangeUsername_AlreadyExist); - } + using var client = await _factory.CreateClientAsAdmin(); + var res = await client.PostAsJsonAsync(url, + 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); } [Fact] public async Task Success() { - using (var client = await _factory.CreateClientAsAdmin()) - { - const string newUsername = "hahaha"; - var res = await client.PostAsJsonAsync(url, - new ChangeUsernameRequest { OldUsername = MockUser.User.Username, NewUsername = newUsername }); - res.Should().HaveStatusCodeOk(); - await client.CreateUserTokenAsync(newUsername, MockUser.User.Password); - } + using var client = await _factory.CreateClientAsAdmin(); + const string newUsername = "hahaha"; + var res = await client.PostAsJsonAsync(url, + new ChangeUsernameRequest { OldUsername = MockUser.User.Username, NewUsername = newUsername }); + res.Should().HaveStatusCode(200); + await client.CreateUserTokenAsync(newUsername, MockUser.User.Password); } } @@ -278,42 +259,41 @@ namespace Timeline.Tests _testApp.Dispose(); } - [Fact] - public async Task InvalidModel() + public static IEnumerable InvalidModel_Data() + { + yield return new[] { null, "ppp" }; + yield return new[] { "ppp", null }; + } + + [Theory] + [MemberData(nameof(InvalidModel_Data))] + public async Task InvalidModel(string oldPassword, string newPassword) { - using (var client = await _factory.CreateClientAsUser()) - { - // missing old password - await InvalidModelTestHelpers.TestPostInvalidModel(client, url, - new ChangePasswordRequest { OldPassword = null, NewPassword = "???" }); - // missing new password - await InvalidModelTestHelpers.TestPostInvalidModel(client, url, - new ChangePasswordRequest { OldPassword = "???", NewPassword = null }); - } + using var client = await _factory.CreateClientAsUser(); + (await client.PostAsJsonAsync(url, + new ChangePasswordRequest { OldPassword = oldPassword, NewPassword = newPassword })) + .Should().BeInvalidModel(); } [Fact] public async Task BadOldPassword() { - using (var client = await _factory.CreateClientAsUser()) - { - var res = await client.PostAsJsonAsync(url, new ChangePasswordRequest { OldPassword = "???", NewPassword = "???" }); - res.Should().HaveStatusCodeBadRequest() - .And.Should().HaveBodyAsCommonResponseWithCode(UserController.ErrorCodes.ChangePassword_BadOldPassword); - } + using var client = await _factory.CreateClientAsUser(); + 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); } [Fact] public async Task Success() { - using (var client = await _factory.CreateClientAsUser()) - { - const string newPassword = "new"; - var res = await client.PostAsJsonAsync(url, - new ChangePasswordRequest { OldPassword = MockUser.User.Password, NewPassword = newPassword }); - res.Should().HaveStatusCodeOk(); - await client.CreateUserTokenAsync(MockUser.User.Username, newPassword); - } + using var client = await _factory.CreateClientAsUser(); + const string newPassword = "new"; + var res = await client.PostAsJsonAsync(url, + new ChangePasswordRequest { OldPassword = MockUser.User.Password, NewPassword = newPassword }); + res.Should().HaveStatusCode(200); + await client.CreateUserTokenAsync(MockUser.User.Username, newPassword); } } } diff --git a/Timeline/GlobalSuppressions.cs b/Timeline/GlobalSuppressions.cs new file mode 100644 index 00000000..3c9c8341 --- /dev/null +++ b/Timeline/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("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("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Error code constant identifiers use nested names.", Scope = "type", Target = "Timeline.ErrorCodes")] diff --git a/Timeline/Models/Http/Common.cs b/Timeline/Models/Http/Common.cs index af185e85..83e6a072 100644 --- a/Timeline/Models/Http/Common.cs +++ b/Timeline/Models/Http/Common.cs @@ -38,7 +38,7 @@ namespace Timeline.Models.Http Message = message; } - public int Code { get; set; } + public int? Code { get; set; } public string Message { get; set; } } diff --git a/Timeline/Program.cs b/Timeline/Program.cs index dfc93b9e..7474fe2f 100644 --- a/Timeline/Program.cs +++ b/Timeline/Program.cs @@ -6,7 +6,7 @@ using Microsoft.Extensions.Hosting; namespace Timeline { - public class Program + public static class Program { public static void Main(string[] args) { diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs index 347b8cbb..9564b34b 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -5,6 +5,7 @@ using System; using System.Linq; using System.Threading.Tasks; using Timeline.Entities; +using Timeline.Helpers; using Timeline.Models; using Timeline.Models.Validation; using static Timeline.Helpers.MyLogHelper; @@ -23,14 +24,20 @@ namespace Timeline.Services { private const string message = "The user does not exist."; + public UserNotExistException() + : base(message) + { + + } + public UserNotExistException(string username) - : base(FormatLogMessage(message, Pair("Username", username))) + : base(Log.Format(message, ("Username", username))) { Username = username; } public UserNotExistException(long id) - : base(FormatLogMessage(message, Pair("Id", id))) + : base(Log.Format(message, ("Id", id))) { Id = id; } @@ -42,21 +49,29 @@ namespace Timeline.Services System.Runtime.Serialization.StreamingContext context) : base(info, context) { } /// - /// The username that does not exist. May be null then is not null. + /// The username that does not exist. /// - public string Username { get; private set; } + public string Username { get; set; } /// - /// The id that does not exist. May be null then is not null. + /// The id that does not exist. /// - public long? Id { get; private set; } + public long? Id { get; set; } } [Serializable] public class BadPasswordException : Exception { + private const string message = "Password is wrong."; + + public BadPasswordException() + : base(message) + { + + } + public BadPasswordException(string badPassword) - : base(FormatLogMessage("Password is wrong.", Pair("Bad Password", badPassword))) + : base(Log.Format(message, ("Bad Password", badPassword))) { Password = badPassword; } @@ -70,22 +85,31 @@ namespace Timeline.Services /// /// The wrong password. /// - public string Password { get; private set; } + public string Password { get; set; } } [Serializable] public class BadTokenVersionException : Exception { + private const string message = "Token version is expired."; + + public BadTokenVersionException() + : base(message) + { + + } + public BadTokenVersionException(long tokenVersion, long requiredVersion) - : base(FormatLogMessage("Token version is expired.", - Pair("Token Version", tokenVersion), - Pair("Required Version", requiredVersion))) + : base(Log.Format(message, + ("Token Version", tokenVersion), + ("Required Version", requiredVersion))) { TokenVersion = tokenVersion; RequiredVersion = requiredVersion; } + public BadTokenVersionException(string message) : base(message) { } public BadTokenVersionException(string message, Exception inner) : base(message, inner) { } protected BadTokenVersionException( @@ -95,12 +119,12 @@ namespace Timeline.Services /// /// The version in the token. /// - public long TokenVersion { get; private set; } + public long? TokenVersion { get; set; } /// /// The version required. /// - public long RequiredVersion { get; private set; } + public long? RequiredVersion { get; set; } } /// @@ -109,6 +133,12 @@ namespace Timeline.Services [Serializable] public class UsernameBadFormatException : Exception { + private const string message = "Username is of bad format."; + + public UsernameBadFormatException() : base(message) { } + 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; } protected UsernameBadFormatException( @@ -128,7 +158,10 @@ namespace Timeline.Services [Serializable] public class UserAlreadyExistException : Exception { - public UserAlreadyExistException(string username) : base($"User {username} already exists.") { Username = username; } + private const string message = "User already exists."; + + public UserAlreadyExistException() : base(message) { } + public UserAlreadyExistException(string username) : base(Log.Format(message, ("Username", username))) { Username = username; } public UserAlreadyExistException(string username, string message) : base(message) { Username = username; } public UserAlreadyExistException(string message, Exception inner) : base(message, inner) { } protected UserAlreadyExistException( diff --git a/Timeline/Startup.cs b/Timeline/Startup.cs index 8e8a6393..fc570fdd 100644 --- a/Timeline/Startup.cs +++ b/Timeline/Startup.cs @@ -27,11 +27,12 @@ namespace Timeline // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { - services.AddMvc() + services.AddControllers() .ConfigureApiBehaviorOptions(options => { options.InvalidModelStateResponseFactory = InvalidModelResponseFactory.Factory; - }); + }) + .AddNewtonsoftJson(); services.Configure(Configuration.GetSection(nameof(JwtConfig))); var jwtConfig = Configuration.GetSection(nameof(JwtConfig)).Get(); diff --git a/Timeline/Timeline.csproj b/Timeline/Timeline.csproj index f01b8e31..836dfb47 100644 --- a/Timeline/Timeline.csproj +++ b/Timeline/Timeline.csproj @@ -13,9 +13,14 @@ + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - + -- cgit v1.2.3 From 7bcf891d9d68ac0f12570b94938347ab8eec3247 Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Sat, 19 Oct 2019 21:37:15 +0800 Subject: ... --- Timeline.Tests/Controllers/TokenControllerTest.cs | 21 ++++++++++ Timeline.Tests/Timeline.Tests.csproj | 49 ++++++++++++----------- Timeline/Controllers/TokenController.cs | 6 +-- Timeline/Entities/DatabaseContext.cs | 10 ++--- Timeline/Entities/UserAvatar.cs | 6 +-- Timeline/Entities/UserDetail.cs | 10 ++--- Timeline/Helpers/InvalidModelResponseFactory.cs | 1 + Timeline/Helpers/Log.cs | 2 +- Timeline/Models/Http/Common.cs | 4 +- Timeline/Models/Http/Token.cs | 12 +++--- Timeline/Models/Http/User.cs | 12 +++--- Timeline/Models/UserDetail.cs | 12 +++--- Timeline/Services/JwtService.cs | 8 +++- Timeline/Startup.cs | 2 + Timeline/Timeline.csproj | 7 +++- 15 files changed, 99 insertions(+), 63 deletions(-) (limited to 'Timeline/Models/Http/Common.cs') diff --git a/Timeline.Tests/Controllers/TokenControllerTest.cs b/Timeline.Tests/Controllers/TokenControllerTest.cs index 60ba75dc..8b1cf071 100644 --- a/Timeline.Tests/Controllers/TokenControllerTest.cs +++ b/Timeline.Tests/Controllers/TokenControllerTest.cs @@ -3,6 +3,7 @@ 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.Http; @@ -90,6 +91,26 @@ namespace Timeline.Tests.Controllers .Which.User.Should().BeEquivalentTo(MockUser.User.Info); } + public static IEnumerable Verify_BadRequest_Data() + { + yield return new object[] { new JwtTokenVerifyException(JwtTokenVerifyException.ErrorCodes.Expired), Verify.Expired }; + yield return new object[] { new JwtTokenVerifyException(JwtTokenVerifyException.ErrorCodes.IdClaimBadFormat), Verify.BadFormat }; + yield return new object[] { new BadTokenVersionException(), Verify.OldVersion }; + yield return new object[] { new UserNotExistException(), 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.Should().BeAssignableTo() + .Which.Value.Should().BeAssignableTo() + .Which.Code.Should().Be(code); + } + // TODO! Verify unit tests } } diff --git a/Timeline.Tests/Timeline.Tests.csproj b/Timeline.Tests/Timeline.Tests.csproj index 36bc03bc..3f88f174 100644 --- a/Timeline.Tests/Timeline.Tests.csproj +++ b/Timeline.Tests/Timeline.Tests.csproj @@ -1,28 +1,31 @@  - - netcoreapp3.0 - + + netcoreapp3.0 - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers - - + 8.0 + enable + - - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + diff --git a/Timeline/Controllers/TokenController.cs b/Timeline/Controllers/TokenController.cs index 2e661695..ce5786ca 100644 --- a/Timeline/Controllers/TokenController.cs +++ b/Timeline/Controllers/TokenController.cs @@ -54,7 +54,7 @@ namespace Timeline.Controllers [AllowAnonymous] public async Task Create([FromBody] CreateTokenRequest request) { - void LogFailure(string reason, Exception e = null) + void LogFailure(string reason, Exception? e = null) { _logger.LogInformation(e, Log.Format("Attemp to login failed.", ("Reason", reason), @@ -100,7 +100,7 @@ namespace Timeline.Controllers [AllowAnonymous] public async Task Verify([FromBody] VerifyTokenRequest request) { - void LogFailure(string reason, Exception e = null, params (string, object)[] otherProperties) + void LogFailure(string reason, Exception? e = null, params (string, object?)[] otherProperties) { var properties = new (string, object)[2 + otherProperties.Length]; properties[0] = ("Reason", reason); @@ -125,7 +125,7 @@ namespace Timeline.Controllers { const string message = "Token is expired."; var innerException = e.InnerException as SecurityTokenExpiredException; - LogFailure(message, e, ("Expires", innerException.Expires), ("Current Time", _clock.GetCurrentTime())); + LogFailure(message, e, ("Expires", innerException?.Expires), ("Current Time", _clock.GetCurrentTime())); return BadRequest(new CommonResponse(ErrorCodes.Http.Token.Verify.Expired, message)); } else diff --git a/Timeline/Entities/DatabaseContext.cs b/Timeline/Entities/DatabaseContext.cs index d9815660..550db216 100644 --- a/Timeline/Entities/DatabaseContext.cs +++ b/Timeline/Entities/DatabaseContext.cs @@ -17,20 +17,20 @@ namespace Timeline.Entities public long Id { get; set; } [Column("name"), MaxLength(26), Required] - public string Name { get; set; } + public string Name { get; set; } = default!; [Column("password"), Required] - public string EncryptedPassword { get; set; } + public string EncryptedPassword { get; set; } = default!; [Column("roles"), Required] - public string RoleString { get; set; } + public string RoleString { get; set; } = default!; [Column("version"), Required] public long Version { get; set; } - public UserAvatar Avatar { get; set; } + public UserAvatar? Avatar { get; set; } - public UserDetailEntity Detail { get; set; } + public UserDetailEntity? Detail { get; set; } } public class DatabaseContext : DbContext diff --git a/Timeline/Entities/UserAvatar.cs b/Timeline/Entities/UserAvatar.cs index d549aea5..d47bb28b 100644 --- a/Timeline/Entities/UserAvatar.cs +++ b/Timeline/Entities/UserAvatar.cs @@ -11,13 +11,13 @@ namespace Timeline.Entities public long Id { get; set; } [Column("data")] - public byte[] Data { get; set; } + public byte[]? Data { get; set; } [Column("type")] - public string Type { get; set; } + public string? Type { get; set; } [Column("etag"), MaxLength(30)] - public string ETag { get; set; } + public string? ETag { get; set; } [Column("last_modified"), Required] public DateTime LastModified { get; set; } diff --git a/Timeline/Entities/UserDetail.cs b/Timeline/Entities/UserDetail.cs index bc14dbe6..e02d15c4 100644 --- a/Timeline/Entities/UserDetail.cs +++ b/Timeline/Entities/UserDetail.cs @@ -10,19 +10,19 @@ namespace Timeline.Entities public long Id { get; set; } [Column("nickname"), MaxLength(15)] - public string Nickname { get; set; } + public string? Nickname { get; set; } [Column("qq"), MaxLength(15)] - public string QQ { get; set; } + public string? QQ { get; set; } [Column("email"), MaxLength(50)] - public string Email { get; set; } + public string? Email { get; set; } [Column("phone_number"), MaxLength(15)] - public string PhoneNumber { get; set; } + public string? PhoneNumber { get; set; } [Column("description")] - public string Description { get; set; } + public string? Description { get; set; } public long UserId { get; set; } } diff --git a/Timeline/Helpers/InvalidModelResponseFactory.cs b/Timeline/Helpers/InvalidModelResponseFactory.cs index c792e845..643c99ac 100644 --- a/Timeline/Helpers/InvalidModelResponseFactory.cs +++ b/Timeline/Helpers/InvalidModelResponseFactory.cs @@ -6,6 +6,7 @@ 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/Helpers/Log.cs b/Timeline/Helpers/Log.cs index 64391cd1..8deebf1d 100644 --- a/Timeline/Helpers/Log.cs +++ b/Timeline/Helpers/Log.cs @@ -25,7 +25,7 @@ namespace Timeline.Helpers public static class Log { - public static string Format(string summary, params (string, object)[] properties) + public static string Format(string summary, params (string, object?)[] properties) { var builder = new StringBuilder(); builder.Append(summary); diff --git a/Timeline/Models/Http/Common.cs b/Timeline/Models/Http/Common.cs index 83e6a072..6f6dbc1e 100644 --- a/Timeline/Models/Http/Common.cs +++ b/Timeline/Models/Http/Common.cs @@ -38,8 +38,8 @@ namespace Timeline.Models.Http Message = message; } - public int? Code { get; set; } - public string Message { get; set; } + public int Code { get; set; } + public string? Message { get; set; } } public class CommonDataResponse : CommonResponse diff --git a/Timeline/Models/Http/Token.cs b/Timeline/Models/Http/Token.cs index 615b6d8a..ea8b59ed 100644 --- a/Timeline/Models/Http/Token.cs +++ b/Timeline/Models/Http/Token.cs @@ -5,9 +5,9 @@ namespace Timeline.Models.Http public class CreateTokenRequest { [Required] - public string Username { get; set; } + public string Username { get; set; } = default!; [Required] - public string Password { get; set; } + public string Password { get; set; } = default!; // in days, optional [Range(1, 365)] public int? Expire { get; set; } @@ -15,18 +15,18 @@ namespace Timeline.Models.Http public class CreateTokenResponse { - public string Token { get; set; } - public UserInfo User { get; set; } + public string Token { get; set; } = default!; + public UserInfo User { get; set; } = default!; } public class VerifyTokenRequest { [Required] - public string Token { get; set; } + public string Token { get; set; } = default!; } public class VerifyTokenResponse { - public UserInfo User { get; set; } + public UserInfo User { get; set; } = default!; } } diff --git a/Timeline/Models/Http/User.cs b/Timeline/Models/Http/User.cs index 4308a19c..98406fec 100644 --- a/Timeline/Models/Http/User.cs +++ b/Timeline/Models/Http/User.cs @@ -6,31 +6,31 @@ namespace Timeline.Models.Http public class UserPutRequest { [Required] - public string Password { get; set; } + public string Password { get; set; } = default!; [Required] public bool? Administrator { get; set; } } public class UserPatchRequest { - public string Password { get; set; } + public string? Password { get; set; } public bool? Administrator { get; set; } } public class ChangeUsernameRequest { [Required] - public string OldUsername { get; set; } + public string OldUsername { get; set; } = default!; [Required, ValidateWith(typeof(UsernameValidator))] - public string NewUsername { get; set; } + public string NewUsername { get; set; } = default!; } public class ChangePasswordRequest { [Required] - public string OldPassword { get; set; } + public string OldPassword { get; set; } = default!; [Required] - public string NewPassword { get; set; } + public string NewPassword { get; set; } = default!; } } diff --git a/Timeline/Models/UserDetail.cs b/Timeline/Models/UserDetail.cs index 1a6c0c6a..302e3bb1 100644 --- a/Timeline/Models/UserDetail.cs +++ b/Timeline/Models/UserDetail.cs @@ -8,21 +8,21 @@ namespace Timeline.Models public class UserDetail { [MaxLength(10)] - public string Nickname { get; set; } + public string? Nickname { get; set; } [ValidateWith(typeof(UserDetailValidators.QQValidator))] [JsonProperty(PropertyName = "qq")] - public string QQ { get; set; } + public string? QQ { get; set; } [ValidateWith(typeof(UserDetailValidators.EMailValidator))] - public string Email { get; set; } + public string? Email { get; set; } [ValidateWith(typeof(UserDetailValidators.PhoneNumberValidator))] - public string PhoneNumber { get; set; } + public string? PhoneNumber { get; set; } - public string Description { get; set; } + public string? Description { get; set; } - private static string CoerceEmptyToNull(string value) + private static string? CoerceEmptyToNull(string? value) { if (string.IsNullOrEmpty(value)) return null; diff --git a/Timeline/Services/JwtService.cs b/Timeline/Services/JwtService.cs index 350c5e80..90d0c217 100644 --- a/Timeline/Services/JwtService.cs +++ b/Timeline/Services/JwtService.cs @@ -33,6 +33,12 @@ namespace Timeline.Services public const int Expired = -2001; } + private const string message = "Jwt token is bad."; + + public JwtTokenVerifyException() : base(message) { } + public JwtTokenVerifyException(string message) : base(message) { } + public JwtTokenVerifyException(string message, Exception inner) : base(message, inner) { } + public JwtTokenVerifyException(int code) : base(GetErrorMessage(code)) { ErrorCode = code; } public JwtTokenVerifyException(string message, int code) : base(message) { ErrorCode = code; } public JwtTokenVerifyException(Exception inner, int code) : base(GetErrorMessage(code), inner) { ErrorCode = code; } @@ -41,7 +47,7 @@ namespace Timeline.Services System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - public int ErrorCode { get; private set; } + public int ErrorCode { get; set; } private static string GetErrorMessage(int errorCode) { diff --git a/Timeline/Startup.cs b/Timeline/Startup.cs index fc570fdd..ddbe0840 100644 --- a/Timeline/Startup.cs +++ b/Timeline/Startup.cs @@ -13,6 +13,7 @@ using Timeline.Services; namespace Timeline { + [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static")] public class Startup { public Startup(IConfiguration configuration, IWebHostEnvironment environment) @@ -69,6 +70,7 @@ namespace Timeline services.AddMemoryCache(); } + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app) { diff --git a/Timeline/Timeline.csproj b/Timeline/Timeline.csproj index 836dfb47..c634563a 100644 --- a/Timeline/Timeline.csproj +++ b/Timeline/Timeline.csproj @@ -4,6 +4,9 @@ false 1f6fb74d-4277-4bc0-aeea-b1fc5ffb0b43 crupest + + 8.0 + enable @@ -15,8 +18,8 @@ - all - runtime; build; native; contentfiles; analyzers; buildtransitive + all + runtime; build; native; contentfiles; analyzers; buildtransitive -- cgit v1.2.3 From 38cef20cd509648d50e289cd4c7ec4a772031b12 Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Sun, 20 Oct 2019 21:32:38 +0800 Subject: ... --- Timeline/Controllers/UserController.cs | 91 ++++++++---- Timeline/GlobalSuppressions.cs | 3 +- .../Helpers/StringLocalizerFactoryExtensions.cs | 14 ++ Timeline/InvalidBranchException.cs | 16 +++ Timeline/Models/Http/Common.cs | 36 +++-- Timeline/Resources/Common.Designer.cs | 72 ++++++++++ Timeline/Resources/Common.resx | 123 +++++++++++++++++ .../Controllers/TokenController.Designer.cs | 153 --------------------- .../Resources/Controllers/UserController.en.resx | 129 +++++++++++++++++ Timeline/Resources/Controllers/UserController.resx | 141 +++++++++++++++++++ .../Resources/Controllers/UserController.zh.resx | 129 +++++++++++++++++ Timeline/Resources/Http/Common.en.resx | 132 ++++++++++++++++++ Timeline/Resources/Http/Common.zh.resx | 132 ++++++++++++++++++ Timeline/Timeline.csproj | 20 +-- 14 files changed, 991 insertions(+), 200 deletions(-) create mode 100644 Timeline/Helpers/StringLocalizerFactoryExtensions.cs create mode 100644 Timeline/InvalidBranchException.cs create mode 100644 Timeline/Resources/Common.Designer.cs create mode 100644 Timeline/Resources/Common.resx delete mode 100644 Timeline/Resources/Controllers/TokenController.Designer.cs create mode 100644 Timeline/Resources/Controllers/UserController.en.resx create mode 100644 Timeline/Resources/Controllers/UserController.resx create mode 100644 Timeline/Resources/Controllers/UserController.zh.resx create mode 100644 Timeline/Resources/Http/Common.en.resx create mode 100644 Timeline/Resources/Http/Common.zh.resx (limited to 'Timeline/Models/Http/Common.cs') diff --git a/Timeline/Controllers/UserController.cs b/Timeline/Controllers/UserController.cs index c0cd3cdb..b01d06fb 100644 --- a/Timeline/Controllers/UserController.cs +++ b/Timeline/Controllers/UserController.cs @@ -1,40 +1,73 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; -using System; using System.Threading.Tasks; using Timeline.Authenticate; +using Timeline.Helpers; using Timeline.Models; using Timeline.Models.Http; using Timeline.Services; -using static Timeline.Helpers.MyLogHelper; -namespace Timeline.Controllers +namespace Timeline { - [ApiController] - public class UserController : Controller + public static partial class ErrorCodes { - public static class ErrorCodes + public static partial class Http { - public const int Get_NotExist = -1001; + public static class User // bbb = 002 + { + public static class Get // cc = 01 + { + public const int NotExist = 10020101; // dd = 01 + } - public const int Put_BadUsername = -2001; + public static class Put // cc = 02 + { + public const int BadUsername = 10020201; // dd = 01 + } + + public static class Patch // cc = 03 + { + public const int NotExist = 10020301; // dd = 01 + } - public const int Patch_NotExist = -3001; + 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 const int ChangeUsername_NotExist = -4001; - public const int ChangeUsername_AlreadyExist = -4002; + public static class ChangePassword // cc = 12 + { + public const int BadOldPassword = 10021201; // dd = 01 + } + } - public const int ChangePassword_BadOldPassword = -5001; + } } + } +} + +namespace Timeline.Controllers +{ + [ApiController] + public class UserController : Controller + { private readonly ILogger _logger; private readonly IUserService _userService; + private readonly IStringLocalizerFactory _localizerFactory; + private readonly IStringLocalizer _localizer; - public UserController(ILogger logger, IUserService userService) + public UserController(ILogger logger, IUserService userService, IStringLocalizerFactory localizerFactory) { _logger = logger; _userService = userService; + _localizerFactory = localizerFactory; + _localizer = localizerFactory.Create(GetType()); } [HttpGet("users"), AdminAuthorize] @@ -49,8 +82,8 @@ namespace Timeline.Controllers var user = await _userService.GetUser(username); if (user == null) { - _logger.LogInformation(FormatLogMessage("Attempt to get a non-existent user.", Pair("Username", username))); - return NotFound(new CommonResponse(ErrorCodes.Get_NotExist, "The user does not exist.")); + _logger.LogInformation(Log.Format(_localizer["LogGetUserNotExist"], ("Username", username))); + return NotFound(new CommonResponse(ErrorCodes.Http.User.Get.NotExist, _localizer["ErrorGetUserNotExist"])); } return Ok(user); } @@ -60,23 +93,23 @@ namespace Timeline.Controllers { try { - var result = await _userService.PutUser(username, request.Password, request.Administrator.Value); + var result = await _userService.PutUser(username, request.Password, request.Administrator!.Value); switch (result) { case PutResult.Created: - _logger.LogInformation(FormatLogMessage("A user is created.", Pair("Username", username))); - return CreatedAtAction("Get", new { username }, CommonPutResponse.Create()); + _logger.LogInformation(Log.Format(_localizer["LogPutCreate"], ("Username", username))); + return CreatedAtAction("Get", new { username }, CommonPutResponse.Create(_localizerFactory)); case PutResult.Modified: - _logger.LogInformation(FormatLogMessage("A user is modified.", Pair("Username", username))); - return Ok(CommonPutResponse.Modify()); + _logger.LogInformation(Log.Format(_localizer["LogPutModify"], ("Username", username))); + return Ok(CommonPutResponse.Modify(_localizerFactory)); default: - throw new Exception("Unreachable code."); + throw new InvalidBranchException(); } } catch (UsernameBadFormatException e) { - _logger.LogInformation(e, FormatLogMessage("Attempt to create a user with bad username failed.", Pair("Username", username))); - return BadRequest(new CommonResponse(ErrorCodes.Put_BadUsername, "Username is of bad format.")); + _logger.LogInformation(e, Log.Format(_localizer["LogPutBadUsername"], ("Username", username))); + return BadRequest(new CommonResponse(ErrorCodes.Http.User.Put.BadUsername, _localizer["ErrorPutBadUsername"])); } } @@ -90,8 +123,8 @@ namespace Timeline.Controllers } catch (UserNotExistException e) { - _logger.LogInformation(e, FormatLogMessage("Attempt to patch a non-existent user.", Pair("Username", username))); - return NotFound(new CommonResponse(ErrorCodes.Patch_NotExist, "The user does not exist.")); + _logger.LogInformation(e, Log.Format(_localizer["LogPatchUserNotExist"], ("Username", username))); + return NotFound(new CommonResponse(ErrorCodes.Http.User.Patch.NotExist, _localizer["ErrorPatchUserNotExist"])); } } @@ -101,13 +134,13 @@ namespace Timeline.Controllers try { await _userService.DeleteUser(username); - _logger.LogInformation(FormatLogMessage("A user is deleted.", Pair("Username", username))); - return Ok(CommonDeleteResponse.Delete()); + _logger.LogInformation(Log.Format(_localizer["LogDeleteDelete"], ("Username", username))); + return Ok(CommonDeleteResponse.Delete(_localizerFactory)); } catch (UserNotExistException e) { - _logger.LogInformation(e, FormatLogMessage("Attempt to delete a non-existent user.", Pair("Username", username))); - return Ok(CommonDeleteResponse.NotExist()); + _logger.LogInformation(e, Log.Format(_localizer["LogDeleteUserNotExist"], ("Username", username))); + return Ok(CommonDeleteResponse.NotExist(_localizerFactory)); } } diff --git a/Timeline/GlobalSuppressions.cs b/Timeline/GlobalSuppressions.cs index 3c9c8341..6c89b230 100644 --- a/Timeline/GlobalSuppressions.cs +++ b/Timeline/GlobalSuppressions.cs @@ -6,4 +6,5 @@ [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("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("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Error code constant identifiers use nested names.", Scope = "type", Target = "Timeline.ErrorCodes")] +[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")] diff --git a/Timeline/Helpers/StringLocalizerFactoryExtensions.cs b/Timeline/Helpers/StringLocalizerFactoryExtensions.cs new file mode 100644 index 00000000..3cb561f5 --- /dev/null +++ b/Timeline/Helpers/StringLocalizerFactoryExtensions.cs @@ -0,0 +1,14 @@ + +using Microsoft.Extensions.Localization; +using System.Reflection; + +namespace Timeline.Helpers +{ + internal static class StringLocalizerFactoryExtensions + { + internal static IStringLocalizer Create(this IStringLocalizerFactory factory, string basename) + { + return factory.Create(basename, new AssemblyName(typeof(StringLocalizerFactoryExtensions).Assembly.FullName!).Name); + } + } +} \ No newline at end of file diff --git a/Timeline/InvalidBranchException.cs b/Timeline/InvalidBranchException.cs new file mode 100644 index 00000000..32937c5d --- /dev/null +++ b/Timeline/InvalidBranchException.cs @@ -0,0 +1,16 @@ +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 6f6dbc1e..2735e43c 100644 --- a/Timeline/Models/Http/Common.cs +++ b/Timeline/Models/Http/Common.cs @@ -1,3 +1,6 @@ +using Microsoft.Extensions.Localization; +using Timeline.Helpers; + namespace Timeline.Models.Http { public class CommonResponse @@ -55,7 +58,7 @@ namespace Timeline.Models.Http Data = data; } - public T Data { get; set; } + public T Data { get; set; } = default!; } public static class CommonPutResponse @@ -70,10 +73,18 @@ namespace Timeline.Models.Http public bool Create { get; set; } } - public static CommonDataResponse Create() => - new CommonDataResponse(0, "A new item is created.", new ResponseData(true)); - public static CommonDataResponse Modify() => - new CommonDataResponse(0, "An existent item is modified.", new ResponseData(false)); + internal static CommonDataResponse Create(IStringLocalizerFactory localizerFactory) + { + var localizer = localizerFactory.Create("Http.Common"); + return new CommonDataResponse(0, localizer["ResponsePutCreate"], new ResponseData(true)); + } + + internal static CommonDataResponse Modify(IStringLocalizerFactory localizerFactory) + { + var localizer = localizerFactory.Create("Http.Common"); + return new CommonDataResponse(0, localizer["ResponsePutModify"], new ResponseData(false)); + + } } public static class CommonDeleteResponse @@ -88,9 +99,16 @@ namespace Timeline.Models.Http public bool Delete { get; set; } } - public static CommonDataResponse Delete() => - new CommonDataResponse(0, "An existent item is deleted.", new ResponseData(true)); - public static CommonDataResponse NotExist() => - new CommonDataResponse(0, "The item does not exist.", new ResponseData(false)); + internal static CommonDataResponse Delete(IStringLocalizerFactory localizerFactory) + { + var localizer = localizerFactory.Create("Http.Common"); + return new CommonDataResponse(0, localizer["ResponseDeleteDelete"], new ResponseData(true)); + } + + internal static CommonDataResponse NotExist(IStringLocalizerFactory localizerFactory) + { + var localizer = localizerFactory.Create("Http.Common"); + return new CommonDataResponse(0, localizer["ResponseDeleteNotExist"], new ResponseData(false)); + } } } diff --git a/Timeline/Resources/Common.Designer.cs b/Timeline/Resources/Common.Designer.cs new file mode 100644 index 00000000..4f1c8e3f --- /dev/null +++ b/Timeline/Resources/Common.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 { + 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 new file mode 100644 index 00000000..8a036996 --- /dev/null +++ b/Timeline/Resources/Common.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 + + + The branch is invalid. Normally this branch is not reachable. + + \ No newline at end of file diff --git a/Timeline/Resources/Controllers/TokenController.Designer.cs b/Timeline/Resources/Controllers/TokenController.Designer.cs deleted file mode 100644 index 0dcfb79e..00000000 --- a/Timeline/Resources/Controllers/TokenController.Designer.cs +++ /dev/null @@ -1,153 +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 { - 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 The password is wrong.. - /// - public static string LogBadPassword { - get { - return ResourceManager.GetString("LogBadPassword", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A user failed to create a token.. - /// - public static string LogCreateFailure { - get { - return ResourceManager.GetString("LogCreateFailure", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A user succeeded to create a token.. - /// - public static string LogCreateSuccess { - get { - return ResourceManager.GetString("LogCreateSuccess", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The user does not exist.. - /// - public static string LogUserNotExist { - get { - return ResourceManager.GetString("LogUserNotExist", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The token is of bad format. It might not be created by the server.. - /// - public static string LogVerifyBadFormat { - get { - return ResourceManager.GetString("LogVerifyBadFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The token is expired.. - /// - public static string LogVerifyExpire { - get { - return ResourceManager.GetString("LogVerifyExpire", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A token failed to be verified.. - /// - public static string LogVerifyFailure { - get { - return ResourceManager.GetString("LogVerifyFailure", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Token has an old version. User might have update some info.. - /// - public static string LogVerifyOldVersion { - get { - return ResourceManager.GetString("LogVerifyOldVersion", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A token succeeded to be verified.. - /// - public static string LogVerifySuccess { - get { - return ResourceManager.GetString("LogVerifySuccess", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to User does not exist. Administrator might have deleted this user.. - /// - public static string LogVerifyUserNotExist { - get { - return ResourceManager.GetString("LogVerifyUserNotExist", resourceCulture); - } - } - } -} diff --git a/Timeline/Resources/Controllers/UserController.en.resx b/Timeline/Resources/Controllers/UserController.en.resx new file mode 100644 index 00000000..f0fb372a --- /dev/null +++ b/Timeline/Resources/Controllers/UserController.en.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 user does not exist. + + + Can't patch a user that does not exist. + + + Username is of bad format. + + \ No newline at end of file diff --git a/Timeline/Resources/Controllers/UserController.resx b/Timeline/Resources/Controllers/UserController.resx new file mode 100644 index 00000000..901f8aab --- /dev/null +++ b/Timeline/Resources/Controllers/UserController.resx @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 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 new file mode 100644 index 00000000..519f08f6 --- /dev/null +++ b/Timeline/Resources/Controllers/UserController.zh.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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/Http/Common.en.resx b/Timeline/Resources/Http/Common.en.resx new file mode 100644 index 00000000..40d44191 --- /dev/null +++ b/Timeline/Resources/Http/Common.en.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 + + + An existent item is deleted. + + + The item does not exist, so nothing is changed. + + + A new item is created. + + + An existent item is modified. + + \ No newline at end of file diff --git a/Timeline/Resources/Http/Common.zh.resx b/Timeline/Resources/Http/Common.zh.resx new file mode 100644 index 00000000..b6d955d9 --- /dev/null +++ b/Timeline/Resources/Http/Common.zh.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 + + + 删除了一个项目。 + + + 要删除的项目不存在,什么都没有修改。 + + + 创建了一个新项目。 + + + 修改了一个已存在的项目。 + + \ No newline at end of file diff --git a/Timeline/Timeline.csproj b/Timeline/Timeline.csproj index 302d1677..d2d3fa67 100644 --- a/Timeline/Timeline.csproj +++ b/Timeline/Timeline.csproj @@ -28,27 +28,31 @@ - + True True - TokenController.resx + Common.resx + + ResXFileCodeGenerator + Common.Designer.cs + Designer - TokenController.Designer.cs - PublicResXFileCodeGenerator + Designer - TokenController.en.Designer.cs - PublicResXFileCodeGenerator + - 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/Models/Http/Common.cs') 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 From c442b7ad597f430b186dd8019de70332b574c4ba Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Mon, 21 Oct 2019 20:47:31 +0800 Subject: ... --- Timeline.Tests/Controllers/TokenControllerTest.cs | 6 +- Timeline.Tests/Controllers/UserControllerTest.cs | 6 +- Timeline.Tests/IntegratedTests/UserDetailTest.cs | 145 ----------- Timeline.Tests/UserDetailServiceTest.cs | 271 --------------------- Timeline.Tests/UserDetailValidatorTest.cs | 97 -------- Timeline.Tests/UsernameValidatorUnitTest.cs | 7 +- Timeline/Controllers/TokenController.cs | 38 +-- Timeline/Controllers/UserController.cs | 31 +-- Timeline/Controllers/UserDetailController.cs | 96 -------- Timeline/Models/Http/Common.cs | 8 +- Timeline/Models/PutResult.cs | 4 +- Timeline/Models/UserDetail.cs | 45 ---- Timeline/Models/Validation/UserDetailValidator.cs | 116 --------- Timeline/Models/Validation/UsernameValidator.cs | 5 +- Timeline/Models/Validation/Validator.cs | 44 ++-- .../Controllers/TokenController.Designer.cs | 153 ++++++++++++ .../Controllers/UserController.Designer.cs | 171 +++++++++++++ Timeline/Resources/Http/Common.en.resx | 132 ---------- Timeline/Resources/Http/Common.zh.resx | 132 ---------- Timeline/Resources/Models/Http/Common.en.resx | 132 ++++++++++ Timeline/Resources/Models/Http/Common.zh.resx | 132 ++++++++++ .../Models/Validation/Validator.Designer.cs | 81 ++++++ .../Resources/Models/Validation/Validator.en.resx | 129 ++++++++++ .../Resources/Models/Validation/Validator.resx | 126 ++++++++++ .../Resources/Models/Validation/Validator.zh.resx | 129 ++++++++++ Timeline/Resources/Services/Exception.Designer.cs | 189 ++++++++++++++ Timeline/Resources/Services/Exception.resx | 162 ++++++++++++ Timeline/Services/BadPasswordException.cs | 27 ++ Timeline/Services/JwtBadVersionException.cs | 36 +++ Timeline/Services/JwtService.cs | 76 +----- Timeline/Services/JwtVerifyException.cs | 59 +++++ Timeline/Services/UserDetailService.cs | 135 ---------- Timeline/Services/UserNotExistException.cs | 41 ++++ Timeline/Services/UserService.cs | 195 ++------------- Timeline/Services/UsernameBadFormatException.cs | 27 ++ Timeline/Services/UsernameConfictException.cs | 25 ++ Timeline/Startup.cs | 1 - Timeline/Timeline.csproj | 34 ++- 38 files changed, 1768 insertions(+), 1475 deletions(-) delete mode 100644 Timeline.Tests/IntegratedTests/UserDetailTest.cs delete mode 100644 Timeline.Tests/UserDetailServiceTest.cs delete mode 100644 Timeline.Tests/UserDetailValidatorTest.cs delete mode 100644 Timeline/Controllers/UserDetailController.cs delete mode 100644 Timeline/Models/UserDetail.cs delete mode 100644 Timeline/Models/Validation/UserDetailValidator.cs create mode 100644 Timeline/Resources/Controllers/TokenController.Designer.cs create mode 100644 Timeline/Resources/Controllers/UserController.Designer.cs delete mode 100644 Timeline/Resources/Http/Common.en.resx delete mode 100644 Timeline/Resources/Http/Common.zh.resx create mode 100644 Timeline/Resources/Models/Http/Common.en.resx create mode 100644 Timeline/Resources/Models/Http/Common.zh.resx create mode 100644 Timeline/Resources/Models/Validation/Validator.Designer.cs create mode 100644 Timeline/Resources/Models/Validation/Validator.en.resx create mode 100644 Timeline/Resources/Models/Validation/Validator.resx create mode 100644 Timeline/Resources/Models/Validation/Validator.zh.resx create mode 100644 Timeline/Resources/Services/Exception.Designer.cs create mode 100644 Timeline/Resources/Services/Exception.resx create mode 100644 Timeline/Services/BadPasswordException.cs create mode 100644 Timeline/Services/JwtBadVersionException.cs create mode 100644 Timeline/Services/JwtVerifyException.cs delete mode 100644 Timeline/Services/UserDetailService.cs create mode 100644 Timeline/Services/UserNotExistException.cs create mode 100644 Timeline/Services/UsernameBadFormatException.cs create mode 100644 Timeline/Services/UsernameConfictException.cs (limited to 'Timeline/Models/Http/Common.cs') diff --git a/Timeline.Tests/Controllers/TokenControllerTest.cs b/Timeline.Tests/Controllers/TokenControllerTest.cs index 71520e77..53b6c606 100644 --- a/Timeline.Tests/Controllers/TokenControllerTest.cs +++ b/Timeline.Tests/Controllers/TokenControllerTest.cs @@ -101,9 +101,9 @@ namespace Timeline.Tests.Controllers public static IEnumerable Verify_BadRequest_Data() { - yield return new object[] { new JwtTokenVerifyException(JwtTokenVerifyException.ErrorCodes.Expired), Verify.Expired }; - yield return new object[] { new JwtTokenVerifyException(JwtTokenVerifyException.ErrorCodes.IdClaimBadFormat), Verify.BadFormat }; - yield return new object[] { new BadTokenVersionException(), Verify.OldVersion }; + yield return new object[] { new JwtVerifyException(JwtVerifyException.ErrorCodes.Expired), Verify.Expired }; + 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 ddbc3fbc..471ed851 100644 --- a/Timeline.Tests/Controllers/UserControllerTest.cs +++ b/Timeline.Tests/Controllers/UserControllerTest.cs @@ -69,8 +69,8 @@ namespace Timeline.Tests.Controllers } [Theory] - [InlineData(PutResult.Created, true)] - [InlineData(PutResult.Modified, false)] + [InlineData(PutResult.Create, true)] + [InlineData(PutResult.Modify, false)] public async Task Put_Success(PutResult result, bool create) { const string username = "aaa"; @@ -176,7 +176,7 @@ namespace Timeline.Tests.Controllers [Theory] [InlineData(typeof(UserNotExistException), Op.ChangeUsername.NotExist)] - [InlineData(typeof(UserAlreadyExistException), Op.ChangeUsername.AlreadyExist)] + [InlineData(typeof(UsernameConfictException), Op.ChangeUsername.AlreadyExist)] public async Task Op_ChangeUsername_Failure(Type exceptionType, int code) { const string oldUsername = "aaa"; diff --git a/Timeline.Tests/IntegratedTests/UserDetailTest.cs b/Timeline.Tests/IntegratedTests/UserDetailTest.cs deleted file mode 100644 index 4d268efa..00000000 --- a/Timeline.Tests/IntegratedTests/UserDetailTest.cs +++ /dev/null @@ -1,145 +0,0 @@ -using FluentAssertions; -using Microsoft.AspNetCore.Mvc.Testing; -using System; -using System.Net; -using System.Threading.Tasks; -using Timeline.Controllers; -using Timeline.Models; -using Timeline.Tests.Helpers; -using Timeline.Tests.Helpers.Authentication; -using Timeline.Tests.Mock.Data; -using Xunit; - -namespace Timeline.Tests.IntegratedTests -{ - public class UserDetailTest : IClassFixture>, IDisposable - { - private readonly TestApplication _testApp; - private readonly WebApplicationFactory _factory; - - public UserDetailTest(WebApplicationFactory factory) - { - _testApp = new TestApplication(factory); - _factory = _testApp.Factory; - } - - public void Dispose() - { - _testApp.Dispose(); - } - - [Fact] - public async Task TestAsUser() - { - using (var client = await _factory.CreateClientAsUser()) - { - { - var res = await client.GetAsync($"users/usernotexist/nickname"); - res.Should().HaveStatusCode(404) - .And.Should().HaveCommonBody().Which.Code.Should().Be(UserDetailController.ErrorCodes.GetNickname_UserNotExist); - } - - { - var res = await client.GetAsync($"users/usernotexist/details"); - res.Should().HaveStatusCode(404) - .And.Should().HaveCommonBody().Which.Code.Should().Be(UserDetailController.ErrorCodes.Get_UserNotExist); - } - - async Task GetAndTest(UserDetail d) - { - var res = await client.GetAsync($"users/{MockUser.User.Username}/details"); - res.Should().HaveStatusCode(200) - .And.Should().HaveJsonBody() - .Which.Should().BeEquivalentTo(d); - } - - await GetAndTest(new UserDetail()); - - { - var res = await client.PatchAsJsonAsync($"users/{MockUser.Admin.Username}/details", new UserDetail()); - res.Should().HaveStatusCode(HttpStatusCode.Forbidden) - .And.Should().HaveCommonBody().Which.Code.Should().Be(UserDetailController.ErrorCodes.Patch_Forbid); - } - - { - var res = await client.PatchAsJsonAsync($"users/{MockUser.User.Username}/details", new UserDetail - { - Nickname = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - QQ = "aaaaaaa", - Email = "aaaaaa", - PhoneNumber = "aaaaaaaa" - }); - var body = res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.Should().HaveCommonBody().Which; - body.Code.Should().Be(ErrorCodes.Http.Common.InvalidModel); - foreach (var key in new string[] { "nickname", "qq", "email", "phonenumber" }) - { - body.Message.Should().ContainEquivalentOf(key); - } - } - - - var detail = new UserDetail - { - Nickname = "aaa", - QQ = "1234567", - Email = "aaaa@aaa.net", - Description = "aaaaaaaaa" - }; - - { - var res = await client.PatchAsJsonAsync($"users/{MockUser.User.Username}/details", detail); - res.Should().HaveStatusCode(200); - await GetAndTest(detail); - } - - { - var res = await client.GetAsync($"users/{MockUser.User.Username}/nickname"); - res.Should().HaveStatusCode(200).And.Should().HaveJsonBody() - .Which.Should().BeEquivalentTo(new UserDetail - { - Nickname = detail.Nickname - }); - } - - var detail2 = new UserDetail - { - QQ = "", - PhoneNumber = "12345678910", - Description = "bbbbbbbb" - }; - - { - var res = await client.PatchAsJsonAsync($"users/{MockUser.User.Username}/details", detail2); - res.Should().HaveStatusCode(200); - await GetAndTest(new UserDetail - { - Nickname = detail.Nickname, - QQ = null, - Email = detail.Email, - PhoneNumber = detail2.PhoneNumber, - Description = detail2.Description - }); - } - } - } - - [Fact] - public async Task TestAsAdmin() - { - using (var client = await _factory.CreateClientAsAdmin()) - { - { - var res = await client.PatchAsJsonAsync($"users/{MockUser.User.Username}/details", new UserDetail()); - res.Should().HaveStatusCode(200); - } - - { - var res = await client.PatchAsJsonAsync($"users/usernotexist/details", new UserDetail()); - res.Should().HaveStatusCode(404) - .And.Should().HaveCommonBody().Which.Code.Should().Be(UserDetailController.ErrorCodes.Patch_UserNotExist); - } - } - } - } -} \ No newline at end of file diff --git a/Timeline.Tests/UserDetailServiceTest.cs b/Timeline.Tests/UserDetailServiceTest.cs deleted file mode 100644 index d16d1a40..00000000 --- a/Timeline.Tests/UserDetailServiceTest.cs +++ /dev/null @@ -1,271 +0,0 @@ -using FluentAssertions; -using Microsoft.Extensions.Logging.Abstractions; -using System; -using System.Linq; -using System.Threading.Tasks; -using Timeline.Entities; -using Timeline.Models; -using Timeline.Services; -using Timeline.Tests.Helpers; -using Timeline.Tests.Mock.Data; -using Xunit; - -namespace Timeline.Tests -{ - public class UserDetailServiceTest : IDisposable - { - private readonly TestDatabase _database; - - private readonly UserDetailService _service; - - public UserDetailServiceTest() - { - _database = new TestDatabase(); - - _service = new UserDetailService(NullLogger.Instance, _database.DatabaseContext); - } - - public void Dispose() - { - _database.Dispose(); - } - - [Fact] - public void GetNickname_ShouldThrow_ArgumentException() - { - // no need to await because arguments are checked syncronizedly. - _service.Invoking(s => s.GetUserNickname(null)).Should().Throw() - .Where(e => e.ParamName == "username" && e.Message.Contains("null", StringComparison.OrdinalIgnoreCase)); - _service.Invoking(s => s.GetUserNickname("")).Should().Throw() - .Where(e => e.ParamName == "username" && e.Message.Contains("empty", StringComparison.OrdinalIgnoreCase)); - } - - [Fact] - public void GetNickname_ShouldThrow_UserNotExistException() - { - const string username = "usernotexist"; - _service.Awaiting(s => s.GetUserNickname(username)).Should().Throw() - .Where(e => e.Username == username); - } - - [Fact] - public async Task GetNickname_Should_Create_And_ReturnDefault() - { - { - var nickname = await _service.GetUserNickname(MockUser.User.Username); - nickname.Should().BeNull(); - } - - { - var context = _database.DatabaseContext; - var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUser.User.Username); - var detail = context.UserDetails.Where(e => e.UserId == userId).Single(); - detail.Nickname.Should().BeNullOrEmpty(); - detail.QQ.Should().BeNullOrEmpty(); - detail.Email.Should().BeNullOrEmpty(); - detail.PhoneNumber.Should().BeNullOrEmpty(); - detail.Description.Should().BeNullOrEmpty(); - } - } - - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData("nickname")] - public async Task GetNickname_Should_ReturnData(string nickname) - { - { - var context = _database.DatabaseContext; - var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUser.User.Username); - var entity = new UserDetailEntity - { - Nickname = nickname, - UserId = userId - }; - context.Add(entity); - await context.SaveChangesAsync(); - } - - { - var n = await _service.GetUserNickname(MockUser.User.Username); - n.Should().Equals(string.IsNullOrEmpty(nickname) ? null : nickname); - } - } - - [Fact] - public void GetDetail_ShouldThrow_ArgumentException() - { - // no need to await because arguments are checked syncronizedly. - _service.Invoking(s => s.GetUserDetail(null)).Should().Throw() - .Where(e => e.ParamName == "username" && e.Message.Contains("null", StringComparison.OrdinalIgnoreCase)); - _service.Invoking(s => s.GetUserDetail("")).Should().Throw() - .Where(e => e.ParamName == "username" && e.Message.Contains("empty", StringComparison.OrdinalIgnoreCase)); - } - - [Fact] - public void GetDetail_ShouldThrow_UserNotExistException() - { - const string username = "usernotexist"; - _service.Awaiting(s => s.GetUserDetail(username)).Should().Throw() - .Where(e => e.Username == username); - } - - [Fact] - public async Task GetDetail_Should_Create_And_ReturnDefault() - { - { - var detail = await _service.GetUserDetail(MockUser.User.Username); - detail.Should().BeEquivalentTo(new UserDetail()); - } - - { - var context = _database.DatabaseContext; - var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUser.User.Username); - var detail = context.UserDetails.Where(e => e.UserId == userId).Single(); - detail.Nickname.Should().BeNullOrEmpty(); - detail.QQ.Should().BeNullOrEmpty(); - detail.Email.Should().BeNullOrEmpty(); - detail.PhoneNumber.Should().BeNullOrEmpty(); - detail.Description.Should().BeNullOrEmpty(); - } - } - - [Fact] - public async Task GetDetail_Should_ReturnData() - { - const string email = "ha@aaa.net"; - const string description = "hahaha"; - - - { - var context = _database.DatabaseContext; - var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUser.User.Username); - var entity = new UserDetailEntity - { - Email = email, - Description = description, - UserId = userId - }; - context.Add(entity); - await context.SaveChangesAsync(); - } - - { - var detail = await _service.GetUserDetail(MockUser.User.Username); - detail.Should().BeEquivalentTo(new UserDetail - { - Email = email, - Description = description - }); - } - } - - [Fact] - public void UpdateDetail_ShouldThrow_ArgumentException() - { - // no need to await because arguments are checked syncronizedly. - _service.Invoking(s => s.UpdateUserDetail(null, new UserDetail())).Should().Throw() - .Where(e => e.ParamName == "username" && e.Message.Contains("null", StringComparison.OrdinalIgnoreCase)); - _service.Invoking(s => s.UpdateUserDetail("", new UserDetail())).Should().Throw() - .Where(e => e.ParamName == "username" && e.Message.Contains("empty", StringComparison.OrdinalIgnoreCase)); - _service.Invoking(s => s.UpdateUserDetail("aaa", null)).Should().Throw() - .Where(e => e.ParamName == "detail"); - } - - [Fact] - public void UpdateDetail_ShouldThrow_UserNotExistException() - { - const string username = "usernotexist"; - _service.Awaiting(s => s.UpdateUserDetail(username, new UserDetail())).Should().Throw() - .Where(e => e.Username == username); - } - - [Fact] - public async Task UpdateDetail_Empty_Should_Work() - { - await _service.UpdateUserDetail(MockUser.User.Username, new UserDetail()); - - var context = _database.DatabaseContext; - var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUser.User.Username); - var entity = context.UserDetails.Where(e => e.UserId == userId).Single(); - entity.Nickname.Should().BeNullOrEmpty(); - entity.QQ.Should().BeNullOrEmpty(); - entity.Email.Should().BeNullOrEmpty(); - entity.PhoneNumber.Should().BeNullOrEmpty(); - entity.Description.Should().BeNullOrEmpty(); - } - - [Theory] - [InlineData(nameof(UserDetail.Nickname), nameof(UserDetailEntity.Nickname), "aaaa", "bbbb")] - [InlineData(nameof(UserDetail.QQ), nameof(UserDetailEntity.QQ), "12345678910", "987654321")] - [InlineData(nameof(UserDetail.Email), nameof(UserDetailEntity.Email), "aaa@aaa.aaa", "bbb@bbb.bbb")] - [InlineData(nameof(UserDetail.PhoneNumber), nameof(UserDetailEntity.PhoneNumber), "12345678910", "987654321")] - [InlineData(nameof(UserDetail.Description), nameof(UserDetailEntity.Description), "descriptionA", "descriptionB")] - public async Task UpdateDetail_Single_Should_Work(string propertyName, string entityPropertyName, string mockData1, string mockData2) - { - - UserDetail CreateWith(string propertyValue) - { - var detail = new UserDetail(); - typeof(UserDetail).GetProperty(propertyName).SetValue(detail, propertyValue); - return detail; - } - - await _service.UpdateUserDetail(MockUser.User.Username, CreateWith(mockData1)); - - var context = _database.DatabaseContext; - var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUser.User.Username); - var entity = context.UserDetails.Where(e => e.UserId == userId).Single(); - - void TestWith(string propertyValue) - { - typeof(UserDetailEntity).GetProperty(entityPropertyName).GetValue(entity).Should().Equals(propertyValue); - foreach (var p in typeof(UserDetailEntity).GetProperties().Where(p => p.Name != entityPropertyName)) - (p.GetValue(entity) as string).Should().BeNullOrEmpty(); - } - - TestWith(mockData1); - - await _service.UpdateUserDetail(MockUser.User.Username, CreateWith(mockData2)); - TestWith(mockData2); - await _service.UpdateUserDetail(MockUser.User.Username, CreateWith("")); - TestWith(""); - } - - [Fact] - public async Task UpdateDetail_Multiple_Should_Work() - { - var detail = new UserDetail - { - QQ = "12345678", - Email = "aaa@aaa.aaa", - PhoneNumber = "11111111111", - Description = "aaaaaaaaaa" - }; - - await _service.UpdateUserDetail(MockUser.User.Username, detail); - - var context = _database.DatabaseContext; - var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUser.User.Username); - var entity = context.UserDetails.Where(e => e.UserId == userId).Single(); - entity.QQ.Should().Equals(detail.QQ); - entity.Email.Should().Equals(detail.Email); - entity.PhoneNumber.Should().Equals(detail.PhoneNumber); - entity.Description.Should().Equals(detail.Description); - - var detail2 = new UserDetail - { - QQ = null, - Email = "bbb@bbb.bbb", - PhoneNumber = "", - Description = "bbbbbbbbb" - }; - - await _service.UpdateUserDetail(MockUser.User.Username, detail2); - entity.QQ.Should().Equals(detail.QQ); - entity.Email.Should().Equals(detail2.Email); - entity.PhoneNumber.Should().BeNullOrEmpty(); - entity.Description.Should().Equals(detail2.Description); - } - } -} diff --git a/Timeline.Tests/UserDetailValidatorTest.cs b/Timeline.Tests/UserDetailValidatorTest.cs deleted file mode 100644 index 9b112946..00000000 --- a/Timeline.Tests/UserDetailValidatorTest.cs +++ /dev/null @@ -1,97 +0,0 @@ -using FluentAssertions; -using System.Collections.Generic; -using Timeline.Models.Validation; -using Xunit; - -namespace Timeline.Tests -{ - public static class UserDetailValidatorsTest - { - private static void SucceedWith(object value) where TValidator : class, IValidator, new() - { - var result = new TValidator().Validate(value, out var message); - result.Should().BeTrue(); - message.Should().Equals(ValidationConstants.SuccessMessage); - } - - private static void FailWith(object value, params string[] messageContains) where TValidator : class, IValidator, new() - { - var result = new TValidator().Validate(value, out var message); - result.Should().BeFalse(); - - foreach (var m in messageContains) - { - message.Should().ContainEquivalentOf(m); - } - } - - public class QQ - { - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData("12345678")] - public void Success(object qq) - { - SucceedWith(qq); - } - - [Theory] - [InlineData(123, "type")] - [InlineData("123", "short")] - [InlineData("111111111111111111111111111111111111", "long")] - [InlineData("aaaaaaaa", "digit")] - public void Fail(object qq, string messageContains) - { - FailWith(qq, messageContains); - } - } - - public class EMail - { - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData("aaa@aaa.net")] - public void Success(object email) - { - SucceedWith(email); - } - - public static IEnumerable FailTestData() - { - yield return new object[] { 123, "type" }; - yield return new object[] { new string('a', 100), "long" }; - yield return new object[] { "aaaaaaaa", "format" }; - } - - [Theory] - [MemberData(nameof(FailTestData))] - public void Fail(object email, string messageContains) - { - FailWith(email, messageContains); - } - } - - public class PhoneNumber - { - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData("12345678910")] - public void Success(object phoneNumber) - { - SucceedWith(phoneNumber); - } - - [Theory] - [InlineData(123, "type")] - [InlineData("111111111111111111111111111111111111", "long")] - [InlineData("aaaaaaaa", "digit")] - public void Fail(object phoneNumber, string messageContains) - { - FailWith(phoneNumber, messageContains); - } - } - } -} diff --git a/Timeline.Tests/UsernameValidatorUnitTest.cs b/Timeline.Tests/UsernameValidatorUnitTest.cs index 6a635ba1..9a80a3a2 100644 --- a/Timeline.Tests/UsernameValidatorUnitTest.cs +++ b/Timeline.Tests/UsernameValidatorUnitTest.cs @@ -1,5 +1,6 @@ using FluentAssertions; using Timeline.Models.Validation; +using Timeline.Tests.Mock.Services; using Xunit; namespace Timeline.Tests @@ -15,14 +16,14 @@ namespace Timeline.Tests private string FailAndMessage(string username) { - var result = _validator.Validate(username, out var message); + var result = _validator.Validate(username, TestStringLocalizerFactory.Create(), out var message); result.Should().BeFalse(); return message; } private void Succeed(string username) { - _validator.Validate(username, out var message).Should().BeTrue(); + _validator.Validate(username, TestStringLocalizerFactory.Create(), out var message).Should().BeTrue(); message.Should().Be(ValidationConstants.SuccessMessage); } @@ -35,7 +36,7 @@ namespace Timeline.Tests [Fact] public void NotString() { - var result = _validator.Validate(123, out var message); + var result = _validator.Validate(123, TestStringLocalizerFactory.Create(), out var message); result.Should().BeFalse(); message.Should().ContainEquivalentOf("type"); } diff --git a/Timeline/Controllers/TokenController.cs b/Timeline/Controllers/TokenController.cs index cf32a562..4e32d26f 100644 --- a/Timeline/Controllers/TokenController.cs +++ b/Timeline/Controllers/TokenController.cs @@ -9,6 +9,7 @@ using Timeline.Services; using Timeline.Helpers; using Microsoft.Extensions.Localization; using System.Globalization; +using static Timeline.Resources.Controllers.TokenController; namespace Timeline { @@ -60,7 +61,7 @@ namespace Timeline.Controllers { void LogFailure(string reason, Exception? e = null) { - _logger.LogInformation(e, Log.Format(_localizer["LogCreateFailure"], + _logger.LogInformation(e, Log.Format(LogCreateFailure, ("Reason", reason), ("Username", request.Username), ("Password", request.Password), @@ -76,7 +77,7 @@ namespace Timeline.Controllers var result = await _userService.CreateToken(request.Username, request.Password, expireTime); - _logger.LogInformation(Log.Format(_localizer["LogCreateSuccess"], + _logger.LogInformation(Log.Format(LogCreateSuccess, ("Username", request.Username), ("Expire At", expireTime?.ToString(CultureInfo.CurrentUICulture.DateTimeFormat) ?? "default") )); @@ -88,13 +89,13 @@ namespace Timeline.Controllers } catch (UserNotExistException e) { - LogFailure(_localizer["LogUserNotExist"], e); + LogFailure(LogUserNotExist, e); return BadRequest(new CommonResponse(ErrorCodes.Http.Token.Create.BadCredential, _localizer["ErrorBadCredential"])); } catch (BadPasswordException e) { - LogFailure(_localizer["LogBadPassword"], e); + LogFailure(LogBadPassword, e); return BadRequest(new CommonResponse(ErrorCodes.Http.Token.Create.BadCredential, _localizer["ErrorBadCredential"])); } @@ -110,49 +111,50 @@ namespace Timeline.Controllers properties[0] = ("Reason", reason); properties[1] = ("Token", request.Token); otherProperties.CopyTo(properties, 2); - _logger.LogInformation(e, Log.Format(_localizer["LogVerifyFailure"], properties)); + _logger.LogInformation(e, Log.Format(LogVerifyFailure, properties)); } try { var result = await _userService.VerifyToken(request.Token); - _logger.LogInformation(Log.Format(_localizer["LogVerifySuccess"], + _logger.LogInformation(Log.Format(LogVerifySuccess, ("Username", result.Username), ("Token", request.Token))); return Ok(new VerifyTokenResponse { User = result }); } - catch (JwtTokenVerifyException e) + catch (JwtVerifyException e) { - if (e.ErrorCode == JwtTokenVerifyException.ErrorCodes.Expired) + if (e.ErrorCode == JwtVerifyException.ErrorCodes.Expired) { var innerException = e.InnerException as SecurityTokenExpiredException; - LogFailure(_localizer["LogVerifyExpire"], e, ("Expires", innerException?.Expires), + LogFailure(LogVerifyExpire, e, ("Expires", innerException?.Expires), ("Current Time", _clock.GetCurrentTime())); return BadRequest(new CommonResponse( ErrorCodes.Http.Token.Verify.Expired, _localizer["ErrorVerifyExpire"])); } + 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, _localizer["ErrorVerifyOldVersion"])); + } else { - LogFailure(_localizer["LogVerifyBadFormat"], e); + LogFailure(LogVerifyBadFormat, e); return BadRequest(new CommonResponse( ErrorCodes.Http.Token.Verify.BadFormat, _localizer["ErrorVerifyBadFormat"])); } } catch (UserNotExistException e) { - LogFailure(_localizer["LogVerifyUserNotExist"], e); + LogFailure(LogVerifyUserNotExist, e); return BadRequest(new CommonResponse( ErrorCodes.Http.Token.Verify.UserNotExist, _localizer["ErrorVerifyUserNotExist"])); } - catch (BadTokenVersionException e) - { - LogFailure(_localizer["LogVerifyOldVersion"], e, - ("Token Version", e.TokenVersion), ("Required Version", e.RequiredVersion)); - return BadRequest(new CommonResponse( - ErrorCodes.Http.Token.Verify.OldVersion, _localizer["ErrorVerifyOldVersion"])); - } } } } diff --git a/Timeline/Controllers/UserController.cs b/Timeline/Controllers/UserController.cs index 6afc890c..b8d1d659 100644 --- a/Timeline/Controllers/UserController.cs +++ b/Timeline/Controllers/UserController.cs @@ -8,6 +8,7 @@ using Timeline.Helpers; using Timeline.Models; using Timeline.Models.Http; using Timeline.Services; +using static Timeline.Resources.Controllers.UserController; namespace Timeline { @@ -82,7 +83,7 @@ namespace Timeline.Controllers var user = await _userService.GetUser(username); if (user == null) { - _logger.LogInformation(Log.Format(_localizer["LogGetUserNotExist"], ("Username", username))); + _logger.LogInformation(Log.Format(LogGetUserNotExist, ("Username", username))); return NotFound(new CommonResponse(ErrorCodes.Http.User.Get.NotExist, _localizer["ErrorGetUserNotExist"])); } return Ok(user); @@ -96,11 +97,11 @@ namespace Timeline.Controllers var result = await _userService.PutUser(username, request.Password, request.Administrator!.Value); switch (result) { - case PutResult.Created: - _logger.LogInformation(Log.Format(_localizer["LogPutCreate"], ("Username", username))); + case PutResult.Create: + _logger.LogInformation(Log.Format(LogPutCreate, ("Username", username))); return CreatedAtAction("Get", new { username }, CommonPutResponse.Create(_localizerFactory)); - case PutResult.Modified: - _logger.LogInformation(Log.Format(_localizer["LogPutModify"], ("Username", username))); + case PutResult.Modify: + _logger.LogInformation(Log.Format(LogPutModify, ("Username", username))); return Ok(CommonPutResponse.Modify(_localizerFactory)); default: throw new InvalidBranchException(); @@ -108,7 +109,7 @@ namespace Timeline.Controllers } catch (UsernameBadFormatException e) { - _logger.LogInformation(e, Log.Format(_localizer["LogPutBadUsername"], ("Username", username))); + _logger.LogInformation(e, Log.Format(LogPutBadUsername, ("Username", username))); return BadRequest(new CommonResponse(ErrorCodes.Http.User.Put.BadUsername, _localizer["ErrorPutBadUsername"])); } } @@ -123,7 +124,7 @@ namespace Timeline.Controllers } catch (UserNotExistException e) { - _logger.LogInformation(e, Log.Format(_localizer["LogPatchUserNotExist"], ("Username", username))); + _logger.LogInformation(e, Log.Format(LogPatchUserNotExist, ("Username", username))); return NotFound(new CommonResponse(ErrorCodes.Http.User.Patch.NotExist, _localizer["ErrorPatchUserNotExist"])); } } @@ -134,12 +135,12 @@ namespace Timeline.Controllers try { await _userService.DeleteUser(username); - _logger.LogInformation(Log.Format(_localizer["LogDeleteDelete"], ("Username", username))); + _logger.LogInformation(Log.Format(LogDeleteDelete, ("Username", username))); return Ok(CommonDeleteResponse.Delete(_localizerFactory)); } catch (UserNotExistException e) { - _logger.LogInformation(e, Log.Format(_localizer["LogDeleteUserNotExist"], ("Username", username))); + _logger.LogInformation(e, Log.Format(LogDeleteNotExist, ("Username", username))); return Ok(CommonDeleteResponse.NotExist(_localizerFactory)); } } @@ -150,19 +151,19 @@ namespace Timeline.Controllers try { await _userService.ChangeUsername(request.OldUsername, request.NewUsername); - _logger.LogInformation(Log.Format(_localizer["LogChangeUsernameSuccess"], + _logger.LogInformation(Log.Format(LogChangeUsernameSuccess, ("Old Username", request.OldUsername), ("New Username", request.NewUsername))); return Ok(); } catch (UserNotExistException e) { - _logger.LogInformation(e, Log.Format(_localizer["LogChangeUsernameNotExist"], + _logger.LogInformation(e, Log.Format(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) + catch (UsernameConfictException e) { - _logger.LogInformation(e, Log.Format(_localizer["LogChangeUsernameAlreadyExist"], + _logger.LogInformation(e, Log.Format(LogChangeUsernameAlreadyExist, ("Old Username", request.OldUsername), ("New Username", request.NewUsername))); return BadRequest(new CommonResponse(ErrorCodes.Http.User.Op.ChangeUsername.AlreadyExist, _localizer["ErrorChangeUsernameAlreadyExist"])); } @@ -175,12 +176,12 @@ namespace Timeline.Controllers try { await _userService.ChangePassword(User.Identity.Name!, request.OldPassword, request.NewPassword); - _logger.LogInformation(Log.Format(_localizer["LogChangePasswordSuccess"], ("Username", User.Identity.Name))); + _logger.LogInformation(Log.Format(LogChangePasswordSuccess, ("Username", User.Identity.Name))); return Ok(); } catch (BadPasswordException e) { - _logger.LogInformation(e, Log.Format(_localizer["LogChangePasswordBadPassword"], + _logger.LogInformation(e, Log.Format(LogChangePasswordBadPassword, ("Username", User.Identity.Name), ("Old Password", request.OldPassword))); return BadRequest(new CommonResponse(ErrorCodes.Http.User.Op.ChangePassword.BadOldPassword, _localizer["ErrorChangePasswordBadPassword"])); diff --git a/Timeline/Controllers/UserDetailController.cs b/Timeline/Controllers/UserDetailController.cs deleted file mode 100644 index 5e1183c1..00000000 --- a/Timeline/Controllers/UserDetailController.cs +++ /dev/null @@ -1,96 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using System.Threading.Tasks; -using Timeline.Authenticate; -using Timeline.Models; -using Timeline.Models.Http; -using Timeline.Services; - -namespace Timeline.Controllers -{ - [Route("users/{username}")] - [ProducesErrorResponseType(typeof(CommonResponse))] - [ApiController] - public class UserDetailController : Controller - { - public static class ErrorCodes - { - public const int Get_UserNotExist = -1001; - - public const int Patch_Forbid = -2001; - public const int Patch_UserNotExist = -2002; - - public const int GetNickname_UserNotExist = -3001; - } - - private readonly ILogger _logger; - private readonly IUserDetailService _service; - - public UserDetailController(ILogger logger, IUserDetailService service) - { - _logger = logger; - _service = service; - } - - [HttpGet("nickname")] - [UserAuthorize] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(UserDetail))] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task GetNickname([FromRoute] string username) - { - try - { - var nickname = await _service.GetUserNickname(username); - return Ok(new UserDetail - { - Nickname = nickname - }); - } - catch (UserNotExistException) - { - return NotFound(new CommonResponse(ErrorCodes.GetNickname_UserNotExist, "The user does not exist.")); - } - } - - [HttpGet("details")] - [UserAuthorize] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(UserDetail))] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task Get([FromRoute] string username) - { - try - { - var detail = await _service.GetUserDetail(username); - return Ok(detail); - } - catch (UserNotExistException) - { - return NotFound(new CommonResponse(ErrorCodes.Get_UserNotExist, "The user does not exist.")); - } - } - - [HttpPatch("details")] - [Authorize] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(void))] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task Patch([FromRoute] string username, [FromBody] UserDetail detail) - { - if (!User.IsAdmin() && User.Identity.Name != username) - return StatusCode(StatusCodes.Status403Forbidden, new CommonResponse(ErrorCodes.Patch_Forbid, "You can't change other's details unless you are admin.")); - - try - { - await _service.UpdateUserDetail(username, detail); - return Ok(); - } - catch (UserNotExistException) - { - return NotFound(new CommonResponse(ErrorCodes.Patch_UserNotExist, "The user does not exist.")); - } - } - } -} diff --git a/Timeline/Models/Http/Common.cs b/Timeline/Models/Http/Common.cs index 130439d3..c741837a 100644 --- a/Timeline/Models/Http/Common.cs +++ b/Timeline/Models/Http/Common.cs @@ -86,13 +86,13 @@ namespace Timeline.Models.Http internal static CommonPutResponse Create(IStringLocalizerFactory localizerFactory) { - var localizer = localizerFactory.Create("Http.Common"); + var localizer = localizerFactory.Create("Models.Http.Common"); return new CommonPutResponse(0, localizer["ResponsePutCreate"], true); } internal static CommonPutResponse Modify(IStringLocalizerFactory localizerFactory) { - var localizer = localizerFactory.Create("Http.Common"); + var localizer = localizerFactory.Create("Models.Http.Common"); return new CommonPutResponse(0, localizer["ResponsePutModify"], false); } @@ -123,13 +123,13 @@ namespace Timeline.Models.Http internal static CommonDeleteResponse Delete(IStringLocalizerFactory localizerFactory) { - var localizer = localizerFactory.Create("Http.Common"); + var localizer = localizerFactory.Create("Models.Http.Common"); return new CommonDeleteResponse(0, localizer["ResponseDeleteDelete"], true); } internal static CommonDeleteResponse NotExist(IStringLocalizerFactory localizerFactory) { - var localizer = localizerFactory.Create("Http.Common"); + var localizer = localizerFactory.Create("Models.Models.Http.Common"); return new CommonDeleteResponse(0, localizer["ResponseDeleteNotExist"], false); } } diff --git a/Timeline/Models/PutResult.cs b/Timeline/Models/PutResult.cs index 544602eb..cecf86e6 100644 --- a/Timeline/Models/PutResult.cs +++ b/Timeline/Models/PutResult.cs @@ -8,10 +8,10 @@ namespace Timeline.Models /// /// Indicates the item did not exist and now is created. /// - Created, + Create, /// /// Indicates the item exists already and is modified. /// - Modified + Modify } } diff --git a/Timeline/Models/UserDetail.cs b/Timeline/Models/UserDetail.cs deleted file mode 100644 index 302e3bb1..00000000 --- a/Timeline/Models/UserDetail.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using Timeline.Entities; -using Timeline.Models.Validation; -using Newtonsoft.Json; - -namespace Timeline.Models -{ - public class UserDetail - { - [MaxLength(10)] - public string? Nickname { get; set; } - - [ValidateWith(typeof(UserDetailValidators.QQValidator))] - [JsonProperty(PropertyName = "qq")] - public string? QQ { get; set; } - - [ValidateWith(typeof(UserDetailValidators.EMailValidator))] - public string? Email { get; set; } - - [ValidateWith(typeof(UserDetailValidators.PhoneNumberValidator))] - public string? PhoneNumber { get; set; } - - public string? Description { get; set; } - - private static string? CoerceEmptyToNull(string? value) - { - if (string.IsNullOrEmpty(value)) - return null; - else - return value; - } - - public static UserDetail From(UserDetailEntity entity) - { - return new UserDetail - { - Nickname = CoerceEmptyToNull(entity.Nickname), - QQ = CoerceEmptyToNull(entity.QQ), - Email = CoerceEmptyToNull(entity.Email), - PhoneNumber = CoerceEmptyToNull(entity.PhoneNumber), - Description = CoerceEmptyToNull(entity.Description) - }; - } - } -} diff --git a/Timeline/Models/Validation/UserDetailValidator.cs b/Timeline/Models/Validation/UserDetailValidator.cs deleted file mode 100644 index 19c82edb..00000000 --- a/Timeline/Models/Validation/UserDetailValidator.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System; -using System.Net.Mail; - -namespace Timeline.Models.Validation -{ - public abstract class OptionalStringValidator : IValidator - { - public bool Validate(object value, out string message) - { - if (value == null) - { - message = ValidationConstants.SuccessMessage; - return true; - } - - if (value is string s) - { - if (s.Length == 0) - { - message = ValidationConstants.SuccessMessage; - return true; - } - return DoValidate(s, out message); - } - else - { - message = "Value is not of type string."; - return false; - } - } - - protected abstract bool DoValidate(string value, out string message); - } - - public static class UserDetailValidators - { - - public class QQValidator : OptionalStringValidator - { - protected override bool DoValidate(string value, out string message) - { - if (value.Length < 5) - { - message = "QQ is too short."; - return false; - } - - if (value.Length > 11) - { - message = "QQ is too long."; - return false; - } - - foreach (var c in value) - { - if (!char.IsDigit(c)) - { - message = "QQ must only contain digit."; - return false; - } - } - - message = ValidationConstants.SuccessMessage; - return true; - } - } - - public class EMailValidator : OptionalStringValidator - { - protected override bool DoValidate(string value, out string message) - { - if (value.Length > 50) - { - message = "E-Mail is too long."; - return false; - } - - try - { - var _ = new MailAddress(value); - } - catch (FormatException) - { - message = "The format of E-Mail is bad."; - return false; - } - message = ValidationConstants.SuccessMessage; - return true; - } - } - - public class PhoneNumberValidator : OptionalStringValidator - { - protected override bool DoValidate(string value, out string message) - { - if (value.Length > 14) - { - message = "Phone number is too long."; - return false; - } - - foreach (var c in value) - { - if (!char.IsDigit(c)) - { - message = "Phone number can only contain digit."; - return false; - } - } - - message = ValidationConstants.SuccessMessage; - return true; - } - } - } -} diff --git a/Timeline/Models/Validation/UsernameValidator.cs b/Timeline/Models/Validation/UsernameValidator.cs index e4891400..ecc3b5b3 100644 --- a/Timeline/Models/Validation/UsernameValidator.cs +++ b/Timeline/Models/Validation/UsernameValidator.cs @@ -1,4 +1,5 @@ -using System.Linq; +using Microsoft.Extensions.Localization; +using System.Linq; using System.Text.RegularExpressions; namespace Timeline.Models.Validation @@ -10,7 +11,7 @@ namespace Timeline.Models.Validation private readonly Regex _regex = new Regex(RegexPattern); - protected override bool DoValidate(string value, out string message) + protected override bool DoValidate(string value, IStringLocalizerFactory localizerFactory, out string message) { if (value.Length == 0) { diff --git a/Timeline/Models/Validation/Validator.cs b/Timeline/Models/Validation/Validator.cs index a1acbed9..a3800b71 100644 --- a/Timeline/Models/Validation/Validator.cs +++ b/Timeline/Models/Validation/Validator.cs @@ -1,11 +1,14 @@ -using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Localization; +using System; using System.ComponentModel.DataAnnotations; +using Timeline.Helpers; namespace Timeline.Models.Validation { /// /// A validator to validate value. - /// See . + /// See . /// public interface IValidator { @@ -15,7 +18,7 @@ namespace Timeline.Models.Validation /// The value to validate. /// The validation message. /// True if validation passed. Otherwise false. - bool Validate(object value, out string message); + bool Validate(object? value, IStringLocalizerFactory localizerFactory, out string message); } public static class ValidationConstants @@ -36,27 +39,36 @@ namespace Timeline.Models.Validation /// public abstract class Validator : IValidator { - public bool Validate(object value, out string message) + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "")] + public bool Validate(object? value, IStringLocalizerFactory localizerFactory, out string message) { if (value == null) { - message = "Value is null."; + var localizer = localizerFactory.Create("Models.Validation.Validator"); + message = localizer["ValidatorMessageNull"]; return false; } if (value is T v) { - - return DoValidate(v, out message); + return DoValidate(v, localizerFactory, out message); } else { - message = $"Value is not of type {typeof(T).Name}"; + var localizer = localizerFactory.Create("Models.Validation.Validator"); + message = localizer["ValidatorMessageBadType", typeof(T).FullName]; return false; } } - protected abstract bool DoValidate(T value, out string message); + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")] + protected static string GetSuccessMessage(IStringLocalizerFactory factory) + { + var localizer = factory.Create("Models.Validation.Validator"); + return localizer["ValidatorMessageSuccess"]; + } + + protected abstract bool DoValidate(T value, IStringLocalizerFactory localizerFactory, out string message); } [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, @@ -84,24 +96,28 @@ namespace Timeline.Models.Validation throw new ArgumentNullException(nameof(validatorType)); if (!typeof(IValidator).IsAssignableFrom(validatorType)) - throw new ArgumentException("Given type is not assignable to IValidator.", nameof(validatorType)); + throw new ArgumentException( + Resources.Models.Validation.Validator.ValidateWithAttributeNotValidator, + nameof(validatorType)); try { - _validator = Activator.CreateInstance(validatorType) as IValidator; + _validator = (Activator.CreateInstance(validatorType) as IValidator)!; } catch (Exception e) { - throw new ArgumentException("Failed to create a validator instance from default constructor. See inner exception.", e); + throw new ArgumentException( + Resources.Models.Validation.Validator.ValidateWithAttributeCreateFail, e); } } protected override ValidationResult IsValid(object value, ValidationContext validationContext) { - if (_validator.Validate(value, out var message)) + var localizerFactory = validationContext.GetRequiredService(); + if (_validator.Validate(value, localizerFactory, out var message)) return ValidationResult.Success; else - return new ValidationResult(string.Format("Field {0} is bad. {1}", validationContext.DisplayName, message)); + return new ValidationResult(message); } } } diff --git a/Timeline/Resources/Controllers/TokenController.Designer.cs b/Timeline/Resources/Controllers/TokenController.Designer.cs new file mode 100644 index 00000000..a7c2864b --- /dev/null +++ b/Timeline/Resources/Controllers/TokenController.Designer.cs @@ -0,0 +1,153 @@ +//------------------------------------------------------------------------------ +// +// 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 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)] + 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.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)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to The password is wrong.. + /// + internal static string LogBadPassword { + get { + return ResourceManager.GetString("LogBadPassword", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A user failed to create a token.. + /// + internal static string LogCreateFailure { + get { + return ResourceManager.GetString("LogCreateFailure", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A user succeeded to create a token.. + /// + internal static string LogCreateSuccess { + get { + return ResourceManager.GetString("LogCreateSuccess", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The user does not exist.. + /// + internal static string LogUserNotExist { + get { + return ResourceManager.GetString("LogUserNotExist", 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 LogVerifyBadFormat { + get { + return ResourceManager.GetString("LogVerifyBadFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The token is expired.. + /// + internal static string LogVerifyExpire { + get { + return ResourceManager.GetString("LogVerifyExpire", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A token failed to be verified.. + /// + internal static string LogVerifyFailure { + get { + return ResourceManager.GetString("LogVerifyFailure", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Token has an old version. User might have update some info.. + /// + internal static string LogVerifyOldVersion { + get { + return ResourceManager.GetString("LogVerifyOldVersion", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A token succeeded to be verified.. + /// + internal static string LogVerifySuccess { + get { + return ResourceManager.GetString("LogVerifySuccess", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to User does not exist. Administrator might have deleted this user.. + /// + internal static string LogVerifyUserNotExist { + get { + return ResourceManager.GetString("LogVerifyUserNotExist", resourceCulture); + } + } + } +} diff --git a/Timeline/Resources/Controllers/UserController.Designer.cs b/Timeline/Resources/Controllers/UserController.Designer.cs new file mode 100644 index 00000000..df9cab4c --- /dev/null +++ b/Timeline/Resources/Controllers/UserController.Designer.cs @@ -0,0 +1,171 @@ +//------------------------------------------------------------------------------ +// +// 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 UserController { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal UserController() { + } + + /// + /// 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.UserController", typeof(UserController).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 Attempt to change password with wrong old password failed.. + /// + internal static string LogChangePasswordBadPassword { + get { + return ResourceManager.GetString("LogChangePasswordBadPassword", resourceCulture); + } + } + + /// + /// 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 { + get { + return ResourceManager.GetString("LogChangeUsernameAlreadyExist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Attempt to change a username of a user that does not exist failed.. + /// + internal static string LogChangeUsernameNotExist { + get { + return ResourceManager.GetString("LogChangeUsernameNotExist", resourceCulture); + } + } + + /// + /// 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.. + /// + internal static string LogGetUserNotExist { + get { + return ResourceManager.GetString("LogGetUserNotExist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Attempt to patch a user that does not exist failed.. + /// + internal static string LogPatchUserNotExist { + get { + 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/Http/Common.en.resx b/Timeline/Resources/Http/Common.en.resx deleted file mode 100644 index 40d44191..00000000 --- a/Timeline/Resources/Http/Common.en.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 - - - An existent item is deleted. - - - The item does not exist, so nothing is changed. - - - A new item is created. - - - An existent item is modified. - - \ No newline at end of file diff --git a/Timeline/Resources/Http/Common.zh.resx b/Timeline/Resources/Http/Common.zh.resx deleted file mode 100644 index b6d955d9..00000000 --- a/Timeline/Resources/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/Http/Common.en.resx b/Timeline/Resources/Models/Http/Common.en.resx new file mode 100644 index 00000000..40d44191 --- /dev/null +++ b/Timeline/Resources/Models/Http/Common.en.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 + + + An existent item is deleted. + + + The item does not exist, so nothing is changed. + + + A new item is created. + + + An existent item is modified. + + \ No newline at end of file diff --git a/Timeline/Resources/Models/Http/Common.zh.resx b/Timeline/Resources/Models/Http/Common.zh.resx new file mode 100644 index 00000000..b6d955d9 --- /dev/null +++ b/Timeline/Resources/Models/Http/Common.zh.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 + + + 删除了一个项目。 + + + 要删除的项目不存在,什么都没有修改。 + + + 创建了一个新项目。 + + + 修改了一个已存在的项目。 + + \ No newline at end of file diff --git a/Timeline/Resources/Models/Validation/Validator.Designer.cs b/Timeline/Resources/Models/Validation/Validator.Designer.cs new file mode 100644 index 00000000..f2532af3 --- /dev/null +++ b/Timeline/Resources/Models/Validation/Validator.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.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 Validator { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Validator() { + } + + /// + /// 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.Validator", typeof(Validator).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 create a validator instance from default constructor. See inner exception.. + /// + internal static string ValidateWithAttributeCreateFail { + get { + return ResourceManager.GetString("ValidateWithAttributeCreateFail", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Given type is not assignable to IValidator.. + /// + internal static string ValidateWithAttributeNotValidator { + get { + return ResourceManager.GetString("ValidateWithAttributeNotValidator", resourceCulture); + } + } + } +} diff --git a/Timeline/Resources/Models/Validation/Validator.en.resx b/Timeline/Resources/Models/Validation/Validator.en.resx new file mode 100644 index 00000000..8d2fbede --- /dev/null +++ b/Timeline/Resources/Models/Validation/Validator.en.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Value is not of type {0}. + + + Value can't be null. + + + Validation succeeded. + + \ No newline at end of file diff --git a/Timeline/Resources/Models/Validation/Validator.resx b/Timeline/Resources/Models/Validation/Validator.resx new file mode 100644 index 00000000..8843dc42 --- /dev/null +++ b/Timeline/Resources/Models/Validation/Validator.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 create a validator instance from default constructor. See inner exception. + + + Given type is not assignable to IValidator. + + \ No newline at end of file diff --git a/Timeline/Resources/Models/Validation/Validator.zh.resx b/Timeline/Resources/Models/Validation/Validator.zh.resx new file mode 100644 index 00000000..2f98e7e3 --- /dev/null +++ b/Timeline/Resources/Models/Validation/Validator.zh.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 new file mode 100644 index 00000000..15a8169e --- /dev/null +++ b/Timeline/Resources/Services/Exception.Designer.cs @@ -0,0 +1,189 @@ +//------------------------------------------------------------------------------ +// +// 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 Exception { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Exception() { + } + + /// + /// 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.Exception", typeof(Exception).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 password is wrong.. + /// + internal static string BadPasswordException { + get { + return ResourceManager.GetString("BadPasswordException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The version of the jwt token is old.. + /// + internal static string JwtBadVersionException { + get { + return ResourceManager.GetString("JwtBadVersionException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The token didn't pass verification because {0}, see inner exception for information.. + /// + internal static string JwtVerifyException { + get { + return ResourceManager.GetString("JwtVerifyException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to token is expired.. + /// + internal static string JwtVerifyExceptionExpired { + get { + return ResourceManager.GetString("JwtVerifyExceptionExpired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to id claim is not a number.. + /// + internal static string JwtVerifyExceptionIdClaimBadFormat { + get { + return ResourceManager.GetString("JwtVerifyExceptionIdClaimBadFormat", 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.. + /// + internal static string JwtVerifyExceptionOthers { + get { + return ResourceManager.GetString("JwtVerifyExceptionOthers", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to unknown error code.. + /// + internal static string JwtVerifyExceptionUnknown { + get { + return ResourceManager.GetString("JwtVerifyExceptionUnknown", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to version claim is not a number.. + /// + internal static string JwtVerifyExceptionVersionClaimBadFormat { + get { + return ResourceManager.GetString("JwtVerifyExceptionVersionClaimBadFormat", resourceCulture); + } + } + + /// + /// 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.. + /// + internal static string UsernameConfictException { + get { + return ResourceManager.GetString("UsernameConfictException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The user does not exist.. + /// + internal static string UserNotExistException { + get { + return ResourceManager.GetString("UserNotExistException", resourceCulture); + } + } + } +} diff --git a/Timeline/Resources/Services/Exception.resx b/Timeline/Resources/Services/Exception.resx new file mode 100644 index 00000000..af771393 --- /dev/null +++ b/Timeline/Resources/Services/Exception.resx @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 password is wrong. + + + The version of the jwt token is old. + + + The token didn't pass verification because {0}, see inner exception for information. + + + token is expired. + + + id claim is not a number. + + + id claim does not exist. + + + version claim does not exist. + + + version of token is old. + + + uncommon error. + + + unknown error code. + + + version claim is not a number. + + + The username is of bad format. + + + The username already exists. + + + The user does not exist. + + \ No newline at end of file diff --git a/Timeline/Services/BadPasswordException.cs b/Timeline/Services/BadPasswordException.cs new file mode 100644 index 00000000..ee8a42db --- /dev/null +++ b/Timeline/Services/BadPasswordException.cs @@ -0,0 +1,27 @@ +using System; +using Timeline.Helpers; + +namespace Timeline.Services +{ + [Serializable] + public class BadPasswordException : Exception + { + public BadPasswordException() : base(Resources.Services.Exception.UserNotExistException) { } + public BadPasswordException(string message, Exception inner) : base(message, inner) { } + + public BadPasswordException(string badPassword) + : base(Log.Format(Resources.Services.Exception.UserNotExistException, ("Bad Password", badPassword))) + { + Password = badPassword; + } + + protected BadPasswordException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + /// + /// The wrong password. + /// + public string? Password { get; set; } + } +} diff --git a/Timeline/Services/JwtBadVersionException.cs b/Timeline/Services/JwtBadVersionException.cs new file mode 100644 index 00000000..4ce17710 --- /dev/null +++ b/Timeline/Services/JwtBadVersionException.cs @@ -0,0 +1,36 @@ +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/JwtService.cs b/Timeline/Services/JwtService.cs index 90d0c217..bf92966a 100644 --- a/Timeline/Services/JwtService.cs +++ b/Timeline/Services/JwtService.cs @@ -1,6 +1,7 @@ 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; @@ -14,63 +15,6 @@ namespace Timeline.Services public long Version { get; set; } } - [Serializable] - public class JwtTokenVerifyException : 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; - } - - private const string message = "Jwt token is bad."; - - public JwtTokenVerifyException() : base(message) { } - public JwtTokenVerifyException(string message) : base(message) { } - public JwtTokenVerifyException(string message, Exception inner) : base(message, inner) { } - - public JwtTokenVerifyException(int code) : base(GetErrorMessage(code)) { ErrorCode = code; } - public JwtTokenVerifyException(string message, int code) : base(message) { ErrorCode = code; } - public JwtTokenVerifyException(Exception inner, int code) : base(GetErrorMessage(code), inner) { ErrorCode = code; } - public JwtTokenVerifyException(string message, Exception inner, int code) : base(message, inner) { ErrorCode = code; } - protected JwtTokenVerifyException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - public int ErrorCode { get; set; } - - private static string GetErrorMessage(int errorCode) - { - switch (errorCode) - { - case ErrorCodes.Others: - return "Uncommon error, see inner exception for more information."; - case ErrorCodes.NoIdClaim: - return "Id claim does not exist."; - case ErrorCodes.IdClaimBadFormat: - return "Id claim is not a number."; - case ErrorCodes.NoVersionClaim: - return "Version claim does not exist."; - case ErrorCodes.VersionClaimBadFormat: - return "Version claim is not a number"; - case ErrorCodes.Expired: - return "Token is expired."; - default: - return "Unknown error code."; - } - } - } - public interface IJwtService { /// @@ -89,7 +33,7 @@ namespace Timeline.Services /// The token string to verify. /// Return the saved info in token. /// Thrown when is null. - /// Thrown when the token is invalid. + /// Thrown when the token is invalid. TokenInfo VerifyJwtToken(string token); } @@ -116,8 +60,8 @@ namespace Timeline.Services var config = _jwtConfig.CurrentValue; var identity = new ClaimsIdentity(); - identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, tokenInfo.Id.ToString(), ClaimValueTypes.Integer64)); - identity.AddClaim(new Claim(VersionClaimType, tokenInfo.Version.ToString(), ClaimValueTypes.Integer64)); + 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() { @@ -159,15 +103,15 @@ namespace Timeline.Services var idClaim = principal.FindFirstValue(ClaimTypes.NameIdentifier); if (idClaim == null) - throw new JwtTokenVerifyException(JwtTokenVerifyException.ErrorCodes.NoIdClaim); + throw new JwtVerifyException(JwtVerifyException.ErrorCodes.NoIdClaim); if (!long.TryParse(idClaim, out var id)) - throw new JwtTokenVerifyException(JwtTokenVerifyException.ErrorCodes.IdClaimBadFormat); + throw new JwtVerifyException(JwtVerifyException.ErrorCodes.IdClaimBadFormat); var versionClaim = principal.FindFirstValue(VersionClaimType); if (versionClaim == null) - throw new JwtTokenVerifyException(JwtTokenVerifyException.ErrorCodes.NoVersionClaim); + throw new JwtVerifyException(JwtVerifyException.ErrorCodes.NoVersionClaim); if (!long.TryParse(versionClaim, out var version)) - throw new JwtTokenVerifyException(JwtTokenVerifyException.ErrorCodes.VersionClaimBadFormat); + throw new JwtVerifyException(JwtVerifyException.ErrorCodes.VersionClaimBadFormat); return new TokenInfo { @@ -177,11 +121,11 @@ namespace Timeline.Services } catch (SecurityTokenExpiredException e) { - throw new JwtTokenVerifyException(e, JwtTokenVerifyException.ErrorCodes.Expired); + throw new JwtVerifyException(e, JwtVerifyException.ErrorCodes.Expired); } catch (Exception e) { - throw new JwtTokenVerifyException(e, JwtTokenVerifyException.ErrorCodes.Others); + throw new JwtVerifyException(e, JwtVerifyException.ErrorCodes.Others); } } } diff --git a/Timeline/Services/JwtVerifyException.cs b/Timeline/Services/JwtVerifyException.cs new file mode 100644 index 00000000..a915b51a --- /dev/null +++ b/Timeline/Services/JwtVerifyException.cs @@ -0,0 +1,59 @@ +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/UserDetailService.cs b/Timeline/Services/UserDetailService.cs deleted file mode 100644 index 5e049435..00000000 --- a/Timeline/Services/UserDetailService.cs +++ /dev/null @@ -1,135 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using System; -using System.Linq; -using System.Threading.Tasks; -using Timeline.Entities; -using Timeline.Models; - -namespace Timeline.Services -{ - public interface IUserDetailService - { - /// - /// Get the nickname of user. - /// - /// The username to get nickname of. - /// The user's nickname. Null if not set. - /// Thrown if is null or empty. - /// Thrown if user doesn't exist. - Task GetUserNickname(string username); - - /// - /// Get the detail of user. - /// - /// The username to get user detail of. - /// The user detail. - /// Thrown if is null or empty. - /// Thrown if user doesn't exist. - Task GetUserDetail(string username); - - /// - /// Update the detail of user. This function does not do data check. - /// - /// The username to get user detail of. - /// The detail to update. Can't be null. Any null member means not set. - /// Thrown if is null or empty or is null. - /// Thrown if user doesn't exist. - Task UpdateUserDetail(string username, UserDetail detail); - } - - public class UserDetailService : IUserDetailService - { - private readonly ILogger _logger; - - private readonly DatabaseContext _databaseContext; - - public UserDetailService(ILogger logger, DatabaseContext databaseContext) - { - _logger = logger; - _databaseContext = databaseContext; - } - - private async Task CreateEntity(long userId) - { - var entity = new UserDetailEntity() - { - UserId = userId - }; - _databaseContext.UserDetails.Add(entity); - await _databaseContext.SaveChangesAsync(); - _logger.LogInformation("An entity is created in user_details."); - return entity; - } - - // Check the existence of user detail entry - private async Task CheckAndInit(long userId) - { - var detail = await _databaseContext.UserDetails.Where(e => e.UserId == userId).SingleOrDefaultAsync(); - if (detail == null) - { - detail = await CreateEntity(userId); - } - return detail; - } - - public async Task GetUserNickname(string username) - { - var userId = await DatabaseExtensions.CheckAndGetUser(_databaseContext.Users, username); - var detail = await _databaseContext.UserDetails.Where(e => e.UserId == userId).Select(e => new { e.Nickname }).SingleOrDefaultAsync(); - if (detail == null) - { - var entity = await CreateEntity(userId); - return null; - } - else - { - var nickname = detail.Nickname; - return string.IsNullOrEmpty(nickname) ? null : nickname; - } - } - - public async Task GetUserDetail(string username) - { - var userId = await DatabaseExtensions.CheckAndGetUser(_databaseContext.Users, username); - var detailEntity = await CheckAndInit(userId); - return UserDetail.From(detailEntity); - } - - public async Task UpdateUserDetail(string username, UserDetail detail) - { - if (detail == null) - throw new ArgumentNullException(nameof(detail)); - - var userId = await DatabaseExtensions.CheckAndGetUser(_databaseContext.Users, username); - var detailEntity = await CheckAndInit(userId); - - if (detail.Nickname != null) - detailEntity.Nickname = detail.Nickname; - - if (detail.QQ != null) - detailEntity.QQ = detail.QQ; - - if (detail.Email != null) - detailEntity.Email = detail.Email; - - if (detail.PhoneNumber != null) - detailEntity.PhoneNumber = detail.PhoneNumber; - - if (detail.Description != null) - detailEntity.Description = detail.Description; - - await _databaseContext.SaveChangesAsync(); - _logger.LogInformation("An entity is updated in user_details."); - } - } - - public static class UserDetailServiceCollectionExtensions - { - public static void AddUserDetailService(this IServiceCollection services) - { - services.AddScoped(); - } - } -} diff --git a/Timeline/Services/UserNotExistException.cs b/Timeline/Services/UserNotExistException.cs new file mode 100644 index 00000000..c7317f56 --- /dev/null +++ b/Timeline/Services/UserNotExistException.cs @@ -0,0 +1,41 @@ +using System; +using Timeline.Helpers; + +namespace Timeline.Services +{ + /// + /// The user requested does not exist. + /// + [Serializable] + public class UserNotExistException : Exception + { + public UserNotExistException() : base(Resources.Services.Exception.UserNotExistException) { } + public UserNotExistException(string message, Exception inner) : base(message, inner) { } + + public UserNotExistException(string username) + : base(Log.Format(Resources.Services.Exception.UserNotExistException, ("Username", username))) + { + Username = username; + } + + public UserNotExistException(long id) + : base(Log.Format(Resources.Services.Exception.UserNotExistException, ("Id", id))) + { + Id = id; + } + + protected UserNotExistException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + /// + /// The username of the user that does not exist. + /// + public string? Username { get; set; } + + /// + /// The id of the user that does not exist. + /// + public long? Id { get; set; } + } +} diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs index 9564b34b..aad4a806 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -1,11 +1,11 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; using System; using System.Linq; using System.Threading.Tasks; using Timeline.Entities; -using Timeline.Helpers; using Timeline.Models; using Timeline.Models.Validation; using static Timeline.Helpers.MyLogHelper; @@ -15,163 +15,8 @@ namespace Timeline.Services { public class CreateTokenResult { - public string Token { get; set; } - public UserInfo User { get; set; } - } - - [Serializable] - public class UserNotExistException : Exception - { - private const string message = "The user does not exist."; - - public UserNotExistException() - : base(message) - { - - } - - public UserNotExistException(string username) - : base(Log.Format(message, ("Username", username))) - { - Username = username; - } - - public UserNotExistException(long id) - : base(Log.Format(message, ("Id", id))) - { - Id = id; - } - - public UserNotExistException(string message, Exception inner) : base(message, inner) { } - - protected UserNotExistException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - /// - /// The username that does not exist. - /// - public string Username { get; set; } - - /// - /// The id that does not exist. - /// - public long? Id { get; set; } - } - - [Serializable] - public class BadPasswordException : Exception - { - private const string message = "Password is wrong."; - - public BadPasswordException() - : base(message) - { - - } - - public BadPasswordException(string badPassword) - : base(Log.Format(message, ("Bad Password", badPassword))) - { - Password = badPassword; - } - - public BadPasswordException(string message, Exception inner) : base(message, inner) { } - - protected BadPasswordException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - /// - /// The wrong password. - /// - public string Password { get; set; } - } - - - [Serializable] - public class BadTokenVersionException : Exception - { - private const string message = "Token version is expired."; - - public BadTokenVersionException() - : base(message) - { - - } - - public BadTokenVersionException(long tokenVersion, long requiredVersion) - : base(Log.Format(message, - ("Token Version", tokenVersion), - ("Required Version", requiredVersion))) - { - TokenVersion = tokenVersion; - RequiredVersion = requiredVersion; - } - - public BadTokenVersionException(string message) : base(message) { } - public BadTokenVersionException(string message, Exception inner) : base(message, inner) { } - - protected BadTokenVersionException( - 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; } - } - - /// - /// Thrown when username is of bad format. - /// - [Serializable] - public class UsernameBadFormatException : Exception - { - private const string message = "Username is of bad format."; - - public UsernameBadFormatException() : base(message) { } - 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; } - 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; } - } - - - /// - /// Thrown when the user already exists. - /// - [Serializable] - public class UserAlreadyExistException : Exception - { - private const string message = "User already exists."; - - public UserAlreadyExistException() : base(message) { } - public UserAlreadyExistException(string username) : base(Log.Format(message, ("Username", username))) { Username = username; } - public UserAlreadyExistException(string username, string message) : base(message) { Username = username; } - public UserAlreadyExistException(string message, Exception inner) : base(message, inner) { } - protected UserAlreadyExistException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - /// - /// The username that already exists. - /// - public string Username { get; set; } + public string Token { get; set; } = default!; + public UserInfo User { get; set; } = default!; } public interface IUserService @@ -196,9 +41,8 @@ namespace Timeline.Services /// 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 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. - /// Thrown when the version in the token is expired. User needs to recreate the token. Task VerifyToken(string token); /// @@ -221,10 +65,12 @@ namespace Timeline.Services /// 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 is of bad format. + /// + /// 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); /// @@ -237,7 +83,7 @@ namespace Timeline.Services /// Whether the user is administrator. Null if not modify. /// Thrown if is null. /// Thrown if the user with given username does not exist. - Task PatchUser(string username, string password, bool? administrator); + Task PatchUser(string username, string? password, bool? administrator); /// /// Delete a user of given username. @@ -266,13 +112,13 @@ namespace Timeline.Services /// Thrown if or is null or empty. /// Thrown if the user with old username does not exist. /// Thrown if the new username is not accepted because of bad format. - /// Thrown if user with the new username already exists. + /// Thrown if user with the new username already exists. Task ChangeUsername(string oldUsername, string newUsername); } internal class UserCache { - public string Username { get; set; } + public string Username { get; set; } = default!; public bool Administrator { get; set; } public long Version { get; set; } @@ -294,13 +140,16 @@ namespace Timeline.Services private readonly UsernameValidator _usernameValidator; - public UserService(ILogger logger, IMemoryCache memoryCache, DatabaseContext databaseContext, IJwtService jwtService, IPasswordService passwordService) + private readonly IStringLocalizerFactory _localizerFactory; + + public UserService(ILogger logger, IMemoryCache memoryCache, IStringLocalizerFactory localizerFactory, DatabaseContext databaseContext, IJwtService jwtService, IPasswordService passwordService) { _logger = logger; _memoryCache = memoryCache; _databaseContext = databaseContext; _jwtService = jwtService; _passwordService = passwordService; + _localizerFactory = localizerFactory; _usernameValidator = new UsernameValidator(); } @@ -368,7 +217,7 @@ namespace Timeline.Services } if (tokenInfo.Version != cache.Version) - throw new BadTokenVersionException(tokenInfo.Version, cache.Version); + throw new JwtVerifyException(new JwtBadVersionException(tokenInfo.Version, cache.Version), JwtVerifyException.ErrorCodes.OldVersion); return cache.ToUserInfo(); } @@ -395,7 +244,7 @@ namespace Timeline.Services if (password == null) throw new ArgumentNullException(nameof(password)); - if (!_usernameValidator.Validate(username, out var message)) + if (!_usernameValidator.Validate(username, _localizerFactory, out var message)) { throw new UsernameBadFormatException(username, message); } @@ -414,7 +263,7 @@ namespace Timeline.Services await _databaseContext.AddAsync(newUser); await _databaseContext.SaveChangesAsync(); _logger.LogInformation(FormatLogMessage("A new user entry is added to the database.", Pair("Id", newUser.Id))); - return PutResult.Created; + return PutResult.Create; } user.EncryptedPassword = _passwordService.HashPassword(password); @@ -426,7 +275,7 @@ namespace Timeline.Services //clear cache RemoveCache(user.Id); - return PutResult.Modified; + return PutResult.Modify; } public async Task PatchUser(string username, string password, bool? administrator) @@ -504,7 +353,7 @@ namespace Timeline.Services if (string.IsNullOrEmpty(newUsername)) throw new ArgumentException("New username is null or empty", nameof(newUsername)); - if (!_usernameValidator.Validate(newUsername, out var message)) + if (!_usernameValidator.Validate(newUsername, _localizerFactory, out var message)) throw new UsernameBadFormatException(newUsername, $"New username is of bad format. {message}"); var user = await _databaseContext.Users.Where(u => u.Name == oldUsername).SingleOrDefaultAsync(); @@ -513,7 +362,7 @@ namespace Timeline.Services var conflictUser = await _databaseContext.Users.Where(u => u.Name == newUsername).SingleOrDefaultAsync(); if (conflictUser != null) - throw new UserAlreadyExistException(newUsername); + throw new UsernameConfictException(newUsername); user.Name = newUsername; user.Version += 1; diff --git a/Timeline/Services/UsernameBadFormatException.cs b/Timeline/Services/UsernameBadFormatException.cs new file mode 100644 index 00000000..04354d22 --- /dev/null +++ b/Timeline/Services/UsernameBadFormatException.cs @@ -0,0 +1,27 @@ +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 message) : base(message) { Username = username; } + public UsernameBadFormatException(string username, string message, Exception inner) : base(message, inner) { Username = username; } + + 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; } + } +} diff --git a/Timeline/Services/UsernameConfictException.cs b/Timeline/Services/UsernameConfictException.cs new file mode 100644 index 00000000..fde1eda6 --- /dev/null +++ b/Timeline/Services/UsernameConfictException.cs @@ -0,0 +1,25 @@ +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 5718cf05..be5bce7c 100644 --- a/Timeline/Startup.cs +++ b/Timeline/Startup.cs @@ -64,7 +64,6 @@ namespace Timeline services.AddTransient(); services.AddUserAvatarService(); - services.AddUserDetailService(); var databaseConfig = Configuration.GetSection(nameof(DatabaseConfig)).Get(); diff --git a/Timeline/Timeline.csproj b/Timeline/Timeline.csproj index a948e3f3..d1f9b2ed 100644 --- a/Timeline/Timeline.csproj +++ b/Timeline/Timeline.csproj @@ -39,6 +39,26 @@ True Common.resx + + True + True + TokenController.resx + + + True + True + UserController.resx + + + True + True + Validator.resx + + + True + True + Exception.resx + @@ -48,7 +68,8 @@ Designer - + ResXFileCodeGenerator + TokenController.Designer.cs Designer @@ -58,7 +79,16 @@ - + ResXFileCodeGenerator + UserController.Designer.cs + + + ResXFileCodeGenerator + Validator.Designer.cs + + + ResXFileCodeGenerator + Exception.Designer.cs -- cgit v1.2.3 From 89c106169bd2a16310fdaa6e0c48a3402d97de3a Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Thu, 24 Oct 2019 16:56:41 +0800 Subject: ... --- Timeline.Tests/IntegratedTests/UserAvatarTest.cs | 269 +++++++++++++++++++++ Timeline.Tests/IntegratedTests/UserAvatarTests.cs | 267 -------------------- Timeline.Tests/UserAvatarServiceTest.cs | 12 +- Timeline/Controllers/UserAvatarController.cs | 135 ++++++----- Timeline/Entities/UserAvatar.cs | 1 + Timeline/ErrorCodes.cs | 15 +- Timeline/Filters/ContentHeaderAttributes.cs | 13 +- Timeline/Helpers/LanguageHelper.cs | 12 + Timeline/Helpers/Log.cs | 20 -- Timeline/Models/Http/Common.cs | 66 +++-- .../Controllers/UserAvatarController.Designer.cs | 171 +++++++++++++ .../Controllers/UserAvatarController.en.resx | 144 +++++++++++ .../Controllers/UserAvatarController.resx | 156 ++++++++++++ .../Controllers/UserAvatarController.zh.resx | 144 +++++++++++ Timeline/Resources/Models/Http/Common.en.resx | 29 ++- Timeline/Resources/Models/Http/Common.zh.resx | 29 ++- Timeline/Resources/Services/Exception.Designer.cs | 45 ++++ Timeline/Resources/Services/Exception.resx | 15 ++ .../Services/UserAvatarService.Designer.cs | 108 +++++++++ Timeline/Resources/Services/UserAvatarService.resx | 135 +++++++++++ Timeline/Services/AvatarFormatException.cs | 51 ++++ Timeline/Services/DatabaseExtensions.cs | 15 +- Timeline/Services/ETagGenerator.cs | 17 +- Timeline/Services/UserAvatarService.cs | 187 +++++++------- Timeline/Services/UserService.cs | 2 +- Timeline/Timeline.csproj | 18 ++ 26 files changed, 1587 insertions(+), 489 deletions(-) create mode 100644 Timeline.Tests/IntegratedTests/UserAvatarTest.cs delete mode 100644 Timeline.Tests/IntegratedTests/UserAvatarTests.cs create mode 100644 Timeline/Helpers/LanguageHelper.cs create mode 100644 Timeline/Resources/Controllers/UserAvatarController.Designer.cs create mode 100644 Timeline/Resources/Controllers/UserAvatarController.en.resx create mode 100644 Timeline/Resources/Controllers/UserAvatarController.resx create mode 100644 Timeline/Resources/Controllers/UserAvatarController.zh.resx create mode 100644 Timeline/Resources/Services/UserAvatarService.Designer.cs create mode 100644 Timeline/Resources/Services/UserAvatarService.resx create mode 100644 Timeline/Services/AvatarFormatException.cs (limited to 'Timeline/Models/Http/Common.cs') diff --git a/Timeline.Tests/IntegratedTests/UserAvatarTest.cs b/Timeline.Tests/IntegratedTests/UserAvatarTest.cs new file mode 100644 index 00000000..ba6d98e1 --- /dev/null +++ b/Timeline.Tests/IntegratedTests/UserAvatarTest.cs @@ -0,0 +1,269 @@ +using FluentAssertions; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Png; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using Timeline.Controllers; +using Timeline.Services; +using Timeline.Tests.Helpers; +using Timeline.Tests.Helpers.Authentication; +using Xunit; +using static Timeline.ErrorCodes.Http.Common; +using static Timeline.ErrorCodes.Http.UserAvatar; + +namespace Timeline.Tests.IntegratedTests +{ + public class UserAvatarUnitTest : IClassFixture>, IDisposable + { + private readonly TestApplication _testApp; + private readonly WebApplicationFactory _factory; + + public UserAvatarUnitTest(WebApplicationFactory factory) + { + _testApp = new TestApplication(factory); + _factory = _testApp.Factory; + } + + public void Dispose() + { + _testApp.Dispose(); + } + + [Fact] + public async Task Test() + { + Avatar mockAvatar = new Avatar + { + Data = ImageHelper.CreatePngWithSize(100, 100), + Type = PngFormat.Instance.DefaultMimeType + }; + + using (var client = await _factory.CreateClientAsUser()) + { + { + var res = await client.GetAsync("users/usernotexist/avatar"); + res.Should().HaveStatusCode(404) + .And.Should().HaveCommonBody() + .Which.Code.Should().Be(Get.UserNotExist); + } + + 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") + { + var res = await client.GetAsync($"users/{username}/avatar"); + res.Should().HaveStatusCode(200); + res.Content.Headers.ContentType.MediaType.Should().Be("image/png"); + var body = await res.Content.ReadAsByteArrayAsync(); + body.Should().Equal(defaultAvatarData); + } + + EntityTagHeaderValue eTag; + { + var res = await client.GetAsync($"users/user/avatar"); + res.Should().HaveStatusCode(200); + res.Content.Headers.ContentType.MediaType.Should().Be("image/png"); + var body = await res.Content.ReadAsByteArrayAsync(); + body.Should().Equal(defaultAvatarData); + var cacheControl = res.Headers.CacheControl; + cacheControl.NoCache.Should().BeTrue(); + cacheControl.NoStore.Should().BeFalse(); + cacheControl.MaxAge.Should().NotBeNull().And.Be(TimeSpan.Zero); + eTag = res.Headers.ETag; + } + + await GetReturnDefault("admin"); + + { + var request = new HttpRequestMessage() + { + RequestUri = new Uri(client.BaseAddress, "users/user/avatar"), + Method = HttpMethod.Get, + }; + request.Headers.TryAddWithoutValidation("If-None-Match", "\"dsdfd"); + var res = await client.SendAsync(request); + res.Should().HaveStatusCode(HttpStatusCode.BadRequest) + .And.Should().HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Http.Common.Header.BadFormat_IfNonMatch); + } + + { + var request = new HttpRequestMessage() + { + RequestUri = new Uri(client.BaseAddress, "users/user/avatar"), + Method = HttpMethod.Get, + }; + request.Headers.TryAddWithoutValidation("If-None-Match", "\"aaa\""); + var res = await client.SendAsync(request); + res.Should().HaveStatusCode(HttpStatusCode.OK); + } + + { + var request = new HttpRequestMessage() + { + RequestUri = new Uri(client.BaseAddress, "users/user/avatar"), + Method = HttpMethod.Get, + }; + request.Headers.Add("If-None-Match", eTag.ToString()); + var res = await client.SendAsync(request); + res.Should().HaveStatusCode(HttpStatusCode.NotModified); + } + + { + var content = new ByteArrayContent(new[] { (byte)0x00 }); + content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); + var res = await client.PutAsync("users/user/avatar", content); + res.Should().HaveStatusCode(HttpStatusCode.BadRequest) + .And.Should().HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Http.Common.Header.Missing_ContentLength); + } + + { + var content = new ByteArrayContent(new[] { (byte)0x00 }); + content.Headers.ContentLength = 1; + var res = await client.PutAsync("users/user/avatar", content); + res.Should().HaveStatusCode(HttpStatusCode.BadRequest) + .And.Should().HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Http.Common.Header.Missing_ContentType); + } + + { + 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); + res.Should().HaveStatusCode(HttpStatusCode.BadRequest) + .And.Should().HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Http.Common.Header.Zero_ContentLength); + } + + { + var res = await client.PutByteArrayAsync("users/user/avatar", new[] { (byte)0x00 }, "image/notaccept"); + res.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); + } + + { + 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); + res.Should().HaveStatusCode(HttpStatusCode.BadRequest) + .And.Should().HaveCommonBody().Which.Code.Should().Be(Content.TooBig); + } + + { + 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); + res.Should().HaveStatusCode(HttpStatusCode.BadRequest) + .And.Should().HaveCommonBody().Which.Code.Should().Be(Content.UnmatchedLength_Smaller); + } + + { + 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); + res.Should().HaveStatusCode(HttpStatusCode.BadRequest) + .And.Should().HaveCommonBody().Which.Code.Should().Be(Content.UnmatchedLength_Bigger); + } + + { + var res = await client.PutByteArrayAsync("users/user/avatar", new[] { (byte)0x00 }, "image/png"); + res.Should().HaveStatusCode(HttpStatusCode.BadRequest) + .And.Should().HaveCommonBody().Which.Code.Should().Be(Put.BadFormat_CantDecode); + } + + { + var res = await client.PutByteArrayAsync("users/user/avatar", mockAvatar.Data, "image/jpeg"); + res.Should().HaveStatusCode(HttpStatusCode.BadRequest) + .And.Should().HaveCommonBody().Which.Code.Should().Be(Put.BadFormat_UnmatchedFormat); + } + + { + var res = await client.PutByteArrayAsync("users/user/avatar", ImageHelper.CreatePngWithSize(100, 200), "image/png"); + res.Should().HaveStatusCode(HttpStatusCode.BadRequest) + .And.Should().HaveCommonBody().Which.Code.Should().Be(Put.BadFormat_BadSize); + } + + { + var res = await client.PutByteArrayAsync("users/user/avatar", mockAvatar.Data, mockAvatar.Type); + res.Should().HaveStatusCode(HttpStatusCode.OK); + + var res2 = await client.GetAsync("users/user/avatar"); + res2.Should().HaveStatusCode(200); + res2.Content.Headers.ContentType.MediaType.Should().Be(mockAvatar.Type); + var body = await res2.Content.ReadAsByteArrayAsync(); + body.Should().Equal(mockAvatar.Data); + } + + IEnumerable<(string, IImageFormat)> formats = new (string, IImageFormat)[] + { + ("image/jpeg", JpegFormat.Instance), + ("image/gif", GifFormat.Instance), + ("image/png", PngFormat.Instance), + }; + + foreach ((var mimeType, var format) in formats) + { + var res = await client.PutByteArrayAsync("users/user/avatar", ImageHelper.CreateImageWithSize(100, 100, format), mimeType); + res.Should().HaveStatusCode(HttpStatusCode.OK); + } + + { + var res = await client.PutByteArrayAsync("users/admin/avatar", new[] { (byte)0x00 }, "image/png"); + res.Should().HaveStatusCode(HttpStatusCode.Forbidden) + .And.Should().HaveCommonBody().Which.Code.Should().Be(Put.Forbid); + } + + { + var res = await client.DeleteAsync("users/admin/avatar"); + res.Should().HaveStatusCode(HttpStatusCode.Forbidden) + .And.Should().HaveCommonBody().Which.Code.Should().Be(Delete.Forbid); + } + + for (int i = 0; i < 2; i++) // double delete should work. + { + var res = await client.DeleteAsync("users/user/avatar"); + res.Should().HaveStatusCode(200); + await GetReturnDefault(); + } + } + + // Authorization check. + using (var client = await _factory.CreateClientAsAdmin()) + { + { + var res = await client.PutByteArrayAsync("users/user/avatar", mockAvatar.Data, mockAvatar.Type); + res.Should().HaveStatusCode(HttpStatusCode.OK); + } + + { + var res = await client.DeleteAsync("users/user/avatar"); + res.Should().HaveStatusCode(HttpStatusCode.OK); + } + + { + var res = await client.PutByteArrayAsync("users/usernotexist/avatar", new[] { (byte)0x00 }, "image/png"); + res.Should().HaveStatusCode(400) + .And.Should().HaveCommonBody() + .Which.Code.Should().Be(Put.UserNotExist); + } + + { + var res = await client.DeleteAsync("users/usernotexist/avatar"); + res.Should().HaveStatusCode(400) + .And.Should().HaveCommonBody().Which.Code.Should().Be(Delete.UserNotExist); + } + } + } + } +} \ No newline at end of file diff --git a/Timeline.Tests/IntegratedTests/UserAvatarTests.cs b/Timeline.Tests/IntegratedTests/UserAvatarTests.cs deleted file mode 100644 index ad0e4221..00000000 --- a/Timeline.Tests/IntegratedTests/UserAvatarTests.cs +++ /dev/null @@ -1,267 +0,0 @@ -using FluentAssertions; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Mvc.Testing; -using Microsoft.Extensions.DependencyInjection; -using SixLabors.ImageSharp.Formats; -using SixLabors.ImageSharp.Formats.Gif; -using SixLabors.ImageSharp.Formats.Jpeg; -using SixLabors.ImageSharp.Formats.Png; -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Threading.Tasks; -using Timeline.Controllers; -using Timeline.Services; -using Timeline.Tests.Helpers; -using Timeline.Tests.Helpers.Authentication; -using Xunit; - -namespace Timeline.Tests.IntegratedTests -{ - public class UserAvatarUnitTest : IClassFixture>, IDisposable - { - private readonly TestApplication _testApp; - private readonly WebApplicationFactory _factory; - - public UserAvatarUnitTest(WebApplicationFactory factory) - { - _testApp = new TestApplication(factory); - _factory = _testApp.Factory; - } - - public void Dispose() - { - _testApp.Dispose(); - } - - [Fact] - public async Task Test() - { - Avatar mockAvatar = new Avatar - { - Data = ImageHelper.CreatePngWithSize(100, 100), - Type = PngFormat.Instance.DefaultMimeType - }; - - using (var client = await _factory.CreateClientAsUser()) - { - { - var res = await client.GetAsync("users/usernotexist/avatar"); - res.Should().HaveStatusCode(404) - .And.Should().HaveCommonBody() - .Which.Code.Should().Be(UserAvatarController.ErrorCodes.Get_UserNotExist); - } - - 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") - { - var res = await client.GetAsync($"users/{username}/avatar"); - res.Should().HaveStatusCode(200); - res.Content.Headers.ContentType.MediaType.Should().Be("image/png"); - var body = await res.Content.ReadAsByteArrayAsync(); - body.Should().Equal(defaultAvatarData); - } - - EntityTagHeaderValue eTag; - { - var res = await client.GetAsync($"users/user/avatar"); - res.Should().HaveStatusCode(200); - res.Content.Headers.ContentType.MediaType.Should().Be("image/png"); - var body = await res.Content.ReadAsByteArrayAsync(); - body.Should().Equal(defaultAvatarData); - var cacheControl = res.Headers.CacheControl; - cacheControl.NoCache.Should().BeTrue(); - cacheControl.NoStore.Should().BeFalse(); - cacheControl.MaxAge.Should().NotBeNull().And.Be(TimeSpan.Zero); - eTag = res.Headers.ETag; - } - - await GetReturnDefault("admin"); - - { - var request = new HttpRequestMessage() - { - RequestUri = new Uri(client.BaseAddress, "users/user/avatar"), - Method = HttpMethod.Get, - }; - request.Headers.TryAddWithoutValidation("If-None-Match", "\"dsdfd"); - var res = await client.SendAsync(request); - res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.Should().HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Http.Common.Header.BadFormat_IfNonMatch); - } - - { - var request = new HttpRequestMessage() - { - RequestUri = new Uri(client.BaseAddress, "users/user/avatar"), - Method = HttpMethod.Get, - }; - request.Headers.TryAddWithoutValidation("If-None-Match", "\"aaa\""); - var res = await client.SendAsync(request); - res.Should().HaveStatusCode(HttpStatusCode.OK); - } - - { - var request = new HttpRequestMessage() - { - RequestUri = new Uri(client.BaseAddress, "users/user/avatar"), - Method = HttpMethod.Get, - }; - request.Headers.Add("If-None-Match", eTag.ToString()); - var res = await client.SendAsync(request); - res.Should().HaveStatusCode(HttpStatusCode.NotModified); - } - - { - var content = new ByteArrayContent(new[] { (byte)0x00 }); - content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); - var res = await client.PutAsync("users/user/avatar", content); - res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.Should().HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Http.Common.Header.Missing_ContentLength); - } - - { - var content = new ByteArrayContent(new[] { (byte)0x00 }); - content.Headers.ContentLength = 1; - var res = await client.PutAsync("users/user/avatar", content); - res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.Should().HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Http.Common.Header.Missing_ContentType); - } - - { - 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); - res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.Should().HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Http.Common.Header.Zero_ContentLength); - } - - { - var res = await client.PutByteArrayAsync("users/user/avatar", new[] { (byte)0x00 }, "image/notaccept"); - res.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); - } - - { - 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); - res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.Should().HaveCommonBody().Which.Code.Should().Be(UserAvatarController.ErrorCodes.Put_Content_TooBig); - } - - { - 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); - res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.Should().HaveCommonBody().Which.Code.Should().Be(UserAvatarController.ErrorCodes.Put_Content_UnmatchedLength_Less); - } - - { - 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); - res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.Should().HaveCommonBody().Which.Code.Should().Be(UserAvatarController.ErrorCodes.Put_Content_UnmatchedLength_Bigger); - } - - { - var res = await client.PutByteArrayAsync("users/user/avatar", new[] { (byte)0x00 }, "image/png"); - res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.Should().HaveCommonBody().Which.Code.Should().Be(UserAvatarController.ErrorCodes.Put_BadFormat_CantDecode); - } - - { - var res = await client.PutByteArrayAsync("users/user/avatar", mockAvatar.Data, "image/jpeg"); - res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.Should().HaveCommonBody().Which.Code.Should().Be(UserAvatarController.ErrorCodes.Put_BadFormat_UnmatchedFormat); - } - - { - var res = await client.PutByteArrayAsync("users/user/avatar", ImageHelper.CreatePngWithSize(100, 200), "image/png"); - res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.Should().HaveCommonBody().Which.Code.Should().Be(UserAvatarController.ErrorCodes.Put_BadFormat_BadSize); - } - - { - var res = await client.PutByteArrayAsync("users/user/avatar", mockAvatar.Data, mockAvatar.Type); - res.Should().HaveStatusCode(HttpStatusCode.OK); - - var res2 = await client.GetAsync("users/user/avatar"); - res2.Should().HaveStatusCode(200); - res2.Content.Headers.ContentType.MediaType.Should().Be(mockAvatar.Type); - var body = await res2.Content.ReadAsByteArrayAsync(); - body.Should().Equal(mockAvatar.Data); - } - - IEnumerable<(string, IImageFormat)> formats = new (string, IImageFormat)[] - { - ("image/jpeg", JpegFormat.Instance), - ("image/gif", GifFormat.Instance), - ("image/png", PngFormat.Instance), - }; - - foreach ((var mimeType, var format) in formats) - { - var res = await client.PutByteArrayAsync("users/user/avatar", ImageHelper.CreateImageWithSize(100, 100, format), mimeType); - res.Should().HaveStatusCode(HttpStatusCode.OK); - } - - { - var res = await client.PutByteArrayAsync("users/admin/avatar", new[] { (byte)0x00 }, "image/png"); - res.Should().HaveStatusCode(HttpStatusCode.Forbidden) - .And.Should().HaveCommonBody().Which.Code.Should().Be(UserAvatarController.ErrorCodes.Put_Forbid); - } - - { - var res = await client.DeleteAsync("users/admin/avatar"); - res.Should().HaveStatusCode(HttpStatusCode.Forbidden) - .And.Should().HaveCommonBody().Which.Code.Should().Be(UserAvatarController.ErrorCodes.Delete_Forbid); - } - - for (int i = 0; i < 2; i++) // double delete should work. - { - var res = await client.DeleteAsync("users/user/avatar"); - res.Should().HaveStatusCode(200); - await GetReturnDefault(); - } - } - - // Authorization check. - using (var client = await _factory.CreateClientAsAdmin()) - { - { - var res = await client.PutByteArrayAsync("users/user/avatar", mockAvatar.Data, mockAvatar.Type); - res.Should().HaveStatusCode(HttpStatusCode.OK); - } - - { - var res = await client.DeleteAsync("users/user/avatar"); - res.Should().HaveStatusCode(HttpStatusCode.OK); - } - - { - var res = await client.PutByteArrayAsync("users/usernotexist/avatar", new[] { (byte)0x00 }, "image/png"); - res.Should().HaveStatusCode(400) - .And.Should().HaveCommonBody() - .Which.Code.Should().Be(UserAvatarController.ErrorCodes.Put_UserNotExist); - } - - { - var res = await client.DeleteAsync("users/usernotexist/avatar"); - res.Should().HaveStatusCode(400) - .And.Should().HaveCommonBody().Which.Code.Should().Be(UserAvatarController.ErrorCodes.Delete_UserNotExist); - } - } - } - } -} \ No newline at end of file diff --git a/Timeline.Tests/UserAvatarServiceTest.cs b/Timeline.Tests/UserAvatarServiceTest.cs index d22ad113..7489517b 100644 --- a/Timeline.Tests/UserAvatarServiceTest.cs +++ b/Timeline.Tests/UserAvatarServiceTest.cs @@ -63,8 +63,8 @@ namespace Timeline.Tests Type = "image/png" }; _validator.Awaiting(v => v.Validate(avatar)) - .Should().Throw() - .Where(e => e.Avatar == avatar && e.Error == AvatarDataException.ErrorReason.CantDecode); + .Should().Throw() + .Where(e => e.Avatar == avatar && e.Error == AvatarFormatException.ErrorReason.CantDecode); } [Fact] @@ -76,8 +76,8 @@ namespace Timeline.Tests Type = "image/jpeg" }; _validator.Awaiting(v => v.Validate(avatar)) - .Should().Throw() - .Where(e => e.Avatar == avatar && e.Error == AvatarDataException.ErrorReason.UnmatchedFormat); + .Should().Throw() + .Where(e => e.Avatar == avatar && e.Error == AvatarFormatException.ErrorReason.UnmatchedFormat); } [Fact] @@ -89,8 +89,8 @@ namespace Timeline.Tests Type = PngFormat.Instance.DefaultMimeType }; _validator.Awaiting(v => v.Validate(avatar)) - .Should().Throw() - .Where(e => e.Avatar == avatar && e.Error == AvatarDataException.ErrorReason.BadSize); + .Should().Throw() + .Where(e => e.Avatar == avatar && e.Error == AvatarFormatException.ErrorReason.BadSize); } [Fact] diff --git a/Timeline/Controllers/UserAvatarController.cs b/Timeline/Controllers/UserAvatarController.cs index 5cba1d93..838a3928 100644 --- a/Timeline/Controllers/UserAvatarController.cs +++ b/Timeline/Controllers/UserAvatarController.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; using System; @@ -8,61 +9,67 @@ using System.Linq; using System.Threading.Tasks; using Timeline.Authentication; using Timeline.Filters; +using Timeline.Helpers; using Timeline.Models.Http; +using Timeline.Models.Validation; using Timeline.Services; -namespace Timeline.Controllers +namespace Timeline { - [ApiController] - public class UserAvatarController : Controller + public static partial class ErrorCodes { - public static class ErrorCodes + public static partial class Http { - public const int Get_UserNotExist = -1001; - - public const int Put_UserNotExist = -2001; - public const int Put_Forbid = -2002; - public const int Put_BadFormat_CantDecode = -2011; - public const int Put_BadFormat_UnmatchedFormat = -2012; - public const int Put_BadFormat_BadSize = -2013; - public const int Put_Content_TooBig = -2021; - public const int Put_Content_UnmatchedLength_Less = -2022; - public const int Put_Content_UnmatchedLength_Bigger = -2023; + public static class UserAvatar // bbb = 003 + { + public static class Get // cc = 01 + { + public const int UserNotExist = 10030101; + } - public const int Delete_UserNotExist = -3001; - public const int Delete_Forbid = -3002; + 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 int From(AvatarDataException.ErrorReason error) - { - switch (error) + public static class Delete // cc = 03 { - case AvatarDataException.ErrorReason.CantDecode: - return Put_BadFormat_CantDecode; - case AvatarDataException.ErrorReason.UnmatchedFormat: - return Put_BadFormat_UnmatchedFormat; - case AvatarDataException.ErrorReason.BadSize: - return Put_BadFormat_BadSize; - default: - throw new Exception("Unknown AvatarDataException.ErrorReason value."); + public const int UserNotExist = 10030301; + public const int Forbid = 10030302; } } } + } +} +namespace Timeline.Controllers +{ + [ApiController] + public class UserAvatarController : Controller + { private readonly ILogger _logger; private readonly IUserAvatarService _service; - public UserAvatarController(ILogger logger, IUserAvatarService service) + private readonly IStringLocalizerFactory _localizerFactory; + private readonly IStringLocalizer _localizer; + + public UserAvatarController(ILogger logger, IUserAvatarService service, IStringLocalizerFactory localizerFactory) { _logger = logger; _service = service; + _localizerFactory = localizerFactory; + _localizer = new StringLocalizer(localizerFactory); } [HttpGet("users/{username}/avatar")] - [Authorize] [ResponseCache(NoStore = false, Location = ResponseCacheLocation.None, Duration = 0)] - public async Task Get([FromRoute] string username) + public async Task Get([FromRoute][Username] string username) { const string IfNonMatchHeaderKey = "If-None-Match"; @@ -74,11 +81,16 @@ namespace Timeline.Controllers if (Request.Headers.TryGetValue(IfNonMatchHeaderKey, out var value)) { if (!EntityTagHeaderValue.TryParseStrictList(value, out var eTagList)) - return BadRequest(CommonResponse.BadIfNonMatch()); + { + _logger.LogInformation(Log.Format(Resources.Controllers.UserAvatarController.LogGetBadIfNoneMatch, + ("Username", username), ("If-None-Match", value))); + return BadRequest(HeaderErrorResponse.BadIfNonMatch(_localizerFactory)); + } if (eTagList.FirstOrDefault(e => e.Equals(eTag)) != null) { Response.Headers.Add("ETag", eTagValue); + _logger.LogInformation(Log.Format(Resources.Controllers.UserAvatarController.LogGetReturnNotModify, ("Username", username))); return StatusCode(StatusCodes.Status304NotModified); } } @@ -86,12 +98,13 @@ namespace Timeline.Controllers var avatarInfo = await _service.GetAvatar(username); var avatar = avatarInfo.Avatar; + _logger.LogInformation(Log.Format(Resources.Controllers.UserAvatarController.LogGetReturnData, ("Username", username))); return File(avatar.Data, avatar.Type, new DateTimeOffset(avatarInfo.LastModified), eTag); } catch (UserNotExistException e) { - _logger.LogInformation(e, $"Attempt to get a avatar of a non-existent user failed. Username: {username} ."); - return NotFound(new CommonResponse(ErrorCodes.Get_UserNotExist, "User does not exist.")); + _logger.LogInformation(e, Log.Format(Resources.Controllers.UserAvatarController.LogGetUserNotExist, ("Username", username))); + return NotFound(new CommonResponse(ErrorCodes.Http.UserAvatar.Get.UserNotExist, _localizer["ErrorGetUserNotExist"])); } } @@ -99,18 +112,18 @@ namespace Timeline.Controllers [Authorize] [RequireContentType, RequireContentLength] [Consumes("image/png", "image/jpeg", "image/gif", "image/webp")] - public async Task Put(string username) + public async Task Put([FromRoute][Username] string username) { - var contentLength = Request.ContentLength.Value; + var contentLength = Request.ContentLength!.Value; if (contentLength > 1000 * 1000 * 10) - return BadRequest(new CommonResponse(ErrorCodes.Put_Content_TooBig, - "Content can't be bigger than 10MB.")); + return BadRequest(ContentErrorResponse.TooBig(_localizerFactory, "10MB")); if (!User.IsAdministrator() && User.Identity.Name != username) { - _logger.LogInformation($"Attempt to put a avatar of other user as a non-admin failed. Operator Username: {User.Identity.Name} ; Username To Put Avatar: {username} ."); + _logger.LogInformation(Log.Format(Resources.Controllers.UserAvatarController.LogPutForbid, + ("Operator Username", User.Identity.Name), ("Username To Put Avatar", username))); return StatusCode(StatusCodes.Status403Forbidden, - new CommonResponse(ErrorCodes.Put_Forbid, "Normal user can't change other's avatar.")); + new CommonResponse(ErrorCodes.Http.UserAvatar.Put.Forbid, _localizer["ErrorPutForbid"])); } try @@ -119,13 +132,11 @@ namespace Timeline.Controllers var bytesRead = await Request.Body.ReadAsync(data); if (bytesRead != contentLength) - return BadRequest(new CommonResponse(ErrorCodes.Put_Content_UnmatchedLength_Less, - $"Content length in header is {contentLength} but actual length is {bytesRead}.")); + return BadRequest(ContentErrorResponse.UnmatchedLength_Smaller(_localizerFactory)); var extraByte = new byte[1]; if (await Request.Body.ReadAsync(extraByte) != 0) - return BadRequest(new CommonResponse(ErrorCodes.Put_Content_UnmatchedLength_Bigger, - $"Content length in header is {contentLength} but actual length is bigger than that.")); + return BadRequest(ContentErrorResponse.UnmatchedLength_Bigger(_localizerFactory)); await _service.SetAvatar(username, new Avatar { @@ -133,43 +144,57 @@ namespace Timeline.Controllers Type = Request.ContentType }); - _logger.LogInformation($"Succeed to put a avatar of a user. Username: {username} ; Mime Type: {Request.ContentType} ."); + _logger.LogInformation(Log.Format(Resources.Controllers.UserAvatarController.LogPutSuccess, + ("Username", username), ("Mime Type", Request.ContentType))); return Ok(); } catch (UserNotExistException e) { - _logger.LogInformation(e, $"Attempt to put a avatar of a non-existent user failed. Username: {username} ."); - return BadRequest(new CommonResponse(ErrorCodes.Put_UserNotExist, "User does not exist.")); + _logger.LogInformation(e, Log.Format(Resources.Controllers.UserAvatarController.LogPutUserNotExist, ("Username", username))); + return BadRequest(new CommonResponse(ErrorCodes.Http.UserAvatar.Put.UserNotExist, _localizer["ErrorPutUserNotExist"])); } - catch (AvatarDataException e) + catch (AvatarFormatException e) { - _logger.LogInformation(e, $"Attempt to put a avatar of a bad format failed. Username: {username} ."); - return BadRequest(new CommonResponse(ErrorCodes.From(e.Error), "Bad format.")); + var (code, message) = e.Error switch + { + AvatarFormatException.ErrorReason.CantDecode => + (ErrorCodes.Http.UserAvatar.Put.BadFormat_CantDecode, _localizer["ErrorPutBadFormatCantDecode"]), + AvatarFormatException.ErrorReason.UnmatchedFormat => + (ErrorCodes.Http.UserAvatar.Put.BadFormat_UnmatchedFormat, _localizer["ErrorPutBadFormatUnmatchedFormat"]), + AvatarFormatException.ErrorReason.BadSize => + (ErrorCodes.Http.UserAvatar.Put.BadFormat_BadSize, _localizer["ErrorPutBadFormatBadSize"]), + _ => + throw new Exception(Resources.Controllers.UserAvatarController.ExceptionUnknownAvatarFormatError) + }; + + _logger.LogInformation(e, Log.Format(Resources.Controllers.UserAvatarController.LogPutUserBadFormat, ("Username", username))); + return BadRequest(new CommonResponse(code, message)); } } [HttpDelete("users/{username}/avatar")] [Authorize] - public async Task Delete([FromRoute] string username) + public async Task Delete([FromRoute][Username] string username) { if (!User.IsAdministrator() && User.Identity.Name != username) { - _logger.LogInformation($"Attempt to delete a avatar of other user as a non-admin failed. Operator Username: {User.Identity.Name} ; Username To Put Avatar: {username} ."); + _logger.LogInformation(Log.Format(Resources.Controllers.UserAvatarController.LogPutUserBadFormat, + ("Operator Username", User.Identity.Name), ("Username To Delete Avatar", username))); return StatusCode(StatusCodes.Status403Forbidden, - new CommonResponse(ErrorCodes.Delete_Forbid, "Normal user can't delete other's avatar.")); + new CommonResponse(ErrorCodes.Http.UserAvatar.Delete.Forbid, _localizer["ErrorDeleteForbid"])); } try { await _service.SetAvatar(username, null); - _logger.LogInformation($"Succeed to delete a avatar of a user. Username: {username} ."); + _logger.LogInformation(Log.Format(Resources.Controllers.UserAvatarController.LogDeleteSuccess, ("Username", username))); return Ok(); } catch (UserNotExistException e) { - _logger.LogInformation(e, $"Attempt to delete a avatar of a non-existent user failed. Username: {username} ."); - return BadRequest(new CommonResponse(ErrorCodes.Delete_UserNotExist, "User does not exist.")); + _logger.LogInformation(e, Log.Format(Resources.Controllers.UserAvatarController.LogDeleteNotExist, ("Username", username))); + return BadRequest(new CommonResponse(ErrorCodes.Http.UserAvatar.Delete.UserNotExist, _localizer["ErrorDeleteUserNotExist"])); } } } diff --git a/Timeline/Entities/UserAvatar.cs b/Timeline/Entities/UserAvatar.cs index d47bb28b..3b5388aa 100644 --- a/Timeline/Entities/UserAvatar.cs +++ b/Timeline/Entities/UserAvatar.cs @@ -11,6 +11,7 @@ namespace Timeline.Entities public long Id { get; set; } [Column("data")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1819:Properties should not return arrays", Justification = "This is data base entity.")] public byte[]? Data { get; set; } [Column("type")] diff --git a/Timeline/ErrorCodes.cs b/Timeline/ErrorCodes.cs index 0b325e27..5e7f003a 100644 --- a/Timeline/ErrorCodes.cs +++ b/Timeline/ErrorCodes.cs @@ -17,10 +17,17 @@ public static class Header // cc = 01 { - public const int Missing_ContentType = 10010101; // dd = 01 - public const int Missing_ContentLength = 10010102; // dd = 02 - public const int Zero_ContentLength = 10010103; // dd = 03 - public const int BadFormat_IfNonMatch = 10010104; // dd = 04 + public const int Missing_ContentType = 10000101; // dd = 01 + public const int Missing_ContentLength = 10000102; // dd = 02 + public const int Zero_ContentLength = 10000103; // dd = 03 + public const int BadFormat_IfNonMatch = 10000104; // dd = 04 + } + + public static class Content // cc = 02 + { + public const int TooBig = 1000201; + public const int UnmatchedLength_Smaller = 10030202; + public const int UnmatchedLength_Bigger = 10030203; } } } diff --git a/Timeline/Filters/ContentHeaderAttributes.cs b/Timeline/Filters/ContentHeaderAttributes.cs index 14685a01..e3d4eeb2 100644 --- a/Timeline/Filters/ContentHeaderAttributes.cs +++ b/Timeline/Filters/ContentHeaderAttributes.cs @@ -1,16 +1,20 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Localization; using Timeline.Models.Http; 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) { - context.Result = new BadRequestObjectResult(CommonResponse.MissingContentType()); + var localizerFactory = context.HttpContext.RequestServices.GetRequiredService(); + context.Result = new BadRequestObjectResult(HeaderErrorResponse.MissingContentType(localizerFactory)); } } } @@ -30,17 +34,20 @@ 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) { - context.Result = new BadRequestObjectResult(CommonResponse.MissingContentLength()); + var localizerFactory = context.HttpContext.RequestServices.GetRequiredService(); + context.Result = new BadRequestObjectResult(HeaderErrorResponse.MissingContentLength(localizerFactory)); return; } if (RequireNonZero && context.HttpContext.Request.ContentLength.Value == 0) { - context.Result = new BadRequestObjectResult(CommonResponse.ZeroContentLength()); + var localizerFactory = context.HttpContext.RequestServices.GetRequiredService(); + context.Result = new BadRequestObjectResult(HeaderErrorResponse.ZeroContentLength(localizerFactory)); return; } } diff --git a/Timeline/Helpers/LanguageHelper.cs b/Timeline/Helpers/LanguageHelper.cs new file mode 100644 index 00000000..b0156b8b --- /dev/null +++ b/Timeline/Helpers/LanguageHelper.cs @@ -0,0 +1,12 @@ +using System.Linq; + +namespace Timeline.Helpers +{ + public static class LanguageHelper + { + public static bool AreSame(this bool firstBool, params bool[] otherBools) + { + return otherBools.All(b => b == firstBool); + } + } +} diff --git a/Timeline/Helpers/Log.cs b/Timeline/Helpers/Log.cs index 8deebf1d..68c975fa 100644 --- a/Timeline/Helpers/Log.cs +++ b/Timeline/Helpers/Log.cs @@ -3,26 +3,6 @@ using System.Text; namespace Timeline.Helpers { - // TODO! Remember to remove this after refactor. - public static class MyLogHelper - { - public static KeyValuePair Pair(string key, object value) => new KeyValuePair(key, value); - - public static string FormatLogMessage(string summary, params KeyValuePair[] properties) - { - var builder = new StringBuilder(); - builder.Append(summary); - foreach (var property in properties) - { - builder.AppendLine(); - builder.Append(property.Key); - builder.Append(" : "); - builder.Append(property.Value); - } - return builder.ToString(); - } - } - public static class Log { public static string Format(string summary, params (string, object?)[] properties) diff --git a/Timeline/Models/Http/Common.cs b/Timeline/Models/Http/Common.cs index c741837a..39ddddd9 100644 --- a/Timeline/Models/Http/Common.cs +++ b/Timeline/Models/Http/Common.cs @@ -5,46 +5,74 @@ namespace Timeline.Models.Http { public class CommonResponse { - public static CommonResponse InvalidModel(string message) + internal static CommonResponse InvalidModel(string message) { return new CommonResponse(ErrorCodes.Http.Common.InvalidModel, message); } - public static CommonResponse MissingContentType() + public CommonResponse() { - return new CommonResponse(ErrorCodes.Http.Common.Header.Missing_ContentType, "Header Content-Type is required."); + } - public static CommonResponse MissingContentLength() + public CommonResponse(int code, string message) { - return new CommonResponse(ErrorCodes.Http.Common.Header.Missing_ContentLength, "Header Content-Length is missing or of bad format."); + Code = code; + Message = message; } - public static CommonResponse ZeroContentLength() + public int Code { get; set; } + public string? Message { get; set; } + } + + internal static class HeaderErrorResponse + { + internal static CommonResponse MissingContentType(IStringLocalizerFactory localizerFactory) { - return new CommonResponse(ErrorCodes.Http.Common.Header.Zero_ContentLength, "Header Content-Length must not be 0."); + var localizer = localizerFactory.Create("Models.Http.Common"); + return new CommonResponse(ErrorCodes.Http.Common.Header.Missing_ContentType, localizer["HeaderMissingContentType"]); } - public static CommonResponse BadIfNonMatch() + internal static CommonResponse MissingContentLength(IStringLocalizerFactory localizerFactory) { - return new CommonResponse(ErrorCodes.Http.Common.Header.BadFormat_IfNonMatch, "Header If-Non-Match is of bad format."); + var localizer = localizerFactory.Create("Models.Http.Common"); + return new CommonResponse(ErrorCodes.Http.Common.Header.Missing_ContentLength, localizer["HeaderMissingContentLength"]); } - public CommonResponse() + internal static CommonResponse ZeroContentLength(IStringLocalizerFactory localizerFactory) { + var localizer = localizerFactory.Create("Models.Http.Common"); + return new CommonResponse(ErrorCodes.Http.Common.Header.Zero_ContentLength, localizer["HeaderZeroContentLength"]); + } + internal static CommonResponse BadIfNonMatch(IStringLocalizerFactory localizerFactory) + { + var localizer = localizerFactory.Create("Models.Http.Common"); + return new CommonResponse(ErrorCodes.Http.Common.Header.BadFormat_IfNonMatch, localizer["HeaderBadIfNonMatch"]); } + } - public CommonResponse(int code, string message) + internal static class ContentErrorResponse + { + internal static CommonResponse TooBig(IStringLocalizerFactory localizerFactory, string maxLength) { - Code = code; - Message = message; + var localizer = localizerFactory.Create("Models.Http.Common"); + return new CommonResponse(ErrorCodes.Http.Common.Content.TooBig, localizer["ContentTooBig", maxLength]); } - public int Code { get; set; } - public string? Message { get; set; } + internal static CommonResponse UnmatchedLength_Smaller(IStringLocalizerFactory localizerFactory) + { + var localizer = localizerFactory.Create("Models.Http.Common"); + return new CommonResponse(ErrorCodes.Http.Common.Content.UnmatchedLength_Smaller, localizer["ContentUnmatchedLengthSmaller"]); + } + internal static CommonResponse UnmatchedLength_Bigger(IStringLocalizerFactory localizerFactory) + { + var localizer = localizerFactory.Create("Models.Http.Common"); + return new CommonResponse(ErrorCodes.Http.Common.Content.UnmatchedLength_Bigger, localizer["ContentUnmatchedLengthBigger"]); + } } + public class CommonDataResponse : CommonResponse { public CommonDataResponse() @@ -87,13 +115,13 @@ namespace Timeline.Models.Http internal static CommonPutResponse Create(IStringLocalizerFactory localizerFactory) { var localizer = localizerFactory.Create("Models.Http.Common"); - return new CommonPutResponse(0, localizer["ResponsePutCreate"], true); + return new CommonPutResponse(0, localizer["PutCreate"], true); } internal static CommonPutResponse Modify(IStringLocalizerFactory localizerFactory) { var localizer = localizerFactory.Create("Models.Http.Common"); - return new CommonPutResponse(0, localizer["ResponsePutModify"], false); + return new CommonPutResponse(0, localizer["PutModify"], false); } } @@ -124,13 +152,13 @@ namespace Timeline.Models.Http internal static CommonDeleteResponse Delete(IStringLocalizerFactory localizerFactory) { var localizer = localizerFactory.Create("Models.Http.Common"); - return new CommonDeleteResponse(0, localizer["ResponseDeleteDelete"], true); + return new CommonDeleteResponse(0, localizer["DeleteDelete"], true); } internal static CommonDeleteResponse NotExist(IStringLocalizerFactory localizerFactory) { var localizer = localizerFactory.Create("Models.Models.Http.Common"); - return new CommonDeleteResponse(0, localizer["ResponseDeleteNotExist"], false); + return new CommonDeleteResponse(0, localizer["DeleteNotExist"], false); } } } diff --git a/Timeline/Resources/Controllers/UserAvatarController.Designer.cs b/Timeline/Resources/Controllers/UserAvatarController.Designer.cs new file mode 100644 index 00000000..e6eeb1e8 --- /dev/null +++ b/Timeline/Resources/Controllers/UserAvatarController.Designer.cs @@ -0,0 +1,171 @@ +//------------------------------------------------------------------------------ +// +// 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 UserAvatarController { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal UserAvatarController() { + } + + /// + /// 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.UserAvatarController", typeof(UserAvatarController).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 Unknown AvatarDataException.ErrorReason value.. + /// + internal static string ExceptionUnknownAvatarFormatError { + get { + return ResourceManager.GetString("ExceptionUnknownAvatarFormatError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Attempt to delete a avatar of other user as a non-admin failed.. + /// + internal static string LogDeleteForbid { + get { + return ResourceManager.GetString("LogDeleteForbid", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Attempt to delete a avatar of a non-existent user failed.. + /// + internal static string LogDeleteNotExist { + get { + return ResourceManager.GetString("LogDeleteNotExist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Succeed to delete a avatar of a user.. + /// + internal static string LogDeleteSuccess { + get { + return ResourceManager.GetString("LogDeleteSuccess", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Attempt to get a avatar with If-None-Match in bad format.. + /// + internal static string LogGetBadIfNoneMatch { + get { + return ResourceManager.GetString("LogGetBadIfNoneMatch", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Returned full data for a get avatar attempt.. + /// + internal static string LogGetReturnData { + get { + return ResourceManager.GetString("LogGetReturnData", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Returned NotModify for a get avatar attempt.. + /// + internal static string LogGetReturnNotModify { + get { + return ResourceManager.GetString("LogGetReturnNotModify", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Attempt to get a avatar of a non-existent user failed.. + /// + internal static string LogGetUserNotExist { + get { + return ResourceManager.GetString("LogGetUserNotExist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Attempt to put a avatar of other user as a non-admin failed.. + /// + internal static string LogPutForbid { + get { + return ResourceManager.GetString("LogPutForbid", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Succeed to put a avatar of a user.. + /// + internal static string LogPutSuccess { + get { + return ResourceManager.GetString("LogPutSuccess", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Attempt to put a avatar of a bad format failed.. + /// + internal static string LogPutUserBadFormat { + get { + return ResourceManager.GetString("LogPutUserBadFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Attempt to put a avatar of a non-existent user failed.. + /// + internal static string LogPutUserNotExist { + get { + return ResourceManager.GetString("LogPutUserNotExist", resourceCulture); + } + } + } +} diff --git a/Timeline/Resources/Controllers/UserAvatarController.en.resx b/Timeline/Resources/Controllers/UserAvatarController.en.resx new file mode 100644 index 00000000..cf92ae6d --- /dev/null +++ b/Timeline/Resources/Controllers/UserAvatarController.en.resx @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + 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. + + \ No newline at end of file diff --git a/Timeline/Resources/Controllers/UserAvatarController.resx b/Timeline/Resources/Controllers/UserAvatarController.resx new file mode 100644 index 00000000..58860c83 --- /dev/null +++ b/Timeline/Resources/Controllers/UserAvatarController.resx @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Unknown AvatarDataException.ErrorReason value. + + + Attempt to delete a avatar of other user as a non-admin failed. + + + Attempt to delete a avatar of a non-existent user failed. + + + Succeed to delete a avatar of a user. + + + Attempt to get a avatar with If-None-Match in bad format. + + + Returned full data for a get avatar attempt. + + + Returned NotModify for a get avatar attempt. + + + Attempt to get a avatar of a non-existent user failed. + + + Attempt to put a avatar of other user as a non-admin failed. + + + Succeed to put a avatar of a user. + + + Attempt to put a avatar of a bad format failed. + + + Attempt to put a avatar of a non-existent user failed. + + \ No newline at end of file diff --git a/Timeline/Resources/Controllers/UserAvatarController.zh.resx b/Timeline/Resources/Controllers/UserAvatarController.zh.resx new file mode 100644 index 00000000..94de1606 --- /dev/null +++ b/Timeline/Resources/Controllers/UserAvatarController.zh.resx @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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/Http/Common.en.resx b/Timeline/Resources/Models/Http/Common.en.resx index 40d44191..10407d76 100644 --- a/Timeline/Resources/Models/Http/Common.en.resx +++ b/Timeline/Resources/Models/Http/Common.en.resx @@ -117,16 +117,37 @@ 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. + + + Header Content-Length is missing or of bad format. + + + Header Content-Type is required. + + + Header Content-Length must not be 0. + + A new item is created. - + An existent item is modified. \ No newline at end of file diff --git a/Timeline/Resources/Models/Http/Common.zh.resx b/Timeline/Resources/Models/Http/Common.zh.resx index b6d955d9..528dc7ab 100644 --- a/Timeline/Resources/Models/Http/Common.zh.resx +++ b/Timeline/Resources/Models/Http/Common.zh.resx @@ -117,16 +117,37 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + + 请求体太大。它不能超过{0}. + + + 实际的请求体长度比头中指示的大。 + + + 实际的请求体长度比头中指示的小。 + + 删除了一个项目。 - + 要删除的项目不存在,什么都没有修改。 - + + 头If-Non-Match格式不对。 + + + 头Content-Length缺失或者格式不对。 + + + 缺少必需的头Content-Type。 + + + 头Content-Length不能为0。 + + 创建了一个新项目。 - + 修改了一个已存在的项目。 \ No newline at end of file diff --git a/Timeline/Resources/Services/Exception.Designer.cs b/Timeline/Resources/Services/Exception.Designer.cs index 24f6b8e6..ddf60f45 100644 --- a/Timeline/Resources/Services/Exception.Designer.cs +++ b/Timeline/Resources/Services/Exception.Designer.cs @@ -60,6 +60,51 @@ namespace Timeline.Resources.Services { } } + /// + /// Looks up a localized string similar to Avartar is of bad format because {0}.. + /// + internal static string AvatarFormatException { + get { + return ResourceManager.GetString("AvatarFormatException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to image is not a square, aka, width is not equal to height. + /// + internal static string AvatarFormatExceptionBadSize { + get { + return ResourceManager.GetString("AvatarFormatExceptionBadSize", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to failed to decode image, see inner exception. + /// + internal static string AvatarFormatExceptionCantDecode { + get { + return ResourceManager.GetString("AvatarFormatExceptionCantDecode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to unknown error. + /// + internal static string AvatarFormatExceptionUnknownError { + get { + return ResourceManager.GetString("AvatarFormatExceptionUnknownError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to image's actual mime type is not the specified one. + /// + internal static string AvatarFormatExceptionUnmatchedFormat { + get { + return ResourceManager.GetString("AvatarFormatExceptionUnmatchedFormat", resourceCulture); + } + } + /// /// Looks up a localized string similar to The password is wrong.. /// diff --git a/Timeline/Resources/Services/Exception.resx b/Timeline/Resources/Services/Exception.resx index 408c45a1..12bf9afb 100644 --- a/Timeline/Resources/Services/Exception.resx +++ b/Timeline/Resources/Services/Exception.resx @@ -117,6 +117,21 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Avartar is of bad format because {0}. + + + image is not a square, aka, width is not equal to height + + + failed to decode image, see inner exception + + + unknown error + + + image's actual mime type is not the specified one + The password is wrong. diff --git a/Timeline/Resources/Services/UserAvatarService.Designer.cs b/Timeline/Resources/Services/UserAvatarService.Designer.cs new file mode 100644 index 00000000..cabc9ede --- /dev/null +++ b/Timeline/Resources/Services/UserAvatarService.Designer.cs @@ -0,0 +1,108 @@ +//------------------------------------------------------------------------------ +// +// 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 UserAvatarService { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal UserAvatarService() { + } + + /// + /// 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.UserAvatarService", typeof(UserAvatarService).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 Data of avatar is null.. + /// + internal static string ArgumentAvatarDataNull { + get { + return ResourceManager.GetString("ArgumentAvatarDataNull", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Type of avatar is null.. + /// + internal static string ArgumentAvatarTypeNull { + get { + return ResourceManager.GetString("ArgumentAvatarTypeNull", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Database corupted! One of type and data of a avatar is null but the other is not.. + /// + internal static string DatabaseCorruptedDataAndTypeNotSame { + get { + return ResourceManager.GetString("DatabaseCorruptedDataAndTypeNotSame", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Created an entry in user_avatars.. + /// + internal static string LogCreateEntity { + get { + return ResourceManager.GetString("LogCreateEntity", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Updated an entry in user_avatars.. + /// + internal static string LogUpdateEntity { + get { + return ResourceManager.GetString("LogUpdateEntity", resourceCulture); + } + } + } +} diff --git a/Timeline/Resources/Services/UserAvatarService.resx b/Timeline/Resources/Services/UserAvatarService.resx new file mode 100644 index 00000000..ab6389ff --- /dev/null +++ b/Timeline/Resources/Services/UserAvatarService.resx @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Data of avatar is null. + + + Type of avatar is null. + + + Database corupted! One of type and data of a avatar is null but the other is not. + + + Created an entry in user_avatars. + + + Updated an entry in user_avatars. + + \ No newline at end of file diff --git a/Timeline/Services/AvatarFormatException.cs b/Timeline/Services/AvatarFormatException.cs new file mode 100644 index 00000000..788eabb2 --- /dev/null +++ b/Timeline/Services/AvatarFormatException.cs @@ -0,0 +1,51 @@ +using System; +using System.Globalization; + +namespace Timeline.Services +{ + /// + /// Thrown when avatar is of bad format. + /// + [Serializable] + public class AvatarFormatException : Exception + { + public enum ErrorReason + { + /// + /// Decoding image failed. + /// + CantDecode, + /// + /// Decoding succeeded but the real type is not the specified type. + /// + UnmatchedFormat, + /// + /// Image is not a square. + /// + BadSize + } + + public AvatarFormatException() : base(MakeMessage(null)) { } + public AvatarFormatException(string message) : base(message) { } + public AvatarFormatException(string message, Exception inner) : base(message, inner) { } + + public AvatarFormatException(Avatar avatar, ErrorReason error) : base(MakeMessage(error)) { Avatar = avatar; Error = error; } + public AvatarFormatException(Avatar avatar, ErrorReason error, Exception inner) : base(MakeMessage(error), inner) { Avatar = avatar; Error = error; } + + protected AvatarFormatException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + private static string MakeMessage(ErrorReason? reason) => + string.Format(CultureInfo.InvariantCulture, Resources.Services.Exception.AvatarFormatException, reason switch + { + ErrorReason.CantDecode => Resources.Services.Exception.AvatarFormatExceptionCantDecode, + ErrorReason.UnmatchedFormat => Resources.Services.Exception.AvatarFormatExceptionUnmatchedFormat, + ErrorReason.BadSize => Resources.Services.Exception.AvatarFormatExceptionBadSize, + _ => Resources.Services.Exception.AvatarFormatExceptionUnknownError + }); + + public ErrorReason? Error { get; set; } + public Avatar? Avatar { get; set; } + } +} diff --git a/Timeline/Services/DatabaseExtensions.cs b/Timeline/Services/DatabaseExtensions.cs index a37cf05b..62b22f00 100644 --- a/Timeline/Services/DatabaseExtensions.cs +++ b/Timeline/Services/DatabaseExtensions.cs @@ -4,22 +4,27 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Timeline.Entities; +using Timeline.Models.Validation; namespace Timeline.Services { - public static class DatabaseExtensions + internal static class DatabaseExtensions { /// /// Check the existence and get the id of the user. /// /// The username of the user. /// The user id. - /// Thrown if is null or empty. + /// Thrown if is null. + /// Thrown if is of bad format. /// Thrown if user does not exist. - public static async Task CheckAndGetUser(DbSet userDbSet, string username) + internal static async Task CheckAndGetUser(DbSet userDbSet, UsernameValidator validator, string username) { - if (string.IsNullOrEmpty(username)) - throw new ArgumentException("Username is null or empty.", nameof(username)); + if (username == null) + throw new ArgumentNullException(nameof(username)); + var (result, messageGenerator) = validator.Validate(username); + if (!result) + throw new UsernameBadFormatException(username, messageGenerator(null)); var userId = await userDbSet.Where(u => u.Name == username).Select(u => u.Id).SingleOrDefaultAsync(); if (userId == 0) diff --git a/Timeline/Services/ETagGenerator.cs b/Timeline/Services/ETagGenerator.cs index e2abebdc..e518f01f 100644 --- a/Timeline/Services/ETagGenerator.cs +++ b/Timeline/Services/ETagGenerator.cs @@ -5,13 +5,20 @@ namespace Timeline.Services { public interface IETagGenerator { + /// + /// Generate a etag for given source. + /// + /// The source data. + /// The generated etag. + /// Thrown if is null. string Generate(byte[] source); } - public class ETagGenerator : IETagGenerator, IDisposable + public sealed class ETagGenerator : IETagGenerator, IDisposable { private readonly SHA1 _sha1; + [System.Diagnostics.CodeAnalysis.SuppressMessage("Security", "CA5350:Do Not Use Weak Cryptographic Algorithms", Justification = "Sha1 is enough ??? I don't know.")] public ETagGenerator() { _sha1 = SHA1.Create(); @@ -19,15 +26,19 @@ namespace Timeline.Services public string Generate(byte[] source) { - if (source == null || source.Length == 0) - throw new ArgumentException("Source is null or empty.", nameof(source)); + if (source == null) + throw new ArgumentNullException(nameof(source)); return Convert.ToBase64String(_sha1.ComputeHash(source)); } + private bool _disposed = false; // To detect redundant calls + public void Dispose() { + if (_disposed) return; _sha1.Dispose(); + _disposed = true; } } } diff --git a/Timeline/Services/UserAvatarService.cs b/Timeline/Services/UserAvatarService.cs index ecec5a31..4c65a0fa 100644 --- a/Timeline/Services/UserAvatarService.cs +++ b/Timeline/Services/UserAvatarService.cs @@ -10,53 +10,24 @@ using System.IO; using System.Linq; using System.Threading.Tasks; using Timeline.Entities; +using Timeline.Helpers; +using Timeline.Models.Validation; namespace Timeline.Services { public class Avatar { - public string Type { get; set; } - public byte[] Data { get; set; } + public string Type { get; set; } = default!; + [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1819:Properties should not return arrays", Justification = "DTO Object")] + public byte[] Data { get; set; } = default!; } public class AvatarInfo { - public Avatar Avatar { get; set; } + public Avatar Avatar { get; set; } = default!; public DateTime LastModified { get; set; } } - /// - /// Thrown when avatar is of bad format. - /// - [Serializable] - public class AvatarDataException : Exception - { - public enum ErrorReason - { - /// - /// Decoding image failed. - /// - CantDecode, - /// - /// Decoding succeeded but the real type is not the specified type. - /// - UnmatchedFormat, - /// - /// Image is not a square. - /// - BadSize - } - - public AvatarDataException(Avatar avatar, ErrorReason error, string message) : base(message) { Avatar = avatar; Error = error; } - public AvatarDataException(Avatar avatar, ErrorReason error, string message, Exception inner) : base(message, inner) { Avatar = avatar; Error = error; } - protected AvatarDataException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - public ErrorReason Error { get; set; } - public Avatar Avatar { get; set; } - } - /// /// Provider for default user avatar. /// @@ -83,7 +54,7 @@ namespace Timeline.Services /// Validate a avatar's format and size info. /// /// The avatar to validate. - /// Thrown when validation failed. + /// Thrown when validation failed. Task Validate(Avatar avatar); } @@ -94,16 +65,18 @@ namespace Timeline.Services /// /// The username of the user to get avatar etag of. /// The etag. - /// Thrown if is null or empty. + /// Thrown if is null. + /// Thrown if the is of bad format. /// Thrown if the user does not exist. Task GetAvatarETag(string username); /// - /// Get avatar of a user. If the user has no avatar, a default one is returned. + /// Get avatar of a user. If the user has no avatar set, a default one is returned. /// /// The username of the user to get avatar of. /// The avatar info. - /// Thrown if is null or empty. + /// Thrown if is null. + /// Thrown if the is of bad format. /// Thrown if the user does not exist. Task GetAvatar(string username); @@ -112,38 +85,41 @@ namespace Timeline.Services /// /// The username of the user to set avatar for. /// The avatar. Can be null to delete the saved avatar. - /// Throw if is null or empty. - /// Or thrown if is not null but is null or empty or is null. + /// 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); + /// Thrown if avatar is of bad format. + Task SetAvatar(string username, Avatar? avatar); } + // TODO! : Make this configurable. public class DefaultUserAvatarProvider : IDefaultUserAvatarProvider { - private readonly IWebHostEnvironment _environment; - private readonly IETagGenerator _eTagGenerator; - private byte[] _cacheData; + private readonly string _avatarPath; + + private byte[] _cacheData = default!; private DateTime _cacheLastModified; - private string _cacheETag; + private string _cacheETag = default!; + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "DI.")] public DefaultUserAvatarProvider(IWebHostEnvironment environment, IETagGenerator eTagGenerator) { - _environment = environment; + _avatarPath = Path.Combine(environment.ContentRootPath, "default-avatar.png"); _eTagGenerator = eTagGenerator; } private async Task CheckAndInit() { - if (_cacheData != null) - return; - - var path = Path.Combine(_environment.ContentRootPath, "default-avatar.png"); - _cacheData = await File.ReadAllBytesAsync(path); - _cacheLastModified = File.GetLastWriteTime(path); - _cacheETag = _eTagGenerator.Generate(_cacheData); + var path = _avatarPath; + if (_cacheData == null || File.GetLastWriteTime(path) > _cacheLastModified) + { + _cacheData = await File.ReadAllBytesAsync(path); + _cacheLastModified = File.GetLastWriteTime(path); + _cacheETag = _eTagGenerator.Generate(_cacheData); + } } public async Task GetDefaultAvatarETag() @@ -175,17 +151,15 @@ namespace Timeline.Services { try { - using (var image = Image.Load(avatar.Data, out IImageFormat format)) - { - if (!format.MimeTypes.Contains(avatar.Type)) - throw new AvatarDataException(avatar, AvatarDataException.ErrorReason.UnmatchedFormat, "Image's actual mime type is not the specified one."); - if (image.Width != image.Height) - throw new AvatarDataException(avatar, AvatarDataException.ErrorReason.BadSize, "Image is not a square, aka, width is not equal to height."); - } + using var image = Image.Load(avatar.Data, out IImageFormat format); + if (!format.MimeTypes.Contains(avatar.Type)) + throw new AvatarFormatException(avatar, AvatarFormatException.ErrorReason.UnmatchedFormat); + if (image.Width != image.Height) + throw new AvatarFormatException(avatar, AvatarFormatException.ErrorReason.BadSize); } catch (UnknownImageFormatException e) { - throw new AvatarDataException(avatar, AvatarDataException.ErrorReason.CantDecode, "Failed to decode image. See inner exception.", e); + throw new AvatarFormatException(avatar, AvatarFormatException.ErrorReason.CantDecode, e); } }); } @@ -203,6 +177,8 @@ namespace Timeline.Services private readonly IETagGenerator _eTagGenerator; + private readonly UsernameValidator _usernameValidator; + public UserAvatarService( ILogger logger, DatabaseContext database, @@ -215,13 +191,14 @@ namespace Timeline.Services _defaultUserAvatarProvider = defaultUserAvatarProvider; _avatarValidator = avatarValidator; _eTagGenerator = eTagGenerator; + _usernameValidator = new UsernameValidator(); } public async Task GetAvatarETag(string username) { - var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username); + var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, _usernameValidator, username); - var eTag = (await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.ETag }).SingleAsync()).ETag; + var eTag = (await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.ETag }).SingleOrDefaultAsync())?.ETag; if (eTag == null) return await _defaultUserAvatarProvider.GetDefaultAvatarETag(); else @@ -230,54 +207,57 @@ namespace Timeline.Services public async Task GetAvatar(string username) { - var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username); + var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, _usernameValidator, username); - var avatar = await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.Type, a.Data, a.LastModified }).SingleAsync(); + var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.Type, a.Data, a.LastModified }).SingleOrDefaultAsync(); - if ((avatar.Type == null) != (avatar.Data == null)) + if (avatarEntity != null) { - _logger.LogCritical("Database corupted! One of type and data of a avatar is null but the other is not."); - throw new DatabaseCorruptedException(); - } + if (!LanguageHelper.AreSame(avatarEntity.Data == null, avatarEntity.Type == null)) + { + var message = Resources.Services.UserAvatarService.DatabaseCorruptedDataAndTypeNotSame; + _logger.LogCritical(message); + throw new DatabaseCorruptedException(message); + } - if (avatar.Data == null) - { - var defaultAvatar = await _defaultUserAvatarProvider.GetDefaultAvatar(); - defaultAvatar.LastModified = defaultAvatar.LastModified > avatar.LastModified ? defaultAvatar.LastModified : avatar.LastModified; - return defaultAvatar; - } - else - { - return new AvatarInfo + if (avatarEntity.Data != null) { - Avatar = new Avatar + return new AvatarInfo { - Type = avatar.Type, - Data = avatar.Data - }, - LastModified = avatar.LastModified - }; + Avatar = new Avatar + { + Type = avatarEntity.Type!, + Data = avatarEntity.Data + }, + LastModified = avatarEntity.LastModified + }; + } } + var defaultAvatar = await _defaultUserAvatarProvider.GetDefaultAvatar(); + if (avatarEntity != null) + defaultAvatar.LastModified = defaultAvatar.LastModified > avatarEntity.LastModified ? defaultAvatar.LastModified : avatarEntity.LastModified; + return defaultAvatar; } - public async Task SetAvatar(string username, Avatar avatar) + public async Task SetAvatar(string username, Avatar? avatar) { if (avatar != null) { - if (string.IsNullOrEmpty(avatar.Type)) - throw new ArgumentException("Type of avatar is null or empty.", nameof(avatar)); if (avatar.Data == null) - throw new ArgumentException("Data of avatar is null.", nameof(avatar)); + throw new ArgumentException(Resources.Services.UserAvatarService.ArgumentAvatarDataNull, nameof(avatar)); + if (avatar.Type == null) + throw new ArgumentException(Resources.Services.UserAvatarService.ArgumentAvatarTypeNull, nameof(avatar)); } - var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username); - - var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == userId).SingleAsync(); + var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, _usernameValidator, username); + var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == userId).SingleOrDefaultAsync(); if (avatar == null) { - if (avatarEntity.Data == null) + if (avatarEntity == null || avatarEntity.Data == null) + { return; + } else { avatarEntity.Data = null; @@ -285,18 +265,29 @@ namespace Timeline.Services avatarEntity.ETag = null; avatarEntity.LastModified = DateTime.Now; await _database.SaveChangesAsync(); - _logger.LogInformation("Updated an entry in user_avatars."); + _logger.LogInformation(Resources.Services.UserAvatarService.LogUpdateEntity); } } else { await _avatarValidator.Validate(avatar); - avatarEntity.Type = avatar.Type; + var create = avatarEntity == null; + if (create) + { + avatarEntity = new UserAvatar(); + } + avatarEntity!.Type = avatar.Type; avatarEntity.Data = avatar.Data; avatarEntity.ETag = _eTagGenerator.Generate(avatar.Data); avatarEntity.LastModified = DateTime.Now; + if (create) + { + _database.UserAvatars.Add(avatarEntity); + } await _database.SaveChangesAsync(); - _logger.LogInformation("Updated an entry in user_avatars."); + _logger.LogInformation(create ? + Resources.Services.UserAvatarService.LogCreateEntity + : Resources.Services.UserAvatarService.LogUpdateEntity); } } } @@ -308,7 +299,7 @@ namespace Timeline.Services services.TryAddTransient(); services.AddScoped(); services.AddSingleton(); - services.AddSingleton(); + services.AddTransient(); } } } diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs index d706d05e..f1317856 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -272,7 +272,7 @@ namespace Timeline.Services Name = username, EncryptedPassword = _passwordService.HashPassword(password), RoleString = UserRoleConvert.ToString(administrator), - Avatar = UserAvatar.Create(DateTime.Now) + Avatar = null }; await _databaseContext.AddAsync(newUser); await _databaseContext.SaveChangesAsync(); diff --git a/Timeline/Timeline.csproj b/Timeline/Timeline.csproj index 0ba34471..519a802d 100644 --- a/Timeline/Timeline.csproj +++ b/Timeline/Timeline.csproj @@ -49,6 +49,11 @@ True TokenController.resx + + True + True + UserAvatarController.resx + True True @@ -69,6 +74,11 @@ True Exception.resx + + True + True + UserAvatarService.resx + True True @@ -97,6 +107,10 @@ + + ResXFileCodeGenerator + UserAvatarController.Designer.cs + ResXFileCodeGenerator UserController.Designer.cs @@ -116,6 +130,10 @@ ResXFileCodeGenerator Exception.Designer.cs + + ResXFileCodeGenerator + UserAvatarService.Designer.cs + ResXFileCodeGenerator UserService.Designer.cs -- cgit v1.2.3