From fa2a3282c51d831b25f374803301e75eac15d11c Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Thu, 17 Oct 2019 20:46:57 +0800 Subject: ... --- .../Helpers/AssertionResponseExtensions.cs | 24 ++++--- .../Authentication/AuthenticationExtensions.cs | 6 +- Timeline.Tests/Helpers/InvalidModelTestHelpers.cs | 5 +- Timeline.Tests/Helpers/MyTestLoggerFactory.cs | 25 -------- Timeline.Tests/Helpers/MyWebApplicationFactory.cs | 73 ---------------------- Timeline.Tests/Helpers/TestApplication.cs | 52 +++++++++++++++ 6 files changed, 74 insertions(+), 111 deletions(-) delete mode 100644 Timeline.Tests/Helpers/MyTestLoggerFactory.cs delete mode 100644 Timeline.Tests/Helpers/MyWebApplicationFactory.cs create mode 100644 Timeline.Tests/Helpers/TestApplication.cs (limited to 'Timeline.Tests/Helpers') diff --git a/Timeline.Tests/Helpers/AssertionResponseExtensions.cs b/Timeline.Tests/Helpers/AssertionResponseExtensions.cs index e67a172a..c7ebdb7a 100644 --- a/Timeline.Tests/Helpers/AssertionResponseExtensions.cs +++ b/Timeline.Tests/Helpers/AssertionResponseExtensions.cs @@ -110,29 +110,39 @@ namespace Timeline.Tests.Helpers 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) { assertions.HaveBodyAsCommonResponse(because, becauseArgs).Which.Code.Should().Be(expected, because, becauseArgs); } - public static void BePutCreated(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs) + public static void HaveBodyAsCommonDataResponseWithCode(this HttpResponseMessageAssertions assertions, int expected, string because = "", params object[] becauseArgs) + { + assertions.HaveBodyAsCommonDataResponse(because, becauseArgs).Which.Code.Should().Be(expected, because, becauseArgs); + } + + public static void BePutCreate(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs) { - assertions.HaveStatusCodeCreated(because, becauseArgs).And.Should().HaveBodyAsCommonResponse(because, becauseArgs).Which.Should().BeEquivalentTo(CommonPutResponse.Created, because, becauseArgs); + assertions.HaveStatusCodeCreated(because, becauseArgs).And.Should().HaveBodyAsCommonDataResponse(because, becauseArgs).Which.Should().BeEquivalentTo(CommonPutResponse.Create(), because, becauseArgs); } - public static void BePutModified(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs) + public static void BePutModify(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs) { - assertions.HaveStatusCodeOk(because, becauseArgs).And.Should().HaveBodyAsCommonResponse(because, becauseArgs).Which.Should().BeEquivalentTo(CommonPutResponse.Modified, because, becauseArgs); + assertions.HaveStatusCodeOk(because, becauseArgs).And.Should().HaveBodyAsCommonDataResponse(because, becauseArgs).Which.Should().BeEquivalentTo(CommonPutResponse.Modify(), because, becauseArgs); } - public static void BeDeleteDeleted(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs) + public static void BeDeleteDelete(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs) { - assertions.HaveStatusCodeOk(because, becauseArgs).And.Should().HaveBodyAsCommonResponse(because, becauseArgs).Which.Should().BeEquivalentTo(CommonDeleteResponse.Deleted, because, becauseArgs); + assertions.HaveStatusCodeOk(because, becauseArgs).And.Should().HaveBodyAsCommonDataResponse(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().HaveBodyAsCommonResponse(because, becauseArgs).Which.Should().BeEquivalentTo(CommonDeleteResponse.NotExists, because, becauseArgs); + assertions.HaveStatusCodeOk(because, becauseArgs).And.Should().HaveBodyAsCommonDataResponse(because, becauseArgs).Which.Should().BeEquivalentTo(CommonDeleteResponse.NotExist(), because, becauseArgs); } } } diff --git a/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs b/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs index c8a79e58..d068a08a 100644 --- a/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs +++ b/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs @@ -13,7 +13,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, ExpireOffset = expireOffset }); + var response = await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest { Username = username, Password = password, Expire = expireOffset }); response.Should().HaveStatusCodeOk(); var result = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); return result; @@ -29,12 +29,12 @@ namespace Timeline.Tests.Helpers.Authentication public static Task CreateClientAsUser(this WebApplicationFactory factory) where T : class { - return factory.CreateClientWithCredential(MockUsers.UserUsername, MockUsers.UserPassword); + return factory.CreateClientWithCredential(MockUser.User.Username, MockUser.User.Password); } public static Task CreateClientAsAdmin(this WebApplicationFactory factory) where T : class { - return factory.CreateClientWithCredential(MockUsers.AdminUsername, MockUsers.AdminPassword); + return factory.CreateClientWithCredential(MockUser.Admin.Username, MockUser.Admin.Password); } } } diff --git a/Timeline.Tests/Helpers/InvalidModelTestHelpers.cs b/Timeline.Tests/Helpers/InvalidModelTestHelpers.cs index af432095..4a445ca4 100644 --- a/Timeline.Tests/Helpers/InvalidModelTestHelpers.cs +++ b/Timeline.Tests/Helpers/InvalidModelTestHelpers.cs @@ -1,6 +1,5 @@ using System.Net.Http; using System.Threading.Tasks; -using Timeline.Models.Http; namespace Timeline.Tests.Helpers { @@ -10,14 +9,14 @@ namespace Timeline.Tests.Helpers { var response = await client.PostAsJsonAsync(url, body); response.Should().HaveStatusCodeBadRequest() - .And.Should().HaveBodyAsCommonResponseWithCode(CommonResponse.ErrorCodes.InvalidModel); + .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(CommonResponse.ErrorCodes.InvalidModel); + .And.Should().HaveBodyAsCommonResponseWithCode(ErrorCodes.Http.Common.InvalidModel); } } } diff --git a/Timeline.Tests/Helpers/MyTestLoggerFactory.cs b/Timeline.Tests/Helpers/MyTestLoggerFactory.cs deleted file mode 100644 index b9960378..00000000 --- a/Timeline.Tests/Helpers/MyTestLoggerFactory.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Xunit.Abstractions; - -namespace Timeline.Tests.Helpers -{ - public static class Logging - { - public static ILoggerFactory Create(ITestOutputHelper outputHelper) - { - // TODO: Use test output. - return NullLoggerFactory.Instance; - } - - public static IWebHostBuilder ConfigureTestLogging(this IWebHostBuilder builder) - { - builder.ConfigureLogging(logging => - { - //logging.AddXunit(outputHelper); - }); - return builder; - } - } -} diff --git a/Timeline.Tests/Helpers/MyWebApplicationFactory.cs b/Timeline.Tests/Helpers/MyWebApplicationFactory.cs deleted file mode 100644 index dfbe6620..00000000 --- a/Timeline.Tests/Helpers/MyWebApplicationFactory.cs +++ /dev/null @@ -1,73 +0,0 @@ -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Mvc.Testing; -using Microsoft.AspNetCore.TestHost; -using Microsoft.Data.Sqlite; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Diagnostics; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using System; -using Timeline.Entities; -using Timeline.Services; -using Timeline.Tests.Mock.Data; -using Timeline.Tests.Mock.Services; -using Xunit.Abstractions; - -namespace Timeline.Tests.Helpers -{ - public class MyWebApplicationFactory : WebApplicationFactory where TStartup : class - { - protected override void ConfigureWebHost(IWebHostBuilder builder) - { - builder.ConfigureTestServices(services => - { - services.AddSingleton(); - }); - } - } - - public static class WebApplicationFactoryExtensions - { - public static WebApplicationFactory WithTestConfig(this WebApplicationFactory factory, ITestOutputHelper outputHelper, out Action disposeAction) where TEntry : class - { - // We should keep the connection, so the database is persisted but not recreate every time. - // See https://docs.microsoft.com/en-us/ef/core/miscellaneous/testing/sqlite#writing-tests . - SqliteConnection _databaseConnection = new SqliteConnection("Data Source=:memory:;"); - _databaseConnection.Open(); - - { - var options = new DbContextOptionsBuilder() - .UseSqlite(_databaseConnection) - .ConfigureWarnings(builder => - { - builder.Throw(RelationalEventId.QueryClientEvaluationWarning); - }) - .Options; - - using (var context = new DatabaseContext(options)) - { - TestDatabase.InitDatabase(context); - }; - } - - disposeAction = () => - { - _databaseConnection.Close(); - _databaseConnection.Dispose(); - }; - - return factory.WithWebHostBuilder(builder => - { - builder.ConfigureTestLogging() - .ConfigureServices(services => - { - services.AddEntityFrameworkSqlite(); - services.AddDbContext(options => - { - options.UseSqlite(_databaseConnection); - }); - }); - }); - } - } -} diff --git a/Timeline.Tests/Helpers/TestApplication.cs b/Timeline.Tests/Helpers/TestApplication.cs new file mode 100644 index 00000000..b0187a30 --- /dev/null +++ b/Timeline.Tests/Helpers/TestApplication.cs @@ -0,0 +1,52 @@ +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using System; +using Timeline.Entities; +using Timeline.Tests.Mock.Data; + +namespace Timeline.Tests.Helpers +{ + public class TestApplication : IDisposable + { + public SqliteConnection DatabaseConnection { get; } = new SqliteConnection("Data Source=:memory:;"); + public WebApplicationFactory Factory { get; } + + public TestApplication(WebApplicationFactory factory) + { + // We should keep the connection, so the database is persisted but not recreate every time. + // See https://docs.microsoft.com/en-us/ef/core/miscellaneous/testing/sqlite#writing-tests . + DatabaseConnection.Open(); + + { + var options = new DbContextOptionsBuilder() + .UseSqlite(DatabaseConnection) + .Options; + + using (var context = new DatabaseContext(options)) + { + TestDatabase.InitDatabase(context); + }; + } + + Factory = factory.WithWebHostBuilder(builder => + { + builder.ConfigureServices(services => + { + services.AddEntityFrameworkSqlite(); + services.AddDbContext(options => + { + options.UseSqlite(DatabaseConnection); + }); + }); + }); + } + + public void Dispose() + { + DatabaseConnection.Close(); + DatabaseConnection.Dispose(); + } + } +} -- 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.Tests/Helpers') 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 5e64e3385ae8eb9b877c032418da9e5086d50a06 Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Mon, 21 Oct 2019 13:41:46 +0800 Subject: ... --- Timeline.Tests/Controllers/TokenControllerTest.cs | 13 +-- Timeline.Tests/Controllers/UserControllerTest.cs | 109 +++++++++++++++++++++ Timeline.Tests/GlobalSuppressions.cs | 5 + .../Helpers/AssertionResponseExtensions.cs | 46 +++------ Timeline.Tests/Helpers/HttpClientExtensions.cs | 13 +++ Timeline.Tests/Helpers/ImageHelper.cs | 24 ++--- Timeline.Tests/IntegratedTests/UserUnitTest.cs | 22 ++--- .../Mock/Services/MockStringLocalizer.cs | 31 ------ .../Mock/Services/TestStringLocalizerFactory.cs | 25 +++++ Timeline/Controllers/TokenController.cs | 4 +- Timeline/Controllers/UserController.cs | 39 ++++---- Timeline/Models/Http/Common.cs | 42 ++++++-- .../Resources/Controllers/UserController.en.resx | 9 ++ Timeline/Resources/Controllers/UserController.resx | 15 +++ .../Resources/Controllers/UserController.zh.resx | 9 ++ 15 files changed, 281 insertions(+), 125 deletions(-) create mode 100644 Timeline.Tests/Controllers/UserControllerTest.cs delete mode 100644 Timeline.Tests/Mock/Services/MockStringLocalizer.cs create mode 100644 Timeline.Tests/Mock/Services/TestStringLocalizerFactory.cs (limited to 'Timeline.Tests/Helpers') 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 912455ac19161533205c2fe56b91ff4595ea4fdb Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Thu, 24 Oct 2019 19:57:59 +0800 Subject: ... --- Timeline.Tests/DatabaseTest.cs | 14 +- Timeline.Tests/GlobalSuppressions.cs | 2 +- .../Helpers/AsyncFunctionAssertionsExtensions.cs | 16 ++ Timeline.Tests/IntegratedTests/UserAvatarTest.cs | 20 ++ Timeline.Tests/Mock/Data/TestUsers.cs | 2 +- Timeline.Tests/UserAvatarServiceTest.cs | 206 +++++++++------------ Timeline/Entities/UserAvatar.cs | 12 -- .../Services/UserAvatarService.Designer.cs | 6 +- Timeline/Resources/Services/UserAvatarService.resx | 4 +- Timeline/Services/ETagGenerator.cs | 7 +- Timeline/Services/UserAvatarService.cs | 19 +- 11 files changed, 162 insertions(+), 146 deletions(-) create mode 100644 Timeline.Tests/Helpers/AsyncFunctionAssertionsExtensions.cs (limited to 'Timeline.Tests/Helpers') diff --git a/Timeline.Tests/DatabaseTest.cs b/Timeline.Tests/DatabaseTest.cs index c45c0f66..b5681491 100644 --- a/Timeline.Tests/DatabaseTest.cs +++ b/Timeline.Tests/DatabaseTest.cs @@ -26,11 +26,21 @@ namespace Timeline.Tests [Fact] public void DeleteUserShouldAlsoDeleteAvatar() { - _context.UserAvatars.Count().Should().Be(2); var user = _context.Users.First(); - _context.Users.Remove(user); + _context.UserAvatars.Count().Should().Be(0); + _context.UserAvatars.Add(new UserAvatar + { + Data = null, + Type = null, + ETag = null, + LastModified = DateTime.Now, + UserId = user.Id + }); _context.SaveChanges(); _context.UserAvatars.Count().Should().Be(1); + _context.Users.Remove(user); + _context.SaveChanges(); + _context.UserAvatars.Count().Should().Be(0); } } } diff --git a/Timeline.Tests/GlobalSuppressions.cs b/Timeline.Tests/GlobalSuppressions.cs index 2191a5c4..1d1d294b 100644 --- a/Timeline.Tests/GlobalSuppressions.cs +++ b/Timeline.Tests/GlobalSuppressions.cs @@ -5,10 +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", "CA1031:Do not catch general exception types", Justification = "Test may catch all exceptions.")] [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/AsyncFunctionAssertionsExtensions.cs b/Timeline.Tests/Helpers/AsyncFunctionAssertionsExtensions.cs new file mode 100644 index 00000000..b78309c0 --- /dev/null +++ b/Timeline.Tests/Helpers/AsyncFunctionAssertionsExtensions.cs @@ -0,0 +1,16 @@ +using FluentAssertions; +using FluentAssertions.Primitives; +using FluentAssertions.Specialized; +using System; +using System.Threading.Tasks; + +namespace Timeline.Tests.Helpers +{ + public static class AsyncFunctionAssertionsExtensions + { + public static async Task> ThrowAsync(this AsyncFunctionAssertions assertions, Type exceptionType, string because = "", params object[] becauseArgs) + { + return (await assertions.ThrowAsync(because, becauseArgs)).Which.Should().BeAssignableTo(exceptionType); + } + } +} diff --git a/Timeline.Tests/IntegratedTests/UserAvatarTest.cs b/Timeline.Tests/IntegratedTests/UserAvatarTest.cs index ba6d98e1..ce389046 100644 --- a/Timeline.Tests/IntegratedTests/UserAvatarTest.cs +++ b/Timeline.Tests/IntegratedTests/UserAvatarTest.cs @@ -40,6 +40,7 @@ namespace Timeline.Tests.IntegratedTests } [Fact] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "HttpMessageRequest should be disposed ???")] public async Task Test() { Avatar mockAvatar = new Avatar @@ -264,6 +265,25 @@ namespace Timeline.Tests.IntegratedTests .And.Should().HaveCommonBody().Which.Code.Should().Be(Delete.UserNotExist); } } + + // bad username check + using (var client = await _factory.CreateClientAsAdmin()) + { + { + var res = await client.GetAsync("users/u!ser/avatar"); + res.Should().BeInvalidModel(); + } + + { + var res = await client.PutByteArrayAsync("users/u!ser/avatar", ImageHelper.CreatePngWithSize(100, 100), "image/png"); + res.Should().BeInvalidModel(); + } + + { + var res = await client.DeleteAsync("users/u!ser/avatar"); + res.Should().BeInvalidModel(); + } + } } } } \ No newline at end of file diff --git a/Timeline.Tests/Mock/Data/TestUsers.cs b/Timeline.Tests/Mock/Data/TestUsers.cs index 6b0a9997..fa75236a 100644 --- a/Timeline.Tests/Mock/Data/TestUsers.cs +++ b/Timeline.Tests/Mock/Data/TestUsers.cs @@ -36,7 +36,7 @@ namespace Timeline.Tests.Mock.Data Name = user.Username, EncryptedPassword = passwordService.HashPassword(user.Password), RoleString = UserRoleConvert.ToString(user.Administrator), - Avatar = UserAvatar.Create(DateTime.Now) + Avatar = null }; } diff --git a/Timeline.Tests/UserAvatarServiceTest.cs b/Timeline.Tests/UserAvatarServiceTest.cs index 7489517b..79fa6f5c 100644 --- a/Timeline.Tests/UserAvatarServiceTest.cs +++ b/Timeline.Tests/UserAvatarServiceTest.cs @@ -2,8 +2,10 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Moq; using SixLabors.ImageSharp.Formats.Png; using System; +using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -11,40 +13,12 @@ using Timeline.Entities; using Timeline.Services; using Timeline.Tests.Helpers; using Timeline.Tests.Mock.Data; +using Timeline.Tests.Mock.Services; using Xunit; using Xunit.Abstractions; namespace Timeline.Tests { - public class MockDefaultUserAvatarProvider : IDefaultUserAvatarProvider - { - public static string ETag { get; } = "Hahaha"; - - public static AvatarInfo AvatarInfo { get; } = new AvatarInfo - { - Avatar = new Avatar { Type = "image/test", Data = Encoding.ASCII.GetBytes("test") }, - LastModified = DateTime.Now - }; - - public Task GetDefaultAvatarETag() - { - return Task.FromResult(ETag); - } - - public Task GetDefaultAvatar() - { - return Task.FromResult(AvatarInfo); - } - } - - public class MockUserAvatarValidator : IUserAvatarValidator - { - public Task Validate(Avatar avatar) - { - return Task.CompletedTask; - } - } - public class UserAvatarValidatorTest : IClassFixture { private readonly UserAvatarValidator _validator; @@ -106,22 +80,30 @@ namespace Timeline.Tests } } - public class UserAvatarServiceTest : IDisposable, IClassFixture, IClassFixture + public class UserAvatarServiceTest : IDisposable { - private UserAvatar MockAvatarEntity1 { get; } = new UserAvatar + private UserAvatar CreateMockAvatarEntity(string key) => new UserAvatar { - Type = "image/testaaa", - Data = Encoding.ASCII.GetBytes("amock"), - ETag = "aaaa", + Type = $"image/test{key}", + Data = Encoding.ASCII.GetBytes($"mock{key}"), + ETag = $"etag{key}", LastModified = DateTime.Now }; - private UserAvatar MockAvatarEntity2 { get; } = new UserAvatar + private AvatarInfo CreateMockAvatarInfo(string key) => new AvatarInfo { - Type = "image/testbbb", - Data = Encoding.ASCII.GetBytes("bmock"), - ETag = "bbbb", - LastModified = DateTime.Now + TimeSpan.FromMinutes(1) + Avatar = new Avatar + { + Type = $"image/test{key}", + Data = Encoding.ASCII.GetBytes($"mock{key}") + }, + LastModified = DateTime.Now + }; + + private Avatar CreateMockAvatar(string key) => new Avatar + { + Type = $"image/test{key}", + Data = Encoding.ASCII.GetBytes($"mock{key}") }; private Avatar ToAvatar(UserAvatar entity) @@ -150,137 +132,115 @@ namespace Timeline.Tests to.LastModified = from.LastModified; } - private readonly MockDefaultUserAvatarProvider _mockDefaultUserAvatarProvider; + private readonly Mock _mockDefaultAvatarProvider; + private readonly Mock _mockValidator; + private readonly Mock _mockETagGenerator; + private readonly Mock _mockClock; private readonly TestDatabase _database; - private readonly IETagGenerator _eTagGenerator; - private readonly UserAvatarService _service; - public UserAvatarServiceTest(MockDefaultUserAvatarProvider mockDefaultUserAvatarProvider, MockUserAvatarValidator mockUserAvatarValidator) + public UserAvatarServiceTest() { - _mockDefaultUserAvatarProvider = mockDefaultUserAvatarProvider; + _mockDefaultAvatarProvider = new Mock(); + _mockValidator = new Mock(); + _mockETagGenerator = new Mock(); + _mockClock = new Mock(); _database = new TestDatabase(); - _eTagGenerator = new ETagGenerator(); - - _service = new UserAvatarService(NullLogger.Instance, _database.DatabaseContext, _mockDefaultUserAvatarProvider, mockUserAvatarValidator, _eTagGenerator); + _service = new UserAvatarService(NullLogger.Instance, _database.DatabaseContext, _mockDefaultAvatarProvider.Object, _mockValidator.Object, _mockETagGenerator.Object, _mockClock.Object); } - public void Dispose() { _database.Dispose(); } - [Fact] - public void GetAvatarETag_ShouldThrow_ArgumentException() + [Theory] + [InlineData(null, typeof(ArgumentNullException))] + [InlineData("", typeof(UsernameBadFormatException))] + [InlineData("a!a", typeof(UsernameBadFormatException))] + [InlineData("usernotexist", typeof(UserNotExistException))] + public async Task GetAvatarETag_ShouldThrow(string username, Type exceptionType) { - // no need to await because arguments are checked syncronizedly. - _service.Invoking(s => s.GetAvatarETag(null)).Should().Throw() - .Where(e => e.ParamName == "username" && e.Message.Contains("null", StringComparison.OrdinalIgnoreCase)); - _service.Invoking(s => s.GetAvatarETag("")).Should().Throw() - .Where(e => e.ParamName == "username" && e.Message.Contains("empty", StringComparison.OrdinalIgnoreCase)); - } - - [Fact] - public void GetAvatarETag_ShouldThrow_UserNotExistException() - { - const string username = "usernotexist"; - _service.Awaiting(s => s.GetAvatarETag(username)).Should().Throw() - .Where(e => e.Username == username); + await _service.Awaiting(s => s.GetAvatarETag(username)).Should().ThrowAsync(exceptionType); } [Fact] public async Task GetAvatarETag_ShouldReturn_Default() { - string username = MockUser.User.Username; - (await _service.GetAvatarETag(username)).Should().BeEquivalentTo((await _mockDefaultUserAvatarProvider.GetDefaultAvatarETag())); + const string etag = "aaaaaa"; + _mockDefaultAvatarProvider.Setup(p => p.GetDefaultAvatarETag()).ReturnsAsync(etag); + (await _service.GetAvatarETag(MockUser.User.Username)).Should().Be(etag); } [Fact] public async Task GetAvatarETag_ShouldReturn_Data() { string username = MockUser.User.Username; + var mockAvatarEntity = CreateMockAvatarEntity("aaa"); { - // create mock data var context = _database.DatabaseContext; var user = await context.Users.Where(u => u.Name == username).Include(u => u.Avatar).SingleAsync(); - Set(user.Avatar, MockAvatarEntity1); + user.Avatar = mockAvatarEntity; await context.SaveChangesAsync(); } - - (await _service.GetAvatarETag(username)).Should().BeEquivalentTo(MockAvatarEntity1.ETag); + (await _service.GetAvatarETag(username)).Should().BeEquivalentTo(mockAvatarEntity.ETag); } - [Fact] - public void GetAvatar_ShouldThrow_ArgumentException() + [Theory] + [InlineData(null, typeof(ArgumentNullException))] + [InlineData("", typeof(UsernameBadFormatException))] + [InlineData("a!a", typeof(UsernameBadFormatException))] + [InlineData("usernotexist", typeof(UserNotExistException))] + public async Task GetAvatar_ShouldThrow(string username, Type exceptionType) { - // no need to await because arguments are checked syncronizedly. - _service.Invoking(s => s.GetAvatar(null)).Should().Throw() - .Where(e => e.ParamName == "username" && e.Message.Contains("null", StringComparison.OrdinalIgnoreCase)); - _service.Invoking(s => s.GetAvatar("")).Should().Throw() - .Where(e => e.ParamName == "username" && e.Message.Contains("empty", StringComparison.OrdinalIgnoreCase)); - } + await _service.Awaiting(s => s.GetAvatar(username)).Should().ThrowAsync(exceptionType); - [Fact] - public void GetAvatar_ShouldThrow_UserNotExistException() - { - const string username = "usernotexist"; - _service.Awaiting(s => s.GetAvatar(username)).Should().Throw() - .Where(e => e.Username == username); } [Fact] public async Task GetAvatar_ShouldReturn_Default() { + var mockAvatar = CreateMockAvatarInfo("aaa"); + _mockDefaultAvatarProvider.Setup(p => p.GetDefaultAvatar()).ReturnsAsync(mockAvatar); string username = MockUser.User.Username; - (await _service.GetAvatar(username)).Avatar.Should().BeEquivalentTo((await _mockDefaultUserAvatarProvider.GetDefaultAvatar()).Avatar); + (await _service.GetAvatar(username)).Should().BeEquivalentTo(mockAvatar); } [Fact] public async Task GetAvatar_ShouldReturn_Data() { string username = MockUser.User.Username; - + var mockAvatarEntity = CreateMockAvatarEntity("aaa"); { - // create mock data var context = _database.DatabaseContext; var user = await context.Users.Where(u => u.Name == username).Include(u => u.Avatar).SingleAsync(); - Set(user.Avatar, MockAvatarEntity1); + user.Avatar = mockAvatarEntity; await context.SaveChangesAsync(); } - (await _service.GetAvatar(username)).Should().BeEquivalentTo(ToAvatarInfo(MockAvatarEntity1)); + (await _service.GetAvatar(username)).Should().BeEquivalentTo(ToAvatarInfo(mockAvatarEntity)); } - [Fact] - public void SetAvatar_ShouldThrow_ArgumentException() + public static IEnumerable SetAvatar_ShouldThrow_Data() { - var avatar = ToAvatar(MockAvatarEntity1); - // no need to await because arguments are checked syncronizedly. - _service.Invoking(s => s.SetAvatar(null, avatar)).Should().Throw() - .Where(e => e.ParamName == "username" && e.Message.Contains("null", StringComparison.OrdinalIgnoreCase)); - _service.Invoking(s => s.SetAvatar("", avatar)).Should().Throw() - .Where(e => e.ParamName == "username" && e.Message.Contains("empty", StringComparison.OrdinalIgnoreCase)); - - _service.Invoking(s => s.SetAvatar("aaa", new Avatar { Type = null, Data = new[] { (byte)0x00 } })).Should().Throw() - .Where(e => e.ParamName == "avatar" && e.Message.Contains("null", StringComparison.OrdinalIgnoreCase)); - _service.Invoking(s => s.SetAvatar("aaa", new Avatar { Type = "", Data = new[] { (byte)0x00 } })).Should().Throw() - .Where(e => e.ParamName == "avatar" && e.Message.Contains("empty", StringComparison.OrdinalIgnoreCase)); - - _service.Invoking(s => s.SetAvatar("aaa", new Avatar { Type = "aaa", Data = null })).Should().Throw() - .Where(e => e.ParamName == "avatar" && e.Message.Contains("null", StringComparison.OrdinalIgnoreCase)); + yield return new object[] { null, null, typeof(ArgumentNullException) }; + yield return new object[] { "", null, typeof(UsernameBadFormatException) }; + yield return new object[] { "u!u", null, typeof(UsernameBadFormatException) }; + yield return new object[] { null, new Avatar { Type = null, Data = new[] { (byte)0x00 } }, typeof(ArgumentException) }; + yield return new object[] { null, new Avatar { Type = "", Data = new[] { (byte)0x00 } }, typeof(ArgumentException) }; + yield return new object[] { null, new Avatar { Type = "aaa", Data = null }, typeof(ArgumentException) }; + yield return new object[] { "usernotexist", null, typeof(UserNotExistException) }; } - [Fact] - public void SetAvatar_ShouldThrow_UserNotExistException() + [Theory] + [MemberData(nameof(SetAvatar_ShouldThrow_Data))] + public async Task SetAvatar_ShouldThrow(string username, Avatar avatar, Type exceptionType) { - const string username = "usernotexist"; - _service.Awaiting(s => s.SetAvatar(username, ToAvatar(MockAvatarEntity1))).Should().Throw() - .Where(e => e.Username == username); + await _service.Awaiting(s => s.SetAvatar(username, avatar)).Should().ThrowAsync(exceptionType); } [Fact] @@ -290,27 +250,43 @@ namespace Timeline.Tests var user = await _database.DatabaseContext.Users.Where(u => u.Name == username).Include(u => u.Avatar).SingleAsync(); + var avatar1 = CreateMockAvatar("aaa"); + var avatar2 = CreateMockAvatar("bbb"); + + string etag1 = "etagaaa"; + string etag2 = "etagbbb"; + + DateTime dateTime1 = DateTime.Now.AddSeconds(2); + DateTime dateTime2 = DateTime.Now.AddSeconds(10); + DateTime dateTime3 = DateTime.Now.AddSeconds(20); + // create - var avatar1 = ToAvatar(MockAvatarEntity1); + _mockETagGenerator.Setup(g => g.Generate(avatar1.Data)).ReturnsAsync(etag1); + _mockClock.Setup(c => c.GetCurrentTime()).Returns(dateTime1); await _service.SetAvatar(username, avatar1); user.Avatar.Should().NotBeNull(); user.Avatar.Type.Should().Be(avatar1.Type); user.Avatar.Data.Should().Equal(avatar1.Data); - user.Avatar.ETag.Should().NotBeNull(); + user.Avatar.ETag.Should().Be(etag1); + user.Avatar.LastModified.Should().Be(dateTime1); // modify - var avatar2 = ToAvatar(MockAvatarEntity2); + _mockETagGenerator.Setup(g => g.Generate(avatar2.Data)).ReturnsAsync(etag2); + _mockClock.Setup(c => c.GetCurrentTime()).Returns(dateTime2); await _service.SetAvatar(username, avatar2); user.Avatar.Should().NotBeNull(); - user.Avatar.Type.Should().Be(MockAvatarEntity2.Type); - user.Avatar.Data.Should().Equal(MockAvatarEntity2.Data); - user.Avatar.ETag.Should().NotBeNull(); + user.Avatar.Type.Should().Be(avatar2.Type); + user.Avatar.Data.Should().Equal(avatar2.Data); + user.Avatar.ETag.Should().Be(etag2); + user.Avatar.LastModified.Should().Be(dateTime2); // delete + _mockClock.Setup(c => c.GetCurrentTime()).Returns(dateTime3); await _service.SetAvatar(username, null); user.Avatar.Type.Should().BeNull(); user.Avatar.Data.Should().BeNull(); user.Avatar.ETag.Should().BeNull(); + user.Avatar.LastModified.Should().Be(dateTime3); } } } diff --git a/Timeline/Entities/UserAvatar.cs b/Timeline/Entities/UserAvatar.cs index 3b5388aa..a5b18b94 100644 --- a/Timeline/Entities/UserAvatar.cs +++ b/Timeline/Entities/UserAvatar.cs @@ -24,17 +24,5 @@ namespace Timeline.Entities public DateTime LastModified { get; set; } public long UserId { get; set; } - - public static UserAvatar Create(DateTime lastModified) - { - return new UserAvatar - { - Id = 0, - Data = null, - Type = null, - ETag = null, - LastModified = lastModified - }; - } } } diff --git a/Timeline/Resources/Services/UserAvatarService.Designer.cs b/Timeline/Resources/Services/UserAvatarService.Designer.cs index cabc9ede..6ee6fef9 100644 --- a/Timeline/Resources/Services/UserAvatarService.Designer.cs +++ b/Timeline/Resources/Services/UserAvatarService.Designer.cs @@ -70,11 +70,11 @@ namespace Timeline.Resources.Services { } /// - /// Looks up a localized string similar to Type of avatar is null.. + /// Looks up a localized string similar to Type of avatar is null or empty.. /// - internal static string ArgumentAvatarTypeNull { + internal static string ArgumentAvatarTypeNullOrEmpty { get { - return ResourceManager.GetString("ArgumentAvatarTypeNull", resourceCulture); + return ResourceManager.GetString("ArgumentAvatarTypeNullOrEmpty", resourceCulture); } } diff --git a/Timeline/Resources/Services/UserAvatarService.resx b/Timeline/Resources/Services/UserAvatarService.resx index ab6389ff..3269bf13 100644 --- a/Timeline/Resources/Services/UserAvatarService.resx +++ b/Timeline/Resources/Services/UserAvatarService.resx @@ -120,8 +120,8 @@ Data of avatar is null. - - Type of avatar is null. + + Type of avatar is null or empty. Database corupted! One of type and data of a avatar is null but the other is not. diff --git a/Timeline/Services/ETagGenerator.cs b/Timeline/Services/ETagGenerator.cs index e518f01f..d328ea20 100644 --- a/Timeline/Services/ETagGenerator.cs +++ b/Timeline/Services/ETagGenerator.cs @@ -1,5 +1,6 @@ using System; using System.Security.Cryptography; +using System.Threading.Tasks; namespace Timeline.Services { @@ -11,7 +12,7 @@ namespace Timeline.Services /// The source data. /// The generated etag. /// Thrown if is null. - string Generate(byte[] source); + Task Generate(byte[] source); } public sealed class ETagGenerator : IETagGenerator, IDisposable @@ -24,12 +25,12 @@ namespace Timeline.Services _sha1 = SHA1.Create(); } - public string Generate(byte[] source) + public Task Generate(byte[] source) { if (source == null) throw new ArgumentNullException(nameof(source)); - return Convert.ToBase64String(_sha1.ComputeHash(source)); + return Task.Run(() => Convert.ToBase64String(_sha1.ComputeHash(source))); } private bool _disposed = false; // To detect redundant calls diff --git a/Timeline/Services/UserAvatarService.cs b/Timeline/Services/UserAvatarService.cs index 4c65a0fa..ff80003c 100644 --- a/Timeline/Services/UserAvatarService.cs +++ b/Timeline/Services/UserAvatarService.cs @@ -118,7 +118,7 @@ namespace Timeline.Services { _cacheData = await File.ReadAllBytesAsync(path); _cacheLastModified = File.GetLastWriteTime(path); - _cacheETag = _eTagGenerator.Generate(_cacheData); + _cacheETag = await _eTagGenerator.Generate(_cacheData); } } @@ -179,12 +179,15 @@ namespace Timeline.Services private readonly UsernameValidator _usernameValidator; + private readonly IClock _clock; + public UserAvatarService( ILogger logger, DatabaseContext database, IDefaultUserAvatarProvider defaultUserAvatarProvider, IUserAvatarValidator avatarValidator, - IETagGenerator eTagGenerator) + IETagGenerator eTagGenerator, + IClock clock) { _logger = logger; _database = database; @@ -192,6 +195,7 @@ namespace Timeline.Services _avatarValidator = avatarValidator; _eTagGenerator = eTagGenerator; _usernameValidator = new UsernameValidator(); + _clock = clock; } public async Task GetAvatarETag(string username) @@ -245,8 +249,8 @@ namespace Timeline.Services { if (avatar.Data == null) throw new ArgumentException(Resources.Services.UserAvatarService.ArgumentAvatarDataNull, nameof(avatar)); - if (avatar.Type == null) - throw new ArgumentException(Resources.Services.UserAvatarService.ArgumentAvatarTypeNull, nameof(avatar)); + if (string.IsNullOrEmpty(avatar.Type)) + throw new ArgumentException(Resources.Services.UserAvatarService.ArgumentAvatarTypeNullOrEmpty, nameof(avatar)); } var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, _usernameValidator, username); @@ -263,7 +267,7 @@ namespace Timeline.Services avatarEntity.Data = null; avatarEntity.Type = null; avatarEntity.ETag = null; - avatarEntity.LastModified = DateTime.Now; + avatarEntity.LastModified = _clock.GetCurrentTime(); await _database.SaveChangesAsync(); _logger.LogInformation(Resources.Services.UserAvatarService.LogUpdateEntity); } @@ -278,8 +282,9 @@ namespace Timeline.Services } avatarEntity!.Type = avatar.Type; avatarEntity.Data = avatar.Data; - avatarEntity.ETag = _eTagGenerator.Generate(avatar.Data); - avatarEntity.LastModified = DateTime.Now; + avatarEntity.ETag = await _eTagGenerator.Generate(avatar.Data); + avatarEntity.LastModified = _clock.GetCurrentTime(); + avatarEntity.UserId = userId; if (create) { _database.UserAvatars.Add(avatarEntity); -- cgit v1.2.3