From 79ab2b304d93b1029515bd3f954db4e5a73f4168 Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 30 Jan 2020 20:26:52 +0800 Subject: ... --- Timeline/Models/Http/UserInfo.cs | 58 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 Timeline/Models/Http/UserInfo.cs (limited to 'Timeline/Models/Http/UserInfo.cs') diff --git a/Timeline/Models/Http/UserInfo.cs b/Timeline/Models/Http/UserInfo.cs new file mode 100644 index 00000000..6029b8aa --- /dev/null +++ b/Timeline/Models/Http/UserInfo.cs @@ -0,0 +1,58 @@ +using AutoMapper; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using Timeline.Controllers; +using Timeline.Services; + +namespace Timeline.Models.Http +{ + public interface IUserInfo + { + string Username { get; set; } + string Nickname { get; set; } + string AvatarUrl { get; set; } + } + + public class UserInfo : IUserInfo + { + public string Username { get; set; } = default!; + public string Nickname { get; set; } = default!; + public string AvatarUrl { get; set; } = default!; + } + + public class UserInfoForAdmin : IUserInfo + { + public string Username { get; set; } = default!; + public string Nickname { get; set; } = default!; + public string AvatarUrl { get; set; } = default!; + public bool Administrator { get; set; } + } + + public class UserInfoSetAvatarUrlAction : IMappingAction + { + private readonly IActionContextAccessor _actionContextAccessor; + private readonly IUrlHelperFactory _urlHelperFactory; + + public UserInfoSetAvatarUrlAction(IActionContextAccessor actionContextAccessor, IUrlHelperFactory urlHelperFactory) + { + _actionContextAccessor = actionContextAccessor; + _urlHelperFactory = urlHelperFactory; + } + + public void Process(object source, IUserInfo destination, ResolutionContext context) + { + var urlHelper = _urlHelperFactory.GetUrlHelper(_actionContextAccessor.ActionContext); + destination.AvatarUrl = urlHelper.ActionLink(nameof(UserAvatarController.Get), nameof(UserAvatarController), new { destination.Username }); + } + } + + public class UserInfoAutoMapperProfile : Profile + { + public UserInfoAutoMapperProfile() + { + CreateMap().AfterMap(); + CreateMap().AfterMap(); + } + } +} -- cgit v1.2.3 From 5aaa4f95e6bdd46e6740c1ecbbd46bdf415eedd2 Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 30 Jan 2020 23:49:02 +0800 Subject: Finish reafctor, TODO: Database migration. --- Timeline.Tests/Helpers/TestApplication.cs | 12 +++++++++- .../IntegratedTests/IntegratedTestBase.cs | 2 +- .../IntegratedTests/PersonalTimelineTest.cs | 28 +++++++++++----------- Timeline.Tests/IntegratedTests/UserAvatarTest.cs | 2 +- Timeline.Tests/IntegratedTests/UserTest.cs | 8 +++---- Timeline.Tests/UsernameValidatorUnitTest.cs | 8 +------ Timeline/Controllers/ControllerAuthExtensions.cs | 2 +- Timeline/Controllers/UserAvatarController.cs | 2 +- Timeline/Controllers/UserController.cs | 18 +++++++------- Timeline/Entities/UserEntity.cs | 2 +- Timeline/Models/Http/UserInfo.cs | 21 +++++++++++----- Timeline/Models/Validation/NicknameValidator.cs | 2 +- Timeline/Services/TimelineService.cs | 2 +- Timeline/Services/UserService.cs | 2 ++ 14 files changed, 64 insertions(+), 47 deletions(-) (limited to 'Timeline/Models/Http/UserInfo.cs') diff --git a/Timeline.Tests/Helpers/TestApplication.cs b/Timeline.Tests/Helpers/TestApplication.cs index 14cafea3..bc5deeec 100644 --- a/Timeline.Tests/Helpers/TestApplication.cs +++ b/Timeline.Tests/Helpers/TestApplication.cs @@ -17,10 +17,20 @@ namespace Timeline.Tests.Helpers public TestApplication(WebApplicationFactory factory) { DatabaseConnection = new SqliteConnection("Data Source=:memory:;"); + DatabaseConnection.Open(); + + var options = new DbContextOptionsBuilder() + .UseSqlite(DatabaseConnection) + .Options; + + using (var context = new DevelopmentDatabaseContext(options)) + { + context.Database.EnsureCreated(); + } Factory = factory.WithWebHostBuilder(builder => { - builder.ConfigureTestServices(services => + builder.ConfigureServices(services => { services.AddDbContext(options => { diff --git a/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs b/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs index af3e0c2f..242e96cd 100644 --- a/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs +++ b/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs @@ -63,7 +63,7 @@ namespace Timeline.Tests.IntegratedTests foreach (var user in users) { - userService.CreateUser(user); + userService.CreateUser(user).Wait(); userInfoList.Add(mapper.Map(user)); userInfoForAdminList.Add(mapper.Map(user)); } diff --git a/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs b/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs index 5c472e52..d787d87d 100644 --- a/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs +++ b/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs @@ -103,12 +103,12 @@ namespace Timeline.Tests.IntegratedTests } await AssertMembers(new List { UserInfoList[2] }); { - var res = await client.DeleteAsync("/users/users1/timeline/members/users2"); + var res = await client.DeleteAsync("/users/user1/timeline/members/user2"); res.Should().BeDelete(true); } await AssertEmptyMembers(); { - var res = await client.DeleteAsync("/users/users1/timeline/members/users2"); + var res = await client.DeleteAsync("/users/user1/timeline/members/users2"); res.Should().BeDelete(false); } await AssertEmptyMembers(); @@ -137,22 +137,22 @@ namespace Timeline.Tests.IntegratedTests } { - var res = await client.PutAsync("users/user1/timeline/member/user2", null); + var res = await client.PutAsync("users/user1/timeline/members/user2", null); res.Should().HaveStatusCode(opMemberUser); } { - var res = await client.DeleteAsync("users/user1/timeline/member/user2"); + var res = await client.DeleteAsync("users/user1/timeline/members/user2"); res.Should().HaveStatusCode(opMemberUser); } { - var res = await client.PutAsync("users/admin/timeline/member/user2", null); + var res = await client.PutAsync("users/admin/timeline/members/user2", null); res.Should().HaveStatusCode(opMemberAdmin); } { - var res = await client.DeleteAsync("users/admin/timeline/member/user2"); + var res = await client.DeleteAsync("users/admin/timeline/members/user2"); res.Should().HaveStatusCode(opMemberAdmin); } } @@ -244,7 +244,7 @@ namespace Timeline.Tests.IntegratedTests { using (var client = await CreateClientAsUser()) { - var res = await client.PutAsync("users/user/timeline/members/user2", null); + var res = await client.PutAsync("users/user1/timeline/members/user2", null); res.Should().HaveStatusCode(200); } @@ -283,7 +283,7 @@ namespace Timeline.Tests.IntegratedTests using (var client = await CreateClientAs(2)) { { // post as member - var res = await client.PostAsJsonAsync("users/user1/timeline/postop/create", + var res = await client.PostAsJsonAsync("users/user1/timeline/posts", new TimelinePostCreateRequest { Content = "aaa" }); res.Should().HaveStatusCode(200); } @@ -296,7 +296,7 @@ namespace Timeline.Tests.IntegratedTests async Task CreatePost(int userNumber) { using var client = await CreateClientAs(userNumber); - var res = await client.PostAsJsonAsync($"users/user1/timeline/postop/create", + var res = await client.PostAsJsonAsync($"users/user1/timeline/posts", new TimelinePostCreateRequest { Content = "aaa" }); return res.Should().HaveStatusCode(200) .And.HaveJsonBody() @@ -383,7 +383,7 @@ namespace Timeline.Tests.IntegratedTests .Which; body.Should().NotBeNull(); body.Content.Should().Be(mockContent); - body.Author.Should().Be(UserInfoList[1]); + body.Author.Should().BeEquivalentTo(UserInfoList[1]); createRes = body; } { @@ -402,9 +402,9 @@ namespace Timeline.Tests.IntegratedTests .And.HaveJsonBody() .Which; body.Should().NotBeNull(); - body.Content.Should().Be(mockContent); - body.Author.Should().Be(UserInfoList[1]); - body.Time.Should().Be(mockTime2); + body.Content.Should().Be(mockContent2); + body.Author.Should().BeEquivalentTo(UserInfoList[1]); + body.Time.Should().BeCloseTo(mockTime2, 1000); createRes2 = body; } { @@ -450,7 +450,7 @@ namespace Timeline.Tests.IntegratedTests var id2 = await CreatePost(now); { - var res = await client.GetAsync("users/user/timeline/posts"); + var res = await client.GetAsync("users/user1/timeline/posts"); res.Should().HaveStatusCode(200) .And.HaveJsonBody() .Which.Select(p => p.Id).Should().Equal(id1, id2, id0); diff --git a/Timeline.Tests/IntegratedTests/UserAvatarTest.cs b/Timeline.Tests/IntegratedTests/UserAvatarTest.cs index 989207e2..67c2dd9a 100644 --- a/Timeline.Tests/IntegratedTests/UserAvatarTest.cs +++ b/Timeline.Tests/IntegratedTests/UserAvatarTest.cs @@ -49,7 +49,7 @@ namespace Timeline.Tests.IntegratedTests var env = Factory.Server.Host.Services.GetRequiredService(); var defaultAvatarData = await File.ReadAllBytesAsync(Path.Combine(env.ContentRootPath, "default-avatar.png")); - async Task GetReturnDefault(string username = "user") + async Task GetReturnDefault(string username = "user1") { var res = await client.GetAsync($"users/{username}/avatar"); res.Should().HaveStatusCode(200); diff --git a/Timeline.Tests/IntegratedTests/UserTest.cs b/Timeline.Tests/IntegratedTests/UserTest.cs index 4c2ccf7a..1b9733ff 100644 --- a/Timeline.Tests/IntegratedTests/UserTest.cs +++ b/Timeline.Tests/IntegratedTests/UserTest.cs @@ -44,7 +44,7 @@ namespace Timeline.Tests.IntegratedTests using var client = await CreateClientAsAdministrator(); var res = await client.GetAsync("/users"); res.Should().HaveStatusCode(200) - .And.HaveJsonBody() + .And.HaveJsonBody() .Which.Should().BeEquivalentTo(UserInfoForAdminList); } @@ -74,7 +74,7 @@ namespace Timeline.Tests.IntegratedTests using var client = await CreateClientAsAdministrator(); var res = await client.GetAsync($"/users/user1"); res.Should().HaveStatusCode(200) - .And.HaveJsonBody() + .And.HaveJsonBody() .Which.Should().BeEquivalentTo(UserInfoForAdminList[1]); } @@ -142,7 +142,7 @@ namespace Timeline.Tests.IntegratedTests { // Token should expire. - var res = await userClient.GetAsync("/users"); + var res = await userClient.GetAsync("/testing/auth/Authorize"); res.Should().HaveStatusCode(HttpStatusCode.Unauthorized); } @@ -198,7 +198,7 @@ namespace Timeline.Tests.IntegratedTests [Fact] public async Task Patch_NoAuth_Unauthorized() { - using var client = await CreateClientAsUser(); + using var client = await CreateDefaultClient(); var res = await client.PatchAsJsonAsync("/users/user1", new UserPatchRequest { Nickname = "aaa" }); res.Should().HaveStatusCode(HttpStatusCode.Unauthorized); } diff --git a/Timeline.Tests/UsernameValidatorUnitTest.cs b/Timeline.Tests/UsernameValidatorUnitTest.cs index 1a09d477..0f844452 100644 --- a/Timeline.Tests/UsernameValidatorUnitTest.cs +++ b/Timeline.Tests/UsernameValidatorUnitTest.cs @@ -21,12 +21,6 @@ namespace Timeline.Tests return message; } - [Fact] - public void Null() - { - FailAndMessage(null).Should().ContainEquivalentOf("null"); - } - [Fact] public void NotString() { @@ -58,6 +52,7 @@ namespace Timeline.Tests } [Theory] + [InlineData(null)] [InlineData("abc")] [InlineData("-abc")] [InlineData("_abc")] @@ -68,7 +63,6 @@ namespace Timeline.Tests [InlineData("a-b_c")] public void Success(string value) { - var (result, _) = _validator.Validate(value); result.Should().BeTrue(); } diff --git a/Timeline/Controllers/ControllerAuthExtensions.cs b/Timeline/Controllers/ControllerAuthExtensions.cs index 90da8a93..34fd4d99 100644 --- a/Timeline/Controllers/ControllerAuthExtensions.cs +++ b/Timeline/Controllers/ControllerAuthExtensions.cs @@ -34,7 +34,7 @@ namespace Timeline.Controllers var claim = controller.User.FindFirst(ClaimTypes.NameIdentifier); if (claim == null) - throw new InvalidOperationException("Failed to get user id because User has no NameIdentifier claim."); + return null; if (long.TryParse(claim.Value, out var value)) return value; diff --git a/Timeline/Controllers/UserAvatarController.cs b/Timeline/Controllers/UserAvatarController.cs index ab0ad8e7..2dd279a8 100644 --- a/Timeline/Controllers/UserAvatarController.cs +++ b/Timeline/Controllers/UserAvatarController.cs @@ -78,7 +78,7 @@ namespace Timeline.Controllers [HttpPut("users/{username}/avatar")] [Authorize] - [RequireContentLength] + [RequireContentType, RequireContentLength] [Consumes("image/png", "image/jpeg", "image/gif", "image/webp")] public async Task Put([FromRoute][Username] string username) { diff --git a/Timeline/Controllers/UserController.cs b/Timeline/Controllers/UserController.cs index 400a518c..fa73c6f9 100644 --- a/Timeline/Controllers/UserController.cs +++ b/Timeline/Controllers/UserController.cs @@ -42,7 +42,13 @@ namespace Timeline.Controllers { var users = await _userService.GetUsers(); var administrator = this.IsAdministrator(); - return Ok(users.Select(u => ConvertToUserInfo(u, administrator)).ToArray()); + // Note: the (object) explicit conversion. If not convert, + // then result is a IUserInfo array and JsonSerializer will + // treat all element as IUserInfo and deserialize only properties + // in IUserInfo. So we convert it to object to make an object + // array so that JsonSerializer use the runtime type. + var result = users.Select(u => (object)ConvertToUserInfo(u, administrator)).ToArray(); + return Ok(result); } [HttpGet("users/{username}")] @@ -106,15 +112,11 @@ namespace Timeline.Controllers [HttpDelete("users/{username}"), AdminAuthorize] public async Task> Delete([FromRoute][Username] string username) { - try - { - await _userService.DeleteUser(username); + var delete = await _userService.DeleteUser(username); + if (delete) return Ok(CommonDeleteResponse.Delete()); - } - catch (UserNotExistException) - { + else return Ok(CommonDeleteResponse.NotExist()); - } } [HttpPost("userop/createuser"), AdminAuthorize] diff --git a/Timeline/Entities/UserEntity.cs b/Timeline/Entities/UserEntity.cs index dae6979f..946c3fa2 100644 --- a/Timeline/Entities/UserEntity.cs +++ b/Timeline/Entities/UserEntity.cs @@ -29,7 +29,7 @@ namespace Timeline.Entities [Column("version"), Required] public long Version { get; set; } - [Column("nickname"), MaxLength(40)] + [Column("nickname"), MaxLength(100)] public string? Nickname { get; set; } public UserAvatarEntity? Avatar { get; set; } diff --git a/Timeline/Models/Http/UserInfo.cs b/Timeline/Models/Http/UserInfo.cs index 6029b8aa..62d989a2 100644 --- a/Timeline/Models/Http/UserInfo.cs +++ b/Timeline/Models/Http/UserInfo.cs @@ -29,21 +29,30 @@ namespace Timeline.Models.Http public bool Administrator { get; set; } } - public class UserInfoSetAvatarUrlAction : IMappingAction + public class UserInfoAvatarUrlValueResolver : IValueResolver { private readonly IActionContextAccessor _actionContextAccessor; private readonly IUrlHelperFactory _urlHelperFactory; - public UserInfoSetAvatarUrlAction(IActionContextAccessor actionContextAccessor, IUrlHelperFactory urlHelperFactory) + public UserInfoAvatarUrlValueResolver() + { + } + + public UserInfoAvatarUrlValueResolver(IActionContextAccessor actionContextAccessor, IUrlHelperFactory urlHelperFactory) { _actionContextAccessor = actionContextAccessor; _urlHelperFactory = urlHelperFactory; } - public void Process(object source, IUserInfo destination, ResolutionContext context) + public string Resolve(User source, IUserInfo destination, string destMember, ResolutionContext context) { + if (_actionContextAccessor == null) + { + return $"/users/{destination.Username}/avatar"; + } + var urlHelper = _urlHelperFactory.GetUrlHelper(_actionContextAccessor.ActionContext); - destination.AvatarUrl = urlHelper.ActionLink(nameof(UserAvatarController.Get), nameof(UserAvatarController), new { destination.Username }); + return urlHelper.ActionLink(nameof(UserAvatarController.Get), nameof(UserAvatarController), new { destination.Username }); } } @@ -51,8 +60,8 @@ namespace Timeline.Models.Http { public UserInfoAutoMapperProfile() { - CreateMap().AfterMap(); - CreateMap().AfterMap(); + CreateMap().ForMember(u => u.AvatarUrl, opt => opt.MapFrom()); + CreateMap().ForMember(u => u.AvatarUrl, opt => opt.MapFrom()); } } } diff --git a/Timeline/Models/Validation/NicknameValidator.cs b/Timeline/Models/Validation/NicknameValidator.cs index 53a2916b..1d6ab163 100644 --- a/Timeline/Models/Validation/NicknameValidator.cs +++ b/Timeline/Models/Validation/NicknameValidator.cs @@ -7,7 +7,7 @@ namespace Timeline.Models.Validation { protected override (bool, string) DoValidate(string value) { - if (value.Length > 10) + if (value.Length > 25) return (false, MessageTooLong); return (true, GetSuccessMessage()); diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs index 89936aa2..16402f3e 100644 --- a/Timeline/Services/TimelineService.cs +++ b/Timeline/Services/TimelineService.cs @@ -295,7 +295,7 @@ namespace Timeline.Services { if (entity.Content != null) // otherwise it is deleted { - var author = Mapper.Map(UserService.GetUserById(entity.AuthorId)); + var author = Mapper.Map(await UserService.GetUserById(entity.AuthorId)); posts.Add(new TimelinePostInfo { Id = entity.Id, diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs index 1197bb73..d2dc969e 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -300,12 +300,14 @@ namespace Timeline.Services var administrator = info.Administrator ?? false; var password = info.Password; + var nickname = info.Nickname; var newEntity = new UserEntity { Username = username, Password = _passwordService.HashPassword(password), Roles = UserRoleConvert.ToString(administrator), + Nickname = nickname, Version = 1 }; _databaseContext.Users.Add(newEntity); -- cgit v1.2.3 From 038e8dcf461d4d4ebd51c8fdf7680497869f691c Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 31 Jan 2020 00:10:23 +0800 Subject: ... --- .../IntegratedTests/PersonalTimelineTest.cs | 75 +++++++++++- Timeline.Tests/IntegratedTests/UserAvatarTest.cs | 18 +-- Timeline/Controllers/ControllerAuthExtensions.cs | 15 +-- Timeline/Filters/Timeline.cs | 2 +- Timeline/Models/Http/UserInfo.cs | 8 +- .../ControllerAuthExtensions.Designer.cs | 81 +++++++++++++ .../Controllers/ControllerAuthExtensions.resx | 126 +++++++++++++++++++++ Timeline/Services/TimelineService.cs | 3 + Timeline/Timeline.csproj | 9 ++ 9 files changed, 311 insertions(+), 26 deletions(-) create mode 100644 Timeline/Resources/Controllers/ControllerAuthExtensions.Designer.cs create mode 100644 Timeline/Resources/Controllers/ControllerAuthExtensions.resx (limited to 'Timeline/Models/Http/UserInfo.cs') diff --git a/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs b/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs index d787d87d..dacfea62 100644 --- a/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs +++ b/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs @@ -30,6 +30,74 @@ namespace Timeline.Tests.IntegratedTests body.Visibility.Should().Be(TimelineVisibility.Register); body.Description.Should().Be(""); body.Members.Should().NotBeNull().And.BeEmpty(); + } + + [Fact] + public async Task InvalidModel_BadUsername() + { + using var client = await CreateClientAsAdministrator(); + { + var res = await client.GetAsync("users/user!!!/timeline"); + res.Should().BeInvalidModel(); + } + { + var res = await client.PatchAsJsonAsync("users/user!!!/timeline", new TimelinePatchRequest { }); + res.Should().BeInvalidModel(); + } + { + var res = await client.PutAsync("users/user!!!/timeline/members/user1", null); + res.Should().BeInvalidModel(); + } + { + var res = await client.DeleteAsync("users/user!!!/timeline/members/user1"); + res.Should().BeInvalidModel(); + } + { + var res = await client.GetAsync("users/user!!!/timeline/posts"); + res.Should().BeInvalidModel(); + } + { + var res = await client.PostAsJsonAsync("users/user!!!/timeline/posts", new TimelinePostCreateRequest { Content = "aaa" }); + res.Should().BeInvalidModel(); + } + { + var res = await client.DeleteAsync("users/user!!!/timeline/posts/123"); + res.Should().BeInvalidModel(); + } + } + + [Fact] + public async Task NotFound() + { + using var client = await CreateClientAsAdministrator(); + { + var res = await client.GetAsync("users/usernotexist/timeline"); + res.Should().HaveStatusCode(404).And.HaveCommonBody(ErrorCodes.UserCommon.NotExist); + } + { + var res = await client.PatchAsJsonAsync("users/usernotexist/timeline", new TimelinePatchRequest { }); + res.Should().HaveStatusCode(404).And.HaveCommonBody(ErrorCodes.UserCommon.NotExist); + } + { + var res = await client.PutAsync("users/usernotexist/timeline/members/user1", null); + res.Should().HaveStatusCode(404).And.HaveCommonBody(ErrorCodes.UserCommon.NotExist); + } + { + var res = await client.DeleteAsync("users/usernotexist/timeline/members/user1"); + res.Should().HaveStatusCode(404).And.HaveCommonBody(ErrorCodes.UserCommon.NotExist); + } + { + var res = await client.GetAsync("users/usernotexist/timeline/posts"); + res.Should().HaveStatusCode(404).And.HaveCommonBody(ErrorCodes.UserCommon.NotExist); + } + { + var res = await client.PostAsJsonAsync("users/usernotexist/timeline/posts", new TimelinePostCreateRequest { Content = "aaa" }); + res.Should().HaveStatusCode(404).And.HaveCommonBody(ErrorCodes.UserCommon.NotExist); + } + { + var res = await client.DeleteAsync("users/usernotexist/timeline/posts/123"); + res.Should().HaveStatusCode(404).And.HaveCommonBody(ErrorCodes.UserCommon.NotExist); + } } [Fact] @@ -162,10 +230,11 @@ namespace Timeline.Tests.IntegratedTests { const string userUrl = "users/user1/timeline/posts"; const string adminUrl = "users/admin/timeline/posts"; - { + { + using var client = await CreateClientAsUser(); - var res = await client.PatchAsync("users/user1/timeline", - new StringContent(@"{""visibility"":""abcdefg""}", System.Text.Encoding.UTF8, System.Net.Mime.MediaTypeNames.Application.Json)); + using var content = new StringContent(@"{""visibility"":""abcdefg""}", System.Text.Encoding.UTF8, System.Net.Mime.MediaTypeNames.Application.Json); + var res = await client.PatchAsync("users/user1/timeline", content); res.Should().BeInvalidModel(); } { // default visibility is registered diff --git a/Timeline.Tests/IntegratedTests/UserAvatarTest.cs b/Timeline.Tests/IntegratedTests/UserAvatarTest.cs index 67c2dd9a..fa0120f1 100644 --- a/Timeline.Tests/IntegratedTests/UserAvatarTest.cs +++ b/Timeline.Tests/IntegratedTests/UserAvatarTest.cs @@ -75,7 +75,7 @@ namespace Timeline.Tests.IntegratedTests await GetReturnDefault("admin"); { - var request = new HttpRequestMessage() + using var request = new HttpRequestMessage() { RequestUri = new Uri(client.BaseAddress, "users/user1/avatar"), Method = HttpMethod.Get, @@ -87,7 +87,7 @@ namespace Timeline.Tests.IntegratedTests } { - var request = new HttpRequestMessage() + using var request = new HttpRequestMessage() { RequestUri = new Uri(client.BaseAddress, "users/user1/avatar"), Method = HttpMethod.Get, @@ -98,7 +98,7 @@ namespace Timeline.Tests.IntegratedTests } { - var request = new HttpRequestMessage() + using var request = new HttpRequestMessage() { RequestUri = new Uri(client.BaseAddress, "users/user1/avatar"), Method = HttpMethod.Get, @@ -109,7 +109,7 @@ namespace Timeline.Tests.IntegratedTests } { - var content = new ByteArrayContent(new[] { (byte)0x00 }); + using var content = new ByteArrayContent(new[] { (byte)0x00 }); content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); var res = await client.PutAsync("users/user1/avatar", content); res.Should().HaveStatusCode(HttpStatusCode.BadRequest) @@ -117,7 +117,7 @@ namespace Timeline.Tests.IntegratedTests } { - var content = new ByteArrayContent(new[] { (byte)0x00 }); + using var content = new ByteArrayContent(new[] { (byte)0x00 }); content.Headers.ContentLength = 1; var res = await client.PutAsync("users/user1/avatar", content); res.Should().HaveStatusCode(HttpStatusCode.BadRequest) @@ -125,7 +125,7 @@ namespace Timeline.Tests.IntegratedTests } { - var content = new ByteArrayContent(new[] { (byte)0x00 }); + using var content = new ByteArrayContent(new[] { (byte)0x00 }); content.Headers.ContentLength = 0; content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); var res = await client.PutAsync("users/user1/avatar", content); @@ -139,7 +139,7 @@ namespace Timeline.Tests.IntegratedTests } { - var content = new ByteArrayContent(new[] { (byte)0x00 }); + using var content = new ByteArrayContent(new[] { (byte)0x00 }); content.Headers.ContentLength = 1000 * 1000 * 11; content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); var res = await client.PutAsync("users/user1/avatar", content); @@ -148,7 +148,7 @@ namespace Timeline.Tests.IntegratedTests } { - var content = new ByteArrayContent(new[] { (byte)0x00 }); + using var content = new ByteArrayContent(new[] { (byte)0x00 }); content.Headers.ContentLength = 2; content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); var res = await client.PutAsync("users/user1/avatar", content); @@ -157,7 +157,7 @@ namespace Timeline.Tests.IntegratedTests } { - var content = new ByteArrayContent(new[] { (byte)0x00, (byte)0x01 }); + using var content = new ByteArrayContent(new[] { (byte)0x00, (byte)0x01 }); content.Headers.ContentLength = 1; content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); var res = await client.PutAsync("users/user1/avatar", content); diff --git a/Timeline/Controllers/ControllerAuthExtensions.cs b/Timeline/Controllers/ControllerAuthExtensions.cs index 34fd4d99..00a65454 100644 --- a/Timeline/Controllers/ControllerAuthExtensions.cs +++ b/Timeline/Controllers/ControllerAuthExtensions.cs @@ -1,7 +1,8 @@ using Microsoft.AspNetCore.Mvc; +using System; using System.Security.Claims; using Timeline.Auth; -using System; +using static Timeline.Resources.Controllers.ControllerAuthExtensions; namespace Timeline.Controllers { @@ -14,24 +15,18 @@ namespace Timeline.Controllers public static long GetUserId(this ControllerBase controller) { - if (controller.User == null) - throw new InvalidOperationException("Failed to get user id because User is null."); - var claim = controller.User.FindFirst(ClaimTypes.NameIdentifier); if (claim == null) - throw new InvalidOperationException("Failed to get user id because User has no NameIdentifier claim."); + throw new InvalidOperationException(ExceptionNoUserIdentifierClaim); if (long.TryParse(claim.Value, out var value)) return value; - throw new InvalidOperationException("Failed to get user id because NameIdentifier claim is not a number."); + throw new InvalidOperationException(ExceptionUserIdentifierClaimBadFormat); } public static long? GetOptionalUserId(this ControllerBase controller) { - if (controller.User == null) - return null; - var claim = controller.User.FindFirst(ClaimTypes.NameIdentifier); if (claim == null) return null; @@ -39,7 +34,7 @@ namespace Timeline.Controllers if (long.TryParse(claim.Value, out var value)) return value; - throw new InvalidOperationException("Failed to get user id because NameIdentifier claim is not a number."); + throw new InvalidOperationException(ExceptionUserIdentifierClaimBadFormat); } } } diff --git a/Timeline/Filters/Timeline.cs b/Timeline/Filters/Timeline.cs index 729dbec7..ed78e645 100644 --- a/Timeline/Filters/Timeline.cs +++ b/Timeline/Filters/Timeline.cs @@ -13,7 +13,7 @@ namespace Timeline.Filters { if (e.InnerException is UserNotExistException) { - context.Result = new BadRequestObjectResult(ErrorResponse.UserCommon.NotExist()); + context.Result = new NotFoundObjectResult(ErrorResponse.UserCommon.NotExist()); } else { diff --git a/Timeline/Models/Http/UserInfo.cs b/Timeline/Models/Http/UserInfo.cs index 62d989a2..07ac0aad 100644 --- a/Timeline/Models/Http/UserInfo.cs +++ b/Timeline/Models/Http/UserInfo.cs @@ -31,11 +31,13 @@ namespace Timeline.Models.Http public class UserInfoAvatarUrlValueResolver : IValueResolver { - private readonly IActionContextAccessor _actionContextAccessor; - private readonly IUrlHelperFactory _urlHelperFactory; + private readonly IActionContextAccessor? _actionContextAccessor; + private readonly IUrlHelperFactory? _urlHelperFactory; public UserInfoAvatarUrlValueResolver() { + _actionContextAccessor = null; + _urlHelperFactory = null; } public UserInfoAvatarUrlValueResolver(IActionContextAccessor actionContextAccessor, IUrlHelperFactory urlHelperFactory) @@ -51,7 +53,7 @@ namespace Timeline.Models.Http return $"/users/{destination.Username}/avatar"; } - var urlHelper = _urlHelperFactory.GetUrlHelper(_actionContextAccessor.ActionContext); + var urlHelper = _urlHelperFactory!.GetUrlHelper(_actionContextAccessor.ActionContext); return urlHelper.ActionLink(nameof(UserAvatarController.Get), nameof(UserAvatarController), new { destination.Username }); } } diff --git a/Timeline/Resources/Controllers/ControllerAuthExtensions.Designer.cs b/Timeline/Resources/Controllers/ControllerAuthExtensions.Designer.cs new file mode 100644 index 00000000..70a1d605 --- /dev/null +++ b/Timeline/Resources/Controllers/ControllerAuthExtensions.Designer.cs @@ -0,0 +1,81 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Resources.Controllers { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class ControllerAuthExtensions { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal ControllerAuthExtensions() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Controllers.ControllerAuthExtensions", typeof(ControllerAuthExtensions).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Failed to get user id because User has no NameIdentifier claim.. + /// + internal static string ExceptionNoUserIdentifierClaim { + get { + return ResourceManager.GetString("ExceptionNoUserIdentifierClaim", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failed to get user id because NameIdentifier claim is not a number.. + /// + internal static string ExceptionUserIdentifierClaimBadFormat { + get { + return ResourceManager.GetString("ExceptionUserIdentifierClaimBadFormat", resourceCulture); + } + } + } +} diff --git a/Timeline/Resources/Controllers/ControllerAuthExtensions.resx b/Timeline/Resources/Controllers/ControllerAuthExtensions.resx new file mode 100644 index 00000000..03e6d95a --- /dev/null +++ b/Timeline/Resources/Controllers/ControllerAuthExtensions.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Failed to get user id because User has no NameIdentifier claim. + + + Failed to get user id because NameIdentifier claim is not a number. + + \ No newline at end of file diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs index 16402f3e..85445973 100644 --- a/Timeline/Services/TimelineService.cs +++ b/Timeline/Services/TimelineService.cs @@ -348,6 +348,9 @@ namespace Timeline.Services if (name == null) throw new ArgumentNullException(nameof(name)); + // Currently we don't use the result. But we need to check the timeline. + var _ = await FindTimelineId(name); + var post = await Database.TimelinePosts.Where(p => p.Id == id).SingleOrDefaultAsync(); if (post == null) diff --git a/Timeline/Timeline.csproj b/Timeline/Timeline.csproj index 25d73068..1a3a07cd 100644 --- a/Timeline/Timeline.csproj +++ b/Timeline/Timeline.csproj @@ -44,6 +44,11 @@ True AuthHandler.resx + + True + True + ControllerAuthExtensions.resx + True True @@ -121,6 +126,10 @@ ResXFileCodeGenerator AuthHandler.Designer.cs + + ResXFileCodeGenerator + ControllerAuthExtensions.Designer.cs + ResXFileCodeGenerator TimelineController.Designer.cs -- cgit v1.2.3 From 45a6ca3a189a81060ae44d0764248547fe2b686c Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 31 Jan 2020 15:21:58 +0800 Subject: Combine two user info types. --- .../IntegratedTests/IntegratedTestBase.cs | 15 ++++---- .../IntegratedTests/PersonalTimelineTest.cs | 8 ++-- Timeline.Tests/IntegratedTests/TokenTest.cs | 4 +- Timeline.Tests/IntegratedTests/UserTest.cs | 20 +++++----- Timeline/Controllers/TokenController.cs | 4 +- Timeline/Controllers/UserController.cs | 22 +++-------- Timeline/Models/Http/TokenController.cs | 4 +- Timeline/Models/Http/UserInfo.cs | 45 ++++++++++------------ 8 files changed, 54 insertions(+), 68 deletions(-) (limited to 'Timeline/Models/Http/UserInfo.cs') diff --git a/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs b/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs index 242e96cd..59af5eab 100644 --- a/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs +++ b/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs @@ -15,6 +15,12 @@ namespace Timeline.Tests.IntegratedTests public abstract class IntegratedTestBase : IClassFixture>, IDisposable { + static IntegratedTestBase() + { + FluentAssertions.AssertionOptions.AssertEquivalencyUsing(options => + options.Excluding(m => m.RuntimeType == typeof(UserInfo) && m.SelectedMemberPath == "_links")); + } + protected TestApplication TestApp { get; } protected WebApplicationFactory Factory => TestApp.Factory; @@ -56,7 +62,6 @@ namespace Timeline.Tests.IntegratedTests } var userInfoList = new List(); - var userInfoForAdminList = new List(); var userService = scope.ServiceProvider.GetRequiredService(); var mapper = scope.ServiceProvider.GetRequiredService(); @@ -65,11 +70,9 @@ namespace Timeline.Tests.IntegratedTests { userService.CreateUser(user).Wait(); userInfoList.Add(mapper.Map(user)); - userInfoForAdminList.Add(mapper.Map(user)); } - UserInfoList = userInfoList; - UserInfoForAdminList = userInfoForAdminList; + UserInfos = userInfoList; } } @@ -83,9 +86,7 @@ namespace Timeline.Tests.IntegratedTests TestApp.Dispose(); } - public IReadOnlyList UserInfoList { get; } - - public IReadOnlyList UserInfoForAdminList { get; } + public IReadOnlyList UserInfos { get; } public Task CreateDefaultClient() { diff --git a/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs b/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs index dacfea62..f3d6b172 100644 --- a/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs +++ b/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs @@ -26,7 +26,7 @@ namespace Timeline.Tests.IntegratedTests var res = await client.GetAsync("users/user1/timeline"); var body = res.Should().HaveStatusCode(200) .And.HaveJsonBody().Which; - body.Owner.Should().BeEquivalentTo(UserInfoList[1]); + body.Owner.Should().BeEquivalentTo(UserInfos[1]); body.Visibility.Should().Be(TimelineVisibility.Register); body.Description.Should().Be(""); body.Members.Should().NotBeNull().And.BeEmpty(); @@ -169,7 +169,7 @@ namespace Timeline.Tests.IntegratedTests var res = await client.PutAsync("/users/user1/timeline/members/user2", null); res.Should().HaveStatusCode(200); } - await AssertMembers(new List { UserInfoList[2] }); + await AssertMembers(new List { UserInfos[2] }); { var res = await client.DeleteAsync("/users/user1/timeline/members/user2"); res.Should().BeDelete(true); @@ -452,7 +452,7 @@ namespace Timeline.Tests.IntegratedTests .Which; body.Should().NotBeNull(); body.Content.Should().Be(mockContent); - body.Author.Should().BeEquivalentTo(UserInfoList[1]); + body.Author.Should().BeEquivalentTo(UserInfos[1]); createRes = body; } { @@ -472,7 +472,7 @@ namespace Timeline.Tests.IntegratedTests .Which; body.Should().NotBeNull(); body.Content.Should().Be(mockContent2); - body.Author.Should().BeEquivalentTo(UserInfoList[1]); + body.Author.Should().BeEquivalentTo(UserInfos[1]); body.Time.Should().BeCloseTo(mockTime2, 1000); createRes2 = body; } diff --git a/Timeline.Tests/IntegratedTests/TokenTest.cs b/Timeline.Tests/IntegratedTests/TokenTest.cs index ec7514ff..928d546c 100644 --- a/Timeline.Tests/IntegratedTests/TokenTest.cs +++ b/Timeline.Tests/IntegratedTests/TokenTest.cs @@ -77,7 +77,7 @@ namespace Timeline.Tests.IntegratedTests var body = response.Should().HaveStatusCode(200) .And.HaveJsonBody().Which; body.Token.Should().NotBeNullOrWhiteSpace(); - body.User.Should().BeEquivalentTo(UserInfoForAdminList[1]); + body.User.Should().BeEquivalentTo(UserInfos[1]); } [Fact] @@ -165,7 +165,7 @@ namespace Timeline.Tests.IntegratedTests new VerifyTokenRequest { Token = createTokenResult.Token }); response.Should().HaveStatusCode(200) .And.HaveJsonBody() - .Which.User.Should().BeEquivalentTo(UserInfoForAdminList[1]); + .Which.User.Should().BeEquivalentTo(UserInfos[1]); } } } diff --git a/Timeline.Tests/IntegratedTests/UserTest.cs b/Timeline.Tests/IntegratedTests/UserTest.cs index 1b9733ff..bbeb6ad6 100644 --- a/Timeline.Tests/IntegratedTests/UserTest.cs +++ b/Timeline.Tests/IntegratedTests/UserTest.cs @@ -25,7 +25,7 @@ namespace Timeline.Tests.IntegratedTests var res = await client.GetAsync("/users"); res.Should().HaveStatusCode(200) .And.HaveJsonBody() - .Which.Should().BeEquivalentTo(UserInfoList); + .Which.Should().BeEquivalentTo(UserInfos); } [Fact] @@ -35,7 +35,7 @@ namespace Timeline.Tests.IntegratedTests var res = await client.GetAsync("/users"); res.Should().HaveStatusCode(200) .And.HaveJsonBody() - .Which.Should().BeEquivalentTo(UserInfoList); + .Which.Should().BeEquivalentTo(UserInfos); } [Fact] @@ -44,8 +44,8 @@ namespace Timeline.Tests.IntegratedTests using var client = await CreateClientAsAdministrator(); var res = await client.GetAsync("/users"); res.Should().HaveStatusCode(200) - .And.HaveJsonBody() - .Which.Should().BeEquivalentTo(UserInfoForAdminList); + .And.HaveJsonBody() + .Which.Should().BeEquivalentTo(UserInfos); } [Fact] @@ -55,7 +55,7 @@ namespace Timeline.Tests.IntegratedTests var res = await client.GetAsync($"/users/admin"); res.Should().HaveStatusCode(200) .And.HaveJsonBody() - .Which.Should().BeEquivalentTo(UserInfoList[0]); + .Which.Should().BeEquivalentTo(UserInfos[0]); } [Fact] @@ -65,7 +65,7 @@ namespace Timeline.Tests.IntegratedTests var res = await client.GetAsync($"/users/admin"); res.Should().HaveStatusCode(200) .And.HaveJsonBody() - .Which.Should().BeEquivalentTo(UserInfoList[0]); + .Which.Should().BeEquivalentTo(UserInfos[0]); } [Fact] @@ -74,8 +74,8 @@ namespace Timeline.Tests.IntegratedTests using var client = await CreateClientAsAdministrator(); var res = await client.GetAsync($"/users/user1"); res.Should().HaveStatusCode(200) - .And.HaveJsonBody() - .Which.Should().BeEquivalentTo(UserInfoForAdminList[1]); + .And.HaveJsonBody() + .Which.Should().BeEquivalentTo(UserInfos[1]); } [Fact] @@ -134,7 +134,7 @@ namespace Timeline.Tests.IntegratedTests { var res = await client.GetAsync("/users/newuser"); var body = res.Should().HaveStatusCode(200) - .And.HaveJsonBody() + .And.HaveJsonBody() .Which; body.Administrator.Should().Be(true); body.Nickname.Should().Be("aaa"); @@ -301,7 +301,7 @@ namespace Timeline.Tests.IntegratedTests { var res = await client.GetAsync("users/aaa"); var body = res.Should().HaveStatusCode(200) - .And.HaveJsonBody().Which; + .And.HaveJsonBody().Which; body.Username.Should().Be("aaa"); body.Nickname.Should().Be("ccc"); body.Administrator.Should().BeTrue(); diff --git a/Timeline/Controllers/TokenController.cs b/Timeline/Controllers/TokenController.cs index a7f5fbde..1fb0b17a 100644 --- a/Timeline/Controllers/TokenController.cs +++ b/Timeline/Controllers/TokenController.cs @@ -59,7 +59,7 @@ namespace Timeline.Controllers return Ok(new CreateTokenResponse { Token = result.Token, - User = _mapper.Map(result.User) + User = _mapper.Map(result.User) }); } catch (UserNotExistException e) @@ -94,7 +94,7 @@ namespace Timeline.Controllers ("Username", result.Username), ("Token", request.Token))); return Ok(new VerifyTokenResponse { - User = _mapper.Map(result) + User = _mapper.Map(result) }); } catch (UserTokenTimeExpireException e) diff --git a/Timeline/Controllers/UserController.cs b/Timeline/Controllers/UserController.cs index fa73c6f9..4572296b 100644 --- a/Timeline/Controllers/UserController.cs +++ b/Timeline/Controllers/UserController.cs @@ -29,35 +29,23 @@ namespace Timeline.Controllers _mapper = mapper; } - private IUserInfo ConvertToUserInfo(User user, bool administrator) - { - if (administrator) - return _mapper.Map(user); - else - return _mapper.Map(user); - } + private UserInfo ConvertToUserInfo(User user) => _mapper.Map(user); [HttpGet("users")] - public async Task> List() + public async Task> List() { var users = await _userService.GetUsers(); - var administrator = this.IsAdministrator(); - // Note: the (object) explicit conversion. If not convert, - // then result is a IUserInfo array and JsonSerializer will - // treat all element as IUserInfo and deserialize only properties - // in IUserInfo. So we convert it to object to make an object - // array so that JsonSerializer use the runtime type. - var result = users.Select(u => (object)ConvertToUserInfo(u, administrator)).ToArray(); + var result = users.Select(u => ConvertToUserInfo(u)).ToArray(); return Ok(result); } [HttpGet("users/{username}")] - public async Task> Get([FromRoute][Username] string username) + public async Task> Get([FromRoute][Username] string username) { try { var user = await _userService.GetUserByUsername(username); - return Ok(ConvertToUserInfo(user, this.IsAdministrator())); + return Ok(ConvertToUserInfo(user)); } catch (UserNotExistException e) { diff --git a/Timeline/Models/Http/TokenController.cs b/Timeline/Models/Http/TokenController.cs index 383b2965..ea8b59ed 100644 --- a/Timeline/Models/Http/TokenController.cs +++ b/Timeline/Models/Http/TokenController.cs @@ -16,7 +16,7 @@ namespace Timeline.Models.Http public class CreateTokenResponse { public string Token { get; set; } = default!; - public UserInfoForAdmin User { get; set; } = default!; + public UserInfo User { get; set; } = default!; } public class VerifyTokenRequest @@ -27,6 +27,6 @@ namespace Timeline.Models.Http public class VerifyTokenResponse { - public UserInfoForAdmin User { get; set; } = default!; + public UserInfo User { get; set; } = default!; } } diff --git a/Timeline/Models/Http/UserInfo.cs b/Timeline/Models/Http/UserInfo.cs index 07ac0aad..5890a7a6 100644 --- a/Timeline/Models/Http/UserInfo.cs +++ b/Timeline/Models/Http/UserInfo.cs @@ -7,54 +7,52 @@ using Timeline.Services; namespace Timeline.Models.Http { - public interface IUserInfo - { - string Username { get; set; } - string Nickname { get; set; } - string AvatarUrl { get; set; } - } - - public class UserInfo : IUserInfo + public class UserInfo { public string Username { get; set; } = default!; public string Nickname { get; set; } = default!; - public string AvatarUrl { get; set; } = default!; + public bool? Administrator { get; set; } = default!; +#pragma warning disable CA1707 + public UserInfoLinks? _links { get; set; } +#pragma warning restore CA1707 } - public class UserInfoForAdmin : IUserInfo + public class UserInfoLinks { - public string Username { get; set; } = default!; - public string Nickname { get; set; } = default!; - public string AvatarUrl { get; set; } = default!; - public bool Administrator { get; set; } + public string Avatar { get; set; } = default!; + public string Timeline { get; set; } = default!; } - public class UserInfoAvatarUrlValueResolver : IValueResolver + public class UserInfoLinksValueResolver : IValueResolver { private readonly IActionContextAccessor? _actionContextAccessor; private readonly IUrlHelperFactory? _urlHelperFactory; - public UserInfoAvatarUrlValueResolver() + public UserInfoLinksValueResolver() { _actionContextAccessor = null; _urlHelperFactory = null; } - public UserInfoAvatarUrlValueResolver(IActionContextAccessor actionContextAccessor, IUrlHelperFactory urlHelperFactory) + public UserInfoLinksValueResolver(IActionContextAccessor actionContextAccessor, IUrlHelperFactory urlHelperFactory) { _actionContextAccessor = actionContextAccessor; _urlHelperFactory = urlHelperFactory; } - public string Resolve(User source, IUserInfo destination, string destMember, ResolutionContext context) + public UserInfoLinks? Resolve(User source, UserInfo destination, UserInfoLinks? destMember, ResolutionContext context) { - if (_actionContextAccessor == null) + if (_actionContextAccessor == null || _urlHelperFactory == null) { - return $"/users/{destination.Username}/avatar"; + return null; } - var urlHelper = _urlHelperFactory!.GetUrlHelper(_actionContextAccessor.ActionContext); - return urlHelper.ActionLink(nameof(UserAvatarController.Get), nameof(UserAvatarController), new { destination.Username }); + var urlHelper = _urlHelperFactory.GetUrlHelper(_actionContextAccessor.ActionContext); + return new UserInfoLinks + { + Avatar = urlHelper.ActionLink(nameof(UserAvatarController.Get), nameof(UserAvatarController), new { destination.Username }), + Timeline = urlHelper.ActionLink(nameof(PersonalTimelineController.TimelineGet), nameof(PersonalTimelineController), new { destination.Username }) + }; } } @@ -62,8 +60,7 @@ namespace Timeline.Models.Http { public UserInfoAutoMapperProfile() { - CreateMap().ForMember(u => u.AvatarUrl, opt => opt.MapFrom()); - CreateMap().ForMember(u => u.AvatarUrl, opt => opt.MapFrom()); + CreateMap().ForMember(u => u._links, opt => opt.MapFrom()); } } } -- cgit v1.2.3 From a7fd42ddee50a8066a083c57b7940e4f9896dcb7 Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 31 Jan 2020 20:25:06 +0800 Subject: Fix a bug in url generation and add development database migration. --- .../20200131100517_RefactorUser.Designer.cs | 243 +++++++++++++++++++++ .../20200131100517_RefactorUser.cs | 136 ++++++++++++ .../DevelopmentDatabaseContextModelSnapshot.cs | 92 +++----- Timeline/Models/Http/UserInfo.cs | 7 +- Timeline/Startup.cs | 4 + 5 files changed, 420 insertions(+), 62 deletions(-) create mode 100644 Timeline/Migrations/DevelopmentDatabase/20200131100517_RefactorUser.Designer.cs create mode 100644 Timeline/Migrations/DevelopmentDatabase/20200131100517_RefactorUser.cs (limited to 'Timeline/Models/Http/UserInfo.cs') diff --git a/Timeline/Migrations/DevelopmentDatabase/20200131100517_RefactorUser.Designer.cs b/Timeline/Migrations/DevelopmentDatabase/20200131100517_RefactorUser.Designer.cs new file mode 100644 index 00000000..13e322c8 --- /dev/null +++ b/Timeline/Migrations/DevelopmentDatabase/20200131100517_RefactorUser.Designer.cs @@ -0,0 +1,243 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Timeline.Entities; + +namespace Timeline.Migrations.DevelopmentDatabase +{ + [DbContext(typeof(DevelopmentDatabaseContext))] + [Migration("20200131100517_RefactorUser")] + partial class RefactorUser + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.1"); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("CreateTime") + .HasColumnName("create_time") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnName("description") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnName("name") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnName("owner") + .HasColumnType("INTEGER"); + + b.Property("Visibility") + .HasColumnName("visibility") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("timelines"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("TimelineId") + .HasColumnName("timeline") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnName("user") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TimelineId"); + + b.HasIndex("UserId"); + + b.ToTable("timeline_members"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnName("author") + .HasColumnType("INTEGER"); + + b.Property("Content") + .HasColumnName("content") + .HasColumnType("TEXT"); + + b.Property("LastUpdated") + .HasColumnName("last_updated") + .HasColumnType("TEXT"); + + b.Property("Time") + .HasColumnName("time") + .HasColumnType("TEXT"); + + b.Property("TimelineId") + .HasColumnName("timeline") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("TimelineId"); + + b.ToTable("timeline_posts"); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Data") + .HasColumnName("data") + .HasColumnType("BLOB"); + + b.Property("ETag") + .HasColumnName("etag") + .HasColumnType("TEXT") + .HasMaxLength(30); + + b.Property("LastModified") + .HasColumnName("last_modified") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnName("type") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnName("user") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("user_avatars"); + }); + + modelBuilder.Entity("Timeline.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Nickname") + .HasColumnName("nickname") + .HasColumnType("TEXT") + .HasMaxLength(100); + + b.Property("Password") + .IsRequired() + .HasColumnName("password") + .HasColumnType("TEXT"); + + b.Property("Roles") + .IsRequired() + .HasColumnName("roles") + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasColumnName("username") + .HasColumnType("TEXT") + .HasMaxLength(26); + + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnName("version") + .HasColumnType("INTEGER") + .HasDefaultValue(0L); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("users"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Owner") + .WithMany("Timelines") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Members") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithMany("TimelinesJoined") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Author") + .WithMany("TimelinePosts") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Posts") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithOne("Avatar") + .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Timeline/Migrations/DevelopmentDatabase/20200131100517_RefactorUser.cs b/Timeline/Migrations/DevelopmentDatabase/20200131100517_RefactorUser.cs new file mode 100644 index 00000000..e6f38506 --- /dev/null +++ b/Timeline/Migrations/DevelopmentDatabase/20200131100517_RefactorUser.cs @@ -0,0 +1,136 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Timeline.Migrations.DevelopmentDatabase +{ + public partial class RefactorUser : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn(name: "name", table: "users", newName: "username"); + migrationBuilder.RenameIndex(name: "IX_users_name", table: "users", newName: "IX_users_username"); + + migrationBuilder.AddColumn( + name: "nickname", + table: "users", + maxLength: 100, + nullable: true); + + migrationBuilder.Sql(@" +UPDATE users + SET nickname = ( + SELECT nickname + FROM user_details + WHERE user_details.UserId = users.id + ); + "); + + /* + migrationBuilder.RenameColumn(name: "UserId", table: "user_avatars", newName: "user"); + + migrationBuilder.DropForeignKey( + name: "FK_user_avatars_users_UserId", + table: "user_avatars"); + + migrationBuilder.AddForeignKey( + name: "FK_user_avatars_users_user", + table: "user_avatars", + column: "user", + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.RenameIndex( + name: "IX_user_avatars_UserId", + table: "user_avatars", + newName: "IX_user_avatars_user"); + */ + + migrationBuilder.Sql(@" +CREATE TABLE user_avatars_backup ( + id INTEGER NOT NULL + CONSTRAINT PK_user_avatars PRIMARY KEY AUTOINCREMENT, + data BLOB, + type TEXT, + etag TEXT, + last_modified TEXT NOT NULL, + user INTEGER NOT NULL, + CONSTRAINT FK_user_avatars_users_user FOREIGN KEY ( + user + ) + REFERENCES users (id) ON DELETE CASCADE +); + +INSERT INTO user_avatars_backup (id, data, type, etag, last_modified, user) + SELECT id, data, type, etag, last_modified, UserId FROM user_avatars; + +DROP TABLE user_avatars; + +ALTER TABLE user_avatars_backup + RENAME TO user_avatars; + +CREATE UNIQUE INDEX IX_user_avatars_user ON user_avatars (user); + "); + + // migrationBuilder.DropTable(name: "user_details"); + + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@" +CREATE TABLE user_avatars_backup ( + id INTEGER NOT NULL + CONSTRAINT PK_user_avatars PRIMARY KEY AUTOINCREMENT, + data BLOB, + type TEXT, + etag TEXT, + last_modified TEXT NOT NULL, + UserId INTEGER NOT NULL, + CONSTRAINT FK_user_avatars_users_UserId FOREIGN KEY ( + user + ) + REFERENCES users (id) ON DELETE CASCADE +); + +INSERT INTO user_avatars_backup (id, data, type, etag, last_modified, UserId) + SELECT id, data, type, etag, last_modified, user FROM user_avatars; + +DROP TABLE user_avatars; + +ALTER TABLE user_avatars_backup + RENAME TO user_avatars; + +CREATE UNIQUE INDEX IX_user_avatars_UserId ON user_avatars (UserId); + "); + + migrationBuilder.Sql(@" +CREATE TABLE users_backup ( + id INTEGER NOT NULL + CONSTRAINT PK_users PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + password TEXT NOT NULL, + roles TEXT NOT NULL, + version INTEGER NOT NULL + DEFAULT 0 +); + +INSERT INTO users_backup (id, name, password, roles, version) + SELECT id, username, password, roles, version FROM users; + +DROP TABLE users; + +ALTER TABLE users_backup + RENAME TO users; + +CREATE UNIQUE INDEX IX_users_name ON users (name); + "); + + migrationBuilder.RenameColumn(name: "user", table: "user_avatars", newName: "UserId"); + + migrationBuilder.RenameIndex( + name: "IX_user_avatars_user", + table: "user_avatars", + newName: "IX_user_avatars_UserId"); + } + } +} diff --git a/Timeline/Migrations/DevelopmentDatabase/DevelopmentDatabaseContextModelSnapshot.cs b/Timeline/Migrations/DevelopmentDatabase/DevelopmentDatabaseContextModelSnapshot.cs index 6fbaea5f..5da49dbe 100644 --- a/Timeline/Migrations/DevelopmentDatabase/DevelopmentDatabaseContextModelSnapshot.cs +++ b/Timeline/Migrations/DevelopmentDatabase/DevelopmentDatabaseContextModelSnapshot.cs @@ -14,7 +14,7 @@ namespace Timeline.Migrations.DevelopmentDatabase { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "3.1.0"); + .HasAnnotation("ProductVersion", "3.1.1"); modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => { @@ -110,44 +110,7 @@ namespace Timeline.Migrations.DevelopmentDatabase b.ToTable("timeline_posts"); }); - modelBuilder.Entity("Timeline.Entities.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id") - .HasColumnType("INTEGER"); - - b.Property("EncryptedPassword") - .IsRequired() - .HasColumnName("password") - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasColumnName("name") - .HasColumnType("TEXT") - .HasMaxLength(26); - - b.Property("RoleString") - .IsRequired() - .HasColumnName("roles") - .HasColumnType("TEXT"); - - b.Property("Version") - .ValueGeneratedOnAdd() - .HasColumnName("version") - .HasColumnType("INTEGER") - .HasDefaultValue(0L); - - b.HasKey("Id"); - - b.HasIndex("Name") - .IsUnique(); - - b.ToTable("users"); - }); - - modelBuilder.Entity("Timeline.Entities.UserAvatar", b => + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -172,6 +135,7 @@ namespace Timeline.Migrations.DevelopmentDatabase .HasColumnType("TEXT"); b.Property("UserId") + .HasColumnName("user") .HasColumnType("INTEGER"); b.HasKey("Id"); @@ -182,7 +146,7 @@ namespace Timeline.Migrations.DevelopmentDatabase b.ToTable("user_avatars"); }); - modelBuilder.Entity("Timeline.Entities.UserDetail", b => + modelBuilder.Entity("Timeline.Entities.UserEntity", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -192,22 +156,41 @@ namespace Timeline.Migrations.DevelopmentDatabase b.Property("Nickname") .HasColumnName("nickname") .HasColumnType("TEXT") + .HasMaxLength(100); + + b.Property("Password") + .IsRequired() + .HasColumnName("password") + .HasColumnType("TEXT"); + + b.Property("Roles") + .IsRequired() + .HasColumnName("roles") + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasColumnName("username") + .HasColumnType("TEXT") .HasMaxLength(26); - b.Property("UserId") - .HasColumnType("INTEGER"); + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnName("version") + .HasColumnType("INTEGER") + .HasDefaultValue(0L); b.HasKey("Id"); - b.HasIndex("UserId") + b.HasIndex("Username") .IsUnique(); - b.ToTable("user_details"); + b.ToTable("users"); }); modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => { - b.HasOne("Timeline.Entities.User", "Owner") + b.HasOne("Timeline.Entities.UserEntity", "Owner") .WithMany("Timelines") .HasForeignKey("OwnerId") .OnDelete(DeleteBehavior.Cascade) @@ -222,7 +205,7 @@ namespace Timeline.Migrations.DevelopmentDatabase .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Timeline.Entities.User", "User") + b.HasOne("Timeline.Entities.UserEntity", "User") .WithMany("TimelinesJoined") .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -231,7 +214,7 @@ namespace Timeline.Migrations.DevelopmentDatabase modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => { - b.HasOne("Timeline.Entities.User", "Author") + b.HasOne("Timeline.Entities.UserEntity", "Author") .WithMany("TimelinePosts") .HasForeignKey("AuthorId") .OnDelete(DeleteBehavior.Cascade) @@ -244,20 +227,11 @@ namespace Timeline.Migrations.DevelopmentDatabase .IsRequired(); }); - modelBuilder.Entity("Timeline.Entities.UserAvatar", b => + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => { - b.HasOne("Timeline.Entities.User", null) + b.HasOne("Timeline.Entities.UserEntity", "User") .WithOne("Avatar") - .HasForeignKey("Timeline.Entities.UserAvatar", "UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Timeline.Entities.UserDetail", b => - { - b.HasOne("Timeline.Entities.User", null) - .WithOne("Detail") - .HasForeignKey("Timeline.Entities.UserDetail", "UserId") + .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); diff --git a/Timeline/Models/Http/UserInfo.cs b/Timeline/Models/Http/UserInfo.cs index 5890a7a6..fee53ade 100644 --- a/Timeline/Models/Http/UserInfo.cs +++ b/Timeline/Models/Http/UserInfo.cs @@ -48,11 +48,12 @@ namespace Timeline.Models.Http } var urlHelper = _urlHelperFactory.GetUrlHelper(_actionContextAccessor.ActionContext); - return new UserInfoLinks + var result = new UserInfoLinks { - Avatar = urlHelper.ActionLink(nameof(UserAvatarController.Get), nameof(UserAvatarController), new { destination.Username }), - Timeline = urlHelper.ActionLink(nameof(PersonalTimelineController.TimelineGet), nameof(PersonalTimelineController), new { destination.Username }) + Avatar = urlHelper.ActionLink(nameof(UserAvatarController.Get), nameof(UserAvatarController)[0..^nameof(Controller).Length], new { destination.Username }), + Timeline = urlHelper.ActionLink(nameof(PersonalTimelineController.TimelineGet), nameof(PersonalTimelineController)[0..^nameof(Controller).Length], new { destination.Username }) }; + return result; } } diff --git a/Timeline/Startup.cs b/Timeline/Startup.cs index 998b5c44..14ee37e3 100644 --- a/Timeline/Startup.cs +++ b/Timeline/Startup.cs @@ -2,9 +2,11 @@ using AutoMapper; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using System; using System.Text.Json.Serialization; @@ -87,6 +89,8 @@ namespace Timeline services.AddScoped(); + services.TryAddSingleton(); + var databaseConfig = Configuration.GetSection(nameof(DatabaseConfig)).Get(); if (databaseConfig.UseDevelopment) -- cgit v1.2.3 From 3749a642306b19c84f324b0e94c4d62d8ec60332 Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 31 Jan 2020 21:57:09 +0800 Subject: Fix test bugs in user info mapper. Make create user action return created user info. --- Timeline.Tests/IntegratedTests/IntegratedTestBase.cs | 3 +-- Timeline.Tests/IntegratedTests/UserTest.cs | 6 +++++- Timeline/Controllers/UserController.cs | 6 +++--- Timeline/Models/Http/UserInfo.cs | 14 +++----------- Timeline/Services/UserService.cs | 8 ++++---- 5 files changed, 16 insertions(+), 21 deletions(-) (limited to 'Timeline/Models/Http/UserInfo.cs') diff --git a/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs b/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs index 59af5eab..dfde2ea5 100644 --- a/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs +++ b/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs @@ -12,13 +12,12 @@ using Xunit; namespace Timeline.Tests.IntegratedTests { - public abstract class IntegratedTestBase : IClassFixture>, IDisposable { static IntegratedTestBase() { FluentAssertions.AssertionOptions.AssertEquivalencyUsing(options => - options.Excluding(m => m.RuntimeType == typeof(UserInfo) && m.SelectedMemberPath == "_links")); + options.Excluding(m => m.RuntimeType == typeof(UserInfoLinks))); } protected TestApplication TestApp { get; } diff --git a/Timeline.Tests/IntegratedTests/UserTest.cs b/Timeline.Tests/IntegratedTests/UserTest.cs index bbeb6ad6..f863eb6c 100644 --- a/Timeline.Tests/IntegratedTests/UserTest.cs +++ b/Timeline.Tests/IntegratedTests/UserTest.cs @@ -296,7 +296,11 @@ namespace Timeline.Tests.IntegratedTests Administrator = true, Nickname = "ccc" }); - res.Should().HaveStatusCode(200); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + body.Username.Should().Be("aaa"); + body.Nickname.Should().Be("ccc"); + body.Administrator.Should().BeTrue(); } { var res = await client.GetAsync("users/aaa"); diff --git a/Timeline/Controllers/UserController.cs b/Timeline/Controllers/UserController.cs index 4572296b..26e63f63 100644 --- a/Timeline/Controllers/UserController.cs +++ b/Timeline/Controllers/UserController.cs @@ -108,12 +108,12 @@ namespace Timeline.Controllers } [HttpPost("userop/createuser"), AdminAuthorize] - public async Task CreateUser([FromBody] CreateUserRequest body) + public async Task> CreateUser([FromBody] CreateUserRequest body) { try { - await _userService.CreateUser(_mapper.Map(body)); - return Ok(); + var user = await _userService.CreateUser(_mapper.Map(body)); + return Ok(ConvertToUserInfo(user)); } catch (ConflictException) { diff --git a/Timeline/Models/Http/UserInfo.cs b/Timeline/Models/Http/UserInfo.cs index fee53ade..0d1d702b 100644 --- a/Timeline/Models/Http/UserInfo.cs +++ b/Timeline/Models/Http/UserInfo.cs @@ -25,14 +25,8 @@ namespace Timeline.Models.Http public class UserInfoLinksValueResolver : IValueResolver { - private readonly IActionContextAccessor? _actionContextAccessor; - private readonly IUrlHelperFactory? _urlHelperFactory; - - public UserInfoLinksValueResolver() - { - _actionContextAccessor = null; - _urlHelperFactory = null; - } + private readonly IActionContextAccessor _actionContextAccessor; + private readonly IUrlHelperFactory _urlHelperFactory; public UserInfoLinksValueResolver(IActionContextAccessor actionContextAccessor, IUrlHelperFactory urlHelperFactory) { @@ -42,10 +36,8 @@ namespace Timeline.Models.Http public UserInfoLinks? Resolve(User source, UserInfo destination, UserInfoLinks? destMember, ResolutionContext context) { - if (_actionContextAccessor == null || _urlHelperFactory == null) - { + if (_actionContextAccessor.ActionContext == null) return null; - } var urlHelper = _urlHelperFactory.GetUrlHelper(_actionContextAccessor.ActionContext); var result = new UserInfoLinks diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs index d2dc969e..93d92740 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -64,7 +64,7 @@ namespace Timeline.Services /// /// The info of new user. /// The password, can't be null or empty. - /// The id of the new user. + /// The the new user. /// Thrown when is null. /// Thrown when some fields in is bad. /// Thrown when a user with given username already exists. @@ -75,7 +75,7 @@ namespace Timeline.Services /// must be a valid nickname if set. It is empty by default. /// Other fields are ignored. /// - Task CreateUser(User info); + Task CreateUser(User info); /// /// Modify a user's info. @@ -276,7 +276,7 @@ namespace Timeline.Services return entities.Select(user => CreateUserFromEntity(user)).ToArray(); } - public async Task CreateUser(User info) + public async Task CreateUser(User info) { if (info == null) throw new ArgumentNullException(nameof(info)); @@ -316,7 +316,7 @@ namespace Timeline.Services _logger.LogInformation(Log.Format(LogDatabaseCreate, ("Id", newEntity.Id), ("Username", username), ("Administrator", administrator))); - return newEntity.Id; + return CreateUserFromEntity(newEntity); } private void ValidateModifyUserInfo(User? info) -- cgit v1.2.3