diff options
-rw-r--r-- | Timeline.Tests/Helpers/TestApplication.cs | 12 | ||||
-rw-r--r-- | Timeline.Tests/IntegratedTests/IntegratedTestBase.cs | 2 | ||||
-rw-r--r-- | Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs | 28 | ||||
-rw-r--r-- | Timeline.Tests/IntegratedTests/UserAvatarTest.cs | 2 | ||||
-rw-r--r-- | Timeline.Tests/IntegratedTests/UserTest.cs | 8 | ||||
-rw-r--r-- | Timeline.Tests/UsernameValidatorUnitTest.cs | 8 | ||||
-rw-r--r-- | Timeline/Controllers/ControllerAuthExtensions.cs | 2 | ||||
-rw-r--r-- | Timeline/Controllers/UserAvatarController.cs | 2 | ||||
-rw-r--r-- | Timeline/Controllers/UserController.cs | 18 | ||||
-rw-r--r-- | Timeline/Entities/UserEntity.cs | 2 | ||||
-rw-r--r-- | Timeline/Models/Http/UserInfo.cs | 21 | ||||
-rw-r--r-- | Timeline/Models/Validation/NicknameValidator.cs | 2 | ||||
-rw-r--r-- | Timeline/Services/TimelineService.cs | 2 | ||||
-rw-r--r-- | Timeline/Services/UserService.cs | 2 |
14 files changed, 64 insertions, 47 deletions
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<Startup> factory)
{
DatabaseConnection = new SqliteConnection("Data Source=:memory:;");
+ DatabaseConnection.Open();
+
+ var options = new DbContextOptionsBuilder<DevelopmentDatabaseContext>()
+ .UseSqlite(DatabaseConnection)
+ .Options;
+
+ using (var context = new DevelopmentDatabaseContext(options))
+ {
+ context.Database.EnsureCreated();
+ }
Factory = factory.WithWebHostBuilder(builder =>
{
- builder.ConfigureTestServices(services =>
+ builder.ConfigureServices(services =>
{
services.AddDbContext<DatabaseContext, DevelopmentDatabaseContext>(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<UserInfo>(user));
userInfoForAdminList.Add(mapper.Map<UserInfoForAdmin>(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<UserInfo> { 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<long> 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<TimelinePostInfo>() @@ -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<TimelinePostInfo>() .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<TimelinePostInfo[]>() .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<IWebHostEnvironment>();
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<UserInfo[]>()
+ .And.HaveJsonBody<UserInfoForAdmin[]>()
.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<UserInfo>()
+ .And.HaveJsonBody<UserInfoForAdmin>()
.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 @@ -22,12 +22,6 @@ namespace Timeline.Tests }
[Fact]
- public void Null()
- {
- FailAndMessage(null).Should().ContainEquivalentOf("null");
- }
-
- [Fact]
public void NotString()
{
var (result, message) = _validator.Validate(123);
@@ -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<IActionResult> 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<ActionResult<CommonDeleteResponse>> 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<object, IUserInfo>
+ public class UserInfoAvatarUrlValueResolver : IValueResolver<User, IUserInfo, string>
{
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<User, UserInfo>().AfterMap<UserInfoSetAvatarUrlAction>();
- CreateMap<User, UserInfoForAdmin>().AfterMap<UserInfoSetAvatarUrlAction>();
+ CreateMap<User, UserInfo>().ForMember(u => u.AvatarUrl, opt => opt.MapFrom<UserInfoAvatarUrlValueResolver>());
+ CreateMap<User, UserInfoForAdmin>().ForMember(u => u.AvatarUrl, opt => opt.MapFrom<UserInfoAvatarUrlValueResolver>());
}
}
}
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<UserInfo>(UserService.GetUserById(entity.AuthorId));
+ var author = Mapper.Map<UserInfo>(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);
|