From fa2a3282c51d831b25f374803301e75eac15d11c Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Thu, 17 Oct 2019 20:46:57 +0800 Subject: ... --- Timeline.Tests/AuthorizationUnitTest.cs | 76 ----- Timeline.Tests/Controllers/TokenControllerTest.cs | 84 ++++++ .../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 ++++ .../IntegratedTests/AuthorizationUnitTest.cs | 68 +++++ Timeline.Tests/IntegratedTests/TokenUnitTest.cs | 185 ++++++++++++ Timeline.Tests/IntegratedTests/UserAvatarTests.cs | 23 +- Timeline.Tests/IntegratedTests/UserDetailTest.cs | 29 +- Timeline.Tests/IntegratedTests/UserUnitTest.cs | 320 +++++++++++++++++++++ Timeline.Tests/Mock/Data/TestDatabase.cs | 25 +- Timeline.Tests/Mock/Data/TestUsers.cs | 62 ++-- Timeline.Tests/Mock/Services/TestClock.cs | 10 - Timeline.Tests/Timeline.Tests.csproj | 1 + Timeline.Tests/TokenUnitTest.cs | 190 ------------ Timeline.Tests/UserAvatarServiceTest.cs | 19 +- Timeline.Tests/UserDetailServiceTest.cs | 44 ++- Timeline.Tests/UserUnitTest.cs | 318 -------------------- Timeline/Controllers/TokenController.cs | 131 ++++----- Timeline/Controllers/UserController.cs | 8 +- Timeline/ErrorCodes.cs | 29 ++ Timeline/Helpers/Log.cs | 19 ++ Timeline/Models/Http/Common.cs | 74 +++-- Timeline/Models/Http/Token.cs | 2 +- 27 files changed, 993 insertions(+), 909 deletions(-) delete mode 100644 Timeline.Tests/AuthorizationUnitTest.cs create mode 100644 Timeline.Tests/Controllers/TokenControllerTest.cs delete mode 100644 Timeline.Tests/Helpers/MyTestLoggerFactory.cs delete mode 100644 Timeline.Tests/Helpers/MyWebApplicationFactory.cs create mode 100644 Timeline.Tests/Helpers/TestApplication.cs create mode 100644 Timeline.Tests/IntegratedTests/AuthorizationUnitTest.cs create mode 100644 Timeline.Tests/IntegratedTests/TokenUnitTest.cs create mode 100644 Timeline.Tests/IntegratedTests/UserUnitTest.cs delete mode 100644 Timeline.Tests/TokenUnitTest.cs delete mode 100644 Timeline.Tests/UserUnitTest.cs create mode 100644 Timeline/ErrorCodes.cs diff --git a/Timeline.Tests/AuthorizationUnitTest.cs b/Timeline.Tests/AuthorizationUnitTest.cs deleted file mode 100644 index 4751e95f..00000000 --- a/Timeline.Tests/AuthorizationUnitTest.cs +++ /dev/null @@ -1,76 +0,0 @@ -using FluentAssertions; -using Microsoft.AspNetCore.Mvc.Testing; -using System; -using System.Net; -using System.Threading.Tasks; -using Timeline.Tests.Helpers; -using Timeline.Tests.Helpers.Authentication; -using Xunit; -using Xunit.Abstractions; - -namespace Timeline.Tests -{ - public class AuthorizationUnitTest : IClassFixture>, IDisposable - { - private const string AuthorizeUrl = "Test/User/Authorize"; - private const string UserUrl = "Test/User/User"; - private const string AdminUrl = "Test/User/Admin"; - - private readonly WebApplicationFactory _factory; - private readonly Action _disposeAction; - - public AuthorizationUnitTest(MyWebApplicationFactory factory, ITestOutputHelper outputHelper) - { - _factory = factory.WithTestConfig(outputHelper, out _disposeAction); - } - - public void Dispose() - { - _disposeAction(); - } - - [Fact] - public async Task UnauthenticationTest() - { - using (var client = _factory.CreateDefaultClient()) - { - var response = await client.GetAsync(AuthorizeUrl); - response.Should().HaveStatusCode(HttpStatusCode.Unauthorized); - } - } - - [Fact] - public async Task AuthenticationTest() - { - using (var client = await _factory.CreateClientAsUser()) - { - var response = await client.GetAsync(AuthorizeUrl); - response.Should().HaveStatusCode(HttpStatusCode.OK); - } - } - - [Fact] - public async Task UserAuthorizationTest() - { - using (var client = await _factory.CreateClientAsUser()) - { - var response1 = await client.GetAsync(UserUrl); - response1.Should().HaveStatusCode(HttpStatusCode.OK); - var response2 = await client.GetAsync(AdminUrl); - response2.Should().HaveStatusCode(HttpStatusCode.Forbidden); - } - } - - [Fact] - public async Task AdminAuthorizationTest() - { - using (var client = await _factory.CreateClientAsAdmin()) - { - var response1 = await client.GetAsync(UserUrl); - response1.Should().HaveStatusCode(HttpStatusCode.OK); - var response2 = await client.GetAsync(AdminUrl); - response2.Should().HaveStatusCode(HttpStatusCode.OK); - } - } - } -} diff --git a/Timeline.Tests/Controllers/TokenControllerTest.cs b/Timeline.Tests/Controllers/TokenControllerTest.cs new file mode 100644 index 00000000..fff7c020 --- /dev/null +++ b/Timeline.Tests/Controllers/TokenControllerTest.cs @@ -0,0 +1,84 @@ +using FluentAssertions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using System; +using System.Threading.Tasks; +using Timeline.Controllers; +using Timeline.Models.Http; +using Timeline.Services; +using Timeline.Tests.Mock.Data; +using Timeline.Tests.Mock.Services; +using Xunit; +using static Timeline.ErrorCodes.Http.Token; + +namespace Timeline.Tests.Controllers +{ + public class TokenControllerTest + { + 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); + } + + [Theory] + [InlineData(null)] + [InlineData(100)] + public async Task Create_Ok(int? expire) + { + var mockCurrentTime = DateTime.Now; + _mockClock.MockCurrentTime = mockCurrentTime; + var createResult = new CreateTokenResult + { + Token = "mocktokenaaaaa", + User = MockUser.User.Info + }; + _mockUserService.Setup(s => s.CreateToken("u", "p", expire == null ? null : (DateTime?)mockCurrentTime.AddDays(expire.Value))).ReturnsAsync(createResult); + var action = await _controller.Create(new CreateTokenRequest + { + Username = "u", + Password = "p", + Expire = expire + }); + action.Should().BeAssignableTo() + .Which.Value.Should().BeEquivalentTo(createResult); + } + + [Fact] + public async Task Create_UserNotExist() + { + _mockUserService.Setup(s => s.CreateToken("u", "p", null)).ThrowsAsync(new UserNotExistException("u")); + var action = await _controller.Create(new CreateTokenRequest + { + Username = "u", + Password = "p", + Expire = null + }); + action.Should().BeAssignableTo() + .Which.Value.Should().BeAssignableTo() + .Which.Code.Should().Be(Create.BadCredential); + } + + [Fact] + public async Task Create_BadPassword() + { + _mockUserService.Setup(s => s.CreateToken("u", "p", null)).ThrowsAsync(new BadPasswordException("u")); + var action = await _controller.Create(new CreateTokenRequest + { + Username = "u", + Password = "p", + Expire = null + }); + action.Should().BeAssignableTo() + .Which.Value.Should().BeAssignableTo() + .Which.Code.Should().Be(Create.BadCredential); + } + + // TODO! Verify unit tests + } +} 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(); + } + } +} diff --git a/Timeline.Tests/IntegratedTests/AuthorizationUnitTest.cs b/Timeline.Tests/IntegratedTests/AuthorizationUnitTest.cs new file mode 100644 index 00000000..a67bffcf --- /dev/null +++ b/Timeline.Tests/IntegratedTests/AuthorizationUnitTest.cs @@ -0,0 +1,68 @@ +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.Testing; +using System; +using System.Net; +using System.Threading.Tasks; +using Timeline.Tests.Helpers; +using Timeline.Tests.Helpers.Authentication; +using Xunit; + +namespace Timeline.Tests.IntegratedTests +{ + public class AuthorizationUnitTest : IClassFixture>, IDisposable + { + private const string AuthorizeUrl = "Test/User/Authorize"; + private const string UserUrl = "Test/User/User"; + private const string AdminUrl = "Test/User/Admin"; + + private readonly TestApplication _testApp; + private readonly WebApplicationFactory _factory; + + public AuthorizationUnitTest(WebApplicationFactory factory) + { + _testApp = new TestApplication(factory); + _factory = _testApp.Factory; + } + + public void Dispose() + { + _testApp.Dispose(); + } + + [Fact] + public async Task UnauthenticationTest() + { + using var client = _factory.CreateDefaultClient(); + var response = await client.GetAsync(AuthorizeUrl); + response.Should().HaveStatusCode(HttpStatusCode.Unauthorized); + } + + [Fact] + public async Task AuthenticationTest() + { + using var client = await _factory.CreateClientAsUser(); + var response = await client.GetAsync(AuthorizeUrl); + response.Should().HaveStatusCode(HttpStatusCode.OK); + } + + [Fact] + public async Task UserAuthorizationTest() + { + using var client = await _factory.CreateClientAsUser(); + var response1 = await client.GetAsync(UserUrl); + response1.Should().HaveStatusCode(HttpStatusCode.OK); + var response2 = await client.GetAsync(AdminUrl); + response2.Should().HaveStatusCode(HttpStatusCode.Forbidden); + } + + [Fact] + public async Task AdminAuthorizationTest() + { + using var client = await _factory.CreateClientAsAdmin(); + var response1 = await client.GetAsync(UserUrl); + response1.Should().HaveStatusCode(HttpStatusCode.OK); + var response2 = await client.GetAsync(AdminUrl); + response2.Should().HaveStatusCode(HttpStatusCode.OK); + } + } +} diff --git a/Timeline.Tests/IntegratedTests/TokenUnitTest.cs b/Timeline.Tests/IntegratedTests/TokenUnitTest.cs new file mode 100644 index 00000000..05e2b3e5 --- /dev/null +++ b/Timeline.Tests/IntegratedTests/TokenUnitTest.cs @@ -0,0 +1,185 @@ +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Net.Http; +using System.Threading.Tasks; +using Timeline.Models.Http; +using Timeline.Services; +using Timeline.Tests.Helpers; +using Timeline.Tests.Helpers.Authentication; +using Timeline.Tests.Mock.Data; +using Xunit; +using static Timeline.ErrorCodes.Http.Token; + +namespace Timeline.Tests +{ + public class TokenUnitTest : IClassFixture>, IDisposable + { + private const string CreateTokenUrl = "token/create"; + private const string VerifyTokenUrl = "token/verify"; + + private readonly TestApplication _testApp; + private readonly WebApplicationFactory _factory; + + public TokenUnitTest(WebApplicationFactory factory) + { + _testApp = new TestApplication(factory); + _factory = _testApp.Factory; + } + + public void Dispose() + { + _testApp.Dispose(); + } + [Fact] + public async Task CreateToken_InvalidModel() + { + 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 + }); + } + + [Fact] + public async void CreateToken_UserNotExist() + { + using var client = _factory.CreateDefaultClient(); + var response = await client.PostAsJsonAsync(CreateTokenUrl, + new CreateTokenRequest { Username = "usernotexist", Password = "???" }); + response.Should().HaveStatusCodeBadRequest() + .And.Should().HaveBodyAsCommonResponseWithCode(Create.BadCredential); + } + + [Fact] + public async void CreateToken_BadPassword() + { + 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); + } + } + + [Fact] + public async void 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); + } + } + + [Fact] + public async void VerifyToken_InvalidModel() + { + using (var client = _factory.CreateDefaultClient()) + { + // missing token + await InvalidModelTestHelpers.TestPostInvalidModel(client, VerifyTokenUrl, + new VerifyTokenRequest { Token = null }); + } + } + + [Fact] + public async void 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); + } + } + + [Fact] + public async void VerifyToken_BadVersion() + { + 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. + { + // 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); + } + } + + [Fact] + public async void VerifyToken_UserNotExist() + { + 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. + { + // 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); + } + } + + //[Fact] + //public async void VerifyToken_Expired() + //{ + // using (var client = _factory.CreateDefaultClient()) + // { + // // I can only control the token expired time but not current time + // // because verify logic is encapsuled in other library. + // var mockClock = _factory.GetTestClock(); + // mockClock.MockCurrentTime = DateTime.Now - TimeSpan.FromDays(2); + // var token = (await client.CreateUserTokenAsync(MockUsers.UserUsername, MockUsers.UserPassword, 1)).Token; + // var response = await client.PostAsJsonAsync(VerifyTokenUrl, + // new VerifyTokenRequest { Token = token }); + // response.Should().HaveStatusCodeBadRequest() + // .And.Should().HaveBodyAsCommonResponseWithCode(TokenController.ErrorCodes.Verify_Expired); + // mockClock.MockCurrentTime = null; + // } + //} + + [Fact] + public async void 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); + } + } + } +} diff --git a/Timeline.Tests/IntegratedTests/UserAvatarTests.cs b/Timeline.Tests/IntegratedTests/UserAvatarTests.cs index 2a3442d1..439e8d9b 100644 --- a/Timeline.Tests/IntegratedTests/UserAvatarTests.cs +++ b/Timeline.Tests/IntegratedTests/UserAvatarTests.cs @@ -3,9 +3,9 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.DependencyInjection; using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Png; -using SixLabors.ImageSharp.Formats.Gif; using System; using System.Collections.Generic; using System.IO; @@ -14,28 +14,27 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; using Timeline.Controllers; -using Timeline.Models.Http; using Timeline.Services; using Timeline.Tests.Helpers; using Timeline.Tests.Helpers.Authentication; using Xunit; -using Xunit.Abstractions; namespace Timeline.Tests.IntegratedTests { - public class UserAvatarUnitTest : IClassFixture>, IDisposable + public class UserAvatarUnitTest : IClassFixture>, IDisposable { + private readonly TestApplication _testApp; private readonly WebApplicationFactory _factory; - private readonly Action _disposeAction; - public UserAvatarUnitTest(MyWebApplicationFactory factory, ITestOutputHelper outputHelper) + public UserAvatarUnitTest(WebApplicationFactory factory) { - _factory = factory.WithTestConfig(outputHelper, out _disposeAction); + _testApp = new TestApplication(factory); + _factory = _testApp.Factory; } public void Dispose() { - _disposeAction(); + _testApp.Dispose(); } [Fact] @@ -92,7 +91,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(CommonResponse.ErrorCodes.Header_BadFormat_IfNonMatch); + .And.Should().HaveBodyAsCommonResponseWithCode(ErrorCodes.Http.Common.Header.BadFormat_IfNonMatch); } { @@ -122,7 +121,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(CommonResponse.ErrorCodes.Header_Missing_ContentLength); + .And.Should().HaveBodyAsCommonResponseWithCode(ErrorCodes.Http.Common.Header.Missing_ContentLength); } { @@ -130,7 +129,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(CommonResponse.ErrorCodes.Header_Missing_ContentType); + .And.Should().HaveBodyAsCommonResponseWithCode(ErrorCodes.Http.Common.Header.Missing_ContentType); } { @@ -139,7 +138,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(CommonResponse.ErrorCodes.Header_Zero_ContentLength); + .And.Should().HaveBodyAsCommonResponseWithCode(ErrorCodes.Http.Common.Header.Zero_ContentLength); } { diff --git a/Timeline.Tests/IntegratedTests/UserDetailTest.cs b/Timeline.Tests/IntegratedTests/UserDetailTest.cs index ba15b7ca..d1a67d9d 100644 --- a/Timeline.Tests/IntegratedTests/UserDetailTest.cs +++ b/Timeline.Tests/IntegratedTests/UserDetailTest.cs @@ -5,28 +5,27 @@ using System.Net; 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 Xunit.Abstractions; namespace Timeline.Tests.IntegratedTests { - public class UserDetailTest : IClassFixture>, IDisposable + public class UserDetailTest : IClassFixture>, IDisposable { + private readonly TestApplication _testApp; private readonly WebApplicationFactory _factory; - private readonly Action _disposeAction; - public UserDetailTest(MyWebApplicationFactory factory, ITestOutputHelper outputHelper) + public UserDetailTest(WebApplicationFactory factory) { - _factory = factory.WithTestConfig(outputHelper, out _disposeAction); + _testApp = new TestApplication(factory); + _factory = _testApp.Factory; } public void Dispose() { - _disposeAction(); + _testApp.Dispose(); } [Fact] @@ -48,7 +47,7 @@ namespace Timeline.Tests.IntegratedTests async Task GetAndTest(UserDetail d) { - var res = await client.GetAsync($"users/{MockUsers.UserUsername}/details"); + var res = await client.GetAsync($"users/{MockUser.User.Username}/details"); res.Should().HaveStatusCodeOk() .And.Should().HaveBodyAsJson() .Which.Should().BeEquivalentTo(d); @@ -57,13 +56,13 @@ namespace Timeline.Tests.IntegratedTests await GetAndTest(new UserDetail()); { - var res = await client.PatchAsJsonAsync($"users/{MockUsers.AdminUsername}/details", new UserDetail()); + var res = await client.PatchAsJsonAsync($"users/{MockUser.Admin.Username}/details", new UserDetail()); res.Should().HaveStatusCode(HttpStatusCode.Forbidden) .And.Should().HaveBodyAsCommonResponseWithCode(UserDetailController.ErrorCodes.Patch_Forbid); } { - var res = await client.PatchAsJsonAsync($"users/{MockUsers.UserUsername}/details", new UserDetail + var res = await client.PatchAsJsonAsync($"users/{MockUser.User.Username}/details", new UserDetail { Nickname = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", QQ = "aaaaaaa", @@ -72,7 +71,7 @@ namespace Timeline.Tests.IntegratedTests }); var body = res.Should().HaveStatusCode(HttpStatusCode.BadRequest) .And.Should().HaveBodyAsCommonResponse().Which; - body.Code.Should().Be(CommonResponse.ErrorCodes.InvalidModel); + body.Code.Should().Be(ErrorCodes.Http.Common.InvalidModel); foreach (var key in new string[] { "nickname", "qq", "email", "phonenumber" }) { body.Message.Should().ContainEquivalentOf(key); @@ -89,13 +88,13 @@ namespace Timeline.Tests.IntegratedTests }; { - var res = await client.PatchAsJsonAsync($"users/{MockUsers.UserUsername}/details", detail); + var res = await client.PatchAsJsonAsync($"users/{MockUser.User.Username}/details", detail); res.Should().HaveStatusCodeOk(); await GetAndTest(detail); } { - var res = await client.GetAsync($"users/{MockUsers.UserUsername}/nickname"); + var res = await client.GetAsync($"users/{MockUser.User.Username}/nickname"); res.Should().HaveStatusCodeOk().And.Should().HaveBodyAsJson() .Which.Should().BeEquivalentTo(new UserDetail { @@ -111,7 +110,7 @@ namespace Timeline.Tests.IntegratedTests }; { - var res = await client.PatchAsJsonAsync($"users/{MockUsers.UserUsername}/details", detail2); + var res = await client.PatchAsJsonAsync($"users/{MockUser.User.Username}/details", detail2); res.Should().HaveStatusCodeOk(); await GetAndTest(new UserDetail { @@ -131,7 +130,7 @@ namespace Timeline.Tests.IntegratedTests using (var client = await _factory.CreateClientAsAdmin()) { { - var res = await client.PatchAsJsonAsync($"users/{MockUsers.UserUsername}/details", new UserDetail()); + var res = await client.PatchAsJsonAsync($"users/{MockUser.User.Username}/details", new UserDetail()); res.Should().HaveStatusCodeOk(); } diff --git a/Timeline.Tests/IntegratedTests/UserUnitTest.cs b/Timeline.Tests/IntegratedTests/UserUnitTest.cs new file mode 100644 index 00000000..d228c563 --- /dev/null +++ b/Timeline.Tests/IntegratedTests/UserUnitTest.cs @@ -0,0 +1,320 @@ +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.Testing; +using System; +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; + +namespace Timeline.Tests +{ + public class UserUnitTest : IClassFixture>, IDisposable + { + private readonly TestApplication _testApp; + private readonly WebApplicationFactory _factory; + + public UserUnitTest(WebApplicationFactory factory) + { + _testApp = new TestApplication(factory); + _factory = _testApp.Factory; + } + + public void Dispose() + { + _testApp.Dispose(); + } + + [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); + } + } + + [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); + } + } + + [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); + } + } + + [Fact] + public async Task Put_InvalidModel() + { + 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 }); + } + } + + [Fact] + public async Task Put_BadUsername() + { + using (var client = await _factory.CreateClientAsAdmin()) + { + var res = await client.PutAsJsonAsync("users/dsf fddf", new UserPutRequest + { + Password = "???", + Administrator = false + }); + res.Should().HaveStatusCodeBadRequest() + .And.Should().HaveBodyAsCommonResponseWithCode(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() + .Which.Administrator.Should().Be(administrator); + } + + [Fact] + public async Task Put_Modiefied() + { + using (var client = await _factory.CreateClientAsAdmin()) + { + 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); + } + } + + [Fact] + public async Task Put_Created() + { + using (var client = await _factory.CreateClientAsAdmin()) + { + 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); + } + } + + [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); + } + } + + [Fact] + public async Task Patch_Success() + { + 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); + } + } + } + + [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(); + } + } + } + + [Fact] + public async Task Delete_NotExist() + { + using (var client = await _factory.CreateClientAsAdmin()) + { + { + var res = await client.DeleteAsync("users/usernotexist"); + res.Should().BeDeleteNotExist(); + } + } + } + + + public class ChangeUsernameUnitTest : IClassFixture>, IDisposable + { + private const string url = "userop/changeusername"; + + private readonly TestApplication _testApp; + private readonly WebApplicationFactory _factory; + + public ChangeUsernameUnitTest(WebApplicationFactory factory) + { + _testApp = new TestApplication(factory); + _factory = _testApp.Factory; + } + + public void Dispose() + { + _testApp.Dispose(); + } + + [Fact] + public async Task InvalidModel() + { + 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 = "???" }); + } + } + + [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); + } + } + + [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); + } + } + + [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); + } + } + } + + + public class ChangePasswordUnitTest : IClassFixture>, IDisposable + { + private const string url = "userop/changepassword"; + + private readonly TestApplication _testApp; + private readonly WebApplicationFactory _factory; + + public ChangePasswordUnitTest(WebApplicationFactory factory) + { + _testApp = new TestApplication(factory); + _factory = _testApp.Factory; + } + + public void Dispose() + { + _testApp.Dispose(); + } + + [Fact] + public async Task InvalidModel() + { + 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 }); + } + } + + [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); + } + } + + [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); + } + } + } + } +} diff --git a/Timeline.Tests/Mock/Data/TestDatabase.cs b/Timeline.Tests/Mock/Data/TestDatabase.cs index dd04f8f9..1e662546 100644 --- a/Timeline.Tests/Mock/Data/TestDatabase.cs +++ b/Timeline.Tests/Mock/Data/TestDatabase.cs @@ -10,36 +10,33 @@ namespace Timeline.Tests.Mock.Data public static void InitDatabase(DatabaseContext context) { context.Database.EnsureCreated(); - context.Users.AddRange(MockUsers.CreateMockUsers()); + context.Users.AddRange(MockUser.CreateMockEntities()); context.SaveChanges(); } - private readonly SqliteConnection _databaseConnection; - private readonly DatabaseContext _databaseContext; - public TestDatabase() { - _databaseConnection = new SqliteConnection("Data Source=:memory:;"); - _databaseConnection.Open(); + DatabaseConnection = new SqliteConnection("Data Source=:memory:;"); + DatabaseConnection.Open(); var options = new DbContextOptionsBuilder() - .UseSqlite(_databaseConnection) + .UseSqlite(DatabaseConnection) .Options; - _databaseContext = new DatabaseContext(options); + DatabaseContext = new DatabaseContext(options); - InitDatabase(_databaseContext); + InitDatabase(DatabaseContext); } public void Dispose() { - _databaseContext.Dispose(); + DatabaseContext.Dispose(); - _databaseConnection.Close(); - _databaseConnection.Dispose(); + DatabaseConnection.Close(); + DatabaseConnection.Dispose(); } - public SqliteConnection DatabaseConnection => _databaseConnection; - public DatabaseContext DatabaseContext => _databaseContext; + public SqliteConnection DatabaseConnection { get; } + public DatabaseContext DatabaseContext { get; } } } diff --git a/Timeline.Tests/Mock/Data/TestUsers.cs b/Timeline.Tests/Mock/Data/TestUsers.cs index 378fc280..bc2df469 100644 --- a/Timeline.Tests/Mock/Data/TestUsers.cs +++ b/Timeline.Tests/Mock/Data/TestUsers.cs @@ -1,52 +1,50 @@ using System; using System.Collections.Generic; -using System.Linq; using Timeline.Entities; using Timeline.Models; using Timeline.Services; namespace Timeline.Tests.Mock.Data { - public static class MockUsers + public class MockUser { - static MockUsers() + public MockUser(string username, string password, bool administrator) { - var mockUserInfos = CreateMockUsers().Select(u => UserUtility.CreateUserInfo(u)).ToList(); - UserUserInfo = mockUserInfos[0]; - AdminUserInfo = mockUserInfos[1]; - UserInfos = mockUserInfos; + Info = new UserInfo(username, administrator); + Password = password; } - public const string UserUsername = "user"; - public const string AdminUsername = "admin"; - public const string UserPassword = "user"; - public const string AdminPassword = "admin"; + public UserInfo Info { get; set; } + public string Username => Info.Username; + public string Password { get; set; } + public bool Administrator => Info.Administrator; - // emmmmmmm. Never reuse the user instances because EF Core uses them which will cause strange things. - internal static IEnumerable CreateMockUsers() + + public static MockUser User { get; } = new MockUser("user", "userpassword", false); + public static MockUser Admin { get; } = new MockUser("admin", "adminpassword", true); + + public static IReadOnlyList UserInfoList { get; } = new List { User.Info, Admin.Info }; + + // emmmmmmm. Never reuse the user instances because EF Core uses them, which will cause strange things. + public static IEnumerable CreateMockEntities() { - var users = new List(); var passwordService = new PasswordService(); - users.Add(new User + User Create(MockUser user) { - Name = UserUsername, - EncryptedPassword = passwordService.HashPassword(UserPassword), - RoleString = UserUtility.IsAdminToRoleString(false), - Avatar = UserAvatar.Create(DateTime.Now) - }); - users.Add(new User + return new User + { + Name = user.Username, + EncryptedPassword = passwordService.HashPassword(user.Password), + RoleString = UserUtility.IsAdminToRoleString(user.Administrator), + Avatar = UserAvatar.Create(DateTime.Now) + }; + } + + return new List { - Name = AdminUsername, - EncryptedPassword = passwordService.HashPassword(AdminPassword), - RoleString = UserUtility.IsAdminToRoleString(true), - Avatar = UserAvatar.Create(DateTime.Now) - }); - return users; + Create(User), + Create(Admin) + }; } - - public static IReadOnlyList UserInfos { get; } - - public static UserInfo AdminUserInfo { get; } - public static UserInfo UserUserInfo { get; } } } diff --git a/Timeline.Tests/Mock/Services/TestClock.cs b/Timeline.Tests/Mock/Services/TestClock.cs index 0082171e..6671395a 100644 --- a/Timeline.Tests/Mock/Services/TestClock.cs +++ b/Timeline.Tests/Mock/Services/TestClock.cs @@ -1,5 +1,3 @@ -using Microsoft.AspNetCore.Mvc.Testing; -using Microsoft.Extensions.DependencyInjection; using System; using Timeline.Services; @@ -14,12 +12,4 @@ namespace Timeline.Tests.Mock.Services return MockCurrentTime.GetValueOrDefault(DateTime.Now); } } - - public static class TestClockWebApplicationFactoryExtensions - { - public static TestClock GetTestClock(this WebApplicationFactory factory) where T : class - { - return factory.Server.Host.Services.GetRequiredService() as TestClock; - } - } } diff --git a/Timeline.Tests/Timeline.Tests.csproj b/Timeline.Tests/Timeline.Tests.csproj index 1852da5f..36bc03bc 100644 --- a/Timeline.Tests/Timeline.Tests.csproj +++ b/Timeline.Tests/Timeline.Tests.csproj @@ -14,6 +14,7 @@ + all diff --git a/Timeline.Tests/TokenUnitTest.cs b/Timeline.Tests/TokenUnitTest.cs deleted file mode 100644 index 3babacf7..00000000 --- a/Timeline.Tests/TokenUnitTest.cs +++ /dev/null @@ -1,190 +0,0 @@ -using FluentAssertions; -using Microsoft.AspNetCore.Mvc.Testing; -using Microsoft.Extensions.DependencyInjection; -using System; -using System.Net.Http; -using Timeline.Controllers; -using Timeline.Models.Http; -using Timeline.Services; -using Timeline.Tests.Helpers; -using Timeline.Tests.Helpers.Authentication; -using Timeline.Tests.Mock.Data; -using Timeline.Tests.Mock.Services; -using Xunit; -using Xunit.Abstractions; - -namespace Timeline.Tests -{ - public class TokenUnitTest : IClassFixture>, IDisposable - { - private const string CreateTokenUrl = "token/create"; - private const string VerifyTokenUrl = "token/verify"; - - private readonly WebApplicationFactory _factory; - private readonly Action _disposeAction; - - public TokenUnitTest(MyWebApplicationFactory factory, ITestOutputHelper outputHelper) - { - _factory = factory.WithTestConfig(outputHelper, out _disposeAction); - } - - public void Dispose() - { - _disposeAction(); - } - - [Fact] - public async void CreateToken_InvalidModel() - { - 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 = MockUsers.UserUsername, - Password = MockUsers.UserPassword, - ExpireOffset = -1000 - }); - } - } - - [Fact] - public async void CreateToken_UserNotExist() - { - using (var client = _factory.CreateDefaultClient()) - { - var response = await client.PostAsJsonAsync(CreateTokenUrl, - new CreateTokenRequest { Username = "usernotexist", Password = "???" }); - response.Should().HaveStatusCodeBadRequest() - .And.Should().HaveBodyAsCommonResponseWithCode(TokenController.ErrorCodes.Create_UserNotExist); - } - } - - [Fact] - public async void CreateToken_BadPassword() - { - using (var client = _factory.CreateDefaultClient()) - { - var response = await client.PostAsJsonAsync(CreateTokenUrl, - new CreateTokenRequest { Username = MockUsers.UserUsername, Password = "???" }); - response.Should().HaveStatusCodeBadRequest() - .And.Should().HaveBodyAsCommonResponseWithCode(TokenController.ErrorCodes.Create_BadPassword); - } - } - - [Fact] - public async void CreateToken_Success() - { - using (var client = _factory.CreateDefaultClient()) - { - var response = await client.PostAsJsonAsync(CreateTokenUrl, - new CreateTokenRequest { Username = MockUsers.UserUsername, Password = MockUsers.UserPassword }); - var body = response.Should().HaveStatusCodeOk() - .And.Should().HaveBodyAsJson().Which; - body.Token.Should().NotBeNullOrWhiteSpace(); - body.User.Should().BeEquivalentTo(MockUsers.UserUserInfo); - } - } - - [Fact] - public async void VerifyToken_InvalidModel() - { - using (var client = _factory.CreateDefaultClient()) - { - // missing token - await InvalidModelTestHelpers.TestPostInvalidModel(client, VerifyTokenUrl, - new VerifyTokenRequest { Token = null }); - } - } - - [Fact] - public async void 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(TokenController.ErrorCodes.Verify_BadToken); - } - } - - [Fact] - public async void VerifyToken_BadVersion() - { - using (var client = _factory.CreateDefaultClient()) - { - var token = (await client.CreateUserTokenAsync(MockUsers.UserUsername, MockUsers.UserPassword)).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(MockUsers.UserUsername, null, null); - } - - var response = await client.PostAsJsonAsync(VerifyTokenUrl, new VerifyTokenRequest { Token = token }); - response.Should().HaveStatusCodeBadRequest() - .And.Should().HaveBodyAsCommonResponseWithCode(TokenController.ErrorCodes.Verify_BadVersion); - } - } - - [Fact] - public async void VerifyToken_UserNotExist() - { - using (var client = _factory.CreateDefaultClient()) - { - var token = (await client.CreateUserTokenAsync(MockUsers.UserUsername, MockUsers.UserPassword)).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(MockUsers.UserUsername); - } - - var response = await client.PostAsJsonAsync(VerifyTokenUrl, new VerifyTokenRequest { Token = token }); - response.Should().HaveStatusCodeBadRequest() - .And.Should().HaveBodyAsCommonResponseWithCode(TokenController.ErrorCodes.Verify_UserNotExist); - } - } - - [Fact] - public async void VerifyToken_Expired() - { - using (var client = _factory.CreateDefaultClient()) - { - // I can only control the token expired time but not current time - // because verify logic is encapsuled in other library. - var mockClock = _factory.GetTestClock(); - mockClock.MockCurrentTime = DateTime.Now - TimeSpan.FromDays(2); - var token = (await client.CreateUserTokenAsync(MockUsers.UserUsername, MockUsers.UserPassword, 1)).Token; - var response = await client.PostAsJsonAsync(VerifyTokenUrl, - new VerifyTokenRequest { Token = token }); - response.Should().HaveStatusCodeBadRequest() - .And.Should().HaveBodyAsCommonResponseWithCode(TokenController.ErrorCodes.Verify_Expired); - mockClock.MockCurrentTime = null; - } - } - - [Fact] - public async void VerifyToken_Success() - { - using (var client = _factory.CreateDefaultClient()) - { - var createTokenResult = await client.CreateUserTokenAsync(MockUsers.UserUsername, MockUsers.UserPassword); - var response = await client.PostAsJsonAsync(VerifyTokenUrl, - new VerifyTokenRequest { Token = createTokenResult.Token }); - response.Should().HaveStatusCodeOk() - .And.Should().HaveBodyAsJson() - .Which.User.Should().BeEquivalentTo(MockUsers.UserUserInfo); - } - } - } -} diff --git a/Timeline.Tests/UserAvatarServiceTest.cs b/Timeline.Tests/UserAvatarServiceTest.cs index 93bb70ae..d22ad113 100644 --- a/Timeline.Tests/UserAvatarServiceTest.cs +++ b/Timeline.Tests/UserAvatarServiceTest.cs @@ -1,6 +1,7 @@ using FluentAssertions; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using SixLabors.ImageSharp.Formats.Png; using System; using System.Linq; @@ -151,28 +152,26 @@ namespace Timeline.Tests private readonly MockDefaultUserAvatarProvider _mockDefaultUserAvatarProvider; - private readonly ILoggerFactory _loggerFactory; private readonly TestDatabase _database; private readonly IETagGenerator _eTagGenerator; private readonly UserAvatarService _service; - public UserAvatarServiceTest(ITestOutputHelper outputHelper, MockDefaultUserAvatarProvider mockDefaultUserAvatarProvider, MockUserAvatarValidator mockUserAvatarValidator) + public UserAvatarServiceTest(MockDefaultUserAvatarProvider mockDefaultUserAvatarProvider, MockUserAvatarValidator mockUserAvatarValidator) { _mockDefaultUserAvatarProvider = mockDefaultUserAvatarProvider; - _loggerFactory = Logging.Create(outputHelper); _database = new TestDatabase(); _eTagGenerator = new ETagGenerator(); - _service = new UserAvatarService(_loggerFactory.CreateLogger(), _database.DatabaseContext, _mockDefaultUserAvatarProvider, mockUserAvatarValidator, _eTagGenerator); + _service = new UserAvatarService(NullLogger.Instance, _database.DatabaseContext, _mockDefaultUserAvatarProvider, mockUserAvatarValidator, _eTagGenerator); } + public void Dispose() { - _loggerFactory.Dispose(); _database.Dispose(); } @@ -197,14 +196,14 @@ namespace Timeline.Tests [Fact] public async Task GetAvatarETag_ShouldReturn_Default() { - const string username = MockUsers.UserUsername; + string username = MockUser.User.Username; (await _service.GetAvatarETag(username)).Should().BeEquivalentTo((await _mockDefaultUserAvatarProvider.GetDefaultAvatarETag())); } [Fact] public async Task GetAvatarETag_ShouldReturn_Data() { - const string username = MockUsers.UserUsername; + string username = MockUser.User.Username; { // create mock data var context = _database.DatabaseContext; @@ -237,14 +236,14 @@ namespace Timeline.Tests [Fact] public async Task GetAvatar_ShouldReturn_Default() { - const string username = MockUsers.UserUsername; + string username = MockUser.User.Username; (await _service.GetAvatar(username)).Avatar.Should().BeEquivalentTo((await _mockDefaultUserAvatarProvider.GetDefaultAvatar()).Avatar); } [Fact] public async Task GetAvatar_ShouldReturn_Data() { - const string username = MockUsers.UserUsername; + string username = MockUser.User.Username; { // create mock data @@ -287,7 +286,7 @@ namespace Timeline.Tests [Fact] public async Task SetAvatar_Should_Work() { - const string username = MockUsers.UserUsername; + string username = MockUser.User.Username; var user = await _database.DatabaseContext.Users.Where(u => u.Name == username).Include(u => u.Avatar).SingleAsync(); diff --git a/Timeline.Tests/UserDetailServiceTest.cs b/Timeline.Tests/UserDetailServiceTest.cs index 98613429..d16d1a40 100644 --- a/Timeline.Tests/UserDetailServiceTest.cs +++ b/Timeline.Tests/UserDetailServiceTest.cs @@ -1,5 +1,5 @@ using FluentAssertions; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using System; using System.Linq; using System.Threading.Tasks; @@ -9,28 +9,24 @@ using Timeline.Services; using Timeline.Tests.Helpers; using Timeline.Tests.Mock.Data; using Xunit; -using Xunit.Abstractions; namespace Timeline.Tests { public class UserDetailServiceTest : IDisposable { - private readonly ILoggerFactory _loggerFactory; private readonly TestDatabase _database; private readonly UserDetailService _service; - public UserDetailServiceTest(ITestOutputHelper outputHelper) + public UserDetailServiceTest() { - _loggerFactory = Logging.Create(outputHelper); _database = new TestDatabase(); - _service = new UserDetailService(_loggerFactory.CreateLogger(), _database.DatabaseContext); + _service = new UserDetailService(NullLogger.Instance, _database.DatabaseContext); } public void Dispose() { - _loggerFactory.Dispose(); _database.Dispose(); } @@ -56,13 +52,13 @@ namespace Timeline.Tests public async Task GetNickname_Should_Create_And_ReturnDefault() { { - var nickname = await _service.GetUserNickname(MockUsers.UserUsername); + var nickname = await _service.GetUserNickname(MockUser.User.Username); nickname.Should().BeNull(); } { var context = _database.DatabaseContext; - var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUsers.UserUsername); + var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUser.User.Username); var detail = context.UserDetails.Where(e => e.UserId == userId).Single(); detail.Nickname.Should().BeNullOrEmpty(); detail.QQ.Should().BeNullOrEmpty(); @@ -80,7 +76,7 @@ namespace Timeline.Tests { { var context = _database.DatabaseContext; - var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUsers.UserUsername); + var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUser.User.Username); var entity = new UserDetailEntity { Nickname = nickname, @@ -91,7 +87,7 @@ namespace Timeline.Tests } { - var n = await _service.GetUserNickname(MockUsers.UserUsername); + var n = await _service.GetUserNickname(MockUser.User.Username); n.Should().Equals(string.IsNullOrEmpty(nickname) ? null : nickname); } } @@ -118,13 +114,13 @@ namespace Timeline.Tests public async Task GetDetail_Should_Create_And_ReturnDefault() { { - var detail = await _service.GetUserDetail(MockUsers.UserUsername); + var detail = await _service.GetUserDetail(MockUser.User.Username); detail.Should().BeEquivalentTo(new UserDetail()); } { var context = _database.DatabaseContext; - var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUsers.UserUsername); + var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUser.User.Username); var detail = context.UserDetails.Where(e => e.UserId == userId).Single(); detail.Nickname.Should().BeNullOrEmpty(); detail.QQ.Should().BeNullOrEmpty(); @@ -143,7 +139,7 @@ namespace Timeline.Tests { var context = _database.DatabaseContext; - var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUsers.UserUsername); + var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUser.User.Username); var entity = new UserDetailEntity { Email = email, @@ -155,7 +151,7 @@ namespace Timeline.Tests } { - var detail = await _service.GetUserDetail(MockUsers.UserUsername); + var detail = await _service.GetUserDetail(MockUser.User.Username); detail.Should().BeEquivalentTo(new UserDetail { Email = email, @@ -187,10 +183,10 @@ namespace Timeline.Tests [Fact] public async Task UpdateDetail_Empty_Should_Work() { - await _service.UpdateUserDetail(MockUsers.UserUsername, new UserDetail()); + await _service.UpdateUserDetail(MockUser.User.Username, new UserDetail()); var context = _database.DatabaseContext; - var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUsers.UserUsername); + var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUser.User.Username); var entity = context.UserDetails.Where(e => e.UserId == userId).Single(); entity.Nickname.Should().BeNullOrEmpty(); entity.QQ.Should().BeNullOrEmpty(); @@ -215,10 +211,10 @@ namespace Timeline.Tests return detail; } - await _service.UpdateUserDetail(MockUsers.UserUsername, CreateWith(mockData1)); + await _service.UpdateUserDetail(MockUser.User.Username, CreateWith(mockData1)); var context = _database.DatabaseContext; - var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUsers.UserUsername); + var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUser.User.Username); var entity = context.UserDetails.Where(e => e.UserId == userId).Single(); void TestWith(string propertyValue) @@ -230,9 +226,9 @@ namespace Timeline.Tests TestWith(mockData1); - await _service.UpdateUserDetail(MockUsers.UserUsername, CreateWith(mockData2)); + await _service.UpdateUserDetail(MockUser.User.Username, CreateWith(mockData2)); TestWith(mockData2); - await _service.UpdateUserDetail(MockUsers.UserUsername, CreateWith("")); + await _service.UpdateUserDetail(MockUser.User.Username, CreateWith("")); TestWith(""); } @@ -247,10 +243,10 @@ namespace Timeline.Tests Description = "aaaaaaaaaa" }; - await _service.UpdateUserDetail(MockUsers.UserUsername, detail); + await _service.UpdateUserDetail(MockUser.User.Username, detail); var context = _database.DatabaseContext; - var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUsers.UserUsername); + var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUser.User.Username); var entity = context.UserDetails.Where(e => e.UserId == userId).Single(); entity.QQ.Should().Equals(detail.QQ); entity.Email.Should().Equals(detail.Email); @@ -265,7 +261,7 @@ namespace Timeline.Tests Description = "bbbbbbbbb" }; - await _service.UpdateUserDetail(MockUsers.UserUsername, detail2); + await _service.UpdateUserDetail(MockUser.User.Username, detail2); entity.QQ.Should().Equals(detail.QQ); entity.Email.Should().Equals(detail2.Email); entity.PhoneNumber.Should().BeNullOrEmpty(); diff --git a/Timeline.Tests/UserUnitTest.cs b/Timeline.Tests/UserUnitTest.cs deleted file mode 100644 index 77ec37ee..00000000 --- a/Timeline.Tests/UserUnitTest.cs +++ /dev/null @@ -1,318 +0,0 @@ -using FluentAssertions; -using Microsoft.AspNetCore.Mvc.Testing; -using System; -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 Xunit.Abstractions; - -namespace Timeline.Tests -{ - public class UserUnitTest : IClassFixture>, IDisposable - { - private readonly WebApplicationFactory _factory; - private readonly Action _disposeAction; - - public UserUnitTest(MyWebApplicationFactory factory, ITestOutputHelper outputHelper) - { - _factory = factory.WithTestConfig(outputHelper, out _disposeAction); - } - - public void Dispose() - { - _disposeAction(); - } - - [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(MockUsers.UserInfos); - } - } - - [Fact] - public async Task Get_Users_User() - { - using (var client = await _factory.CreateClientAsAdmin()) - { - var res = await client.GetAsync("users/" + MockUsers.UserUsername); - res.Should().HaveStatusCodeOk() - .And.Should().HaveBodyAsJson() - .Which.Should().BeEquivalentTo(MockUsers.UserUserInfo); - } - } - - [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); - } - } - - [Fact] - public async Task Put_InvalidModel() - { - 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 }); - } - } - - [Fact] - public async Task Put_BadUsername() - { - using (var client = await _factory.CreateClientAsAdmin()) - { - var res = await client.PutAsJsonAsync("users/dsf fddf", new UserPutRequest - { - Password = "???", - Administrator = false - }); - res.Should().HaveStatusCodeBadRequest() - .And.Should().HaveBodyAsCommonResponseWithCode(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() - .Which.Administrator.Should().Be(administrator); - } - - [Fact] - public async Task Put_Modiefied() - { - using (var client = await _factory.CreateClientAsAdmin()) - { - var res = await client.PutAsJsonAsync("users/" + MockUsers.UserUsername, new UserPutRequest - { - Password = "password", - Administrator = false - }); - res.Should().BePutModified(); - await CheckAdministrator(client, MockUsers.UserUsername, false); - } - } - - [Fact] - public async Task Put_Created() - { - using (var client = await _factory.CreateClientAsAdmin()) - { - const string username = "puttest"; - const string url = "users/" + username; - - var res = await client.PutAsJsonAsync(url, new UserPutRequest - { - Password = "password", - Administrator = false - }); - res.Should().BePutCreated(); - 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); - } - } - - [Fact] - public async Task Patch_Success() - { - using (var client = await _factory.CreateClientAsAdmin()) - { - { - var res = await client.PatchAsJsonAsync("users/" + MockUsers.UserUsername, - new UserPatchRequest { Administrator = false }); - res.Should().HaveStatusCodeOk(); - await CheckAdministrator(client, MockUsers.UserUsername, false); - } - } - } - - [Fact] - public async Task Delete_Deleted() - { - using (var client = await _factory.CreateClientAsAdmin()) - { - { - var url = "users/" + MockUsers.UserUsername; - var res = await client.DeleteAsync(url); - res.Should().BeDeleteDeleted(); - - var res2 = await client.GetAsync(url); - res2.Should().HaveStatusCodeNotFound(); - } - } - } - - [Fact] - public async Task Delete_NotExist() - { - using (var client = await _factory.CreateClientAsAdmin()) - { - { - var res = await client.DeleteAsync("users/usernotexist"); - res.Should().BeDeleteNotExist(); - } - } - } - - - public class ChangeUsernameUnitTest : IClassFixture>, IDisposable - { - private const string url = "userop/changeusername"; - - private readonly WebApplicationFactory _factory; - private readonly Action _disposeAction; - - public ChangeUsernameUnitTest(MyWebApplicationFactory factory, ITestOutputHelper outputHelper) - { - _factory = factory.WithTestConfig(outputHelper, out _disposeAction); - } - - public void Dispose() - { - _disposeAction(); - } - - [Fact] - public async Task InvalidModel() - { - 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 = "???" }); - } - } - - [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); - } - } - - [Fact] - public async Task UserAlreadyExist() - { - using (var client = await _factory.CreateClientAsAdmin()) - { - var res = await client.PostAsJsonAsync(url, - new ChangeUsernameRequest { OldUsername = MockUsers.UserUsername, NewUsername = MockUsers.AdminUsername }); - res.Should().HaveStatusCodeBadRequest() - .And.Should().HaveBodyAsCommonResponseWithCode(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 = MockUsers.UserUsername, NewUsername = newUsername }); - res.Should().HaveStatusCodeOk(); - await client.CreateUserTokenAsync(newUsername, MockUsers.UserPassword); - } - } - } - - - public class ChangePasswordUnitTest : IClassFixture>, IDisposable - { - private const string url = "userop/changepassword"; - - private readonly WebApplicationFactory _factory; - private readonly Action _disposeAction; - - public ChangePasswordUnitTest(MyWebApplicationFactory factory, ITestOutputHelper outputHelper) - { - _factory = factory.WithTestConfig(outputHelper, out _disposeAction); - } - - public void Dispose() - { - _disposeAction(); - } - - [Fact] - public async Task InvalidModel() - { - 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 }); - } - } - - [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); - } - } - - [Fact] - public async Task Success() - { - using (var client = await _factory.CreateClientAsUser()) - { - const string newPassword = "new"; - var res = await client.PostAsJsonAsync(url, - new ChangePasswordRequest { OldPassword = MockUsers.UserPassword, NewPassword = newPassword }); - res.Should().HaveStatusCodeOk(); - await client.CreateUserTokenAsync(MockUsers.UserUsername, newPassword); - } - } - } - } -} diff --git a/Timeline/Controllers/TokenController.cs b/Timeline/Controllers/TokenController.cs index 3c166448..2e661695 100644 --- a/Timeline/Controllers/TokenController.cs +++ b/Timeline/Controllers/TokenController.cs @@ -3,39 +3,42 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Tokens; using System; -using System.Collections.Generic; using System.Threading.Tasks; using Timeline.Models.Http; using Timeline.Services; -using static Timeline.Helpers.MyLogHelper; +using Timeline.Helpers; -namespace Timeline.Controllers +namespace Timeline { - [Route("token")] - [ApiController] - public class TokenController : Controller + public static partial class ErrorCodes { - private static class LoggingEventIds - { - public const int CreateSucceeded = 1000; - public const int CreateFailed = 1001; - - public const int VerifySucceeded = 2000; - public const int VerifyFailed = 2001; - } - - public static class ErrorCodes + public static partial class Http { - public const int Create_UserNotExist = -1001; - public const int Create_BadPassword = -1002; - public const int Create_BadExpireOffset = -1003; + public static class Token // bbb = 001 + { + public static class Create // cc = 01 + { + public const int BadCredential = 10010101; + } - public const int Verify_BadToken = -2001; - public const int Verify_UserNotExist = -2002; - public const int Verify_BadVersion = -2003; - public const int Verify_Expired = -2004; + public static class Verify // cc = 02 + { + public const int BadFormat = 10010201; + public const int UserNotExist = 10010202; + public const int OldVersion = 10010203; + public const int Expired = 10010204; + } + } } + } +} +namespace Timeline.Controllers +{ + [Route("token")] + [ApiController] + public class TokenController : Controller + { private readonly IUserService _userService; private readonly ILogger _logger; private readonly IClock _clock; @@ -51,23 +54,28 @@ namespace Timeline.Controllers [AllowAnonymous] public async Task Create([FromBody] CreateTokenRequest request) { - void LogFailure(string reason, int code, Exception e = null) + void LogFailure(string reason, Exception e = null) { - _logger.LogInformation(LoggingEventIds.CreateFailed, e, FormatLogMessage("Attemp to login failed.", - Pair("Reason", reason), - Pair("Code", code), - Pair("Username", request.Username), - Pair("Password", request.Password), - Pair("Expire Offset (in days)", request.ExpireOffset))); + _logger.LogInformation(e, Log.Format("Attemp to login failed.", + ("Reason", reason), + ("Username", request.Username), + ("Password", request.Password), + ("Expire (in days)", request.Expire) + )); } try { - var expiredTime = request.ExpireOffset == null ? null : (DateTime?)(_clock.GetCurrentTime().AddDays(request.ExpireOffset.Value)); - var result = await _userService.CreateToken(request.Username, request.Password, expiredTime); - _logger.LogInformation(LoggingEventIds.CreateSucceeded, FormatLogMessage("Attemp to login succeeded.", - Pair("Username", request.Username), - Pair("Expire Time", expiredTime == null ? "default" : expiredTime.Value.ToString()))); + DateTime? expireTime = null; + if (request.Expire != null) + expireTime = _clock.GetCurrentTime().AddDays(request.Expire.Value); + + var result = await _userService.CreateToken(request.Username, request.Password, expireTime); + + _logger.LogInformation(Log.Format("Attemp to login succeeded.", + ("Username", request.Username), + ("Expire At", expireTime?.ToString() ?? "default") + )); return Ok(new CreateTokenResponse { Token = result.Token, @@ -76,15 +84,15 @@ namespace Timeline.Controllers } catch (UserNotExistException e) { - var code = ErrorCodes.Create_UserNotExist; - LogFailure("User does not exist.", code, e); - return BadRequest(new CommonResponse(code, "Bad username or password.")); + LogFailure("User does not exist.", e); + return BadRequest(new CommonResponse(ErrorCodes.Http.Token.Create.BadCredential, + "Bad username or password.")); } catch (BadPasswordException e) { - var code = ErrorCodes.Create_BadPassword; - LogFailure("Password is wrong.", code, e); - return BadRequest(new CommonResponse(code, "Bad username or password.")); + LogFailure("Password is wrong.", e); + return BadRequest(new CommonResponse(ErrorCodes.Http.Token.Create.BadCredential, + "Bad username or password.")); } } @@ -92,22 +100,20 @@ namespace Timeline.Controllers [AllowAnonymous] public async Task Verify([FromBody] VerifyTokenRequest request) { - void LogFailure(string reason, int code, Exception e = null, params KeyValuePair[] otherProperties) + void LogFailure(string reason, Exception e = null, params (string, object)[] otherProperties) { - var properties = new KeyValuePair[3 + otherProperties.Length]; - properties[0] = Pair("Reason", reason); - properties[1] = Pair("Code", code); - properties[2] = Pair("Token", request.Token); - otherProperties.CopyTo(properties, 3); - _logger.LogInformation(LoggingEventIds.VerifyFailed, e, FormatLogMessage("Token verification failed.", properties)); + var properties = new (string, object)[2 + otherProperties.Length]; + properties[0] = ("Reason", reason); + properties[1] = ("Token", request.Token); + otherProperties.CopyTo(properties, 2); + _logger.LogInformation(e, Log.Format("Token verification failed.", properties)); } try { var result = await _userService.VerifyToken(request.Token); - _logger.LogInformation(LoggingEventIds.VerifySucceeded, - FormatLogMessage("Token verification succeeded.", - Pair("Username", result.Username), Pair("Token", request.Token))); + _logger.LogInformation(Log.Format("Token verification succeeded.", + ("Username", result.Username), ("Token", request.Token))); return Ok(new VerifyTokenResponse { User = result @@ -118,33 +124,28 @@ namespace Timeline.Controllers if (e.ErrorCode == JwtTokenVerifyException.ErrorCodes.Expired) { const string message = "Token is expired."; - var code = ErrorCodes.Verify_Expired; var innerException = e.InnerException as SecurityTokenExpiredException; - LogFailure(message, code, e, Pair("Expires", innerException.Expires)); - return BadRequest(new CommonResponse(code, message)); + LogFailure(message, e, ("Expires", innerException.Expires), ("Current Time", _clock.GetCurrentTime())); + return BadRequest(new CommonResponse(ErrorCodes.Http.Token.Verify.Expired, message)); } else { const string message = "Token is of bad format."; - var code = ErrorCodes.Verify_BadToken; - LogFailure(message, code, e); - return BadRequest(new CommonResponse(code, message)); + LogFailure(message, e); + return BadRequest(new CommonResponse(ErrorCodes.Http.Token.Verify.BadFormat, message)); } } catch (UserNotExistException e) { const string message = "User does not exist. Administrator might have deleted this user."; - var code = ErrorCodes.Verify_UserNotExist; - LogFailure(message, code, e); - return BadRequest(new CommonResponse(code, message)); + LogFailure(message, e); + return BadRequest(new CommonResponse(ErrorCodes.Http.Token.Verify.UserNotExist, message)); } catch (BadTokenVersionException e) { - const string message = "Token has a old version."; - var code = ErrorCodes.Verify_BadVersion; - LogFailure(message, code, e); - _logger.LogInformation(LoggingEventIds.VerifyFailed, e, "Attemp to verify a bad token because version is old. Code: {} Token: {}.", code, request.Token); - return BadRequest(new CommonResponse(code, message)); + const string message = "Token has an old version."; + LogFailure(message, e, ("Token Version", e.TokenVersion), ("Required Version", e.RequiredVersion)); + return BadRequest(new CommonResponse(ErrorCodes.Http.Token.Verify.OldVersion, message)); } } } diff --git a/Timeline/Controllers/UserController.cs b/Timeline/Controllers/UserController.cs index bd13f0a3..c0cd3cdb 100644 --- a/Timeline/Controllers/UserController.cs +++ b/Timeline/Controllers/UserController.cs @@ -65,10 +65,10 @@ namespace Timeline.Controllers { case PutResult.Created: _logger.LogInformation(FormatLogMessage("A user is created.", Pair("Username", username))); - return CreatedAtAction("Get", new { username }, CommonPutResponse.Created); + return CreatedAtAction("Get", new { username }, CommonPutResponse.Create()); case PutResult.Modified: _logger.LogInformation(FormatLogMessage("A user is modified.", Pair("Username", username))); - return Ok(CommonPutResponse.Modified); + return Ok(CommonPutResponse.Modify()); default: throw new Exception("Unreachable code."); } @@ -102,12 +102,12 @@ namespace Timeline.Controllers { await _userService.DeleteUser(username); _logger.LogInformation(FormatLogMessage("A user is deleted.", Pair("Username", username))); - return Ok(CommonDeleteResponse.Deleted); + return Ok(CommonDeleteResponse.Delete()); } catch (UserNotExistException e) { _logger.LogInformation(e, FormatLogMessage("Attempt to delete a non-existent user.", Pair("Username", username))); - return Ok(CommonDeleteResponse.NotExists); + return Ok(CommonDeleteResponse.NotExist()); } } diff --git a/Timeline/ErrorCodes.cs b/Timeline/ErrorCodes.cs new file mode 100644 index 00000000..0b325e27 --- /dev/null +++ b/Timeline/ErrorCodes.cs @@ -0,0 +1,29 @@ +namespace Timeline +{ + /// + /// All error code constants. + /// + /// + /// Scheme: + /// abbbccdd + /// + public static partial class ErrorCodes + { + public static partial class Http // a = 1 + { + public static class Common // bbb = 000 + { + public const int InvalidModel = 10000000; + + public static class Header // cc = 01 + { + public const int Missing_ContentType = 10010101; // dd = 01 + public const int Missing_ContentLength = 10010102; // dd = 02 + public const int Zero_ContentLength = 10010103; // dd = 03 + public const int BadFormat_IfNonMatch = 10010104; // dd = 04 + } + } + } + + } +} diff --git a/Timeline/Helpers/Log.cs b/Timeline/Helpers/Log.cs index 123e8a8e..64391cd1 100644 --- a/Timeline/Helpers/Log.cs +++ b/Timeline/Helpers/Log.cs @@ -3,6 +3,7 @@ using System.Text; namespace Timeline.Helpers { + // TODO! Remember to remove this after refactor. public static class MyLogHelper { public static KeyValuePair Pair(string key, object value) => new KeyValuePair(key, value); @@ -21,4 +22,22 @@ namespace Timeline.Helpers return builder.ToString(); } } + + public static class Log + { + public static string Format(string summary, params (string, object)[] properties) + { + var builder = new StringBuilder(); + builder.Append(summary); + foreach (var property in properties) + { + var (key, value) = property; + builder.AppendLine(); + builder.Append(key); + builder.Append(" : "); + builder.Append(value); + } + return builder.ToString(); + } + } } diff --git a/Timeline/Models/Http/Common.cs b/Timeline/Models/Http/Common.cs index a72f187c..af185e85 100644 --- a/Timeline/Models/Http/Common.cs +++ b/Timeline/Models/Http/Common.cs @@ -2,43 +2,29 @@ namespace Timeline.Models.Http { public class CommonResponse { - public static class ErrorCodes - { - /// - /// Used when the model is invaid. - /// For example a required field is null. - /// - public const int InvalidModel = -100; - - public const int Header_Missing_ContentType = -111; - public const int Header_Missing_ContentLength = -112; - public const int Header_Zero_ContentLength = -113; - public const int Header_BadFormat_IfNonMatch = -114; - } - public static CommonResponse InvalidModel(string message) { - return new CommonResponse(ErrorCodes.InvalidModel, message); + return new CommonResponse(ErrorCodes.Http.Common.InvalidModel, message); } public static CommonResponse MissingContentType() { - return new CommonResponse(ErrorCodes.Header_Missing_ContentType, "Header Content-Type is required."); + return new CommonResponse(ErrorCodes.Http.Common.Header.Missing_ContentType, "Header Content-Type is required."); } public static CommonResponse MissingContentLength() { - return new CommonResponse(ErrorCodes.Header_Missing_ContentLength, "Header Content-Length is missing or of bad format."); + return new CommonResponse(ErrorCodes.Http.Common.Header.Missing_ContentLength, "Header Content-Length is missing or of bad format."); } public static CommonResponse ZeroContentLength() { - return new CommonResponse(ErrorCodes.Header_Zero_ContentLength, "Header Content-Length must not be 0."); + return new CommonResponse(ErrorCodes.Http.Common.Header.Zero_ContentLength, "Header Content-Length must not be 0."); } public static CommonResponse BadIfNonMatch() { - return new CommonResponse(ErrorCodes.Header_BadFormat_IfNonMatch, "Header If-Non-Match is of bad format."); + return new CommonResponse(ErrorCodes.Http.Common.Header.BadFormat_IfNonMatch, "Header If-Non-Match is of bad format."); } public CommonResponse() @@ -56,21 +42,55 @@ namespace Timeline.Models.Http public string Message { get; set; } } + public class CommonDataResponse : CommonResponse + { + public CommonDataResponse() + { + + } + + public CommonDataResponse(int code, string message, T data) + : base(code, message) + { + Data = data; + } + + public T Data { get; set; } + } + public static class CommonPutResponse { - public const int CreatedCode = 0; - public const int ModifiedCode = 1; + public class ResponseData + { + public ResponseData(bool create) + { + Create = create; + } - public static CommonResponse Created { get; } = new CommonResponse(CreatedCode, "A new item is created."); - public static CommonResponse Modified { get; } = new CommonResponse(ModifiedCode, "An existent item is modified."); + public bool Create { get; set; } + } + + public static CommonDataResponse Create() => + new CommonDataResponse(0, "A new item is created.", new ResponseData(true)); + public static CommonDataResponse Modify() => + new CommonDataResponse(0, "An existent item is modified.", new ResponseData(false)); } public static class CommonDeleteResponse { - public const int DeletedCode = 0; - public const int NotExistsCode = 1; + public class ResponseData + { + public ResponseData(bool delete) + { + Delete = delete; + } + + public bool Delete { get; set; } + } - public static CommonResponse Deleted { get; } = new CommonResponse(DeletedCode, "An existent item is deleted."); - public static CommonResponse NotExists { get; } = new CommonResponse(NotExistsCode, "The item does not exist."); + public static CommonDataResponse Delete() => + new CommonDataResponse(0, "An existent item is deleted.", new ResponseData(true)); + public static CommonDataResponse NotExist() => + new CommonDataResponse(0, "The item does not exist.", new ResponseData(false)); } } diff --git a/Timeline/Models/Http/Token.cs b/Timeline/Models/Http/Token.cs index 68a66d0a..615b6d8a 100644 --- a/Timeline/Models/Http/Token.cs +++ b/Timeline/Models/Http/Token.cs @@ -10,7 +10,7 @@ namespace Timeline.Models.Http public string Password { get; set; } // in days, optional [Range(1, 365)] - public int? ExpireOffset { get; set; } + public int? Expire { get; set; } } public class CreateTokenResponse -- cgit v1.2.3