diff options
author | crupest <crupest@outlook.com> | 2020-01-30 20:26:52 +0800 |
---|---|---|
committer | crupest <crupest@outlook.com> | 2020-01-30 20:26:52 +0800 |
commit | 52acf41e331ddbd66befed4692c804b754ba7d5c (patch) | |
tree | 538ceea06640f501d2a950cac813c10561036e4d | |
parent | abde51b167f17301c25e32ae247e7909c0dc9367 (diff) | |
download | timeline-52acf41e331ddbd66befed4692c804b754ba7d5c.tar.gz timeline-52acf41e331ddbd66befed4692c804b754ba7d5c.tar.bz2 timeline-52acf41e331ddbd66befed4692c804b754ba7d5c.zip |
...
75 files changed, 963 insertions, 3635 deletions
diff --git a/Timeline.ErrorCodes/ErrorCodes.cs b/Timeline.ErrorCodes/ErrorCodes.cs index 730f42e0..ea74cf0e 100644 --- a/Timeline.ErrorCodes/ErrorCodes.cs +++ b/Timeline.ErrorCodes/ErrorCodes.cs @@ -45,7 +45,7 @@ public static class UserController
{
- public const int ChangeUsername_Conflict = 1_102_01_01;
+ public const int UsernameConflict = 1_102_01_01;
public const int ChangePassword_BadOldPassword = 1_102_02_01;
}
@@ -58,7 +58,7 @@ public static class TimelineController
{
- public const int PostOperationDelete_NotExist = 1_104_01_01;
+ public const int MemberPut_NotExist = 1_104_01_01;
}
}
}
diff --git a/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs b/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs deleted file mode 100644 index bbc8ba75..00000000 --- a/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs +++ /dev/null @@ -1,388 +0,0 @@ -using FluentAssertions;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Logging.Abstractions;
-using Moq;
-using System;
-using System.Collections.Generic;
-using System.Reflection;
-using System.Threading.Tasks;
-using Timeline.Controllers;
-using Timeline.Filters;
-using Timeline.Models;
-using Timeline.Models.Http;
-using Timeline.Models.Validation;
-using Timeline.Services;
-using Timeline.Tests.Helpers;
-using Xunit;
-
-namespace Timeline.Tests.Controllers
-{
- public class PersonalTimelineControllerTest : IDisposable
- {
- private readonly Mock<IPersonalTimelineService> _service;
-
- private readonly PersonalTimelineController _controller;
-
- public PersonalTimelineControllerTest()
- {
- _service = new Mock<IPersonalTimelineService>();
- _controller = new PersonalTimelineController(NullLogger<PersonalTimelineController>.Instance, _service.Object);
- }
-
- public void Dispose()
- {
- _controller.Dispose();
- }
-
- [Fact]
- public void AttributeTest()
- {
- static void AssertUsernameParameter(MethodInfo m)
- {
- m.GetParameter("username")
- .Should().BeDecoratedWith<FromRouteAttribute>()
- .And.BeDecoratedWith<UsernameAttribute>();
- }
-
- static void AssertBodyParamter<TBody>(MethodInfo m)
- {
- var p = m.GetParameter("body");
- p.Should().BeDecoratedWith<FromBodyAttribute>();
- p.ParameterType.Should().Be(typeof(TBody));
- }
-
- var type = typeof(PersonalTimelineController);
- type.Should().BeDecoratedWith<ApiControllerAttribute>();
-
- {
- var m = type.GetMethod(nameof(PersonalTimelineController.TimelineGet));
- m.Should().BeDecoratedWith<CatchTimelineNotExistExceptionAttribute>()
- .And.BeDecoratedWith<HttpGetAttribute>();
- AssertUsernameParameter(m);
- }
-
- {
- var m = type.GetMethod(nameof(PersonalTimelineController.PostListGet));
- m.Should().BeDecoratedWith<CatchTimelineNotExistExceptionAttribute>()
- .And.BeDecoratedWith<HttpGetAttribute>();
- AssertUsernameParameter(m);
- }
-
- {
- var m = type.GetMethod(nameof(PersonalTimelineController.PostOperationCreate));
- m.Should().BeDecoratedWith<CatchTimelineNotExistExceptionAttribute>()
- .And.BeDecoratedWith<AuthorizeAttribute>()
- .And.BeDecoratedWith<HttpPostAttribute>();
- AssertUsernameParameter(m);
- AssertBodyParamter<TimelinePostCreateRequest>(m);
- }
-
- {
- var m = type.GetMethod(nameof(PersonalTimelineController.PostOperationDelete));
- m.Should().BeDecoratedWith<CatchTimelineNotExistExceptionAttribute>()
- .And.BeDecoratedWith<AuthorizeAttribute>()
- .And.BeDecoratedWith<HttpPostAttribute>();
- AssertUsernameParameter(m);
- AssertBodyParamter<TimelinePostDeleteRequest>(m);
- }
-
- {
- var m = type.GetMethod(nameof(PersonalTimelineController.TimelineChangeProperty));
- m.Should().BeDecoratedWith<CatchTimelineNotExistExceptionAttribute>()
- .And.BeDecoratedWith<AuthorizeAttribute>()
- .And.BeDecoratedWith<SelfOrAdminAttribute>()
- .And.BeDecoratedWith<HttpPostAttribute>();
- AssertUsernameParameter(m);
- AssertBodyParamter<TimelinePropertyChangeRequest>(m);
- }
-
- {
- var m = type.GetMethod(nameof(PersonalTimelineController.TimelineChangeMember));
- m.Should().BeDecoratedWith<CatchTimelineNotExistExceptionAttribute>()
- .And.BeDecoratedWith<AuthorizeAttribute>()
- .And.BeDecoratedWith<SelfOrAdminAttribute>()
- .And.BeDecoratedWith<HttpPostAttribute>();
- AssertUsernameParameter(m);
- AssertBodyParamter<TimelineMemberChangeRequest>(m);
- }
- }
-
- const string authUsername = "authuser";
- private void SetUser(bool administrator)
- {
- _controller.ControllerContext = new ControllerContext
- {
- HttpContext = new DefaultHttpContext
- {
- User = PrincipalHelper.Create(authUsername, administrator)
- }
- };
- }
-
- [Fact]
- public async Task TimelineGet()
- {
- const string username = "username";
- var timelineInfo = new BaseTimelineInfo();
- _service.Setup(s => s.GetTimeline(username)).ReturnsAsync(timelineInfo);
- (await _controller.TimelineGet(username)).Value.Should().Be(timelineInfo);
- _service.VerifyAll();
- }
-
- [Fact]
- public async Task PostListGet_Forbid()
- {
- const string username = "username";
- SetUser(false);
- _service.Setup(s => s.HasReadPermission(username, authUsername)).ReturnsAsync(false);
- var result = (await _controller.PostListGet(username)).Result
- .Should().BeAssignableTo<ObjectResult>()
- .Which;
- result.StatusCode.Should().Be(StatusCodes.Status403Forbidden);
- result.Value.Should().BeAssignableTo<CommonResponse>()
- .Which.Code.Should().Be(ErrorCodes.Common.Forbid);
- _service.VerifyAll();
- }
-
- [Fact]
- public async Task PostListGet_Admin_Success()
- {
- const string username = "username";
- SetUser(true);
- _service.Setup(s => s.GetPosts(username)).ReturnsAsync(new List<TimelinePostInfo>());
- (await _controller.PostListGet(username)).Value
- .Should().BeAssignableTo<IList<TimelinePostInfo>>()
- .Which.Should().NotBeNull().And.BeEmpty();
- _service.VerifyAll();
- }
-
- [Fact]
- public async Task PostListGet_User_Success()
- {
- const string username = "username";
- SetUser(false);
- _service.Setup(s => s.HasReadPermission(username, authUsername)).ReturnsAsync(true);
- _service.Setup(s => s.GetPosts(username)).ReturnsAsync(new List<TimelinePostInfo>());
- (await _controller.PostListGet(username)).Value
- .Should().BeAssignableTo<IList<TimelinePostInfo>>()
- .Which.Should().NotBeNull().And.BeEmpty();
- _service.VerifyAll();
- }
-
- [Fact]
- public async Task PostOperationCreate_Forbid()
- {
- const string username = "username";
- const string content = "cccc";
- SetUser(false);
- _service.Setup(s => s.IsMemberOf(username, authUsername)).ReturnsAsync(false);
- var result = (await _controller.PostOperationCreate(username, new TimelinePostCreateRequest
- {
- Content = content,
- Time = null
- })).Result.Should().NotBeNull().And.BeAssignableTo<ObjectResult>().Which;
- result.StatusCode.Should().Be(StatusCodes.Status403Forbidden);
- result.Value.Should().BeAssignableTo<CommonResponse>()
- .Which.Code.Should().Be(ErrorCodes.Common.Forbid);
- _service.VerifyAll();
- }
-
- [Fact]
- public async Task PostOperationCreate_Admin_Success()
- {
- const string username = "username";
- const string content = "cccc";
- var response = new TimelinePostCreateResponse
- {
- Id = 3,
- Time = DateTime.Now
- };
- SetUser(true);
- _service.Setup(s => s.CreatePost(username, authUsername, content, null)).ReturnsAsync(response);
- var resultValue = (await _controller.PostOperationCreate(username, new TimelinePostCreateRequest
- {
- Content = content,
- Time = null
- })).Value;
- resultValue.Should().NotBeNull()
- .And.BeAssignableTo<TimelinePostCreateResponse>()
- .And.BeEquivalentTo(response);
- _service.VerifyAll();
- }
-
- [Fact]
- public async Task PostOperationCreate_User_Success()
- {
- const string username = "username";
- const string content = "cccc";
- var response = new TimelinePostCreateResponse
- {
- Id = 3,
- Time = DateTime.Now
- };
- SetUser(false);
- _service.Setup(s => s.IsMemberOf(username, authUsername)).ReturnsAsync(true);
- _service.Setup(s => s.CreatePost(username, authUsername, content, null)).ReturnsAsync(response);
- var resultValue = (await _controller.PostOperationCreate(username, new TimelinePostCreateRequest
- {
- Content = content,
- Time = null
- })).Value;
- resultValue.Should().NotBeNull()
- .And.BeAssignableTo<TimelinePostCreateResponse>()
- .And.BeEquivalentTo(response);
- _service.VerifyAll();
- }
-
- [Fact]
- public async Task PostOperationDelete_Forbid()
- {
- const string username = "username";
- const long postId = 2;
- SetUser(false);
- _service.Setup(s => s.HasPostModifyPermission(username, postId, authUsername)).ReturnsAsync(false);
- var result = (await _controller.PostOperationDelete(username, new TimelinePostDeleteRequest
- {
- Id = postId
- })).Should().NotBeNull().And.BeAssignableTo<ObjectResult>().Which;
- result.StatusCode.Should().Be(StatusCodes.Status403Forbidden);
- result.Value.Should().BeAssignableTo<CommonResponse>()
- .Which.Code.Should().Be(ErrorCodes.Common.Forbid);
- _service.VerifyAll();
- }
-
- [Fact]
- public async Task PostOperationDelete_NotExist()
- {
- const string username = "username";
- const long postId = 2;
- SetUser(true);
- _service.Setup(s => s.DeletePost(username, postId)).ThrowsAsync(new TimelinePostNotExistException());
- var result = (await _controller.PostOperationDelete(username, new TimelinePostDeleteRequest
- {
- Id = postId
- })).Should().NotBeNull().And.BeAssignableTo<ObjectResult>().Which;
- result.StatusCode.Should().Be(StatusCodes.Status400BadRequest);
- result.Value.Should().BeAssignableTo<CommonResponse>()
- .Which.Code.Should().Be(ErrorCodes.TimelineController.PostOperationDelete_NotExist);
- _service.VerifyAll();
- }
-
- [Fact]
- public async Task PostOperationDelete_Admin_Success()
- {
- const string username = "username";
- const long postId = 2;
- SetUser(true);
- _service.Setup(s => s.DeletePost(username, postId)).Returns(Task.CompletedTask);
- var result = await _controller.PostOperationDelete(username, new TimelinePostDeleteRequest
- {
- Id = postId
- });
- result.Should().NotBeNull().And.BeAssignableTo<OkResult>();
- _service.VerifyAll();
- }
-
- [Fact]
- public async Task PostOperationDelete_User_Success()
- {
- const string username = "username";
- const long postId = 2;
- SetUser(false);
- _service.Setup(s => s.DeletePost(username, postId)).Returns(Task.CompletedTask);
- _service.Setup(s => s.HasPostModifyPermission(username, postId, authUsername)).ReturnsAsync(true);
- var result = await _controller.PostOperationDelete(username, new TimelinePostDeleteRequest
- {
- Id = postId
- });
- result.Should().NotBeNull().And.BeAssignableTo<OkResult>();
- _service.VerifyAll();
- }
-
- [Fact]
- public async Task TimelineChangeProperty_Success()
- {
- const string username = "username";
- var req = new TimelinePropertyChangeRequest
- {
- Description = "",
- Visibility = TimelineVisibility.Private
- };
- _service.Setup(s => s.ChangeProperty(username, req)).Returns(Task.CompletedTask);
- var result = await _controller.TimelineChangeProperty(username, req);
- result.Should().NotBeNull().And.BeAssignableTo<OkResult>();
- _service.VerifyAll();
- }
-
- [Fact]
- public async Task TimelineChangeMember_Success()
- {
- const string username = "username";
- var add = new List<string> { "aaa" };
- var remove = new List<string> { "rrr" };
- _service.Setup(s => s.ChangeMember(username, add, remove)).Returns(Task.CompletedTask);
- var result = await _controller.TimelineChangeMember(username, new TimelineMemberChangeRequest
- {
- Add = add,
- Remove = remove
- });
- result.Should().NotBeNull().And.BeAssignableTo<OkResult>();
- _service.VerifyAll();
- }
-
- [Fact]
- public async Task TimelineChangeMember_UsernameBadFormat()
- {
- const string username = "username";
- var add = new List<string> { "aaa" };
- var remove = new List<string> { "rrr" };
- _service.Setup(s => s.ChangeMember(username, add, remove)).ThrowsAsync(
- new TimelineMemberOperationUserException("test", new UsernameBadFormatException()));
- var result = await _controller.TimelineChangeMember(username, new TimelineMemberChangeRequest
- {
- Add = add,
- Remove = remove
- });
- result.Should().NotBeNull().And.BeAssignableTo<BadRequestObjectResult>()
- .Which.Value.Should().BeAssignableTo<CommonResponse>()
- .Which.Code.Should().Be(ErrorCodes.Common.InvalidModel);
- _service.VerifyAll();
- }
-
- [Fact]
- public async Task TimelineChangeMember_AddNotExist()
- {
- const string username = "username";
- var add = new List<string> { "aaa" };
- var remove = new List<string> { "rrr" };
- _service.Setup(s => s.ChangeMember(username, add, remove)).ThrowsAsync(
- new TimelineMemberOperationUserException("test", new UserNotExistException()));
- var result = await _controller.TimelineChangeMember(username, new TimelineMemberChangeRequest
- {
- Add = add,
- Remove = remove
- });
- result.Should().NotBeNull().And.BeAssignableTo<BadRequestObjectResult>()
- .Which.Value.Should().BeAssignableTo<CommonResponse>()
- .Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist);
- _service.VerifyAll();
- }
-
- [Fact]
- public async Task TimelineChangeMember_UnknownTimelineMemberOperationUserException()
- {
- const string username = "username";
- var add = new List<string> { "aaa" };
- var remove = new List<string> { "rrr" };
- _service.Setup(s => s.ChangeMember(username, add, remove)).ThrowsAsync(
- new TimelineMemberOperationUserException("test", null));
- await _controller.Awaiting(c => c.TimelineChangeMember(username, new TimelineMemberChangeRequest
- {
- Add = add,
- Remove = remove
- })).Should().ThrowAsync<TimelineMemberOperationUserException>(); // Should rethrow.
- }
- }
-}
diff --git a/Timeline.Tests/Controllers/TokenControllerTest.cs b/Timeline.Tests/Controllers/TokenControllerTest.cs deleted file mode 100644 index 43e1a413..00000000 --- a/Timeline.Tests/Controllers/TokenControllerTest.cs +++ /dev/null @@ -1,135 +0,0 @@ -using FluentAssertions;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Logging.Abstractions;
-using Moq;
-using System;
-using System.Collections.Generic;
-using System.Threading.Tasks;
-using Timeline.Controllers;
-using Timeline.Models;
-using Timeline.Models.Http;
-using Timeline.Services;
-using Timeline.Tests.Helpers;
-using Xunit;
-
-namespace Timeline.Tests.Controllers
-{
- public class TokenControllerTest : IDisposable
- {
- private readonly Mock<IUserTokenManager> _mockUserService = new Mock<IUserTokenManager>();
- private readonly TestClock _mockClock = new TestClock();
-
-
- private readonly TokenController _controller;
-
- public TokenControllerTest()
- {
- _controller = new TokenController(_mockUserService.Object, NullLogger<TokenController>.Instance, _mockClock);
- }
-
- public void Dispose()
- {
- _controller.Dispose();
- }
-
- [Theory]
- [InlineData(null)]
- [InlineData(100)]
- public async Task Create_Ok(int? expire)
- {
- var mockCurrentTime = DateTime.Now;
- _mockClock.MockCurrentTime = mockCurrentTime;
- var mockCreateResult = new UserTokenCreateResult
- {
- Token = "mocktokenaaaaa",
- User = new Models.User
- {
- Id = 1,
- Username = MockUser.User.Username,
- Administrator = MockUser.User.Administrator,
- Version = 1
- }
- };
- _mockUserService.Setup(s => s.CreateToken("u", "p", expire == null ? null : (DateTime?)mockCurrentTime.AddDays(expire.Value))).ReturnsAsync(mockCreateResult);
- var action = await _controller.Create(new CreateTokenRequest
- {
- Username = "u",
- Password = "p",
- Expire = expire
- });
- action.Result.Should().BeAssignableTo<OkObjectResult>()
- .Which.Value.Should().BeEquivalentTo(new CreateTokenResponse
- {
- Token = mockCreateResult.Token,
- User = MockUser.User.Info
- });
- }
-
- [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.Result.Should().BeAssignableTo<BadRequestObjectResult>()
- .Which.Value.Should().BeAssignableTo<CommonResponse>()
- .Which.Code.Should().Be(ErrorCodes.TokenController.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.Result.Should().BeAssignableTo<BadRequestObjectResult>()
- .Which.Value.Should().BeAssignableTo<CommonResponse>()
- .Which.Code.Should().Be(ErrorCodes.TokenController.Create_BadCredential);
- }
-
- [Fact]
- public async Task Verify_Ok()
- {
- const string token = "aaaaaaaaaaaaaa";
- _mockUserService.Setup(s => s.VerifyToken(token)).ReturnsAsync(new Models.User
- {
- Id = 1,
- Username = MockUser.User.Username,
- Administrator = MockUser.User.Administrator,
- Version = 1
- });
- var action = await _controller.Verify(new VerifyTokenRequest { Token = token });
- action.Result.Should().BeAssignableTo<OkObjectResult>()
- .Which.Value.Should().BeAssignableTo<VerifyTokenResponse>()
- .Which.User.Should().BeEquivalentTo(MockUser.User.Info);
- }
-
- public static IEnumerable<object[]> Verify_BadRequest_Data()
- {
- yield return new object[] { new UserTokenTimeExpireException(), ErrorCodes.TokenController.Verify_TimeExpired };
- yield return new object[] { new UserTokenBadVersionException(), ErrorCodes.TokenController.Verify_OldVersion };
- yield return new object[] { new UserTokenBadFormatException(), ErrorCodes.TokenController.Verify_BadFormat };
- yield return new object[] { new UserNotExistException(), ErrorCodes.TokenController.Verify_UserNotExist };
- }
-
- [Theory]
- [MemberData(nameof(Verify_BadRequest_Data))]
- public async Task Verify_BadRequest(Exception e, int code)
- {
- const string token = "aaaaaaaaaaaaaa";
- _mockUserService.Setup(s => s.VerifyToken(token)).ThrowsAsync(e);
- var action = await _controller.Verify(new VerifyTokenRequest { Token = token });
- action.Result.Should().BeAssignableTo<BadRequestObjectResult>()
- .Which.Value.Should().BeAssignableTo<CommonResponse>()
- .Which.Code.Should().Be(code);
- }
- }
-}
diff --git a/Timeline.Tests/Controllers/UserControllerTest.cs b/Timeline.Tests/Controllers/UserControllerTest.cs deleted file mode 100644 index 3890712a..00000000 --- a/Timeline.Tests/Controllers/UserControllerTest.cs +++ /dev/null @@ -1,228 +0,0 @@ -using FluentAssertions;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Logging.Abstractions;
-using Moq;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Security.Claims;
-using System.Threading.Tasks;
-using Timeline.Controllers;
-using Timeline.Models;
-using Timeline.Models.Http;
-using Timeline.Services;
-using Timeline.Tests.Helpers;
-using Xunit;
-
-namespace Timeline.Tests.Controllers
-{
- public class UserControllerTest : IDisposable
- {
- private readonly Mock<IUserService> _mockUserService = new Mock<IUserService>();
-
- private readonly UserController _controller;
-
- public UserControllerTest()
- {
- _controller = new UserController(NullLogger<UserController>.Instance, _mockUserService.Object);
- }
-
- public void Dispose()
- {
- _controller.Dispose();
- }
-
- [Fact]
- public async Task GetList_Success()
- {
- var mockUserList = new Models.User[] {
- new Models.User { Id = 1, Username = "aaa", Administrator = true, Version = 1 },
- new Models.User { Id = 2, Username = "bbb", Administrator = false, Version = 1 }
- };
- _mockUserService.Setup(s => s.GetUsers()).ReturnsAsync(mockUserList);
- var action = await _controller.List();
- action.Result.Should().BeAssignableTo<OkObjectResult>()
- .Which.Value.Should().BeEquivalentTo(
- mockUserList.Select(u => new User { Username = u.Username, Administrator = u.Administrator }).ToArray());
- }
-
- [Fact]
- public async Task Get_Success()
- {
- const string username = "aaa";
- _mockUserService.Setup(s => s.GetUserByUsername(username)).ReturnsAsync(new Models.User
- {
- Id = 1,
- Username = MockUser.User.Username,
- Administrator = MockUser.User.Administrator,
- Version = 1
- });
- var action = await _controller.Get(username);
- action.Result.Should().BeAssignableTo<OkObjectResult>()
- .Which.Value.Should().BeEquivalentTo(MockUser.User.Info);
- }
-
- [Fact]
- public async Task Get_NotFound()
- {
- const string username = "aaa";
- _mockUserService.Setup(s => s.GetUserByUsername(username)).ThrowsAsync(new UserNotExistException());
- var action = await _controller.Get(username);
- action.Result.Should().BeAssignableTo<NotFoundObjectResult>()
- .Which.Value.Should().BeAssignableTo<CommonResponse>()
- .Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist);
- }
-
- [Theory]
- [InlineData(PutResult.Create, true)]
- [InlineData(PutResult.Modify, false)]
- public async Task Put_Success(PutResult result, bool create)
- {
- const string username = "aaa";
- const string password = "ppp";
- const bool administrator = true;
- _mockUserService.Setup(s => s.PutUser(username, password, administrator)).ReturnsAsync(result);
- var action = await _controller.Put(new UserPutRequest
- {
- Password = password,
- Administrator = administrator
- }, username);
- var response = action.Result.Should().BeAssignableTo<ObjectResult>()
- .Which.Value.Should().BeAssignableTo<CommonPutResponse>()
- .Which;
- response.Code.Should().Be(0);
- response.Data.Create.Should().Be(create);
- }
-
- [Fact]
- public async Task Patch_Success()
- {
- const string username = "aaa";
- const string password = "ppp";
- const bool administrator = true;
- _mockUserService.Setup(s => s.PatchUser(username, password, administrator)).Returns(Task.CompletedTask);
- var action = await _controller.Patch(new UserPatchRequest
- {
- Password = password,
- Administrator = administrator
- }, username);
- action.Should().BeAssignableTo<OkResult>();
- }
-
- [Fact]
- public async Task Patch_NotExist()
- {
- const string username = "aaa";
- const string password = "ppp";
- const bool administrator = true;
- _mockUserService.Setup(s => s.PatchUser(username, password, administrator)).ThrowsAsync(new UserNotExistException());
- var action = await _controller.Patch(new UserPatchRequest
- {
- Password = password,
- Administrator = administrator
- }, username);
- action.Should().BeAssignableTo<NotFoundObjectResult>()
- .Which.Value.Should().BeAssignableTo<CommonResponse>()
- .Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist);
- }
-
- [Fact]
- public async Task Delete_Delete()
- {
- const string username = "aaa";
- _mockUserService.Setup(s => s.DeleteUser(username)).Returns(Task.CompletedTask);
- var action = await _controller.Delete(username);
- var body = action.Result.Should().BeAssignableTo<OkObjectResult>()
- .Which.Value.Should().BeAssignableTo<CommonDeleteResponse>()
- .Which;
- body.Code.Should().Be(0);
- body.Data.Delete.Should().BeTrue();
- }
-
- [Fact]
- public async Task Delete_NotExist()
- {
- const string username = "aaa";
- _mockUserService.Setup(s => s.DeleteUser(username)).ThrowsAsync(new UserNotExistException());
- var action = await _controller.Delete(username);
- var body = action.Result.Should().BeAssignableTo<OkObjectResult>()
- .Which.Value.Should().BeAssignableTo<CommonDeleteResponse>()
- .Which;
- body.Code.Should().Be(0);
- body.Data.Delete.Should().BeFalse();
- }
-
- [Fact]
- public async Task Op_ChangeUsername_Success()
- {
- const string oldUsername = "aaa";
- const string newUsername = "bbb";
- _mockUserService.Setup(s => s.ChangeUsername(oldUsername, newUsername)).Returns(Task.CompletedTask);
- var action = await _controller.ChangeUsername(new ChangeUsernameRequest { OldUsername = oldUsername, NewUsername = newUsername });
- action.Should().BeAssignableTo<OkResult>();
- }
-
- [Theory]
- [InlineData(typeof(UserNotExistException), ErrorCodes.UserCommon.NotExist)]
- [InlineData(typeof(ConfictException), ErrorCodes.UserController.ChangeUsername_Conflict)]
- public async Task Op_ChangeUsername_Failure(Type exceptionType, int code)
- {
- const string oldUsername = "aaa";
- const string newUsername = "bbb";
- _mockUserService.Setup(s => s.ChangeUsername(oldUsername, newUsername)).ThrowsAsync(Activator.CreateInstance(exceptionType) as Exception);
- var action = await _controller.ChangeUsername(new ChangeUsernameRequest { OldUsername = oldUsername, NewUsername = newUsername });
- action.Should().BeAssignableTo<BadRequestObjectResult>()
- .Which.Value.Should().BeAssignableTo<CommonResponse>()
- .Which.Code.Should().Be(code);
- }
-
- [Fact]
- public async Task Op_ChangePassword_Success()
- {
- const string username = "aaa";
- const string oldPassword = "aaa";
- const string newPassword = "bbb";
- _mockUserService.Setup(s => s.ChangePassword(username, oldPassword, newPassword)).Returns(Task.CompletedTask);
-
- _controller.ControllerContext = new ControllerContext()
- {
- HttpContext = new DefaultHttpContext()
- {
- User = new ClaimsPrincipal(new ClaimsIdentity(new Claim[]
- {
- new Claim(ClaimTypes.Name, username)
- }, "TestAuthType"))
- }
- };
-
- var action = await _controller.ChangePassword(new ChangePasswordRequest { OldPassword = oldPassword, NewPassword = newPassword });
- action.Should().BeAssignableTo<OkResult>();
- }
-
- [Fact]
- public async Task Op_ChangePassword_BadPassword()
- {
- const string username = "aaa";
- const string oldPassword = "aaa";
- const string newPassword = "bbb";
- _mockUserService.Setup(s => s.ChangePassword(username, oldPassword, newPassword)).ThrowsAsync(new BadPasswordException());
-
- _controller.ControllerContext = new ControllerContext()
- {
- HttpContext = new DefaultHttpContext()
- {
- User = new ClaimsPrincipal(new ClaimsIdentity(new Claim[]
- {
- new Claim(ClaimTypes.Name, username)
- }, "TestAuthType"))
- }
- };
-
- var action = await _controller.ChangePassword(new ChangePasswordRequest { OldPassword = oldPassword, NewPassword = newPassword });
- action.Should().BeAssignableTo<BadRequestObjectResult>()
- .Which.Value.Should().BeAssignableTo<CommonResponse>()
- .Which.Code.Should().Be(ErrorCodes.UserController.ChangePassword_BadOldPassword);
- }
- }
-}
diff --git a/Timeline.Tests/Controllers/UserDetailControllerTest.cs b/Timeline.Tests/Controllers/UserDetailControllerTest.cs deleted file mode 100644 index ffd88790..00000000 --- a/Timeline.Tests/Controllers/UserDetailControllerTest.cs +++ /dev/null @@ -1,98 +0,0 @@ -using FluentAssertions;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Mvc;
-using Moq;
-using System;
-using System.ComponentModel.DataAnnotations;
-using System.Threading.Tasks;
-using Timeline.Controllers;
-using Timeline.Filters;
-using Timeline.Models.Validation;
-using Timeline.Services;
-using Timeline.Tests.Helpers;
-using Xunit;
-
-namespace Timeline.Tests.Controllers
-{
- public class UserDetailControllerTest : IDisposable
- {
- private readonly Mock<IUserDetailService> _mockUserDetailService;
- private readonly UserDetailController _controller;
-
- public UserDetailControllerTest()
- {
- _mockUserDetailService = new Mock<IUserDetailService>();
- _controller = new UserDetailController(_mockUserDetailService.Object);
- }
-
- public void Dispose()
- {
- _controller.Dispose();
- }
-
- [Fact]
- public void AttributeTest()
- {
- typeof(UserDetailController).Should().BeDecoratedWith<ApiControllerAttribute>();
-
- var getNickname = typeof(UserDetailController).GetMethod(nameof(UserDetailController.GetNickname));
- getNickname.Should().BeDecoratedWith<HttpGetAttribute>()
- .And.BeDecoratedWith<CatchUserNotExistExceptionAttribute>();
- getNickname.GetParameter("username").Should().BeDecoratedWith<UsernameAttribute>()
- .And.BeDecoratedWith<FromRouteAttribute>();
-
- var putNickname = typeof(UserDetailController).GetMethod(nameof(UserDetailController.PutNickname));
- putNickname.Should().BeDecoratedWith<HttpPutAttribute>()
- .And.BeDecoratedWith<AuthorizeAttribute>()
- .And.BeDecoratedWith<SelfOrAdminAttribute>()
- .And.BeDecoratedWith<CatchUserNotExistExceptionAttribute>();
- putNickname.GetParameter("username").Should().BeDecoratedWith<UsernameAttribute>()
- .And.BeDecoratedWith<FromRouteAttribute>();
- var stringLengthAttributeOnPutBody = putNickname.GetParameter("body").Should().BeDecoratedWith<FromBodyAttribute>()
- .And.BeDecoratedWith<StringLengthAttribute>()
- .Which;
- stringLengthAttributeOnPutBody.MinimumLength.Should().Be(1);
- stringLengthAttributeOnPutBody.MaximumLength.Should().Be(10);
-
- var deleteNickname = typeof(UserDetailController).GetMethod(nameof(UserDetailController.DeleteNickname));
- deleteNickname.Should().BeDecoratedWith<HttpDeleteAttribute>()
- .And.BeDecoratedWith<AuthorizeAttribute>()
- .And.BeDecoratedWith<SelfOrAdminAttribute>()
- .And.BeDecoratedWith<CatchUserNotExistExceptionAttribute>();
- deleteNickname.GetParameter("username").Should().BeDecoratedWith<UsernameAttribute>()
- .And.BeDecoratedWith<FromRouteAttribute>();
- }
-
- [Fact]
- public async Task GetNickname_ShouldWork()
- {
- const string username = "uuu";
- const string nickname = "nnn";
- _mockUserDetailService.Setup(s => s.GetNickname(username)).ReturnsAsync(nickname);
- var actionResult = await _controller.GetNickname(username);
- actionResult.Result.Should().BeAssignableTo<OkObjectResult>(nickname);
- _mockUserDetailService.VerifyAll();
- }
-
- [Fact]
- public async Task PutNickname_ShouldWork()
- {
- const string username = "uuu";
- const string nickname = "nnn";
- _mockUserDetailService.Setup(s => s.SetNickname(username, nickname)).Returns(Task.CompletedTask);
- var actionResult = await _controller.PutNickname(username, nickname);
- actionResult.Should().BeAssignableTo<OkResult>();
- _mockUserDetailService.VerifyAll();
- }
-
- [Fact]
- public async Task DeleteNickname_ShouldWork()
- {
- const string username = "uuu";
- _mockUserDetailService.Setup(s => s.SetNickname(username, null)).Returns(Task.CompletedTask);
- var actionResult = await _controller.DeleteNickname(username);
- actionResult.Should().BeAssignableTo<OkResult>();
- _mockUserDetailService.VerifyAll();
- }
- }
-}
diff --git a/Timeline.Tests/DatabaseTest.cs b/Timeline.Tests/DatabaseTest.cs deleted file mode 100644 index a15823a9..00000000 --- a/Timeline.Tests/DatabaseTest.cs +++ /dev/null @@ -1,63 +0,0 @@ -using FluentAssertions;
-using System;
-using System.Linq;
-using Timeline.Entities;
-using Timeline.Tests.Helpers;
-using Xunit;
-
-namespace Timeline.Tests
-{
- public class DatabaseTest : IDisposable
- {
- private readonly TestDatabase _database;
- private readonly DatabaseContext _context;
-
- public DatabaseTest()
- {
- _database = new TestDatabase();
- _context = _database.Context;
- }
-
- public void Dispose()
- {
- _database.Dispose();
- }
-
- [Fact]
- public void DeleteUserShouldAlsoDeleteAvatar()
- {
- var user = _context.Users.First();
- _context.UserAvatars.Count().Should().Be(0);
- _context.UserAvatars.Add(new UserAvatarEntity
- {
- Data = null,
- Type = null,
- ETag = null,
- LastModified = DateTime.Now,
- UserId = user.Id
- });
- _context.SaveChanges();
- _context.UserAvatars.Count().Should().Be(1);
- _context.Users.Remove(user);
- _context.SaveChanges();
- _context.UserAvatars.Count().Should().Be(0);
- }
-
- [Fact]
- public void DeleteUserShouldAlsoDeleteDetail()
- {
- var user = _context.Users.First();
- _context.UserDetails.Count().Should().Be(0);
- _context.UserDetails.Add(new UserDetailEntity
- {
- Nickname = null,
- UserId = user.Id
- });
- _context.SaveChanges();
- _context.UserDetails.Count().Should().Be(1);
- _context.Users.Remove(user);
- _context.SaveChanges();
- _context.UserDetails.Count().Should().Be(0);
- }
- }
-}
diff --git a/Timeline.Tests/GlobalSuppressions.cs b/Timeline.Tests/GlobalSuppressions.cs index 1d1d294b..0f873033 100644 --- a/Timeline.Tests/GlobalSuppressions.cs +++ b/Timeline.Tests/GlobalSuppressions.cs @@ -12,3 +12,5 @@ [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA1816:Dispose methods should call SuppressFinalize", Justification = "Test classes do not need to implement it that way.")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2234:Pass system uri objects instead of strings", Justification = "I really don't understand this rule.")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "Tests do not need make strings resources.")]
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1054:Uri parameters should not be strings", Justification = "That's unnecessary.")]
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1056:Uri properties should not be strings", Justification = "That's unnecessary.")]
diff --git a/Timeline.Tests/Helpers/MockUser.cs b/Timeline.Tests/Helpers/MockUser.cs deleted file mode 100644 index 49576842..00000000 --- a/Timeline.Tests/Helpers/MockUser.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Collections.Generic; -using Timeline.Models.Http;
-
-namespace Timeline.Tests.Helpers -{ - public class MockUser - { - public MockUser(string username, string password, bool administrator) - { - Info = new User { Username = username, Administrator = administrator }; - Password = password; - } - - public User Info { get; set; } - public string Username => Info.Username; - public string Password { get; set; } - public bool Administrator => Info.Administrator; - - public static MockUser User { get; } = new MockUser("user", "userpassword", false); - public static MockUser Admin { get; } = new MockUser("admin", "adminpassword", true); - - public static IReadOnlyList<User> UserInfoList { get; } = new List<User> { User.Info, Admin.Info }; - } -} diff --git a/Timeline.Tests/Helpers/PrincipalHelper.cs b/Timeline.Tests/Helpers/PrincipalHelper.cs deleted file mode 100644 index 89f3f7b1..00000000 --- a/Timeline.Tests/Helpers/PrincipalHelper.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Linq; -using System.Security.Claims; -using Timeline.Models; - -namespace Timeline.Tests.Helpers -{ - public static class PrincipalHelper - { - internal const string AuthScheme = "TESTAUTH"; - - internal static ClaimsPrincipal Create(string username, bool administrator) - { - var identity = new ClaimsIdentity(AuthScheme); - identity.AddClaim(new Claim(identity.NameClaimType, username, ClaimValueTypes.String)); - identity.AddClaims(UserRoleConvert.ToArray(administrator).Select(role => new Claim(identity.RoleClaimType, role, ClaimValueTypes.String))); - - var principal = new ClaimsPrincipal(); - principal.AddIdentity(identity); - - return principal; - } - } -} diff --git a/Timeline.Tests/Helpers/ResponseAssertions.cs b/Timeline.Tests/Helpers/ResponseAssertions.cs index be0043dc..f01a0677 100644 --- a/Timeline.Tests/Helpers/ResponseAssertions.cs +++ b/Timeline.Tests/Helpers/ResponseAssertions.cs @@ -125,6 +125,13 @@ namespace Timeline.Tests.Helpers return assertions.HaveJsonBody<CommonResponse>(because, becauseArgs);
}
+ public static void HaveCommonBody(this HttpResponseMessageAssertions assertions, int code, string message = null, params object[] messageArgs)
+ {
+ message = string.IsNullOrEmpty(message) ? "" : ", " + string.Format(CultureInfo.CurrentCulture, message, messageArgs);
+ var body = assertions.HaveCommonBody("Response body should be CommonResponse{0}", message).Which;
+ body.Code.Should().Be(code, "Response body code is not the specified one{0}", message);
+ }
+
public static AndWhichConstraint<HttpResponseMessageAssertions, CommonDataResponse<TData>> HaveCommonDataBody<TData>(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs)
{
return assertions.HaveJsonBody<CommonDataResponse<TData>>(because, becauseArgs);
@@ -148,13 +155,6 @@ namespace Timeline.Tests.Helpers body.Data.Delete.Should().Be(delete);
}
- public static void HaveCommonResponseBody(this HttpResponseMessageAssertions assertions, int code, string message = null, params object[] messageArgs)
- {
- message = string.IsNullOrEmpty(message) ? "" : ", " + string.Format(CultureInfo.CurrentCulture, message, messageArgs);
- var body = assertions.HaveJsonBody<CommonResponse>("Response body should be CommonResponse{0}", message).Which;
- body.Code.Should().Be(code, "Response body code is not the specified one{0}", message);
- }
-
public static void BeInvalidModel(this HttpResponseMessageAssertions assertions, string message = null)
{
message = string.IsNullOrEmpty(message) ? "" : ", " + message;
diff --git a/Timeline.Tests/Helpers/TestApplication.cs b/Timeline.Tests/Helpers/TestApplication.cs index d18f2848..14cafea3 100644 --- a/Timeline.Tests/Helpers/TestApplication.cs +++ b/Timeline.Tests/Helpers/TestApplication.cs @@ -1,4 +1,6 @@ using Microsoft.AspNetCore.Mvc.Testing;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using System;
@@ -8,19 +10,21 @@ namespace Timeline.Tests.Helpers {
public class TestApplication : IDisposable
{
- public TestDatabase Database { get; } = new TestDatabase();
+ public SqliteConnection DatabaseConnection { get; }
+
public WebApplicationFactory<Startup> Factory { get; }
public TestApplication(WebApplicationFactory<Startup> factory)
{
+ DatabaseConnection = new SqliteConnection("Data Source=:memory:;");
+
Factory = factory.WithWebHostBuilder(builder =>
{
- builder.ConfigureServices(services =>
+ builder.ConfigureTestServices(services =>
{
- services.AddEntityFrameworkSqlite();
services.AddDbContext<DatabaseContext, DevelopmentDatabaseContext>(options =>
{
- options.UseSqlite(Database.Connection);
+ options.UseSqlite(DatabaseConnection);
});
});
});
@@ -28,7 +32,8 @@ namespace Timeline.Tests.Helpers public void Dispose()
{
- Database.Dispose();
+ DatabaseConnection.Close();
+ DatabaseConnection.Dispose();
}
}
}
diff --git a/Timeline.Tests/Helpers/TestClock.cs b/Timeline.Tests/Helpers/TestClock.cs deleted file mode 100644 index 12b320d3..00000000 --- a/Timeline.Tests/Helpers/TestClock.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using Timeline.Services; - -namespace Timeline.Tests.Helpers -{ - public class TestClock : IClock - { - public DateTime? MockCurrentTime { get; set; } = null; - - public DateTime GetCurrentTime() - { - return MockCurrentTime.GetValueOrDefault(DateTime.Now); - } - } -} diff --git a/Timeline.Tests/Helpers/TestDatabase.cs b/Timeline.Tests/Helpers/TestDatabase.cs deleted file mode 100644 index e29a71fa..00000000 --- a/Timeline.Tests/Helpers/TestDatabase.cs +++ /dev/null @@ -1,89 +0,0 @@ -using Microsoft.Data.Sqlite; -using Microsoft.EntityFrameworkCore; -using System; -using System.Collections.Generic; -using System.Linq; -using Timeline.Entities; -using Timeline.Models; -using Timeline.Services; - -namespace Timeline.Tests.Helpers -{ - public class TestDatabase : IDisposable - { - // currently password service is thread safe, so we share a static one. - private static PasswordService PasswordService { get; } = new PasswordService(); - - private static UserEntity CreateEntityFromMock(MockUser user) - { - return new UserEntity - { - Username = user.Username, - Password = PasswordService.HashPassword(user.Password), - Roles = UserRoleConvert.ToString(user.Administrator), - Avatar = null - }; - } - - private static IEnumerable<UserEntity> CreateDefaultMockEntities() - { - // emmmmmmm. Never reuse the user instances because EF Core uses them, which will cause strange things. - yield return CreateEntityFromMock(MockUser.User); - yield return CreateEntityFromMock(MockUser.Admin); - } - - private static void InitDatabase(DatabaseContext context) - { - context.Database.EnsureCreated(); - context.Users.AddRange(CreateDefaultMockEntities()); - context.SaveChanges(); - } - - public SqliteConnection Connection { get; } - public DatabaseContext Context { get; } - - public TestDatabase() - { - Connection = new SqliteConnection("Data Source=:memory:;"); - Connection.Open(); - - var options = new DbContextOptionsBuilder<DevelopmentDatabaseContext>() - .UseSqlite(Connection) - .Options; - - Context = new DevelopmentDatabaseContext(options); - - InitDatabase(Context); - } - - private List<MockUser> _extraMockUsers; - - public IReadOnlyList<MockUser> ExtraMockUsers => _extraMockUsers; - - public void CreateExtraMockUsers(int count) - { - if (count <= 0) - throw new ArgumentOutOfRangeException(nameof(count), count, "Additional user count must be bigger than 0."); - if (_extraMockUsers != null) - throw new InvalidOperationException("Already create mock users."); - - _extraMockUsers = new List<MockUser>(); - for (int i = 0; i < count; i++) - { - _extraMockUsers.Add(new MockUser($"user{i}", $"password", false)); - } - - Context.AddRange(_extraMockUsers.Select(u => CreateEntityFromMock(u))); - Context.SaveChanges(); - } - - public void Dispose() - { - Context.Dispose(); - - Connection.Close(); - Connection.Dispose(); - } - - } -} diff --git a/Timeline.Tests/Helpers/UseCultureAttribute.cs b/Timeline.Tests/Helpers/UseCultureAttribute.cs deleted file mode 100644 index 017d77a8..00000000 --- a/Timeline.Tests/Helpers/UseCultureAttribute.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System; -using System.Globalization; -using System.Reflection; -using System.Threading; -using Xunit.Sdk; - - -namespace Timeline.Tests.Helpers -{ - // Copied from https://github.com/xunit/samples.xunit/blob/master/UseCulture/UseCultureAttribute.cs - - /// <summary> - /// Apply this attribute to your test method to replace the - /// <see cref="Thread.CurrentThread" /> <see cref="CultureInfo.CurrentCulture" /> and - /// <see cref="CultureInfo.CurrentUICulture" /> with another culture. - /// </summary> - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] - public class UseCultureAttribute : BeforeAfterTestAttribute - { - readonly Lazy<CultureInfo> culture; - readonly Lazy<CultureInfo> uiCulture; - - CultureInfo originalCulture; - CultureInfo originalUICulture; - - /// <summary> - /// Replaces the culture and UI culture of the current thread with - /// <paramref name="culture" /> - /// </summary> - /// <param name="culture">The name of the culture.</param> - /// <remarks> - /// <para> - /// This constructor overload uses <paramref name="culture" /> for both - /// <see cref="Culture" /> and <see cref="UICulture" />. - /// </para> - /// </remarks> - public UseCultureAttribute(string culture) - : this(culture, culture) { } - - /// <summary> - /// Replaces the culture and UI culture of the current thread with - /// <paramref name="culture" /> and <paramref name="uiCulture" /> - /// </summary> - /// <param name="culture">The name of the culture.</param> - /// <param name="uiCulture">The name of the UI culture.</param> - public UseCultureAttribute(string culture, string uiCulture) - { - this.culture = new Lazy<CultureInfo>(() => new CultureInfo(culture, false)); - this.uiCulture = new Lazy<CultureInfo>(() => new CultureInfo(uiCulture, false)); - } - - /// <summary> - /// Gets the culture. - /// </summary> - public CultureInfo Culture { get { return culture.Value; } } - - /// <summary> - /// Gets the UI culture. - /// </summary> - public CultureInfo UICulture { get { return uiCulture.Value; } } - - /// <summary> - /// Stores the current <see cref="Thread.CurrentPrincipal" /> - /// <see cref="CultureInfo.CurrentCulture" /> and <see cref="CultureInfo.CurrentUICulture" /> - /// and replaces them with the new cultures defined in the constructor. - /// </summary> - /// <param name="methodUnderTest">The method under test</param> - public override void Before(MethodInfo methodUnderTest) - { - originalCulture = Thread.CurrentThread.CurrentCulture; - originalUICulture = Thread.CurrentThread.CurrentUICulture; - - Thread.CurrentThread.CurrentCulture = Culture; - Thread.CurrentThread.CurrentUICulture = UICulture; - - CultureInfo.CurrentCulture.ClearCachedData(); - CultureInfo.CurrentUICulture.ClearCachedData(); - } - - /// <summary> - /// Restores the original <see cref="CultureInfo.CurrentCulture" /> and - /// <see cref="CultureInfo.CurrentUICulture" /> to <see cref="Thread.CurrentPrincipal" /> - /// </summary> - /// <param name="methodUnderTest">The method under test</param> - public override void After(MethodInfo methodUnderTest) - { - Thread.CurrentThread.CurrentCulture = originalCulture; - Thread.CurrentThread.CurrentUICulture = originalUICulture; - - CultureInfo.CurrentCulture.ClearCachedData(); - CultureInfo.CurrentUICulture.ClearCachedData(); - } - } -} diff --git a/Timeline.Tests/IntegratedTests/AuthorizationTest.cs b/Timeline.Tests/IntegratedTests/AuthorizationTest.cs index 0bc094af..4aa6b3ae 100644 --- a/Timeline.Tests/IntegratedTests/AuthorizationTest.cs +++ b/Timeline.Tests/IntegratedTests/AuthorizationTest.cs @@ -22,7 +22,7 @@ namespace Timeline.Tests.IntegratedTests [Fact]
public async Task UnauthenticationTest()
{
- using var client = await CreateClientWithNoAuth();
+ using var client = await CreateDefaultClient();
var response = await client.GetAsync(AuthorizeUrl);
response.Should().HaveStatusCode(HttpStatusCode.Unauthorized);
}
@@ -48,7 +48,7 @@ namespace Timeline.Tests.IntegratedTests [Fact]
public async Task AdminAuthorizationTest()
{
- using var client = await CreateClientAsAdmin();
+ using var client = await CreateClientAsAdministrator();
var response1 = await client.GetAsync(UserUrl);
response1.Should().HaveStatusCode(HttpStatusCode.OK);
var response2 = await client.GetAsync(AdminUrl);
diff --git a/Timeline.Tests/IntegratedTests/I18nTest.cs b/Timeline.Tests/IntegratedTests/I18nTest.cs deleted file mode 100644 index 855179af..00000000 --- a/Timeline.Tests/IntegratedTests/I18nTest.cs +++ /dev/null @@ -1,59 +0,0 @@ -using FluentAssertions;
-using Microsoft.AspNetCore.Mvc.Testing;
-using System;
-using System.Net.Http;
-using System.Net.Http.Headers;
-using System.Threading.Tasks;
-using Timeline.Tests.Helpers;
-using Xunit;
-
-namespace Timeline.Tests.IntegratedTests
-{
- [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1054:Uri parameters should not be strings")]
- public class I18nTest : IntegratedTestBase
- {
- private readonly HttpClient _client;
-
- public I18nTest(WebApplicationFactory<Startup> factory)
- : base(factory)
- {
- _client = Factory.CreateDefaultClient();
- }
-
- protected override void OnDispose()
- {
- _client.Dispose();
- }
-
- private const string DirectUrl = "testing/i18n/direct";
- private const string LocalizerUrl = "testing/i18n/localizer";
-
- [Theory]
- [InlineData(DirectUrl)]
- [InlineData(LocalizerUrl)]
- public async Task DefaultShouldReturnEnglish(string url)
- {
- (await _client.GetStringAsync(url)).Should().ContainEquivalentOf("English");
- }
-
- [Theory]
- [InlineData(DirectUrl, "en", true)]
- [InlineData(LocalizerUrl, "en", true)]
- [InlineData(DirectUrl, "en-US", true)]
- [InlineData(LocalizerUrl, "en-US", true)]
- [InlineData(DirectUrl, "zh", false)]
- [InlineData(LocalizerUrl, "zh", false)]
- public async Task ShouldWork(string url, string acceptLanguage, bool english)
- {
- var request = new HttpRequestMessage
- {
- Method = HttpMethod.Get,
- RequestUri = new Uri(url, UriKind.RelativeOrAbsolute)
- };
- request.Headers.AcceptLanguage.Add(new StringWithQualityHeaderValue(acceptLanguage));
- var body = await (await _client.SendAsync(request)).Content.ReadAsStringAsync();
- body.Should().ContainEquivalentOf(english ? "English" : "中文");
- request.Dispose();
- }
- }
-}
diff --git a/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs b/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs index 242a452d..721a25af 100644 --- a/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs +++ b/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs @@ -1,36 +1,17 @@ -using Microsoft.AspNetCore.Mvc.Testing; +using AutoMapper;
+using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection;
using System; -using System.Collections.Generic; +using System.Collections.Generic;
using System.Net.Http; using System.Threading.Tasks; using Timeline.Models.Http; +using Timeline.Services;
using Timeline.Tests.Helpers; using Xunit; namespace Timeline.Tests.IntegratedTests { - public enum AuthType - { - None, - User, - Admin - } - - public static class AuthTypeExtensions - { - public static MockUser GetMockUser(this AuthType authType) - { - return authType switch - { - AuthType.None => null, - AuthType.User => MockUser.User, - AuthType.Admin => MockUser.Admin, - _ => throw new InvalidOperationException("Unknown auth type.") - }; - } - - public static string GetUsername(this AuthType authType) => authType.GetMockUser().Username; - } public abstract class IntegratedTestBase : IClassFixture<WebApplicationFactory<Startup>>, IDisposable { @@ -38,14 +19,62 @@ namespace Timeline.Tests.IntegratedTests protected WebApplicationFactory<Startup> Factory => TestApp.Factory; - public IntegratedTestBase(WebApplicationFactory<Startup> factory) + public IntegratedTestBase(WebApplicationFactory<Startup> factory) : this(factory, 1) + { + + }
+
+ public IntegratedTestBase(WebApplicationFactory<Startup> factory, int userCount) { + if (userCount < 0) + throw new ArgumentOutOfRangeException(nameof(userCount), userCount, "User count can't be negative."); + TestApp = new TestApplication(factory); + + using (var scope = Factory.Services.CreateScope())
+ {
+ var users = new List<User>()
+ {
+ new User
+ {
+ Username = "admin",
+ Password = "adminpw",
+ Administrator = true,
+ Nickname = "administrator"
+ }
+ };
+
+ for (int i = 1; i <= userCount; i++)
+ {
+ users.Add(new User
+ {
+ Username = $"user{i}",
+ Password = $"user{i}pw",
+ Administrator = false,
+ Nickname = $"imuser{i}"
+ });
+ }
+
+ var userInfoList = new List<UserInfo>();
+ var userInfoForAdminList = new List<UserInfoForAdmin>();
+
+ var userService = scope.ServiceProvider.GetRequiredService<IUserService>();
+ var mapper = scope.ServiceProvider.GetRequiredService<IMapper>();
+
+ foreach (var user in users)
+ {
+ userService.CreateUser(user);
+ userInfoList.Add(mapper.Map<UserInfo>(user));
+ userInfoForAdminList.Add(mapper.Map<UserInfoForAdmin>(user));
+ }
+
+ UserInfoList = userInfoList;
+ UserInfoForAdminList = userInfoForAdminList;
+ }
} protected virtual void OnDispose() { - } public void Dispose() @@ -54,14 +83,11 @@ namespace Timeline.Tests.IntegratedTests TestApp.Dispose(); } - protected void CreateExtraMockUsers(int count) - { - TestApp.Database.CreateExtraMockUsers(count); - } + public IReadOnlyList<UserInfo> UserInfoList { get; } - protected IReadOnlyList<MockUser> ExtraMockUsers => TestApp.Database.ExtraMockUsers; + public IReadOnlyList<UserInfoForAdmin> UserInfoForAdminList { get; } - public Task<HttpClient> CreateClientWithNoAuth() + public Task<HttpClient> CreateDefaultClient() { return Task.FromResult(Factory.CreateDefaultClient()); } @@ -77,18 +103,25 @@ namespace Timeline.Tests.IntegratedTests return client; } - public Task<HttpClient> CreateClientAs(MockUser user) + public Task<HttpClient> CreateClientAs(int userNumber) { - if (user == null) - return CreateClientWithNoAuth(); - return CreateClientWithCredential(user.Username, user.Password); - } - - public Task<HttpClient> CreateClientAs(AuthType authType) => CreateClientAs(authType.GetMockUser()); - + if (userNumber < 0) + throw new ArgumentOutOfRangeException(nameof(userNumber), "User number can't be negative."); - public Task<HttpClient> CreateClientAsUser() => CreateClientAs(MockUser.User); - public Task<HttpClient> CreateClientAsAdmin() => CreateClientAs(MockUser.Admin); + if (userNumber == 0) + return CreateClientWithCredential("admin", "adminpw"); + else + return CreateClientWithCredential($"user{userNumber}", $"user{userNumber}pw"); + } + public Task<HttpClient> CreateClientAsAdministrator()
+ {
+ return CreateClientAs(0);
+ }
+
+ public Task<HttpClient> CreateClientAsUser()
+ {
+ return CreateClientAs(1);
+ } } } diff --git a/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs b/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs index 51e2d05e..14600659 100644 --- a/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs +++ b/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Threading.Tasks; -using Timeline.Models; using Timeline.Models.Http; using Timeline.Tests.Helpers; using Xunit; @@ -15,7 +14,7 @@ namespace Timeline.Tests.IntegratedTests public class PersonalTimelineTest : IntegratedTestBase { public PersonalTimelineTest(WebApplicationFactory<Startup> factory) - : base(factory) + : base(factory, 3) { } @@ -23,11 +22,11 @@ namespace Timeline.Tests.IntegratedTests [Fact] public async Task TimelineGet_Should_Work() { - using var client = await CreateClientWithNoAuth(); - var res = await client.GetAsync("users/user/timeline"); + using var client = await CreateDefaultClient(); + var res = await client.GetAsync("users/user1/timeline"); var body = res.Should().HaveStatusCode(200) .And.HaveJsonBody<BaseTimelineInfo>().Which; - body.Owner.Should().Be("user"); + body.Owner.Should().BeEquivalentTo(UserInfoList[1]); body.Visibility.Should().Be(TimelineVisibility.Register); body.Description.Should().Be(""); body.Members.Should().NotBeNull().And.BeEmpty(); @@ -40,7 +39,7 @@ namespace Timeline.Tests.IntegratedTests async Task AssertDescription(string description) { - var res = await client.GetAsync("users/user/timeline"); + var res = await client.GetAsync("users/user1/timeline"); var body = res.Should().HaveStatusCode(200) .And.HaveJsonBody<BaseTimelineInfo>() .Which.Description.Should().Be(description); @@ -50,20 +49,20 @@ namespace Timeline.Tests.IntegratedTests await AssertDescription(""); { - var res = await client.PostAsJsonAsync("users/user/timeline/op/property", - new TimelinePropertyChangeRequest { Description = mockDescription }); + var res = await client.PatchAsJsonAsync("users/user1/timeline", + new TimelinePatchRequest { Description = mockDescription }); res.Should().HaveStatusCode(200); await AssertDescription(mockDescription); } { - var res = await client.PostAsJsonAsync("users/user/timeline/op/property", - new TimelinePropertyChangeRequest { Description = null }); + var res = await client.PatchAsJsonAsync("users/user1/timeline", + new TimelinePatchRequest { Description = null }); res.Should().HaveStatusCode(200); await AssertDescription(mockDescription); } { - var res = await client.PostAsJsonAsync("users/user/timeline/op/property", - new TimelinePropertyChangeRequest { Description = "" }); + var res = await client.PatchAsJsonAsync("users/user1/timeline", + new TimelinePatchRequest { Description = "" }); res.Should().HaveStatusCode(200); await AssertDescription(""); } @@ -141,13 +140,13 @@ namespace Timeline.Tests.IntegratedTests { var res = await client.PostAsJsonAsync("users/user/timeline/op/property", - new TimelinePropertyChangeRequest { Description = "hahaha" }); + new TimelinePatchRequest { Description = "hahaha" }); res.Should().HaveStatusCode(opPropertyUser); } { var res = await client.PostAsJsonAsync("users/admin/timeline/op/property", - new TimelinePropertyChangeRequest { Description = "hahaha" }); + new TimelinePatchRequest { Description = "hahaha" }); res.Should().HaveStatusCode(opPropertyAdmin); } @@ -193,7 +192,7 @@ namespace Timeline.Tests.IntegratedTests { using var client = await CreateClientAsUser(); var res = await client.PostAsJsonAsync("users/user/timeline/op/property", - new TimelinePropertyChangeRequest { Visibility = TimelineVisibility.Public }); + new TimelinePatchRequest { Visibility = TimelineVisibility.Public }); res.Should().HaveStatusCode(200); } { @@ -208,12 +207,12 @@ namespace Timeline.Tests.IntegratedTests using var client = await CreateClientAsAdmin(); { var res = await client.PostAsJsonAsync("users/user/timeline/op/property", - new TimelinePropertyChangeRequest { Visibility = TimelineVisibility.Private }); + new TimelinePatchRequest { Visibility = TimelineVisibility.Private }); res.Should().HaveStatusCode(200); } { var res = await client.PostAsJsonAsync("users/admin/timeline/op/property", - new TimelinePropertyChangeRequest { Visibility = TimelineVisibility.Private }); + new TimelinePatchRequest { Visibility = TimelineVisibility.Private }); res.Should().HaveStatusCode(200); } } @@ -331,7 +330,7 @@ namespace Timeline.Tests.IntegratedTests } { // self can delete self - var postId = await CreatePost(MockUser.User, "user"); + var postId = await CreatePost(MockUser.Ordinary, "user"); using var client = await CreateClientAsUser(); var res = await client.PostAsJsonAsync("users/user/timeline/postop/delete", new TimelinePostDeleteRequest { Id = postId }); @@ -339,7 +338,7 @@ namespace Timeline.Tests.IntegratedTests } { // admin can delete any - var postId = await CreatePost(MockUser.User, "user"); + var postId = await CreatePost(MockUser.Ordinary, "user"); using var client = await CreateClientAsAdmin(); var res = await client.PostAsJsonAsync("users/user/timeline/postop/delete", new TimelinePostDeleteRequest { Id = postId }); diff --git a/Timeline.Tests/IntegratedTests/TokenTest.cs b/Timeline.Tests/IntegratedTests/TokenTest.cs index ecd5d0b8..8ee19999 100644 --- a/Timeline.Tests/IntegratedTests/TokenTest.cs +++ b/Timeline.Tests/IntegratedTests/TokenTest.cs @@ -53,7 +53,7 @@ namespace Timeline.Tests.IntegratedTests public static IEnumerable<object[]> CreateToken_UserCredential_Data()
{
yield return new[] { "usernotexist", "p" };
- yield return new[] { MockUser.User.Username, "???" };
+ yield return new[] { MockUser.Ordinary.Username, "???" };
}
[Theory]
@@ -73,11 +73,11 @@ namespace Timeline.Tests.IntegratedTests {
using var client = await CreateClientWithNoAuth();
var response = await client.PostAsJsonAsync(CreateTokenUrl,
- new CreateTokenRequest { Username = MockUser.User.Username, Password = MockUser.User.Password });
+ new CreateTokenRequest { Username = MockUser.Ordinary.Username, Password = MockUser.Ordinary.Password });
var body = response.Should().HaveStatusCode(200)
.And.HaveJsonBody<CreateTokenResponse>().Which;
body.Token.Should().NotBeNullOrWhiteSpace();
- body.User.Should().BeEquivalentTo(MockUser.User.Info);
+ body.User.Should().BeEquivalentTo(MockUser.Ordinary.Info);
}
[Fact]
@@ -103,13 +103,13 @@ namespace Timeline.Tests.IntegratedTests public async Task VerifyToken_OldVersion()
{
using var client = await CreateClientWithNoAuth();
- var token = (await CreateUserTokenAsync(client, MockUser.User.Username, MockUser.User.Password)).Token;
+ var token = (await CreateUserTokenAsync(client, MockUser.Ordinary.Username, MockUser.Ordinary.Password)).Token;
using (var scope = Factory.Server.Host.Services.CreateScope()) // UserService is scoped.
{
// create a user for test
var userService = scope.ServiceProvider.GetRequiredService<IUserService>();
- await userService.PatchUser(MockUser.User.Username, null, null);
+ await userService.PatchUser(MockUser.Ordinary.Username, null, null);
}
(await client.PostAsJsonAsync(VerifyTokenUrl,
@@ -123,12 +123,12 @@ namespace Timeline.Tests.IntegratedTests public async Task VerifyToken_UserNotExist()
{
using var client = await CreateClientWithNoAuth();
- var token = (await CreateUserTokenAsync(client, MockUser.User.Username, MockUser.User.Password)).Token;
+ var token = (await CreateUserTokenAsync(client, MockUser.Ordinary.Username, MockUser.Ordinary.Password)).Token;
using (var scope = Factory.Server.Host.Services.CreateScope()) // UserService is scoped.
{
var userService = scope.ServiceProvider.GetRequiredService<IUserService>();
- await userService.DeleteUser(MockUser.User.Username);
+ await userService.DeleteUser(MockUser.Ordinary.Username);
}
(await client.PostAsJsonAsync(VerifyTokenUrl,
@@ -160,12 +160,12 @@ namespace Timeline.Tests.IntegratedTests public async Task VerifyToken_Success()
{
using var client = await CreateClientWithNoAuth();
- var createTokenResult = await CreateUserTokenAsync(client, MockUser.User.Username, MockUser.User.Password);
+ var createTokenResult = await CreateUserTokenAsync(client, MockUser.Ordinary.Username, MockUser.Ordinary.Password);
var response = await client.PostAsJsonAsync(VerifyTokenUrl,
new VerifyTokenRequest { Token = createTokenResult.Token });
response.Should().HaveStatusCode(200)
.And.HaveJsonBody<VerifyTokenResponse>()
- .Which.User.Should().BeEquivalentTo(MockUser.User.Info);
+ .Which.User.Should().BeEquivalentTo(MockUser.Ordinary.Info);
}
}
}
diff --git a/Timeline.Tests/IntegratedTests/UserAvatarTest.cs b/Timeline.Tests/IntegratedTests/UserAvatarTest.cs index a4e10634..989207e2 100644 --- a/Timeline.Tests/IntegratedTests/UserAvatarTest.cs +++ b/Timeline.Tests/IntegratedTests/UserAvatarTest.cs @@ -29,7 +29,6 @@ namespace Timeline.Tests.IntegratedTests }
[Fact]
- [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "HttpMessageRequest should be disposed ???")]
public async Task Test()
{
Avatar mockAvatar = new Avatar
@@ -61,7 +60,7 @@ namespace Timeline.Tests.IntegratedTests EntityTagHeaderValue eTag;
{
- var res = await client.GetAsync($"users/user/avatar");
+ var res = await client.GetAsync($"users/user1/avatar");
res.Should().HaveStatusCode(200);
res.Content.Headers.ContentType.MediaType.Should().Be("image/png");
var body = await res.Content.ReadAsByteArrayAsync();
@@ -78,7 +77,7 @@ namespace Timeline.Tests.IntegratedTests {
var request = new HttpRequestMessage()
{
- RequestUri = new Uri(client.BaseAddress, "users/user/avatar"),
+ RequestUri = new Uri(client.BaseAddress, "users/user1/avatar"),
Method = HttpMethod.Get,
};
request.Headers.TryAddWithoutValidation("If-None-Match", "\"dsdfd");
@@ -90,7 +89,7 @@ namespace Timeline.Tests.IntegratedTests {
var request = new HttpRequestMessage()
{
- RequestUri = new Uri(client.BaseAddress, "users/user/avatar"),
+ RequestUri = new Uri(client.BaseAddress, "users/user1/avatar"),
Method = HttpMethod.Get,
};
request.Headers.TryAddWithoutValidation("If-None-Match", "\"aaa\"");
@@ -101,7 +100,7 @@ namespace Timeline.Tests.IntegratedTests {
var request = new HttpRequestMessage()
{
- RequestUri = new Uri(client.BaseAddress, "users/user/avatar"),
+ RequestUri = new Uri(client.BaseAddress, "users/user1/avatar"),
Method = HttpMethod.Get,
};
request.Headers.Add("If-None-Match", eTag.ToString());
@@ -112,7 +111,7 @@ namespace Timeline.Tests.IntegratedTests {
var content = new ByteArrayContent(new[] { (byte)0x00 });
content.Headers.ContentType = new MediaTypeHeaderValue("image/png");
- var res = await client.PutAsync("users/user/avatar", content);
+ var res = await client.PutAsync("users/user1/avatar", content);
res.Should().HaveStatusCode(HttpStatusCode.BadRequest)
.And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Common.Header.ContentLength_Missing); ;
}
@@ -120,7 +119,7 @@ namespace Timeline.Tests.IntegratedTests {
var content = new ByteArrayContent(new[] { (byte)0x00 });
content.Headers.ContentLength = 1;
- var res = await client.PutAsync("users/user/avatar", content);
+ var res = await client.PutAsync("users/user1/avatar", content);
res.Should().HaveStatusCode(HttpStatusCode.BadRequest)
.And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Common.Header.ContentType_Missing);
}
@@ -129,13 +128,13 @@ namespace Timeline.Tests.IntegratedTests var content = new ByteArrayContent(new[] { (byte)0x00 });
content.Headers.ContentLength = 0;
content.Headers.ContentType = new MediaTypeHeaderValue("image/png");
- var res = await client.PutAsync("users/user/avatar", content);
+ var res = await client.PutAsync("users/user1/avatar", content);
res.Should().HaveStatusCode(HttpStatusCode.BadRequest)
.And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Common.Header.ContentLength_Zero);
}
{
- var res = await client.PutByteArrayAsync("users/user/avatar", new[] { (byte)0x00 }, "image/notaccept");
+ var res = await client.PutByteArrayAsync("users/user1/avatar", new[] { (byte)0x00 }, "image/notaccept");
res.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType);
}
@@ -143,7 +142,7 @@ namespace Timeline.Tests.IntegratedTests var content = new ByteArrayContent(new[] { (byte)0x00 });
content.Headers.ContentLength = 1000 * 1000 * 11;
content.Headers.ContentType = new MediaTypeHeaderValue("image/png");
- var res = await client.PutAsync("users/user/avatar", content);
+ var res = await client.PutAsync("users/user1/avatar", content);
res.Should().HaveStatusCode(HttpStatusCode.BadRequest)
.And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Common.Content.TooBig);
}
@@ -152,7 +151,7 @@ namespace Timeline.Tests.IntegratedTests var content = new ByteArrayContent(new[] { (byte)0x00 });
content.Headers.ContentLength = 2;
content.Headers.ContentType = new MediaTypeHeaderValue("image/png");
- var res = await client.PutAsync("users/user/avatar", content);
+ var res = await client.PutAsync("users/user1/avatar", content);
res.Should().HaveStatusCode(HttpStatusCode.BadRequest)
.And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Common.Content.UnmatchedLength_Smaller);
}
@@ -161,34 +160,34 @@ namespace Timeline.Tests.IntegratedTests var content = new ByteArrayContent(new[] { (byte)0x00, (byte)0x01 });
content.Headers.ContentLength = 1;
content.Headers.ContentType = new MediaTypeHeaderValue("image/png");
- var res = await client.PutAsync("users/user/avatar", content);
+ var res = await client.PutAsync("users/user1/avatar", content);
res.Should().HaveStatusCode(HttpStatusCode.BadRequest)
.And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Common.Content.UnmatchedLength_Bigger);
}
{
- var res = await client.PutByteArrayAsync("users/user/avatar", new[] { (byte)0x00 }, "image/png");
+ var res = await client.PutByteArrayAsync("users/user1/avatar", new[] { (byte)0x00 }, "image/png");
res.Should().HaveStatusCode(HttpStatusCode.BadRequest)
.And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.UserAvatar.BadFormat_CantDecode);
}
{
- var res = await client.PutByteArrayAsync("users/user/avatar", mockAvatar.Data, "image/jpeg");
+ var res = await client.PutByteArrayAsync("users/user1/avatar", mockAvatar.Data, "image/jpeg");
res.Should().HaveStatusCode(HttpStatusCode.BadRequest)
.And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.UserAvatar.BadFormat_UnmatchedFormat);
}
{
- var res = await client.PutByteArrayAsync("users/user/avatar", ImageHelper.CreatePngWithSize(100, 200), "image/png");
+ var res = await client.PutByteArrayAsync("users/user1/avatar", ImageHelper.CreatePngWithSize(100, 200), "image/png");
res.Should().HaveStatusCode(HttpStatusCode.BadRequest)
.And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.UserAvatar.BadFormat_BadSize);
}
{
- var res = await client.PutByteArrayAsync("users/user/avatar", mockAvatar.Data, mockAvatar.Type);
+ var res = await client.PutByteArrayAsync("users/user1/avatar", mockAvatar.Data, mockAvatar.Type);
res.Should().HaveStatusCode(HttpStatusCode.OK);
- var res2 = await client.GetAsync("users/user/avatar");
+ var res2 = await client.GetAsync("users/user1/avatar");
res2.Should().HaveStatusCode(200);
res2.Content.Headers.ContentType.MediaType.Should().Be(mockAvatar.Type);
var body = await res2.Content.ReadAsByteArrayAsync();
@@ -204,7 +203,7 @@ namespace Timeline.Tests.IntegratedTests foreach ((var mimeType, var format) in formats)
{
- var res = await client.PutByteArrayAsync("users/user/avatar", ImageHelper.CreateImageWithSize(100, 100, format), mimeType);
+ var res = await client.PutByteArrayAsync("users/user1/avatar", ImageHelper.CreateImageWithSize(100, 100, format), mimeType);
res.Should().HaveStatusCode(HttpStatusCode.OK);
}
@@ -222,22 +221,22 @@ namespace Timeline.Tests.IntegratedTests for (int i = 0; i < 2; i++) // double delete should work.
{
- var res = await client.DeleteAsync("users/user/avatar");
+ var res = await client.DeleteAsync("users/user1/avatar");
res.Should().HaveStatusCode(200);
await GetReturnDefault();
}
}
// Authorization check.
- using (var client = await CreateClientAsAdmin())
+ using (var client = await CreateClientAsAdministrator())
{
{
- var res = await client.PutByteArrayAsync("users/user/avatar", mockAvatar.Data, mockAvatar.Type);
+ var res = await client.PutByteArrayAsync("users/user1/avatar", mockAvatar.Data, mockAvatar.Type);
res.Should().HaveStatusCode(HttpStatusCode.OK);
}
{
- var res = await client.DeleteAsync("users/user/avatar");
+ var res = await client.DeleteAsync("users/user1/avatar");
res.Should().HaveStatusCode(HttpStatusCode.OK);
}
@@ -256,7 +255,7 @@ namespace Timeline.Tests.IntegratedTests }
// bad username check
- using (var client = await CreateClientAsAdmin())
+ using (var client = await CreateClientAsAdministrator())
{
{
var res = await client.GetAsync("users/u!ser/avatar");
diff --git a/Timeline.Tests/IntegratedTests/UserDetailTest.cs b/Timeline.Tests/IntegratedTests/UserDetailTest.cs deleted file mode 100644 index 3781a816..00000000 --- a/Timeline.Tests/IntegratedTests/UserDetailTest.cs +++ /dev/null @@ -1,154 +0,0 @@ -using FluentAssertions;
-using Microsoft.AspNetCore.Mvc.Testing;
-using System.Net;
-using System.Net.Http.Headers;
-using System.Net.Mime;
-using System.Threading.Tasks;
-using Timeline.Models.Http;
-using Timeline.Tests.Helpers;
-using Xunit;
-
-namespace Timeline.Tests.IntegratedTests
-{
- public class UserDetailTest : IntegratedTestBase
- {
- public UserDetailTest(WebApplicationFactory<Startup> factory)
- : base(factory)
- {
-
- }
-
- [Fact]
- public async Task PermissionTest()
- {
- { // unauthorize
- using var client = await CreateClientWithNoAuth();
- { // GET
- var res = await client.GetAsync($"users/{MockUser.User.Username}/nickname");
- res.Should().HaveStatusCode(HttpStatusCode.OK);
- }
- { // PUT
- var res = await client.PutStringAsync($"users/{MockUser.User.Username}/nickname", "aaa");
- res.Should().HaveStatusCode(HttpStatusCode.Unauthorized);
- }
- { // DELETE
- var res = await client.DeleteAsync($"users/{MockUser.User.Username}/nickname");
- res.Should().HaveStatusCode(HttpStatusCode.Unauthorized);
- }
- }
- { // user
- using var client = await CreateClientAsUser();
- { // GET
- var res = await client.GetAsync($"users/{MockUser.User.Username}/nickname");
- res.Should().HaveStatusCode(HttpStatusCode.OK);
- }
- { // PUT self
- var res = await client.PutStringAsync($"users/{MockUser.User.Username}/nickname", "aaa");
- res.Should().HaveStatusCode(HttpStatusCode.OK);
- }
- { // PUT other
- var res = await client.PutStringAsync($"users/{MockUser.Admin.Username}/nickname", "aaa");
- res.Should().HaveStatusCode(HttpStatusCode.Forbidden);
- }
- { // DELETE self
- var res = await client.DeleteAsync($"users/{MockUser.User.Username}/nickname");
- res.Should().HaveStatusCode(HttpStatusCode.OK);
- }
- { // DELETE other
- var res = await client.DeleteAsync($"users/{MockUser.Admin.Username}/nickname");
- res.Should().HaveStatusCode(HttpStatusCode.Forbidden);
- }
- }
- { // user
- using var client = await CreateClientAsAdmin();
- { // PUT other
- var res = await client.PutStringAsync($"users/{MockUser.User.Username}/nickname", "aaa");
- res.Should().HaveStatusCode(HttpStatusCode.OK);
- }
- { // DELETE other
- var res = await client.DeleteAsync($"users/{MockUser.User.Username}/nickname");
- res.Should().HaveStatusCode(HttpStatusCode.OK);
- }
- }
- }
-
- [Fact]
- public async Task FunctionTest()
- {
- var url = $"users/{MockUser.User.Username}/nickname";
- var userNotExistUrl = "users/usernotexist/nickname";
- {
- using var client = await CreateClientAsUser();
- {
- var res = await client.GetAsync(userNotExistUrl);
- res.Should().HaveStatusCode(HttpStatusCode.NotFound)
- .And.HaveCommonBody()
- .Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist);
-
- }
- {
- var res = await client.GetAsync(url);
- res.Should().HaveStatusCode(HttpStatusCode.OK);
- res.Content.Headers.ContentType.Should().Be(new MediaTypeHeaderValue(MediaTypeNames.Text.Plain) { CharSet = "utf-8" });
- var body = await res.Content.ReadAsStringAsync();
- body.Should().Be(MockUser.User.Username);
- }
- {
- var res = await client.PutStringAsync(url, "");
- res.Should().BeInvalidModel();
- }
- {
- var res = await client.PutStringAsync(url, new string('a', 11));
- res.Should().BeInvalidModel();
- }
- var nickname1 = "nnn";
- var nickname2 = "nn2";
- {
- var res = await client.PutStringAsync(url, nickname1);
- res.Should().HaveStatusCode(HttpStatusCode.OK);
- (await client.GetStringAsync(url)).Should().Be(nickname1);
- }
- {
- var res = await client.PutStringAsync(url, nickname2);
- res.Should().HaveStatusCode(HttpStatusCode.OK);
- (await client.GetStringAsync(url)).Should().Be(nickname2);
- }
- {
- var res = await client.DeleteAsync(url);
- res.Should().HaveStatusCode(HttpStatusCode.OK);
- (await client.GetStringAsync(url)).Should().Be(MockUser.User.Username);
- }
- {
- var res = await client.DeleteAsync(url);
- res.Should().HaveStatusCode(HttpStatusCode.OK);
- }
- }
- {
- using var client = await CreateClientAsAdmin();
- {
- var res = await client.PutStringAsync(userNotExistUrl, "aaa");
- res.Should().HaveStatusCode(HttpStatusCode.BadRequest)
- .And.HaveCommonBody()
- .Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist);
- }
- {
- var res = await client.DeleteAsync(userNotExistUrl);
- res.Should().HaveStatusCode(HttpStatusCode.BadRequest)
- .And.HaveCommonBody()
- .Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist);
- }
- var nickname = "nnn";
- {
- var res = await client.PutStringAsync(url, nickname);
- res.Should().HaveStatusCode(HttpStatusCode.OK);
- (await client.GetStringAsync(url)).Should().Be(nickname);
- }
- {
- var res = await client.DeleteAsync(url);
- res.Should().HaveStatusCode(HttpStatusCode.OK);
- (await client.GetStringAsync(url)).Should().Be(MockUser.User.Username);
- }
- }
- }
- }
-}
diff --git a/Timeline.Tests/IntegratedTests/UserTest.cs b/Timeline.Tests/IntegratedTests/UserTest.cs index ea9f1177..4c2ccf7a 100644 --- a/Timeline.Tests/IntegratedTests/UserTest.cs +++ b/Timeline.Tests/IntegratedTests/UserTest.cs @@ -1,9 +1,9 @@ using FluentAssertions;
using Microsoft.AspNetCore.Mvc.Testing;
using System.Collections.Generic;
+using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
-using Timeline.Models;
using Timeline.Models.Http;
using Timeline.Tests.Helpers;
using Xunit;
@@ -19,102 +19,144 @@ namespace Timeline.Tests.IntegratedTests }
[Fact]
- public async Task Get_List_Success()
+ public async Task GetList_NoAuth()
{
- using var client = await CreateClientAsAdmin();
- var res = await client.GetAsync("users");
+ using var client = await CreateDefaultClient();
+ var res = await client.GetAsync("/users");
res.Should().HaveStatusCode(200)
- .And.HaveJsonBody<User[]>()
- .Which.Should().BeEquivalentTo(MockUser.UserInfoList);
+ .And.HaveJsonBody<UserInfo[]>()
+ .Which.Should().BeEquivalentTo(UserInfoList);
}
[Fact]
- public async Task Get_Single_Success()
+ public async Task GetList_User()
{
- using var client = await CreateClientAsAdmin();
- var res = await client.GetAsync("users/" + MockUser.User.Username);
+ using var client = await CreateClientAsUser();
+ var res = await client.GetAsync("/users");
res.Should().HaveStatusCode(200)
- .And.HaveJsonBody<User>()
- .Which.Should().BeEquivalentTo(MockUser.User.Info);
+ .And.HaveJsonBody<UserInfo[]>()
+ .Which.Should().BeEquivalentTo(UserInfoList);
}
[Fact]
- public async Task Get_InvalidModel()
+ public async Task GetList_Admin()
{
- using var client = await CreateClientAsAdmin();
- var res = await client.GetAsync("users/aaa!a");
- res.Should().BeInvalidModel();
+ using var client = await CreateClientAsAdministrator();
+ var res = await client.GetAsync("/users");
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<UserInfo[]>()
+ .Which.Should().BeEquivalentTo(UserInfoForAdminList);
}
[Fact]
- public async Task Get_Users_404()
+ public async Task Get_NoAuth()
{
- using var client = await CreateClientAsAdmin();
- var res = await client.GetAsync("users/usernotexist");
- res.Should().HaveStatusCode(404)
- .And.HaveCommonBody()
- .Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist);
+ using var client = await CreateDefaultClient();
+ var res = await client.GetAsync($"/users/admin");
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<UserInfo>()
+ .Which.Should().BeEquivalentTo(UserInfoList[0]);
}
- public static IEnumerable<object[]> Put_InvalidModel_Data()
+ [Fact]
+ public async Task Get_User()
{
- yield return new object[] { "aaa", null, false };
- yield return new object[] { "aaa", "p", null };
- yield return new object[] { "aa!a", "p", false };
+ using var client = await CreateClientAsUser();
+ var res = await client.GetAsync($"/users/admin");
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<UserInfo>()
+ .Which.Should().BeEquivalentTo(UserInfoList[0]);
}
- [Theory]
- [MemberData(nameof(Put_InvalidModel_Data))]
- public async Task Put_InvalidModel(string username, string password, bool? administrator)
+ [Fact]
+ public async Task Get_Admin()
{
- using var client = await CreateClientAsAdmin();
- (await client.PutAsJsonAsync("users/" + username,
- new UserPutRequest { Password = password, Administrator = administrator }))
- .Should().BeInvalidModel();
+ using var client = await CreateClientAsAdministrator();
+ var res = await client.GetAsync($"/users/user1");
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<UserInfo>()
+ .Which.Should().BeEquivalentTo(UserInfoForAdminList[1]);
}
- private async Task CheckAdministrator(HttpClient client, string username, bool administrator)
+ [Fact]
+ public async Task Get_InvalidModel()
{
- var res = await client.GetAsync("users/" + username);
- res.Should().HaveStatusCode(200)
- .And.HaveJsonBody<User>()
- .Which.Administrator.Should().Be(administrator);
+ using var client = await CreateClientAsUser();
+ var res = await client.GetAsync("/users/aaa!a");
+ res.Should().BeInvalidModel();
}
[Fact]
- public async Task Put_Modiefied()
+ public async Task Get_404()
{
- using var client = await CreateClientAsAdmin();
- var res = await client.PutAsJsonAsync("users/" + MockUser.User.Username, new UserPutRequest
+ using var client = await CreateClientAsUser();
+ var res = await client.GetAsync("/users/usernotexist");
+ res.Should().HaveStatusCode(404)
+ .And.HaveCommonBody(ErrorCodes.UserCommon.NotExist);
+ }
+
+ [Fact]
+ public async Task Patch_User()
+ {
+ using var client = await CreateClientAsUser();
{
- Password = "password",
- Administrator = false
- });
- res.Should().BePut(false);
- await CheckAdministrator(client, MockUser.User.Username, false);
+ var res = await client.PatchAsJsonAsync("/users/user1",
+ new UserPatchRequest { Nickname = "aaa" });
+ res.Should().HaveStatusCode(200);
+ }
+
+ {
+ var res = await client.GetAsync("/users/user1");
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<UserInfo>()
+ .Which.Nickname.Should().Be("aaa");
+ }
}
[Fact]
- public async Task Put_Created()
+ public async Task Patch_Admin()
{
- using var client = await CreateClientAsAdmin();
- const string username = "puttest";
- const string url = "users/" + username;
+ using var client = await CreateClientAsAdministrator();
+ using var userClient = await CreateClientAsUser();
+
+ {
+ var res = await client.PatchAsJsonAsync("/users/user1",
+ new UserPatchRequest
+ {
+ Username = "newuser",
+ Password = "newpw",
+ Administrator = true,
+ Nickname = "aaa"
+ });
+ res.Should().HaveStatusCode(200);
+ }
- var res = await client.PutAsJsonAsync(url, new UserPutRequest
{
- Password = "password",
- Administrator = false
- });
- res.Should().BePut(true);
- await CheckAdministrator(client, username, false);
+ var res = await client.GetAsync("/users/newuser");
+ var body = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<UserInfoForAdmin>()
+ .Which;
+ body.Administrator.Should().Be(true);
+ body.Nickname.Should().Be("aaa");
+ }
+
+ {
+ // Token should expire.
+ var res = await userClient.GetAsync("/users");
+ res.Should().HaveStatusCode(HttpStatusCode.Unauthorized);
+ }
+
+ {
+ // Check password.
+ (await CreateClientWithCredential("newuser", "newpw")).Dispose();
+ }
}
[Fact]
public async Task Patch_NotExist()
{
- using var client = await CreateClientAsAdmin();
- var res = await client.PatchAsJsonAsync("users/usernotexist", new UserPatchRequest { });
+ using var client = await CreateClientAsAdministrator();
+ var res = await client.PatchAsJsonAsync("/users/usernotexist", new UserPatchRequest { });
res.Should().HaveStatusCode(404)
.And.HaveCommonBody()
.Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist);
@@ -123,114 +165,239 @@ namespace Timeline.Tests.IntegratedTests [Fact]
public async Task Patch_InvalidModel()
{
- using var client = await CreateClientAsAdmin();
- var res = await client.PatchAsJsonAsync("users/aaa!a", new UserPatchRequest { });
+ using var client = await CreateClientAsAdministrator();
+ var res = await client.PatchAsJsonAsync("/users/aaa!a", new UserPatchRequest { });
+ res.Should().BeInvalidModel();
+ }
+
+ public static IEnumerable<object[]> Patch_InvalidModel_Body_Data()
+ {
+ yield return new[] { new UserPatchRequest { Username = "aaa!a" } };
+ yield return new[] { new UserPatchRequest { Password = "" } };
+ yield return new[] { new UserPatchRequest { Nickname = new string('a', 50) } };
+ }
+
+ [Theory]
+ [MemberData(nameof(Patch_InvalidModel_Body_Data))]
+ public async Task Patch_InvalidModel_Body(UserPatchRequest body)
+ {
+ using var client = await CreateClientAsAdministrator();
+ var res = await client.PatchAsJsonAsync("/users/user1", body);
res.Should().BeInvalidModel();
}
[Fact]
- public async Task Patch_Success()
+ public async Task Patch_UsernameConflict()
{
- using var client = await CreateClientAsAdmin();
- {
- var res = await client.PatchAsJsonAsync("users/" + MockUser.User.Username,
- new UserPatchRequest { Administrator = false });
- res.Should().HaveStatusCode(200);
- await CheckAdministrator(client, MockUser.User.Username, false);
- }
+ using var client = await CreateClientAsAdministrator();
+ var res = await client.PatchAsJsonAsync("/users/user1", new UserPatchRequest { Username = "admin" });
+ res.Should().HaveStatusCode(400)
+ .And.HaveCommonBody(ErrorCodes.UserController.UsernameConflict);
}
[Fact]
- public async Task Delete_InvalidModel()
+ public async Task Patch_NoAuth_Unauthorized()
{
- using var client = await CreateClientAsAdmin();
- var url = "users/aaa!a";
- var res = await client.DeleteAsync(url);
- res.Should().BeInvalidModel();
+ using var client = await CreateClientAsUser();
+ var res = await client.PatchAsJsonAsync("/users/user1", new UserPatchRequest { Nickname = "aaa" });
+ res.Should().HaveStatusCode(HttpStatusCode.Unauthorized);
+ }
+
+ [Fact]
+ public async Task Patch_User_Forbid()
+ {
+ using var client = await CreateClientAsUser();
+ var res = await client.PatchAsJsonAsync("/users/admin", new UserPatchRequest { Nickname = "aaa" });
+ res.Should().HaveStatusCode(HttpStatusCode.Forbidden);
+ }
+
+ [Fact]
+ public async Task Patch_Username_Forbid()
+ {
+ using var client = await CreateClientAsUser();
+ var res = await client.PatchAsJsonAsync("/users/user1", new UserPatchRequest { Username = "aaa" });
+ res.Should().HaveStatusCode(HttpStatusCode.Forbidden);
+ }
+
+ [Fact]
+ public async Task Patch_Password_Forbid()
+ {
+ using var client = await CreateClientAsUser();
+ var res = await client.PatchAsJsonAsync("/users/user1", new UserPatchRequest { Password = "aaa" });
+ res.Should().HaveStatusCode(HttpStatusCode.Forbidden);
+ }
+
+ [Fact]
+ public async Task Patch_Administrator_Forbid()
+ {
+ using var client = await CreateClientAsUser();
+ var res = await client.PatchAsJsonAsync("/users/user1", new UserPatchRequest { Administrator = true });
+ res.Should().HaveStatusCode(HttpStatusCode.Forbidden);
}
[Fact]
public async Task Delete_Deleted()
{
- using var client = await CreateClientAsAdmin();
- var url = "users/" + MockUser.User.Username;
- var res = await client.DeleteAsync(url);
- res.Should().BeDelete(true);
+ using var client = await CreateClientAsAdministrator();
+ {
+ var res = await client.DeleteAsync("/users/user1");
+ res.Should().BeDelete(true);
+ }
- var res2 = await client.GetAsync(url);
- res2.Should().HaveStatusCode(404);
+ {
+ var res = await client.GetAsync("/users/user1");
+ res.Should().HaveStatusCode(404);
+ }
}
[Fact]
public async Task Delete_NotExist()
{
- using var client = await CreateClientAsAdmin();
- var res = await client.DeleteAsync("users/usernotexist");
+ using var client = await CreateClientAsAdministrator();
+ var res = await client.DeleteAsync("/users/usernotexist");
res.Should().BeDelete(false);
}
- private const string changeUsernameUrl = "userop/changeusername";
+ [Fact]
+ public async Task Delete_InvalidModel()
+ {
+ using var client = await CreateClientAsAdministrator();
+ var res = await client.DeleteAsync("/users/aaa!a");
+ res.Should().BeInvalidModel();
+ }
- public static IEnumerable<object[]> Op_ChangeUsername_InvalidModel_Data()
+ [Fact]
+ public async Task Delete_NoAuth_Unauthorized()
{
- yield return new[] { null, "uuu" };
- yield return new[] { "uuu", null };
- yield return new[] { "a!a", "uuu" };
- yield return new[] { "uuu", "a!a" };
+ using var client = await CreateDefaultClient();
+ var res = await client.DeleteAsync("/users/aaa!a");
+ res.Should().HaveStatusCode(HttpStatusCode.Unauthorized);
}
- [Theory]
- [MemberData(nameof(Op_ChangeUsername_InvalidModel_Data))]
- public async Task Op_ChangeUsername_InvalidModel(string oldUsername, string newUsername)
+ [Fact]
+ public async Task Delete_User_Forbid()
{
- using var client = await CreateClientAsAdmin();
- (await client.PostAsJsonAsync(changeUsernameUrl,
- new ChangeUsernameRequest { OldUsername = oldUsername, NewUsername = newUsername }))
- .Should().BeInvalidModel();
+ using var client = await CreateClientAsUser();
+ var res = await client.DeleteAsync("/users/aaa!a");
+ res.Should().HaveStatusCode(HttpStatusCode.Forbidden);
}
+ private const string createUserUrl = "/userop/createuser";
+
[Fact]
- public async Task Op_ChangeUsername_UserNotExist()
+ public async Task Op_CreateUser()
{
- using var client = await CreateClientAsAdmin();
- var res = await client.PostAsJsonAsync(changeUsernameUrl,
- new ChangeUsernameRequest { OldUsername = "usernotexist", NewUsername = "newUsername" });
- res.Should().HaveStatusCode(400)
- .And.HaveCommonBody()
- .Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist);
+ using var client = await CreateClientAsAdministrator();
+ {
+ var res = await client.PostAsJsonAsync(createUserUrl, new CreateUserRequest
+ {
+ Username = "aaa",
+ Password = "bbb",
+ Administrator = true,
+ Nickname = "ccc"
+ });
+ res.Should().HaveStatusCode(200);
+ }
+ {
+ var res = await client.GetAsync("users/aaa");
+ var body = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<UserInfoForAdmin>().Which;
+ body.Username.Should().Be("aaa");
+ body.Nickname.Should().Be("ccc");
+ body.Administrator.Should().BeTrue();
+ }
+ {
+ // Test password.
+ (await CreateClientWithCredential("aaa", "bbb")).Dispose();
+ }
+ }
+
+ public static IEnumerable<object[]> Op_CreateUser_InvalidModel_Data()
+ {
+ yield return new[] { new CreateUserRequest { Username = "aaa", Password = "bbb" } };
+ yield return new[] { new CreateUserRequest { Username = "aaa", Administrator = true } };
+ yield return new[] { new CreateUserRequest { Password = "bbb", Administrator = true } };
+ yield return new[] { new CreateUserRequest { Username = "a!a", Password = "bbb", Administrator = true } };
+ yield return new[] { new CreateUserRequest { Username = "aaa", Password = "", Administrator = true } };
+ yield return new[] { new CreateUserRequest { Username = "aaa", Password = "bbb", Administrator = true, Nickname = new string('a', 40) } };
+ }
+
+ [Theory]
+ [MemberData(nameof(Op_CreateUser_InvalidModel_Data))]
+ public async Task Op_CreateUser_InvalidModel(CreateUserRequest body)
+ {
+ using var client = await CreateClientAsAdministrator();
+ {
+ var res = await client.PostAsJsonAsync(createUserUrl, body);
+ res.Should().BeInvalidModel();
+ }
}
[Fact]
- public async Task Op_ChangeUsername_UserAlreadyExist()
+ public async Task Op_CreateUser_UsernameConflict()
{
- using var client = await CreateClientAsAdmin();
- var res = await client.PostAsJsonAsync(changeUsernameUrl,
- new ChangeUsernameRequest { OldUsername = MockUser.User.Username, NewUsername = MockUser.Admin.Username });
- res.Should().HaveStatusCode(400)
- .And.HaveCommonBody()
- .Which.Code.Should().Be(ErrorCodes.UserController.ChangeUsername_Conflict);
+ using var client = await CreateClientAsAdministrator();
+ {
+ var res = await client.PostAsJsonAsync(createUserUrl, new CreateUserRequest
+ {
+ Username = "user1",
+ Password = "bbb",
+ Administrator = false
+ });
+ res.Should().HaveStatusCode(400)
+ .And.HaveCommonBody(ErrorCodes.UserController.UsernameConflict);
+ }
}
- private async Task TestLogin(string username, string password)
+ [Fact]
+ public async Task Op_CreateUser_NoAuth_Unauthorized()
{
- using var client = await CreateClientWithNoAuth();
- var response = await client.PostAsJsonAsync("token/create", new CreateTokenRequest { Username = username, Password = password });
- response.Should().HaveStatusCode(200)
- .And.HaveJsonBody<CreateTokenResponse>();
+ using var client = await CreateDefaultClient();
+ {
+ var res = await client.PostAsJsonAsync(createUserUrl, new CreateUserRequest
+ {
+ Username = "aaa",
+ Password = "bbb",
+ Administrator = false
+ });
+ res.Should().HaveStatusCode(HttpStatusCode.Unauthorized);
+ }
}
[Fact]
- public async Task Op_ChangeUsername_Success()
+ public async Task Op_CreateUser_User_Forbid()
{
- using var client = await CreateClientAsAdmin();
- const string newUsername = "hahaha";
- var res = await client.PostAsJsonAsync(changeUsernameUrl,
- new ChangeUsernameRequest { OldUsername = MockUser.User.Username, NewUsername = newUsername });
- res.Should().HaveStatusCode(200);
- await TestLogin(newUsername, MockUser.User.Password);
+ using var client = await CreateClientAsUser();
+ {
+ var res = await client.PostAsJsonAsync(createUserUrl, new CreateUserRequest
+ {
+ Username = "aaa",
+ Password = "bbb",
+ Administrator = false
+ });
+ res.Should().HaveStatusCode(HttpStatusCode.Forbidden);
+ }
}
- private const string changePasswordUrl = "userop/changepassword";
+ private const string changePasswordUrl = "/userop/changepassword";
+
+ [Fact]
+ public async Task Op_ChangePassword()
+ {
+ using var client = await CreateClientAsUser();
+ {
+ var res = await client.PostAsJsonAsync(changePasswordUrl,
+ new ChangePasswordRequest { OldPassword = "user1pw", NewPassword = "newpw" });
+ res.Should().HaveStatusCode(200);
+ }
+ {
+ var res = await client.PatchAsJsonAsync("/users/user1", new UserPatchRequest { });
+ res.Should().HaveStatusCode(HttpStatusCode.Unauthorized);
+ }
+ {
+ (await CreateClientWithCredential("user1", "newpw")).Dispose();
+ }
+ }
public static IEnumerable<object[]> Op_ChangePassword_InvalidModel_Data()
{
@@ -243,9 +410,9 @@ namespace Timeline.Tests.IntegratedTests public async Task Op_ChangePassword_InvalidModel(string oldPassword, string newPassword)
{
using var client = await CreateClientAsUser();
- (await client.PostAsJsonAsync(changePasswordUrl,
- new ChangePasswordRequest { OldPassword = oldPassword, NewPassword = newPassword }))
- .Should().BeInvalidModel();
+ var res = await client.PostAsJsonAsync(changePasswordUrl,
+ new ChangePasswordRequest { OldPassword = oldPassword, NewPassword = newPassword });
+ res.Should().BeInvalidModel();
}
[Fact]
@@ -254,19 +421,15 @@ namespace Timeline.Tests.IntegratedTests using var client = await CreateClientAsUser();
var res = await client.PostAsJsonAsync(changePasswordUrl, new ChangePasswordRequest { OldPassword = "???", NewPassword = "???" });
res.Should().HaveStatusCode(400)
- .And.HaveCommonBody()
- .Which.Code.Should().Be(ErrorCodes.UserController.ChangePassword_BadOldPassword);
+ .And.HaveCommonBody(ErrorCodes.UserController.ChangePassword_BadOldPassword);
}
[Fact]
- public async Task Op_ChangePassword_Success()
+ public async Task Op_ChangePassword_NoAuth_Unauthorized()
{
- using var client = await CreateClientAsUser();
- const string newPassword = "new";
- var res = await client.PostAsJsonAsync(changePasswordUrl,
- new ChangePasswordRequest { OldPassword = MockUser.User.Password, NewPassword = newPassword });
- res.Should().HaveStatusCode(200);
- await TestLogin(MockUser.User.Username, newPassword);
+ using var client = await CreateDefaultClient();
+ var res = await client.PostAsJsonAsync(changePasswordUrl, new ChangePasswordRequest { OldPassword = "???", NewPassword = "???" });
+ res.Should().HaveStatusCode(HttpStatusCode.Unauthorized);
}
}
}
diff --git a/Timeline.Tests/Services/UserAvatarServiceTest.cs b/Timeline.Tests/Services/UserAvatarServiceTest.cs deleted file mode 100644 index 2dca7ccf..00000000 --- a/Timeline.Tests/Services/UserAvatarServiceTest.cs +++ /dev/null @@ -1,280 +0,0 @@ -using FluentAssertions;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.Extensions.Logging.Abstractions;
-using Moq;
-using SixLabors.ImageSharp.Formats.Png;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using Timeline.Entities;
-using Timeline.Services;
-using Timeline.Tests.Helpers;
-using Xunit;
-
-namespace Timeline.Tests.Services
-{
- public class UserAvatarValidatorTest : IClassFixture<UserAvatarValidator>
- {
- private readonly UserAvatarValidator _validator;
-
- public UserAvatarValidatorTest(UserAvatarValidator validator)
- {
- _validator = validator;
- }
-
- [Fact]
- public void CantDecode()
- {
- Avatar avatar = new Avatar
- {
- Data = Encoding.ASCII.GetBytes("This is not a image."),
- Type = "image/png"
- };
- _validator.Awaiting(v => v.Validate(avatar))
- .Should().Throw<AvatarFormatException>()
- .Where(e => e.Avatar == avatar && e.Error == AvatarFormatException.ErrorReason.CantDecode);
- }
-
- [Fact]
- public void UnmatchedFormat()
- {
- Avatar avatar = new Avatar
- {
- Data = ImageHelper.CreatePngWithSize(100, 100),
- Type = "image/jpeg"
- };
- _validator.Awaiting(v => v.Validate(avatar))
- .Should().Throw<AvatarFormatException>()
- .Where(e => e.Avatar == avatar && e.Error == AvatarFormatException.ErrorReason.UnmatchedFormat);
- }
-
- [Fact]
- public void BadSize()
- {
- Avatar avatar = new Avatar
- {
- Data = ImageHelper.CreatePngWithSize(100, 200),
- Type = PngFormat.Instance.DefaultMimeType
- };
- _validator.Awaiting(v => v.Validate(avatar))
- .Should().Throw<AvatarFormatException>()
- .Where(e => e.Avatar == avatar && e.Error == AvatarFormatException.ErrorReason.BadSize);
- }
-
- [Fact]
- public void Success()
- {
- Avatar avatar = new Avatar
- {
- Data = ImageHelper.CreatePngWithSize(100, 100),
- Type = PngFormat.Instance.DefaultMimeType
- };
- _validator.Awaiting(v => v.Validate(avatar))
- .Should().NotThrow();
- }
- }
-
- public class UserAvatarServiceTest : IDisposable
- {
- private UserAvatarEntity CreateMockAvatarEntity(string key) => new UserAvatarEntity
- {
- Type = $"image/test{key}",
- Data = Encoding.ASCII.GetBytes($"mock{key}"),
- ETag = $"etag{key}",
- LastModified = DateTime.Now
- };
-
- private AvatarInfo CreateMockAvatarInfo(string key) => new AvatarInfo
- {
- Avatar = new Avatar
- {
- Type = $"image/test{key}",
- Data = Encoding.ASCII.GetBytes($"mock{key}")
- },
- LastModified = DateTime.Now
- };
-
- private Avatar CreateMockAvatar(string key) => new Avatar
- {
- Type = $"image/test{key}",
- Data = Encoding.ASCII.GetBytes($"mock{key}")
- };
-
- private static Avatar ToAvatar(UserAvatarEntity entity)
- {
- return new Avatar
- {
- Data = entity.Data,
- Type = entity.Type
- };
- }
-
- private static AvatarInfo ToAvatarInfo(UserAvatarEntity entity)
- {
- return new AvatarInfo
- {
- Avatar = ToAvatar(entity),
- LastModified = entity.LastModified
- };
- }
-
- private readonly Mock<IDefaultUserAvatarProvider> _mockDefaultAvatarProvider;
- private readonly Mock<IUserAvatarValidator> _mockValidator;
- private readonly Mock<IETagGenerator> _mockETagGenerator;
- private readonly Mock<IClock> _mockClock;
-
- private readonly TestDatabase _database;
-
- private readonly UserAvatarService _service;
-
- public UserAvatarServiceTest()
- {
- _mockDefaultAvatarProvider = new Mock<IDefaultUserAvatarProvider>();
- _mockValidator = new Mock<IUserAvatarValidator>();
- _mockETagGenerator = new Mock<IETagGenerator>();
- _mockClock = new Mock<IClock>();
-
- _database = new TestDatabase();
-
- _service = new UserAvatarService(NullLogger<UserAvatarService>.Instance, _database.Context, _mockDefaultAvatarProvider.Object, _mockValidator.Object, _mockETagGenerator.Object, _mockClock.Object);
- }
-
- public void Dispose()
- {
- _database.Dispose();
- }
-
- [Theory]
- [InlineData(null, typeof(ArgumentNullException))]
- [InlineData("", typeof(UsernameBadFormatException))]
- [InlineData("a!a", typeof(UsernameBadFormatException))]
- [InlineData("usernotexist", typeof(UserNotExistException))]
- public async Task GetAvatarETag_ShouldThrow(string username, Type exceptionType)
- {
- await _service.Awaiting(s => s.GetAvatarETag(username)).Should().ThrowAsync(exceptionType);
- }
-
- [Fact]
- public async Task GetAvatarETag_ShouldReturn_Default()
- {
- const string etag = "aaaaaa";
- _mockDefaultAvatarProvider.Setup(p => p.GetDefaultAvatarETag()).ReturnsAsync(etag);
- (await _service.GetAvatarETag(MockUser.User.Username)).Should().Be(etag);
- }
-
- [Fact]
- public async Task GetAvatarETag_ShouldReturn_Data()
- {
- string username = MockUser.User.Username;
- var mockAvatarEntity = CreateMockAvatarEntity("aaa");
- {
- var context = _database.Context;
- var user = await context.Users.Where(u => u.Username == username).Include(u => u.Avatar).SingleAsync();
- user.Avatar = mockAvatarEntity;
- await context.SaveChangesAsync();
- }
- (await _service.GetAvatarETag(username)).Should().BeEquivalentTo(mockAvatarEntity.ETag);
- }
-
- [Theory]
- [InlineData(null, typeof(ArgumentNullException))]
- [InlineData("", typeof(UsernameBadFormatException))]
- [InlineData("a!a", typeof(UsernameBadFormatException))]
- [InlineData("usernotexist", typeof(UserNotExistException))]
- public async Task GetAvatar_ShouldThrow(string username, Type exceptionType)
- {
- await _service.Awaiting(s => s.GetAvatar(username)).Should().ThrowAsync(exceptionType);
-
- }
-
- [Fact]
- public async Task GetAvatar_ShouldReturn_Default()
- {
- var mockAvatar = CreateMockAvatarInfo("aaa");
- _mockDefaultAvatarProvider.Setup(p => p.GetDefaultAvatar()).ReturnsAsync(mockAvatar);
- string username = MockUser.User.Username;
- (await _service.GetAvatar(username)).Should().BeEquivalentTo(mockAvatar);
- }
-
- [Fact]
- public async Task GetAvatar_ShouldReturn_Data()
- {
- string username = MockUser.User.Username;
- var mockAvatarEntity = CreateMockAvatarEntity("aaa");
- {
- var context = _database.Context;
- var user = await context.Users.Where(u => u.Username == username).Include(u => u.Avatar).SingleAsync();
- user.Avatar = mockAvatarEntity;
- await context.SaveChangesAsync();
- }
-
- (await _service.GetAvatar(username)).Should().BeEquivalentTo(ToAvatarInfo(mockAvatarEntity));
- }
-
- public static IEnumerable<object[]> SetAvatar_ShouldThrow_Data()
- {
- yield return new object[] { null, null, typeof(ArgumentNullException) };
- yield return new object[] { "", null, typeof(UsernameBadFormatException) };
- yield return new object[] { "u!u", null, typeof(UsernameBadFormatException) };
- yield return new object[] { null, new Avatar { Type = null, Data = new[] { (byte)0x00 } }, typeof(ArgumentException) };
- yield return new object[] { null, new Avatar { Type = "", Data = new[] { (byte)0x00 } }, typeof(ArgumentException) };
- yield return new object[] { null, new Avatar { Type = "aaa", Data = null }, typeof(ArgumentException) };
- yield return new object[] { "usernotexist", null, typeof(UserNotExistException) };
- }
-
- [Theory]
- [MemberData(nameof(SetAvatar_ShouldThrow_Data))]
- public async Task SetAvatar_ShouldThrow(string username, Avatar avatar, Type exceptionType)
- {
- await _service.Awaiting(s => s.SetAvatar(username, avatar)).Should().ThrowAsync(exceptionType);
- }
-
- [Fact]
- public async Task SetAvatar_Should_Work()
- {
- string username = MockUser.User.Username;
-
- var user = await _database.Context.Users.Where(u => u.Username == username).Include(u => u.Avatar).SingleAsync();
-
- var avatar1 = CreateMockAvatar("aaa");
- var avatar2 = CreateMockAvatar("bbb");
-
- string etag1 = "etagaaa";
- string etag2 = "etagbbb";
-
- DateTime dateTime1 = DateTime.Now.AddSeconds(2);
- DateTime dateTime2 = DateTime.Now.AddSeconds(10);
- DateTime dateTime3 = DateTime.Now.AddSeconds(20);
-
- // create
- _mockETagGenerator.Setup(g => g.Generate(avatar1.Data)).ReturnsAsync(etag1);
- _mockClock.Setup(c => c.GetCurrentTime()).Returns(dateTime1);
- await _service.SetAvatar(username, avatar1);
- user.Avatar.Should().NotBeNull();
- user.Avatar.Type.Should().Be(avatar1.Type);
- user.Avatar.Data.Should().Equal(avatar1.Data);
- user.Avatar.ETag.Should().Be(etag1);
- user.Avatar.LastModified.Should().Be(dateTime1);
-
- // modify
- _mockETagGenerator.Setup(g => g.Generate(avatar2.Data)).ReturnsAsync(etag2);
- _mockClock.Setup(c => c.GetCurrentTime()).Returns(dateTime2);
- await _service.SetAvatar(username, avatar2);
- user.Avatar.Should().NotBeNull();
- user.Avatar.Type.Should().Be(avatar2.Type);
- user.Avatar.Data.Should().Equal(avatar2.Data);
- user.Avatar.ETag.Should().Be(etag2);
- user.Avatar.LastModified.Should().Be(dateTime2);
-
- // delete
- _mockClock.Setup(c => c.GetCurrentTime()).Returns(dateTime3);
- await _service.SetAvatar(username, null);
- user.Avatar.Type.Should().BeNull();
- user.Avatar.Data.Should().BeNull();
- user.Avatar.ETag.Should().BeNull();
- user.Avatar.LastModified.Should().Be(dateTime3);
- }
- }
-}
diff --git a/Timeline.Tests/Services/UserDetailServiceTest.cs b/Timeline.Tests/Services/UserDetailServiceTest.cs deleted file mode 100644 index dbff2705..00000000 --- a/Timeline.Tests/Services/UserDetailServiceTest.cs +++ /dev/null @@ -1,107 +0,0 @@ -using FluentAssertions;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.Extensions.Logging.Abstractions;
-using System;
-using System.Linq;
-using System.Threading.Tasks;
-using Timeline.Entities;
-using Timeline.Services;
-using Timeline.Tests.Helpers;
-using Xunit;
-
-namespace Timeline.Tests.Services
-{
- public class UserDetailServiceTest : IDisposable
- {
- private readonly TestDatabase _testDatabase;
-
- private readonly UserDetailService _service;
-
- public UserDetailServiceTest()
- {
- _testDatabase = new TestDatabase();
- _service = new UserDetailService(_testDatabase.Context, NullLogger<UserDetailService>.Instance);
- }
-
- public void Dispose()
- {
- _testDatabase.Dispose();
- }
-
- [Theory]
- [InlineData(null, typeof(ArgumentNullException))]
- [InlineData("", typeof(UsernameBadFormatException))]
- [InlineData("a!a", typeof(UsernameBadFormatException))]
- [InlineData("usernotexist", typeof(UserNotExistException))]
- public async Task GetNickname_ShouldThrow(string username, Type exceptionType)
- {
- await _service.Awaiting(s => s.GetNickname(username)).Should().ThrowAsync(exceptionType);
- }
-
- [Fact]
- public async Task GetNickname_ShouldReturnUsername()
- {
- var result = await _service.GetNickname(MockUser.User.Username);
- result.Should().Be(MockUser.User.Username);
- }
-
- [Fact]
- public async Task GetNickname_ShouldReturnData()
- {
- const string nickname = "aaaaaa";
- {
- var context = _testDatabase.Context;
- var userId = (await context.Users.Where(u => u.Username == MockUser.User.Username).Select(u => new { u.Id }).SingleAsync()).Id;
- context.UserDetails.Add(new UserDetailEntity
- {
- Nickname = nickname,
- UserId = userId
- });
- await context.SaveChangesAsync();
- }
- var result = await _service.GetNickname(MockUser.User.Username);
- result.Should().Be(nickname);
- }
-
- [Theory]
- [InlineData(null, typeof(ArgumentNullException))]
- [InlineData("", typeof(UsernameBadFormatException))]
- [InlineData("a!a", typeof(UsernameBadFormatException))]
- [InlineData("usernotexist", typeof(UserNotExistException))]
- public async Task SetNickname_ShouldThrow(string username, Type exceptionType)
- {
- await _service.Awaiting(s => s.SetNickname(username, null)).Should().ThrowAsync(exceptionType);
- }
-
- [Fact]
- public async Task SetNickname_ShouldThrow_ArgumentException()
- {
- await _service.Awaiting(s => s.SetNickname("uuu", new string('a', 50))).Should().ThrowAsync<ArgumentException>();
- }
-
- [Fact]
- public async Task SetNickname_ShouldWork()
- {
- var username = MockUser.User.Username;
- var user = await _testDatabase.Context.Users.Where(u => u.Username == username).Include(u => u.Detail).SingleAsync();
-
- var nickname1 = "nickname1";
- var nickname2 = "nickname2";
-
- await _service.SetNickname(username, null);
- user.Detail.Should().BeNull();
-
- await _service.SetNickname(username, nickname1);
- user.Detail.Should().NotBeNull();
- user.Detail.Nickname.Should().Be(nickname1);
-
- await _service.SetNickname(username, nickname2);
- user.Detail.Should().NotBeNull();
- user.Detail.Nickname.Should().Be(nickname2);
-
- await _service.SetNickname(username, null);
- user.Detail.Should().NotBeNull();
- user.Detail.Nickname.Should().BeNull();
- }
- }
-}
diff --git a/Timeline.Tests/Services/UserTokenManagerTest.cs b/Timeline.Tests/Services/UserTokenManagerTest.cs deleted file mode 100644 index e649fbab..00000000 --- a/Timeline.Tests/Services/UserTokenManagerTest.cs +++ /dev/null @@ -1,163 +0,0 @@ -using FluentAssertions;
-using Microsoft.Extensions.Logging.Abstractions;
-using Moq;
-using System;
-using System.Threading.Tasks;
-using Timeline.Models;
-using Timeline.Services;
-using Timeline.Tests.Helpers;
-using Xunit;
-
-namespace Timeline.Tests.Services
-{
- public class UserTokenManagerTest
- {
- private readonly UserTokenManager _service;
-
- private readonly Mock<IUserService> _mockUserService;
- private readonly Mock<IUserTokenService> _mockUserTokenService;
- private readonly TestClock _mockClock;
-
- public UserTokenManagerTest()
- {
- _mockUserService = new Mock<IUserService>();
- _mockUserTokenService = new Mock<IUserTokenService>();
- _mockClock = new TestClock();
-
- _service = new UserTokenManager(NullLogger<UserTokenManager>.Instance, _mockUserService.Object, _mockUserTokenService.Object, _mockClock);
- }
-
- [Theory]
- [InlineData(null, "aaa", "username")]
- [InlineData("aaa", null, "password")]
- public void CreateToken_NullArgument(string username, string password, string paramName)
- {
- _service.Invoking(s => s.CreateToken(username, password)).Should().Throw<ArgumentNullException>()
- .Which.ParamName.Should().Be(paramName);
- }
-
- [Theory]
- [InlineData(typeof(UsernameBadFormatException))]
- [InlineData(typeof(UserNotExistException))]
- [InlineData(typeof(BadPasswordException))]
- public async Task CreateToken_VerifyCredential_Throw(Type exceptionType)
- {
- const string username = "uuu";
- const string password = "ppp";
- _mockUserService.Setup(s => s.VerifyCredential(username, password)).ThrowsAsync((Exception)Activator.CreateInstance(exceptionType));
- await _service.Awaiting(s => s.CreateToken(username, password)).Should().ThrowAsync(exceptionType);
- }
-
- [Theory]
- [InlineData(false)]
- [InlineData(true)]
- public async Task CreateToken_Success(bool setExpireTime)
- {
- const string username = "uuu";
- const string password = "ppp";
- var mockExpireTime = setExpireTime ? (DateTime?)DateTime.Now : null;
- var mockUserInfo = new User
- {
- Id = 1,
- Username = username,
- Administrator = false,
- Version = 1
- };
- const string mockToken = "mocktokenaaaaaaa";
-
- _mockUserService.Setup(s => s.VerifyCredential(username, password)).ReturnsAsync(mockUserInfo);
- _mockUserTokenService.Setup(s => s.GenerateToken(
- It.Is<UserTokenInfo>(userTokenInfo =>
- userTokenInfo.Id == mockUserInfo.Id &&
- userTokenInfo.Version == mockUserInfo.Version &&
- userTokenInfo.ExpireAt == mockExpireTime))).Returns(mockToken);
- (await _service.CreateToken(username, password, mockExpireTime))
- .Should().BeEquivalentTo(new UserTokenCreateResult
- {
- Token = mockToken,
- User = mockUserInfo
- });
- }
-
- [Fact]
- public void VerifyToken_NullArgument()
- {
- _service.Invoking(s => s.VerifyToken(null)).Should().ThrowAsync<ArgumentNullException>();
- }
-
- [Fact]
- public async Task VerifyToken_BadFormat()
- {
- const string mockToken = "mocktokenaaaaaa";
- _mockUserTokenService.Setup(s => s.VerifyToken(mockToken)).Throws(new UserTokenBadFormatException());
-
- await _service.Awaiting(s => s.VerifyToken(mockToken)).Should().ThrowAsync<UserTokenBadFormatException>();
- }
-
- [Fact]
- public async Task VerifyToken_TimeExpire()
- {
- const string mockToken = "mocktokenaaaaaa";
- var mockTime = DateTime.Now;
- _mockClock.MockCurrentTime = mockTime;
- var mockTokenInfo = new UserTokenInfo
- {
- Id = 1,
- Version = 1,
- ExpireAt = mockTime.AddDays(-1)
- };
- _mockUserTokenService.Setup(s => s.VerifyToken(mockToken)).Returns(mockTokenInfo);
-
- await _service.Awaiting(s => s.VerifyToken(mockToken)).Should().ThrowAsync<UserTokenTimeExpireException>();
- }
-
- [Fact]
- public async Task VerifyToken_BadVersion()
- {
- const string mockToken = "mocktokenaaaaaa";
- var mockTime = DateTime.Now;
- _mockClock.MockCurrentTime = mockTime;
- var mockTokenInfo = new UserTokenInfo
- {
- Id = 1,
- Version = 1,
- ExpireAt = mockTime.AddDays(1)
- };
- _mockUserTokenService.Setup(s => s.VerifyToken(mockToken)).Returns(mockTokenInfo);
- _mockUserService.Setup(s => s.GetUserById(1)).ReturnsAsync(new User
- {
- Id = 1,
- Username = "aaa",
- Administrator = false,
- Version = 2
- });
-
- await _service.Awaiting(s => s.VerifyToken(mockToken)).Should().ThrowAsync<UserTokenBadVersionException>();
- }
-
- [Fact]
- public async Task VerifyToken_Success()
- {
- const string mockToken = "mocktokenaaaaaa";
- var mockTime = DateTime.Now;
- _mockClock.MockCurrentTime = mockTime;
- var mockTokenInfo = new UserTokenInfo
- {
- Id = 1,
- Version = 1,
- ExpireAt = mockTime.AddDays(1)
- };
- var mockUserInfo = new User
- {
- Id = 1,
- Username = "aaa",
- Administrator = false,
- Version = 1
- };
- _mockUserTokenService.Setup(s => s.VerifyToken(mockToken)).Returns(mockTokenInfo);
- _mockUserService.Setup(s => s.GetUserById(1)).ReturnsAsync(mockUserInfo);
-
- (await _service.VerifyToken(mockToken)).Should().BeEquivalentTo(mockUserInfo);
- }
- }
-}
diff --git a/Timeline.Tests/Timeline.Tests.csproj b/Timeline.Tests/Timeline.Tests.csproj index bde4f430..40e8cda2 100644 --- a/Timeline.Tests/Timeline.Tests.csproj +++ b/Timeline.Tests/Timeline.Tests.csproj @@ -18,7 +18,6 @@ <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
<PackageReference Include="Moq" Version="4.13.1" />
<PackageReference Include="xunit" Version="2.4.1" />
diff --git a/Timeline/Auth/MyAuthenticationHandler.cs b/Timeline/Auth/MyAuthenticationHandler.cs index e6b26c4b..3c97c329 100644 --- a/Timeline/Auth/MyAuthenticationHandler.cs +++ b/Timeline/Auth/MyAuthenticationHandler.cs @@ -8,7 +8,6 @@ using System.Linq; using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
-using Timeline.Models;
using Timeline.Services;
using static Timeline.Resources.Authentication.AuthHandler;
diff --git a/Timeline/Controllers/ControllerAuthExtensions.cs b/Timeline/Controllers/ControllerAuthExtensions.cs index 81fd2428..90da8a93 100644 --- a/Timeline/Controllers/ControllerAuthExtensions.cs +++ b/Timeline/Controllers/ControllerAuthExtensions.cs @@ -26,5 +26,20 @@ namespace Timeline.Controllers throw new InvalidOperationException("Failed to get user id because NameIdentifier claim is not a number.");
}
+
+ public static long? GetOptionalUserId(this ControllerBase controller)
+ {
+ if (controller.User == null)
+ return 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.");
+
+ 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.");
+ }
}
}
diff --git a/Timeline/Controllers/PersonalTimelineController.cs b/Timeline/Controllers/PersonalTimelineController.cs index 2c70fad1..27618c41 100644 --- a/Timeline/Controllers/PersonalTimelineController.cs +++ b/Timeline/Controllers/PersonalTimelineController.cs @@ -4,45 +4,21 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Threading.Tasks;
-using Timeline.Auth;
using Timeline.Filters;
-using Timeline.Models;
using Timeline.Models.Http;
using Timeline.Models.Validation;
using Timeline.Services;
-using static Timeline.Resources.Controllers.TimelineController;
-using static Timeline.Resources.Messages;
namespace Timeline.Controllers
{
[ApiController]
+ [CatchTimelineNotExistException]
public class PersonalTimelineController : Controller
{
private readonly ILogger<PersonalTimelineController> _logger;
private readonly IPersonalTimelineService _service;
- private bool IsAdmin()
- {
- if (User != null)
- {
- return User.IsAdministrator();
- }
- return false;
- }
-
- private string? GetAuthUsername()
- {
- if (User == null)
- {
- return null;
- }
- else
- {
- return User.Identity.Name;
- }
- }
-
public PersonalTimelineController(ILogger<PersonalTimelineController> logger, IPersonalTimelineService service)
{
_logger = logger;
@@ -50,17 +26,15 @@ namespace Timeline.Controllers }
[HttpGet("users/{username}/timeline")]
- [CatchTimelineNotExistException]
public async Task<ActionResult<BaseTimelineInfo>> TimelineGet([FromRoute][Username] string username)
{
return await _service.GetTimeline(username);
}
[HttpGet("users/{username}/timeline/posts")]
- [CatchTimelineNotExistException]
public async Task<ActionResult<IList<TimelinePostInfo>>> PostListGet([FromRoute][Username] string username)
{
- if (!IsAdmin() && !await _service.HasReadPermission(username, GetAuthUsername()))
+ if (!this.IsAdministrator() && !await _service.HasReadPermission(username, this.GetOptionalUserId()))
{
return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
}
@@ -68,77 +42,88 @@ namespace Timeline.Controllers return await _service.GetPosts(username);
}
- [HttpPost("users/{username}/timeline/postop/create")]
+ [HttpPost("users/{username}/timeline/posts")]
[Authorize]
- [CatchTimelineNotExistException]
- public async Task<ActionResult<TimelinePostCreateResponse>> PostOperationCreate([FromRoute][Username] string username, [FromBody] TimelinePostCreateRequest body)
+ public async Task<ActionResult<TimelinePostInfo>> PostPost([FromRoute][Username] string username, [FromBody] TimelinePostCreateRequest body)
{
- if (!IsAdmin() && !await _service.IsMemberOf(username, GetAuthUsername()!))
+ var id = this.GetUserId();
+ if (!this.IsAdministrator() && !await _service.IsMemberOf(username, id))
{
return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
}
- var res = await _service.CreatePost(username, User.Identity.Name!, body.Content, body.Time);
+ var res = await _service.CreatePost(username, id, body.Content, body.Time);
return res;
}
- [HttpPost("users/{username}/timeline/postop/delete")]
+ [HttpDelete("users/{username}/timeline/posts/{id}")]
[Authorize]
- [CatchTimelineNotExistException]
- public async Task<ActionResult> PostOperationDelete([FromRoute][Username] string username, [FromBody] TimelinePostDeleteRequest body)
+ public async Task<ActionResult> PostDelete([FromRoute][Username] string username, [FromRoute] long id)
{
try
{
- var postId = body.Id!.Value;
- if (!IsAdmin() && !await _service.HasPostModifyPermission(username, postId, GetAuthUsername()!))
+ if (!this.IsAdministrator() && !await _service.HasPostModifyPermission(username, id, this.GetUserId()))
{
return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
}
- await _service.DeletePost(username, postId);
+ await _service.DeletePost(username, id);
+ return Ok(CommonDeleteResponse.Delete());
}
catch (TimelinePostNotExistException)
{
- return BadRequest(ErrorResponse.TimelineController.PostOperationDelete_NotExist());
+ return Ok(CommonDeleteResponse.NotExist());
}
- return Ok();
}
- [HttpPost("users/{username}/timeline/op/property")]
+ [HttpPatch("users/{username}/timeline")]
[Authorize]
- [SelfOrAdmin]
- [CatchTimelineNotExistException]
- public async Task<ActionResult> TimelineChangeProperty([FromRoute][Username] string username, [FromBody] TimelinePropertyChangeRequest body)
+ public async Task<ActionResult> TimelinePatch([FromRoute][Username] string username, [FromBody] TimelinePatchRequest body)
{
+ if (!this.IsAdministrator() && !(User.Identity.Name == username))
+ {
+ return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
+ }
await _service.ChangeProperty(username, body);
return Ok();
}
- [HttpPost("users/{username}/timeline/op/member")]
+ [HttpPut("users/{username}/timeline/members/{member}")]
[Authorize]
- [SelfOrAdmin]
- [CatchTimelineNotExistException]
- public async Task<ActionResult> TimelineChangeMember([FromRoute][Username] string username, [FromBody] TimelineMemberChangeRequest body)
+ public async Task<ActionResult> TimelineMemberPut([FromRoute][Username] string username, [FromRoute][Username] string member)
{
+ if (!this.IsAdministrator() && !(User.Identity.Name == username))
+ {
+ return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
+ }
+
try
{
- await _service.ChangeMember(username, body.Add, body.Remove);
+ await _service.ChangeMember(username, new List<string> { member }, null);
return Ok();
}
- catch (TimelineMemberOperationUserException e)
+ catch (UserNotExistException)
{
- if (e.InnerException is UsernameBadFormatException)
- {
- return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(
- TimelineController_ChangeMember_UsernameBadFormat, e.Index, e.Operation));
- }
- else if (e.InnerException is UserNotExistException)
- {
- return BadRequest(ErrorResponse.UserCommon.CustomMessage_NotExist(
- TimelineController_ChangeMember_UserNotExist, e.Index, e.Operation));
- }
+ return BadRequest(ErrorResponse.TimelineController.MemberPut_NotExist());
+ }
+ }
+
+ [HttpDelete("users/{username}/timeline/members/{member}")]
+ [Authorize]
+ public async Task<ActionResult> TimelineMemberDelete([FromRoute][Username] string username, [FromRoute][Username] string member)
+ {
+ if (!this.IsAdministrator() && !(User.Identity.Name == username))
+ {
+ return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
+ }
- _logger.LogError(e, LogUnknownTimelineMemberOperationUserException);
- throw;
+ try
+ {
+ await _service.ChangeMember(username, null, new List<string> { member });
+ return Ok(CommonDeleteResponse.Delete());
+ }
+ catch (UserNotExistException)
+ {
+ return Ok(CommonDeleteResponse.NotExist());
}
}
}
diff --git a/Timeline/Controllers/Testing/TestingI18nController.cs b/Timeline/Controllers/Testing/TestingI18nController.cs deleted file mode 100644 index febb56a5..00000000 --- a/Timeline/Controllers/Testing/TestingI18nController.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading.Tasks;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Localization;
-
-// For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
-
-namespace Timeline.Controllers.Testing
-{
- [Route("testing/i18n")]
- [ApiController]
- public class TestingI18nController : Controller
- {
- private readonly IStringLocalizer<TestingI18nController> _stringLocalizer;
-
- public TestingI18nController(IStringLocalizer<TestingI18nController> stringLocalizer)
- {
- _stringLocalizer = stringLocalizer;
- }
-
- [HttpGet("direct")]
- [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static")]
- public ActionResult<string> Direct()
- {
- return Resources.Controllers.Testing.TestingI18nController.TestString;
- }
-
- [HttpGet("localizer")]
- public ActionResult<string> Localizer()
- {
- return _stringLocalizer["TestString"].Value;
- }
- }
-}
diff --git a/Timeline/Controllers/TokenController.cs b/Timeline/Controllers/TokenController.cs index 9724c1a6..a7f5fbde 100644 --- a/Timeline/Controllers/TokenController.cs +++ b/Timeline/Controllers/TokenController.cs @@ -1,3 +1,4 @@ +using AutoMapper;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
@@ -5,7 +6,6 @@ using System; using System.Globalization;
using System.Threading.Tasks;
using Timeline.Helpers;
-using Timeline.Models;
using Timeline.Models.Http;
using Timeline.Services;
using static Timeline.Resources.Controllers.TokenController;
@@ -20,20 +20,14 @@ namespace Timeline.Controllers private readonly ILogger<TokenController> _logger;
private readonly IClock _clock;
- private static Models.Http.User CreateUserFromUserInfo(Models.User userInfo)
- {
- return new Models.Http.User
- {
- Username = userInfo.Username,
- Administrator = userInfo.Administrator
- };
- }
+ private readonly IMapper _mapper;
- public TokenController(IUserTokenManager userTokenManager, ILogger<TokenController> logger, IClock clock)
+ public TokenController(IUserTokenManager userTokenManager, ILogger<TokenController> logger, IClock clock, IMapper mapper)
{
_userTokenManager = userTokenManager;
_logger = logger;
_clock = clock;
+ _mapper = mapper;
}
[HttpPost("create")]
@@ -65,7 +59,7 @@ namespace Timeline.Controllers return Ok(new CreateTokenResponse
{
Token = result.Token,
- User = CreateUserFromUserInfo(result.User)
+ User = _mapper.Map<UserInfoForAdmin>(result.User)
});
}
catch (UserNotExistException e)
@@ -100,7 +94,7 @@ namespace Timeline.Controllers ("Username", result.Username), ("Token", request.Token)));
return Ok(new VerifyTokenResponse
{
- User = CreateUserFromUserInfo(result)
+ User = _mapper.Map<UserInfoForAdmin>(result)
});
}
catch (UserTokenTimeExpireException e)
diff --git a/Timeline/Controllers/UserAvatarController.cs b/Timeline/Controllers/UserAvatarController.cs index 62f1d78c..ab0ad8e7 100644 --- a/Timeline/Controllers/UserAvatarController.cs +++ b/Timeline/Controllers/UserAvatarController.cs @@ -21,11 +21,13 @@ namespace Timeline.Controllers {
private readonly ILogger<UserAvatarController> _logger;
+ private readonly IUserService _userService;
private readonly IUserAvatarService _service;
- public UserAvatarController(ILogger<UserAvatarController> logger, IUserAvatarService service)
+ public UserAvatarController(ILogger<UserAvatarController> logger, IUserService userService, IUserAvatarService service)
{
_logger = logger;
+ _userService = userService;
_service = service;
}
@@ -33,46 +35,50 @@ namespace Timeline.Controllers [ResponseCache(NoStore = false, Location = ResponseCacheLocation.None, Duration = 0)]
public async Task<IActionResult> Get([FromRoute][Username] string username)
{
- const string IfNonMatchHeaderKey = "If-None-Match";
-
+ long id;
try
{
- var eTagValue = $"\"{await _service.GetAvatarETag(username)}\"";
- var eTag = new EntityTagHeaderValue(eTagValue);
-
- if (Request.Headers.TryGetValue(IfNonMatchHeaderKey, out var value))
- {
- if (!EntityTagHeaderValue.TryParseStrictList(value, out var eTagList))
- {
- _logger.LogInformation(Log.Format(LogGetBadIfNoneMatch,
- ("Username", username), ("If-None-Match", value)));
- return BadRequest(ErrorResponse.Common.Header.IfNonMatch_BadFormat());
- }
-
- if (eTagList.FirstOrDefault(e => e.Equals(eTag)) != null)
- {
- Response.Headers.Add("ETag", eTagValue);
- _logger.LogInformation(Log.Format(LogGetReturnNotModify, ("Username", username)));
- return StatusCode(StatusCodes.Status304NotModified);
- }
- }
-
- var avatarInfo = await _service.GetAvatar(username);
- var avatar = avatarInfo.Avatar;
-
- _logger.LogInformation(Log.Format(LogGetReturnData, ("Username", username)));
- return File(avatar.Data, avatar.Type, new DateTimeOffset(avatarInfo.LastModified), eTag);
+ id = await _userService.GetUserIdByUsername(username);
}
catch (UserNotExistException e)
{
_logger.LogInformation(e, Log.Format(LogGetUserNotExist, ("Username", username)));
return NotFound(ErrorResponse.UserCommon.NotExist());
}
+
+ const string IfNonMatchHeaderKey = "If-None-Match";
+
+ var eTagValue = $"\"{await _service.GetAvatarETag(id)}\"";
+ var eTag = new EntityTagHeaderValue(eTagValue);
+
+ if (Request.Headers.TryGetValue(IfNonMatchHeaderKey, out var value))
+ {
+ if (!EntityTagHeaderValue.TryParseStrictList(value, out var eTagList))
+ {
+ _logger.LogInformation(Log.Format(LogGetBadIfNoneMatch,
+ ("Username", username), ("If-None-Match", value)));
+ return BadRequest(ErrorResponse.Common.Header.IfNonMatch_BadFormat());
+ }
+
+ if (eTagList.FirstOrDefault(e => e.Equals(eTag)) != null)
+ {
+ Response.Headers.Add("ETag", eTagValue);
+ _logger.LogInformation(Log.Format(LogGetReturnNotModify, ("Username", username)));
+ return StatusCode(StatusCodes.Status304NotModified);
+ }
+ }
+
+ var avatarInfo = await _service.GetAvatar(id);
+ var avatar = avatarInfo.Avatar;
+
+ _logger.LogInformation(Log.Format(LogGetReturnData, ("Username", username)));
+ return File(avatar.Data, avatar.Type, new DateTimeOffset(avatarInfo.LastModified), eTag);
+
}
[HttpPut("users/{username}/avatar")]
[Authorize]
- [RequireContentType, RequireContentLength]
+ [RequireContentLength]
[Consumes("image/png", "image/jpeg", "image/gif", "image/webp")]
public async Task<IActionResult> Put([FromRoute][Username] string username)
{
@@ -87,6 +93,17 @@ namespace Timeline.Controllers return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
}
+ long id;
+ try
+ {
+ id = await _userService.GetUserIdByUsername(username);
+ }
+ catch (UserNotExistException e)
+ {
+ _logger.LogInformation(e, Log.Format(LogPutUserNotExist, ("Username", username)));
+ return BadRequest(ErrorResponse.UserCommon.NotExist());
+ }
+
try
{
var data = new byte[contentLength];
@@ -99,7 +116,7 @@ namespace Timeline.Controllers if (await Request.Body.ReadAsync(extraByte) != 0)
return BadRequest(ErrorResponse.Common.Content.UnmatchedLength_Bigger());
- await _service.SetAvatar(username, new Avatar
+ await _service.SetAvatar(id, new Avatar
{
Data = data,
Type = Request.ContentType
@@ -109,11 +126,6 @@ namespace Timeline.Controllers ("Username", username), ("Mime Type", Request.ContentType)));
return Ok();
}
- catch (UserNotExistException e)
- {
- _logger.LogInformation(e, Log.Format(LogPutUserNotExist, ("Username", username)));
- return BadRequest(ErrorResponse.UserCommon.NotExist());
- }
catch (AvatarFormatException e)
{
_logger.LogInformation(e, Log.Format(LogPutUserBadFormat, ("Username", username)));
@@ -139,16 +151,19 @@ namespace Timeline.Controllers return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
}
+ long id;
try
{
- await _service.SetAvatar(username, null);
- return Ok();
+ id = await _userService.GetUserIdByUsername(username);
}
catch (UserNotExistException e)
{
_logger.LogInformation(e, Log.Format(LogDeleteNotExist, ("Username", username)));
return BadRequest(ErrorResponse.UserCommon.NotExist());
}
+
+ await _service.SetAvatar(id, null);
+ return Ok();
}
}
}
diff --git a/Timeline/Controllers/UserController.cs b/Timeline/Controllers/UserController.cs index 4c585198..400a518c 100644 --- a/Timeline/Controllers/UserController.cs +++ b/Timeline/Controllers/UserController.cs @@ -1,3 +1,4 @@ +using AutoMapper;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -17,30 +18,40 @@ namespace Timeline.Controllers [ApiController]
public class UserController : Controller
{
-
private readonly ILogger<UserController> _logger;
private readonly IUserService _userService;
+ private readonly IMapper _mapper;
- public UserController(ILogger<UserController> logger, IUserService userService)
+ public UserController(ILogger<UserController> logger, IUserService userService, IMapper mapper)
{
_logger = logger;
_userService = userService;
+ _mapper = mapper;
+ }
+
+ private IUserInfo ConvertToUserInfo(User user, bool administrator)
+ {
+ if (administrator)
+ return _mapper.Map<UserInfoForAdmin>(user);
+ else
+ return _mapper.Map<UserInfo>(user);
}
[HttpGet("users")]
- public async Task<ActionResult<User[]>> List()
+ public async Task<ActionResult<IUserInfo[]>> List()
{
var users = await _userService.GetUsers();
- return Ok(users.Select(u => u.EraseSecretAndFinalFill(Url, this.IsAdministrator())).ToArray());
+ var administrator = this.IsAdministrator();
+ return Ok(users.Select(u => ConvertToUserInfo(u, administrator)).ToArray());
}
[HttpGet("users/{username}")]
- public async Task<ActionResult<User>> Get([FromRoute][Username] string username)
+ public async Task<ActionResult<IUserInfo>> Get([FromRoute][Username] string username)
{
try
{
var user = await _userService.GetUserByUsername(username);
- return Ok(user.EraseSecretAndFinalFill(Url, this.IsAdministrator()));
+ return Ok(ConvertToUserInfo(user, this.IsAdministrator()));
}
catch (UserNotExistException e)
{
@@ -52,22 +63,11 @@ namespace Timeline.Controllers [HttpPatch("users/{username}"), Authorize]
public async Task<ActionResult> Patch([FromBody] UserPatchRequest body, [FromRoute][Username] string username)
{
- static User Convert(UserPatchRequest body)
- {
- return new User
- {
- Username = body.Username,
- Password = body.Password,
- Administrator = body.Administrator,
- Nickname = body.Nickname
- };
- }
-
if (this.IsAdministrator())
{
try
{
- await _userService.ModifyUser(username, Convert(body));
+ await _userService.ModifyUser(username, _mapper.Map<User>(body));
return Ok();
}
catch (UserNotExistException e)
@@ -75,6 +75,10 @@ namespace Timeline.Controllers _logger.LogInformation(e, Log.Format(LogPatchUserNotExist, ("Username", username)));
return NotFound(ErrorResponse.UserCommon.NotExist());
}
+ catch (ConflictException)
+ {
+ return BadRequest(ErrorResponse.UserController.UsernameConflict());
+ }
}
else
{
@@ -94,7 +98,7 @@ namespace Timeline.Controllers return StatusCode(StatusCodes.Status403Forbidden,
ErrorResponse.Common.CustomMessage_Forbid(UserController_Patch_Forbid_Administrator));
- await _userService.ModifyUser(this.GetUserId(), Convert(body));
+ await _userService.ModifyUser(this.GetUserId(), _mapper.Map<User>(body));
return Ok();
}
}
@@ -113,10 +117,18 @@ namespace Timeline.Controllers }
}
- [HttpPost("userop/create"), AdminAuthorize]
- public async Task<ActionResult> CreateUser([FromBody] User body)
+ [HttpPost("userop/createuser"), AdminAuthorize]
+ public async Task<ActionResult> CreateUser([FromBody] CreateUserRequest body)
{
-
+ try
+ {
+ await _userService.CreateUser(_mapper.Map<User>(body));
+ return Ok();
+ }
+ catch (ConflictException)
+ {
+ return BadRequest(ErrorResponse.UserController.UsernameConflict());
+ }
}
[HttpPost("userop/changepassword"), Authorize]
@@ -133,7 +145,7 @@ namespace Timeline.Controllers ("Username", User.Identity.Name), ("Old Password", request.OldPassword)));
return BadRequest(ErrorResponse.UserController.ChangePassword_BadOldPassword());
}
- // User can't be non-existent or the token is bad.
+ // User can't be non-existent or the token is bad.
}
}
}
diff --git a/Timeline/Entities/DatabaseContext.cs b/Timeline/Entities/DatabaseContext.cs index ac4ad7b2..cac33379 100644 --- a/Timeline/Entities/DatabaseContext.cs +++ b/Timeline/Entities/DatabaseContext.cs @@ -10,7 +10,6 @@ namespace Timeline.Entities }
- [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")]
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<UserEntity>().Property(e => e.Version).HasDefaultValue(0);
@@ -19,7 +18,6 @@ namespace Timeline.Entities public DbSet<UserEntity> Users { get; set; } = default!;
public DbSet<UserAvatarEntity> UserAvatars { get; set; } = default!;
- public DbSet<UserDetailEntity> UserDetails { get; set; } = default!;
public DbSet<TimelineEntity> Timelines { get; set; } = default!;
public DbSet<TimelinePostEntity> TimelinePosts { get; set; } = default!;
public DbSet<TimelineMemberEntity> TimelineMembers { get; set; } = default!;
diff --git a/Timeline/Entities/TimelineEntity.cs b/Timeline/Entities/TimelineEntity.cs index 2bfd6107..c50fe6dd 100644 --- a/Timeline/Entities/TimelineEntity.cs +++ b/Timeline/Entities/TimelineEntity.cs @@ -2,7 +2,7 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
-using Timeline.Models;
+using Timeline.Models.Http;
namespace Timeline.Entities
{
diff --git a/Timeline/Filters/Header.cs b/Timeline/Filters/Header.cs index 843a619d..0db11faf 100644 --- a/Timeline/Filters/Header.cs +++ b/Timeline/Filters/Header.cs @@ -6,7 +6,6 @@ namespace Timeline.Filters {
public class RequireContentTypeAttribute : ActionFilterAttribute
{
- [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")]
public override void OnActionExecuting(ActionExecutingContext context)
{
if (context.HttpContext.Request.ContentType == null)
@@ -31,7 +30,6 @@ namespace Timeline.Filters public bool RequireNonZero { get; set; }
- [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")]
public override void OnActionExecuting(ActionExecutingContext context)
{
if (context.HttpContext.Request.ContentLength == null)
diff --git a/Timeline/Filters/Timeline.cs b/Timeline/Filters/Timeline.cs index bc142db0..729dbec7 100644 --- a/Timeline/Filters/Timeline.cs +++ b/Timeline/Filters/Timeline.cs @@ -7,7 +7,6 @@ namespace Timeline.Filters {
public class CatchTimelineNotExistExceptionAttribute : ExceptionFilterAttribute
{
- [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")]
public override void OnException(ExceptionContext context)
{
if (context.Exception is TimelineNotExistException e)
diff --git a/Timeline/Filters/User.cs b/Timeline/Filters/User.cs deleted file mode 100644 index 12ed6155..00000000 --- a/Timeline/Filters/User.cs +++ /dev/null @@ -1,68 +0,0 @@ -using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.Filters;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
-using System;
-using Timeline.Auth;
-using Timeline.Models.Http;
-using Timeline.Services;
-using static Timeline.Resources.Filters;
-
-namespace Timeline.Filters
-{
- public class SelfOrAdminAttribute : ActionFilterAttribute
- {
- [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")]
- public override void OnActionExecuting(ActionExecutingContext context)
- {
- var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<SelfOrAdminAttribute>>();
-
- var user = context.HttpContext.User;
-
- if (user == null)
- {
- logger.LogError(LogSelfOrAdminNoUser);
- return;
- }
-
- if (context.ModelState.TryGetValue("username", out var model))
- {
- if (model.RawValue is string username)
- {
- if (!user.IsAdministrator() && user.Identity.Name != username)
- {
- context.Result = new ObjectResult(ErrorResponse.Common.Forbid())
- { StatusCode = StatusCodes.Status403Forbidden };
- }
- }
- else
- {
- logger.LogError(LogSelfOrAdminUsernameNotString);
- }
- }
- else
- {
- logger.LogError(LogSelfOrAdminNoUsername);
- }
- }
- }
-
- [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
- public class CatchUserNotExistExceptionAttribute : ExceptionFilterAttribute
- {
- [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "ASP.Net already checked.")]
- public override void OnException(ExceptionContext context)
- {
- if (context.Exception is UserNotExistException)
- {
- var body = ErrorResponse.UserCommon.NotExist();
-
- if (context.HttpContext.Request.Method == "GET")
- context.Result = new NotFoundObjectResult(body);
- else
- context.Result = new BadRequestObjectResult(body);
- }
- }
- }
-}
diff --git a/Timeline/Formatters/StringInputFormatter.cs b/Timeline/Formatters/StringInputFormatter.cs index 90847e36..b1924268 100644 --- a/Timeline/Formatters/StringInputFormatter.cs +++ b/Timeline/Formatters/StringInputFormatter.cs @@ -15,7 +15,6 @@ namespace Timeline.Formatters SupportedEncodings.Add(Encoding.UTF8);
}
- [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")]
public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding effectiveEncoding)
{
var request = context.HttpContext.Request;
diff --git a/Timeline/GlobalSuppressions.cs b/Timeline/GlobalSuppressions.cs index d27b3c16..2b0da576 100644 --- a/Timeline/GlobalSuppressions.cs +++ b/Timeline/GlobalSuppressions.cs @@ -11,3 +11,4 @@ [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1724:Type names should not match namespaces", Justification = "Generated error response identifiers.", Scope = "type", Target = "Timeline.Models.Http.ErrorResponse")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "Generated error response.", Scope = "type", Target = "Timeline.Models.Http.ErrorResponse")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1056:Uri properties should not be strings", Justification = "That's unnecessary.")]
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "Adundant")]
diff --git a/Timeline/Helpers/InvalidModelResponseFactory.cs b/Timeline/Helpers/InvalidModelResponseFactory.cs index 71ee44a9..9b253e7d 100644 --- a/Timeline/Helpers/InvalidModelResponseFactory.cs +++ b/Timeline/Helpers/InvalidModelResponseFactory.cs @@ -6,7 +6,6 @@ namespace Timeline.Helpers {
public static class InvalidModelResponseFactory
{
- [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")]
public static IActionResult Factory(ActionContext context)
{
var modelState = context.ModelState;
diff --git a/Timeline/Models/Converters/JsonDateTimeConverter.cs b/Timeline/Models/Converters/JsonDateTimeConverter.cs index 69af53c1..ef129a01 100644 --- a/Timeline/Models/Converters/JsonDateTimeConverter.cs +++ b/Timeline/Models/Converters/JsonDateTimeConverter.cs @@ -6,7 +6,6 @@ using System.Text.Json.Serialization; namespace Timeline.Models.Converters { - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")] public class JsonDateTimeConverter : JsonConverter<DateTime> { public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) diff --git a/Timeline/Models/Http/ErrorResponse.cs b/Timeline/Models/Http/ErrorResponse.cs index 6a53e0c3..87516638 100644 --- a/Timeline/Models/Http/ErrorResponse.cs +++ b/Timeline/Models/Http/ErrorResponse.cs @@ -184,14 +184,14 @@ namespace Timeline.Models.Http public static class UserController
{
- public static CommonResponse ChangeUsername_Conflict(params object?[] formatArgs)
+ public static CommonResponse UsernameConflict(params object?[] formatArgs)
{
- return new CommonResponse(ErrorCodes.UserController.ChangeUsername_Conflict, string.Format(UserController_ChangeUsername_Conflict, formatArgs));
+ return new CommonResponse(ErrorCodes.UserController.UsernameConflict, string.Format(UserController_UsernameConflict, formatArgs));
}
- public static CommonResponse CustomMessage_ChangeUsername_Conflict(string message, params object?[] formatArgs)
+ public static CommonResponse CustomMessage_UsernameConflict(string message, params object?[] formatArgs)
{
- return new CommonResponse(ErrorCodes.UserController.ChangeUsername_Conflict, string.Format(message, formatArgs));
+ return new CommonResponse(ErrorCodes.UserController.UsernameConflict, string.Format(message, formatArgs));
}
public static CommonResponse ChangePassword_BadOldPassword(params object?[] formatArgs)
@@ -244,18 +244,18 @@ namespace Timeline.Models.Http public static class TimelineController
{
- public static CommonResponse PostOperationDelete_NotExist(params object?[] formatArgs)
+ public static CommonResponse MemberPut_NotExist(params object?[] formatArgs)
{
- return new CommonResponse(ErrorCodes.TimelineController.PostOperationDelete_NotExist, string.Format(TimelineController_PostOperationDelete_NotExist, formatArgs));
+ return new CommonResponse(ErrorCodes.TimelineController.MemberPut_NotExist, string.Format(TimelineController_MemberPut_NotExist, formatArgs));
}
- public static CommonResponse CustomMessage_PostOperationDelete_NotExist(string message, params object?[] formatArgs)
+ public static CommonResponse CustomMessage_MemberPut_NotExist(string message, params object?[] formatArgs)
{
- return new CommonResponse(ErrorCodes.TimelineController.PostOperationDelete_NotExist, string.Format(message, formatArgs));
+ return new CommonResponse(ErrorCodes.TimelineController.MemberPut_NotExist, string.Format(message, formatArgs));
}
}
}
-}
+}
\ No newline at end of file diff --git a/Timeline/Models/Http/Timeline.cs b/Timeline/Models/Http/Timeline.cs deleted file mode 100644 index 3029434e..00000000 --- a/Timeline/Models/Http/Timeline.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System;
-using System.Collections.Generic;
-using System.ComponentModel.DataAnnotations;
-
-namespace Timeline.Models.Http
-{
- public class TimelinePostCreateRequest
- {
- [Required(AllowEmptyStrings = true)]
- public string Content { get; set; } = default!;
-
- public DateTime? Time { get; set; }
- }
-
- public class TimelinePostCreateResponse
- {
- public long Id { get; set; }
-
- public DateTime Time { get; set; }
- }
-
- public class TimelinePostDeleteRequest
- {
- [Required]
- public long? Id { get; set; }
- }
-
- public class TimelinePropertyChangeRequest
- {
- public string? Description { get; set; }
-
- public TimelineVisibility? Visibility { get; set; }
- }
-
- [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "This is a DTO class.")]
- public class TimelineMemberChangeRequest
- {
- public List<string>? Add { get; set; }
-
- public List<string>? Remove { get; set; }
- }
-}
diff --git a/Timeline/Models/Timeline.cs b/Timeline/Models/Http/TimelineCommon.cs index 752c698d..febb8186 100644 --- a/Timeline/Models/Timeline.cs +++ b/Timeline/Models/Http/TimelineCommon.cs @@ -1,7 +1,7 @@ using System;
using System.Collections.Generic;
-namespace Timeline.Models
+namespace Timeline.Models.Http
{
public enum TimelineVisibility
{
@@ -22,30 +22,19 @@ namespace Timeline.Models public class TimelinePostInfo
{
public long Id { get; set; }
-
- public string? Content { get; set; }
-
+ public string Content { get; set; } = default!;
public DateTime Time { get; set; }
-
- /// <summary>
- /// The username of the author.
- /// </summary>
- public string Author { get; set; } = default!;
+ public UserInfo Author { get; set; } = default!;
+ public DateTime LastUpdated { get; set; } = default!;
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "This is a DTO class.")]
public class BaseTimelineInfo
{
- public string? Description { get; set; }
-
- /// <summary>
- /// The username of the owner.
- /// </summary>
- public string Owner { get; set; } = default!;
-
+ public string Description { get; set; } = default!;
+ public UserInfo Owner { get; set; } = default!;
public TimelineVisibility Visibility { get; set; }
-
- public List<string> Members { get; set; } = default!;
+ public List<UserInfo> Members { get; set; } = default!;
}
public class TimelineInfo : BaseTimelineInfo
diff --git a/Timeline/Models/Http/TimelineController.cs b/Timeline/Models/Http/TimelineController.cs new file mode 100644 index 00000000..f9a4d3e5 --- /dev/null +++ b/Timeline/Models/Http/TimelineController.cs @@ -0,0 +1,20 @@ +using System;
+using System.ComponentModel.DataAnnotations;
+
+namespace Timeline.Models.Http
+{
+ public class TimelinePostCreateRequest
+ {
+ [Required(AllowEmptyStrings = true)]
+ public string Content { get; set; } = default!;
+
+ public DateTime? Time { get; set; }
+ }
+
+ public class TimelinePatchRequest
+ {
+ public string? Description { get; set; }
+
+ public TimelineVisibility? Visibility { get; set; }
+ }
+}
diff --git a/Timeline/Models/Http/Token.cs b/Timeline/Models/Http/TokenController.cs index 0649f1d1..383b2965 100644 --- a/Timeline/Models/Http/Token.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 User User { get; set; } = default!;
+ public UserInfoForAdmin User { get; set; } = default!;
}
public class VerifyTokenRequest
@@ -27,6 +27,6 @@ namespace Timeline.Models.Http public class VerifyTokenResponse
{
- public User User { get; set; } = default!;
+ public UserInfoForAdmin User { get; set; } = default!;
}
}
diff --git a/Timeline/Models/Http/UserController.cs b/Timeline/Models/Http/UserController.cs index 229ca1e5..e4c95cbd 100644 --- a/Timeline/Models/Http/UserController.cs +++ b/Timeline/Models/Http/UserController.cs @@ -1,5 +1,7 @@ +using AutoMapper;
using System.ComponentModel.DataAnnotations;
using Timeline.Models.Validation;
+using Timeline.Services;
namespace Timeline.Models.Http
{
@@ -17,6 +19,21 @@ namespace Timeline.Models.Http public bool? Administrator { get; set; }
}
+ public class CreateUserRequest
+ {
+ [Required, Username]
+ public string Username { get; set; } = default!;
+
+ [Required, MinLength(1)]
+ public string Password { get; set; } = default!;
+
+ [Required]
+ public bool? Administrator { get; set; }
+
+ [Nickname]
+ public string? Nickname { get; set; }
+ }
+
public class ChangePasswordRequest
{
[Required(AllowEmptyStrings = false)]
@@ -24,4 +41,13 @@ namespace Timeline.Models.Http [Required(AllowEmptyStrings = false)]
public string NewPassword { get; set; } = default!;
}
+
+ public class UserControllerAutoMapperProfile : Profile
+ {
+ public UserControllerAutoMapperProfile()
+ {
+ CreateMap<UserPatchRequest, User>(MemberList.Source);
+ CreateMap<CreateUserRequest, User>(MemberList.Source);
+ }
+ }
}
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<object, IUserInfo>
+ {
+ 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<User, UserInfo>().AfterMap<UserInfoSetAvatarUrlAction>();
+ CreateMap<User, UserInfoForAdmin>().AfterMap<UserInfoSetAvatarUrlAction>();
+ }
+ }
+}
diff --git a/Timeline/Models/Validation/NicknameValidator.cs b/Timeline/Models/Validation/NicknameValidator.cs index f6626a2a..53a2916b 100644 --- a/Timeline/Models/Validation/NicknameValidator.cs +++ b/Timeline/Models/Validation/NicknameValidator.cs @@ -5,7 +5,6 @@ namespace Timeline.Models.Validation {
public class NicknameValidator : Validator<string>
{
- [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "Already checked in base.")]
protected override (bool, string) DoValidate(string value)
{
if (value.Length > 10)
diff --git a/Timeline/Models/Validation/UsernameValidator.cs b/Timeline/Models/Validation/UsernameValidator.cs index fc6cdf37..d8f3bdc0 100644 --- a/Timeline/Models/Validation/UsernameValidator.cs +++ b/Timeline/Models/Validation/UsernameValidator.cs @@ -8,7 +8,6 @@ namespace Timeline.Models.Validation {
public const int MaxLength = 26;
- [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "Already checked in base class.")]
protected override (bool, string) DoValidate(string value)
{
if (value.Length == 0)
diff --git a/Timeline/Resources/Controllers/Testing/TestingI18nController.zh.resx b/Timeline/Resources/Controllers/Testing/TestingI18nController.zh.resx deleted file mode 100644 index 6931cdf6..00000000 --- a/Timeline/Resources/Controllers/Testing/TestingI18nController.zh.resx +++ /dev/null @@ -1,123 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?>
-<root>
- <!--
- Microsoft ResX Schema
-
- Version 2.0
-
- The primary goals of this format is to allow a simple XML format
- that is mostly human readable. The generation and parsing of the
- various data types are done through the TypeConverter classes
- associated with the data types.
-
- Example:
-
- ... ado.net/XML headers & schema ...
- <resheader name="resmimetype">text/microsoft-resx</resheader>
- <resheader name="version">2.0</resheader>
- <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
- <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
- <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
- <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
- <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
- <value>[base64 mime encoded serialized .NET Framework object]</value>
- </data>
- <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
- <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
- <comment>This is a comment</comment>
- </data>
-
- There are any number of "resheader" rows that contain simple
- name/value pairs.
-
- Each data row contains a name, and value. The row also contains a
- type or mimetype. Type corresponds to a .NET class that support
- text/value conversion through the TypeConverter architecture.
- Classes that don't support this are serialized and stored with the
- mimetype set.
-
- The mimetype is used for serialized objects, and tells the
- ResXResourceReader how to depersist the object. This is currently not
- extensible. For a given mimetype the value must be set accordingly:
-
- Note - application/x-microsoft.net.object.binary.base64 is the format
- that the ResXResourceWriter will generate, however the reader can
- read any of the formats listed below.
-
- mimetype: application/x-microsoft.net.object.binary.base64
- value : The object must be serialized with
- : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
- : and then encoded with base64 encoding.
-
- mimetype: application/x-microsoft.net.object.soap.base64
- value : The object must be serialized with
- : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
- : and then encoded with base64 encoding.
-
- mimetype: application/x-microsoft.net.object.bytearray.base64
- value : The object must be serialized into a byte array
- : using a System.ComponentModel.TypeConverter
- : and then encoded with base64 encoding.
- -->
- <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
- <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
- <xsd:element name="root" msdata:IsDataSet="true">
- <xsd:complexType>
- <xsd:choice maxOccurs="unbounded">
- <xsd:element name="metadata">
- <xsd:complexType>
- <xsd:sequence>
- <xsd:element name="value" type="xsd:string" minOccurs="0" />
- </xsd:sequence>
- <xsd:attribute name="name" use="required" type="xsd:string" />
- <xsd:attribute name="type" type="xsd:string" />
- <xsd:attribute name="mimetype" type="xsd:string" />
- <xsd:attribute ref="xml:space" />
- </xsd:complexType>
- </xsd:element>
- <xsd:element name="assembly">
- <xsd:complexType>
- <xsd:attribute name="alias" type="xsd:string" />
- <xsd:attribute name="name" type="xsd:string" />
- </xsd:complexType>
- </xsd:element>
- <xsd:element name="data">
- <xsd:complexType>
- <xsd:sequence>
- <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
- <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
- </xsd:sequence>
- <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
- <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
- <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
- <xsd:attribute ref="xml:space" />
- </xsd:complexType>
- </xsd:element>
- <xsd:element name="resheader">
- <xsd:complexType>
- <xsd:sequence>
- <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
- </xsd:sequence>
- <xsd:attribute name="name" type="xsd:string" use="required" />
- </xsd:complexType>
- </xsd:element>
- </xsd:choice>
- </xsd:complexType>
- </xsd:element>
- </xsd:schema>
- <resheader name="resmimetype">
- <value>text/microsoft-resx</value>
- </resheader>
- <resheader name="version">
- <value>2.0</value>
- </resheader>
- <resheader name="reader">
- <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
- </resheader>
- <resheader name="writer">
- <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
- </resheader>
- <data name="TestString" xml:space="preserve">
- <value>中文测试字符串。</value>
- </data>
-</root>
\ No newline at end of file diff --git a/Timeline/Resources/Messages.Designer.cs b/Timeline/Resources/Messages.Designer.cs index 15101661..332c8817 100644 --- a/Timeline/Resources/Messages.Designer.cs +++ b/Timeline/Resources/Messages.Designer.cs @@ -169,6 +169,15 @@ namespace Timeline.Resources { }
/// <summary>
+ /// Looks up a localized string similar to The user to set as member does not exist..
+ /// </summary>
+ internal static string TimelineController_MemberPut_NotExist {
+ get {
+ return ResourceManager.GetString("TimelineController_MemberPut_NotExist", resourceCulture);
+ }
+ }
+
+ /// <summary>
/// Looks up a localized string similar to The post to delete does not exist..
/// </summary>
internal static string TimelineController_PostOperationDelete_NotExist {
@@ -268,15 +277,6 @@ namespace Timeline.Resources { }
/// <summary>
- /// Looks up a localized string similar to The new username already exists..
- /// </summary>
- internal static string UserController_ChangeUsername_Conflict {
- get {
- return ResourceManager.GetString("UserController_ChangeUsername_Conflict", resourceCulture);
- }
- }
-
- /// <summary>
/// Looks up a localized string similar to You can't set permission unless you are administrator..
/// </summary>
internal static string UserController_Patch_Forbid_Administrator {
@@ -302,5 +302,14 @@ namespace Timeline.Resources { return ResourceManager.GetString("UserController_Patch_Forbid_Username", resourceCulture);
}
}
+
+ /// <summary>
+ /// Looks up a localized string similar to A user with given username already exists..
+ /// </summary>
+ internal static string UserController_UsernameConflict {
+ get {
+ return ResourceManager.GetString("UserController_UsernameConflict", resourceCulture);
+ }
+ }
}
}
diff --git a/Timeline/Resources/Messages.resx b/Timeline/Resources/Messages.resx index db56ed02..cb6c3891 100644 --- a/Timeline/Resources/Messages.resx +++ b/Timeline/Resources/Messages.resx @@ -153,6 +153,9 @@ <data name="TimelineController_ChangeMember_UserNotExist" xml:space="preserve">
<value>The {0}-st user to do operation {1} on does not exist.</value>
</data>
+ <data name="TimelineController_MemberPut_NotExist" xml:space="preserve">
+ <value>The user to set as member does not exist.</value>
+ </data>
<data name="TimelineController_PostOperationDelete_NotExist" xml:space="preserve">
<value>The post to delete does not exist.</value>
</data>
@@ -186,9 +189,6 @@ <data name="UserController_ChangePassword_BadOldPassword" xml:space="preserve">
<value>Old password is wrong.</value>
</data>
- <data name="UserController_ChangeUsername_Conflict" xml:space="preserve">
- <value>The new username already exists.</value>
- </data>
<data name="UserController_Patch_Forbid_Administrator" xml:space="preserve">
<value>You can't set permission unless you are administrator.</value>
</data>
@@ -198,4 +198,7 @@ <data name="UserController_Patch_Forbid_Username" xml:space="preserve">
<value>You can't set username unless you are administrator.</value>
</data>
+ <data name="UserController_UsernameConflict" xml:space="preserve">
+ <value>A user with given username already exists.</value>
+ </data>
</root>
\ No newline at end of file diff --git a/Timeline/Resources/Services/Exception.Designer.cs b/Timeline/Resources/Services/Exception.Designer.cs index cada1788..e6806873 100644 --- a/Timeline/Resources/Services/Exception.Designer.cs +++ b/Timeline/Resources/Services/Exception.Designer.cs @@ -117,9 +117,9 @@ namespace Timeline.Resources.Services { /// <summary>
/// Looks up a localized string similar to A present resource conflicts with the given resource..
/// </summary>
- internal static string ConfictException {
+ internal static string ConflictException {
get {
- return ResourceManager.GetString("ConfictException", resourceCulture);
+ return ResourceManager.GetString("ConflictException", resourceCulture);
}
}
@@ -259,42 +259,6 @@ namespace Timeline.Resources.Services { }
/// <summary>
- /// Looks up a localized string similar to The timeline with that name already exists..
- /// </summary>
- internal static string TimelineAlreadyExistException {
- get {
- return ResourceManager.GetString("TimelineAlreadyExistException", resourceCulture);
- }
- }
-
- /// <summary>
- /// Looks up a localized string similar to An exception happened when add or remove member on timeline..
- /// </summary>
- internal static string TimelineMemberOperationException {
- get {
- return ResourceManager.GetString("TimelineMemberOperationException", resourceCulture);
- }
- }
-
- /// <summary>
- /// Looks up a localized string similar to An exception happened when do operation {0} on the {1} member on timeline..
- /// </summary>
- internal static string TimelineMemberOperationExceptionDetail {
- get {
- return ResourceManager.GetString("TimelineMemberOperationExceptionDetail", resourceCulture);
- }
- }
-
- /// <summary>
- /// Looks up a localized string similar to Timeline name is of bad format. If this is a personal timeline, it means the username is of bad format and inner exception should be a UsernameBadFormatException..
- /// </summary>
- internal static string TimelineNameBadFormatException {
- get {
- return ResourceManager.GetString("TimelineNameBadFormatException", resourceCulture);
- }
- }
-
- /// <summary>
/// Looks up a localized string similar to Timeline does not exist. If this is a personal timeline, it means the user does not exist and inner exception should be a UserNotExistException..
/// </summary>
internal static string TimelineNotExistException {
@@ -313,15 +277,6 @@ namespace Timeline.Resources.Services { }
/// <summary>
- /// Looks up a localized string similar to The use is not a member of the timeline..
- /// </summary>
- internal static string TimelineUserNotMemberException {
- get {
- return ResourceManager.GetString("TimelineUserNotMemberException", resourceCulture);
- }
- }
-
- /// <summary>
/// Looks up a localized string similar to The user does not exist..
/// </summary>
internal static string UserNotExistException {
diff --git a/Timeline/Resources/Services/Exception.resx b/Timeline/Resources/Services/Exception.resx index 2cb0f11a..11ae5f27 100644 --- a/Timeline/Resources/Services/Exception.resx +++ b/Timeline/Resources/Services/Exception.resx @@ -135,7 +135,7 @@ <data name="BadPasswordException" xml:space="preserve">
<value>The password is wrong.</value>
</data>
- <data name="ConfictException" xml:space="preserve">
+ <data name="ConflictException" xml:space="preserve">
<value>A present resource conflicts with the given resource.</value>
</data>
<data name="HashedPasswordBadFromatException" xml:space="preserve">
@@ -183,27 +183,12 @@ <data name="PasswordBadFormatException" xml:space="preserve">
<value>Password is of bad format.</value>
</data>
- <data name="TimelineAlreadyExistException" xml:space="preserve">
- <value>The timeline with that name already exists.</value>
- </data>
- <data name="TimelineMemberOperationException" xml:space="preserve">
- <value>An exception happened when add or remove member on timeline.</value>
- </data>
- <data name="TimelineMemberOperationExceptionDetail" xml:space="preserve">
- <value>An exception happened when do operation {0} on the {1} member on timeline.</value>
- </data>
- <data name="TimelineNameBadFormatException" xml:space="preserve">
- <value>Timeline name is of bad format. If this is a personal timeline, it means the username is of bad format and inner exception should be a UsernameBadFormatException.</value>
- </data>
<data name="TimelineNotExistException" xml:space="preserve">
<value>Timeline does not exist. If this is a personal timeline, it means the user does not exist and inner exception should be a UserNotExistException.</value>
</data>
<data name="TimelinePostNotExistException" xml:space="preserve">
<value>The timeline post does not exist. You can't do operation on it.</value>
</data>
- <data name="TimelineUserNotMemberException" xml:space="preserve">
- <value>The use is not a member of the timeline.</value>
- </data>
<data name="UserNotExistException" xml:space="preserve">
<value>The user does not exist.</value>
</data>
diff --git a/Timeline/Resources/Controllers/Testing/TestingI18nController.Designer.cs b/Timeline/Resources/Services/TimelineService.Designer.cs index e015c5fc..8212c252 100644 --- a/Timeline/Resources/Controllers/Testing/TestingI18nController.Designer.cs +++ b/Timeline/Resources/Services/TimelineService.Designer.cs @@ -8,7 +8,7 @@ // </auto-generated>
//------------------------------------------------------------------------------
-namespace Timeline.Resources.Controllers.Testing {
+namespace Timeline.Resources.Services {
using System;
@@ -22,14 +22,14 @@ namespace Timeline.Resources.Controllers.Testing { [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
- internal class TestingI18nController {
+ internal class TimelineService {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
- internal TestingI18nController() {
+ internal TimelineService() {
}
/// <summary>
@@ -39,7 +39,7 @@ namespace Timeline.Resources.Controllers.Testing { 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.Testing.TestingI18nController", typeof(TestingI18nController).Assembly);
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Services.TimelineService", typeof(TimelineService).Assembly);
resourceMan = temp;
}
return resourceMan;
@@ -61,11 +61,20 @@ namespace Timeline.Resources.Controllers.Testing { }
/// <summary>
- /// Looks up a localized string similar to English test string..
+ /// Looks up a localized string similar to The number {0} username is invalid..
/// </summary>
- internal static string TestString {
+ internal static string ExceptionChangeMemberUsernameBadFormat {
get {
- return ResourceManager.GetString("TestString", resourceCulture);
+ return ResourceManager.GetString("ExceptionChangeMemberUsernameBadFormat", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The owner username of personal timeline is of bad format..
+ /// </summary>
+ internal static string ExceptionFindTimelineUsernameBadFormat {
+ get {
+ return ResourceManager.GetString("ExceptionFindTimelineUsernameBadFormat", resourceCulture);
}
}
}
diff --git a/Timeline/Resources/Controllers/Testing/TestingI18nController.resx b/Timeline/Resources/Services/TimelineService.resx index 57dfd5b9..0429a2f8 100644 --- a/Timeline/Resources/Controllers/Testing/TestingI18nController.resx +++ b/Timeline/Resources/Services/TimelineService.resx @@ -117,7 +117,10 @@ <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
- <data name="TestString" xml:space="preserve">
- <value>English test string.</value>
+ <data name="ExceptionChangeMemberUsernameBadFormat" xml:space="preserve">
+ <value>The number {0} username is invalid.</value>
+ </data>
+ <data name="ExceptionFindTimelineUsernameBadFormat" xml:space="preserve">
+ <value>The owner username of personal timeline is of bad format.</value>
</data>
</root>
\ No newline at end of file diff --git a/Timeline/Resources/Services/UserCache.Designer.cs b/Timeline/Resources/Services/UserCache.Designer.cs deleted file mode 100644 index 28a74a6c..00000000 --- a/Timeline/Resources/Services/UserCache.Designer.cs +++ /dev/null @@ -1,99 +0,0 @@ -//------------------------------------------------------------------------------
-// <auto-generated>
-// 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.
-// </auto-generated>
-//------------------------------------------------------------------------------
-
-namespace Timeline.Resources.Services {
- using System;
-
-
- /// <summary>
- /// A strongly-typed resource class, for looking up localized strings, etc.
- /// </summary>
- // 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 UserCache {
-
- private static global::System.Resources.ResourceManager resourceMan;
-
- private static global::System.Globalization.CultureInfo resourceCulture;
-
- [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
- internal UserCache() {
- }
-
- /// <summary>
- /// Returns the cached ResourceManager instance used by this class.
- /// </summary>
- [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
- internal static global::System.Resources.ResourceManager ResourceManager {
- get {
- if (object.ReferenceEquals(resourceMan, null)) {
- global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Services.UserCache", typeof(UserCache).Assembly);
- resourceMan = temp;
- }
- return resourceMan;
- }
- }
-
- /// <summary>
- /// Overrides the current thread's CurrentUICulture property for all
- /// resource lookups using this strongly typed resource class.
- /// </summary>
- [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
- internal static global::System.Globalization.CultureInfo Culture {
- get {
- return resourceCulture;
- }
- set {
- resourceCulture = value;
- }
- }
-
- /// <summary>
- /// Looks up a localized string similar to Found user info from cache. Entry: {0} ..
- /// </summary>
- internal static string LogGetCacheExist {
- get {
- return ResourceManager.GetString("LogGetCacheExist", resourceCulture);
- }
- }
-
- /// <summary>
- /// Looks up a localized string similar to User info not exist in cache. Id: {0} ..
- /// </summary>
- internal static string LogGetCacheNotExist {
- get {
- return ResourceManager.GetString("LogGetCacheNotExist", resourceCulture);
- }
- }
-
- /// <summary>
- /// Looks up a localized string similar to User info remove in cache. Id: {0} ..
- /// </summary>
- internal static string LogRemoveCache {
- get {
- return ResourceManager.GetString("LogRemoveCache", resourceCulture);
- }
- }
-
- /// <summary>
- /// Looks up a localized string similar to User info set in cache. Entry: {0} ..
- /// </summary>
- internal static string LogSetCache {
- get {
- return ResourceManager.GetString("LogSetCache", resourceCulture);
- }
- }
- }
-}
diff --git a/Timeline/Resources/Services/UserCache.resx b/Timeline/Resources/Services/UserCache.resx deleted file mode 100644 index 1102108b..00000000 --- a/Timeline/Resources/Services/UserCache.resx +++ /dev/null @@ -1,132 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?>
-<root>
- <!--
- Microsoft ResX Schema
-
- Version 2.0
-
- The primary goals of this format is to allow a simple XML format
- that is mostly human readable. The generation and parsing of the
- various data types are done through the TypeConverter classes
- associated with the data types.
-
- Example:
-
- ... ado.net/XML headers & schema ...
- <resheader name="resmimetype">text/microsoft-resx</resheader>
- <resheader name="version">2.0</resheader>
- <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
- <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
- <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
- <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
- <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
- <value>[base64 mime encoded serialized .NET Framework object]</value>
- </data>
- <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
- <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
- <comment>This is a comment</comment>
- </data>
-
- There are any number of "resheader" rows that contain simple
- name/value pairs.
-
- Each data row contains a name, and value. The row also contains a
- type or mimetype. Type corresponds to a .NET class that support
- text/value conversion through the TypeConverter architecture.
- Classes that don't support this are serialized and stored with the
- mimetype set.
-
- The mimetype is used for serialized objects, and tells the
- ResXResourceReader how to depersist the object. This is currently not
- extensible. For a given mimetype the value must be set accordingly:
-
- Note - application/x-microsoft.net.object.binary.base64 is the format
- that the ResXResourceWriter will generate, however the reader can
- read any of the formats listed below.
-
- mimetype: application/x-microsoft.net.object.binary.base64
- value : The object must be serialized with
- : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
- : and then encoded with base64 encoding.
-
- mimetype: application/x-microsoft.net.object.soap.base64
- value : The object must be serialized with
- : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
- : and then encoded with base64 encoding.
-
- mimetype: application/x-microsoft.net.object.bytearray.base64
- value : The object must be serialized into a byte array
- : using a System.ComponentModel.TypeConverter
- : and then encoded with base64 encoding.
- -->
- <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
- <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
- <xsd:element name="root" msdata:IsDataSet="true">
- <xsd:complexType>
- <xsd:choice maxOccurs="unbounded">
- <xsd:element name="metadata">
- <xsd:complexType>
- <xsd:sequence>
- <xsd:element name="value" type="xsd:string" minOccurs="0" />
- </xsd:sequence>
- <xsd:attribute name="name" use="required" type="xsd:string" />
- <xsd:attribute name="type" type="xsd:string" />
- <xsd:attribute name="mimetype" type="xsd:string" />
- <xsd:attribute ref="xml:space" />
- </xsd:complexType>
- </xsd:element>
- <xsd:element name="assembly">
- <xsd:complexType>
- <xsd:attribute name="alias" type="xsd:string" />
- <xsd:attribute name="name" type="xsd:string" />
- </xsd:complexType>
- </xsd:element>
- <xsd:element name="data">
- <xsd:complexType>
- <xsd:sequence>
- <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
- <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
- </xsd:sequence>
- <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
- <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
- <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
- <xsd:attribute ref="xml:space" />
- </xsd:complexType>
- </xsd:element>
- <xsd:element name="resheader">
- <xsd:complexType>
- <xsd:sequence>
- <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
- </xsd:sequence>
- <xsd:attribute name="name" type="xsd:string" use="required" />
- </xsd:complexType>
- </xsd:element>
- </xsd:choice>
- </xsd:complexType>
- </xsd:element>
- </xsd:schema>
- <resheader name="resmimetype">
- <value>text/microsoft-resx</value>
- </resheader>
- <resheader name="version">
- <value>2.0</value>
- </resheader>
- <resheader name="reader">
- <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
- </resheader>
- <resheader name="writer">
- <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
- </resheader>
- <data name="LogGetCacheExist" xml:space="preserve">
- <value>Found user info from cache. Entry: {0} .</value>
- </data>
- <data name="LogGetCacheNotExist" xml:space="preserve">
- <value>User info not exist in cache. Id: {0} .</value>
- </data>
- <data name="LogRemoveCache" xml:space="preserve">
- <value>User info remove in cache. Id: {0} .</value>
- </data>
- <data name="LogSetCache" xml:space="preserve">
- <value>User info set in cache. Entry: {0} .</value>
- </data>
-</root>
\ No newline at end of file diff --git a/Timeline/Resources/Services/UserDetailService.Designer.cs b/Timeline/Resources/Services/UserDetailService.Designer.cs deleted file mode 100644 index 2f586b36..00000000 --- a/Timeline/Resources/Services/UserDetailService.Designer.cs +++ /dev/null @@ -1,99 +0,0 @@ -//------------------------------------------------------------------------------
-// <auto-generated>
-// 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.
-// </auto-generated>
-//------------------------------------------------------------------------------
-
-namespace Timeline.Resources.Services {
- using System;
-
-
- /// <summary>
- /// A strongly-typed resource class, for looking up localized strings, etc.
- /// </summary>
- // 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 UserDetailService {
-
- private static global::System.Resources.ResourceManager resourceMan;
-
- private static global::System.Globalization.CultureInfo resourceCulture;
-
- [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
- internal UserDetailService() {
- }
-
- /// <summary>
- /// Returns the cached ResourceManager instance used by this class.
- /// </summary>
- [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
- internal static global::System.Resources.ResourceManager ResourceManager {
- get {
- if (object.ReferenceEquals(resourceMan, null)) {
- global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Services.UserDetailService", typeof(UserDetailService).Assembly);
- resourceMan = temp;
- }
- return resourceMan;
- }
- }
-
- /// <summary>
- /// Overrides the current thread's CurrentUICulture property for all
- /// resource lookups using this strongly typed resource class.
- /// </summary>
- [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
- internal static global::System.Globalization.CultureInfo Culture {
- get {
- return resourceCulture;
- }
- set {
- resourceCulture = value;
- }
- }
-
- /// <summary>
- /// Looks up a localized string similar to Length of nickname can't be bigger than 10..
- /// </summary>
- internal static string ExceptionNicknameTooLong {
- get {
- return ResourceManager.GetString("ExceptionNicknameTooLong", resourceCulture);
- }
- }
-
- /// <summary>
- /// Looks up a localized string similar to A user_details entity has been created. User id is {0}. Nickname is {1}..
- /// </summary>
- internal static string LogEntityNicknameCreate {
- get {
- return ResourceManager.GetString("LogEntityNicknameCreate", resourceCulture);
- }
- }
-
- /// <summary>
- /// Looks up a localized string similar to Nickname of a user_details entity has been updated to a new value. User id is {0}. New value is {1}..
- /// </summary>
- internal static string LogEntityNicknameSetNotNull {
- get {
- return ResourceManager.GetString("LogEntityNicknameSetNotNull", resourceCulture);
- }
- }
-
- /// <summary>
- /// Looks up a localized string similar to Nickname of a user_details entity has been updated to null. User id is {0}..
- /// </summary>
- internal static string LogEntityNicknameSetToNull {
- get {
- return ResourceManager.GetString("LogEntityNicknameSetToNull", resourceCulture);
- }
- }
- }
-}
diff --git a/Timeline/Resources/Services/UserDetailService.resx b/Timeline/Resources/Services/UserDetailService.resx deleted file mode 100644 index ea32aeda..00000000 --- a/Timeline/Resources/Services/UserDetailService.resx +++ /dev/null @@ -1,132 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?>
-<root>
- <!--
- Microsoft ResX Schema
-
- Version 2.0
-
- The primary goals of this format is to allow a simple XML format
- that is mostly human readable. The generation and parsing of the
- various data types are done through the TypeConverter classes
- associated with the data types.
-
- Example:
-
- ... ado.net/XML headers & schema ...
- <resheader name="resmimetype">text/microsoft-resx</resheader>
- <resheader name="version">2.0</resheader>
- <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
- <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
- <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
- <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
- <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
- <value>[base64 mime encoded serialized .NET Framework object]</value>
- </data>
- <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
- <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
- <comment>This is a comment</comment>
- </data>
-
- There are any number of "resheader" rows that contain simple
- name/value pairs.
-
- Each data row contains a name, and value. The row also contains a
- type or mimetype. Type corresponds to a .NET class that support
- text/value conversion through the TypeConverter architecture.
- Classes that don't support this are serialized and stored with the
- mimetype set.
-
- The mimetype is used for serialized objects, and tells the
- ResXResourceReader how to depersist the object. This is currently not
- extensible. For a given mimetype the value must be set accordingly:
-
- Note - application/x-microsoft.net.object.binary.base64 is the format
- that the ResXResourceWriter will generate, however the reader can
- read any of the formats listed below.
-
- mimetype: application/x-microsoft.net.object.binary.base64
- value : The object must be serialized with
- : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
- : and then encoded with base64 encoding.
-
- mimetype: application/x-microsoft.net.object.soap.base64
- value : The object must be serialized with
- : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
- : and then encoded with base64 encoding.
-
- mimetype: application/x-microsoft.net.object.bytearray.base64
- value : The object must be serialized into a byte array
- : using a System.ComponentModel.TypeConverter
- : and then encoded with base64 encoding.
- -->
- <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
- <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
- <xsd:element name="root" msdata:IsDataSet="true">
- <xsd:complexType>
- <xsd:choice maxOccurs="unbounded">
- <xsd:element name="metadata">
- <xsd:complexType>
- <xsd:sequence>
- <xsd:element name="value" type="xsd:string" minOccurs="0" />
- </xsd:sequence>
- <xsd:attribute name="name" use="required" type="xsd:string" />
- <xsd:attribute name="type" type="xsd:string" />
- <xsd:attribute name="mimetype" type="xsd:string" />
- <xsd:attribute ref="xml:space" />
- </xsd:complexType>
- </xsd:element>
- <xsd:element name="assembly">
- <xsd:complexType>
- <xsd:attribute name="alias" type="xsd:string" />
- <xsd:attribute name="name" type="xsd:string" />
- </xsd:complexType>
- </xsd:element>
- <xsd:element name="data">
- <xsd:complexType>
- <xsd:sequence>
- <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
- <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
- </xsd:sequence>
- <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
- <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
- <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
- <xsd:attribute ref="xml:space" />
- </xsd:complexType>
- </xsd:element>
- <xsd:element name="resheader">
- <xsd:complexType>
- <xsd:sequence>
- <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
- </xsd:sequence>
- <xsd:attribute name="name" type="xsd:string" use="required" />
- </xsd:complexType>
- </xsd:element>
- </xsd:choice>
- </xsd:complexType>
- </xsd:element>
- </xsd:schema>
- <resheader name="resmimetype">
- <value>text/microsoft-resx</value>
- </resheader>
- <resheader name="version">
- <value>2.0</value>
- </resheader>
- <resheader name="reader">
- <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
- </resheader>
- <resheader name="writer">
- <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
- </resheader>
- <data name="ExceptionNicknameTooLong" xml:space="preserve">
- <value>Length of nickname can't be bigger than 10.</value>
- </data>
- <data name="LogEntityNicknameCreate" xml:space="preserve">
- <value>A user_details entity has been created. User id is {0}. Nickname is {1}.</value>
- </data>
- <data name="LogEntityNicknameSetNotNull" xml:space="preserve">
- <value>Nickname of a user_details entity has been updated to a new value. User id is {0}. New value is {1}.</value>
- </data>
- <data name="LogEntityNicknameSetToNull" xml:space="preserve">
- <value>Nickname of a user_details entity has been updated to null. User id is {0}.</value>
- </data>
-</root>
\ No newline at end of file diff --git a/Timeline/Services/ConfictException.cs b/Timeline/Services/ConflictException.cs index dcd77366..6ede183a 100644 --- a/Timeline/Services/ConfictException.cs +++ b/Timeline/Services/ConflictException.cs @@ -9,12 +9,12 @@ namespace Timeline.Services /// For example a username already exists and conflicts with the given username.
/// </remarks>
[Serializable]
- public class ConfictException : Exception
+ public class ConflictException : Exception
{
- public ConfictException() : base(Resources.Services.Exception.ConfictException) { }
- public ConfictException(string message) : base(message) { }
- public ConfictException(string message, Exception inner) : base(message, inner) { }
- protected ConfictException(
+ public ConflictException() : base(Resources.Services.Exception.ConflictException) { }
+ public ConflictException(string message) : base(message) { }
+ public ConflictException(string message, Exception inner) : base(message, inner) { }
+ protected ConflictException(
System.Runtime.Serialization.SerializationInfo info,
System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
}
diff --git a/Timeline/Services/DatabaseExtensions.cs b/Timeline/Services/DatabaseExtensions.cs deleted file mode 100644 index e77dd01a..00000000 --- a/Timeline/Services/DatabaseExtensions.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Microsoft.EntityFrameworkCore;
-using System;
-using System.Linq;
-using System.Threading.Tasks;
-using Timeline.Entities;
-using Timeline.Models.Validation;
-
-namespace Timeline.Services
-{
- internal static class DatabaseExtensions
- {
- private static readonly UsernameValidator usernameValidator = new UsernameValidator();
-
- /// <summary>
- /// Check the existence and get the id of the user.
- /// </summary>
- /// <param name="username">The username of the user.</param>
- /// <returns>The user id.</returns>
- /// <exception cref="ArgumentNullException">Thrown if <paramref name="username"/> is null.</exception>
- /// <exception cref="UsernameBadFormatException">Thrown if <paramref name="username"/> is of bad format.</exception>
- /// <exception cref="UserNotExistException">Thrown if user does not exist.</exception>
- internal static async Task<long> CheckAndGetUser(DbSet<UserEntity> userDbSet, string? username)
- {
- if (username == null)
- throw new ArgumentNullException(nameof(username));
- var (result, message) = usernameValidator.Validate(username);
- if (!result)
- throw new UsernameBadFormatException(username, message);
-
- var userId = await userDbSet.Where(u => u.Username == username).Select(u => u.Id).SingleOrDefaultAsync();
- if (userId == 0)
- throw new UserNotExistException(username);
- return userId;
- }
- }
-}
diff --git a/Timeline/Services/TimelineAlreadyExistException.cs b/Timeline/Services/TimelineAlreadyExistException.cs deleted file mode 100644 index c2dea1f9..00000000 --- a/Timeline/Services/TimelineAlreadyExistException.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System;
-
-namespace Timeline.Services
-{
- [Serializable]
- public class TimelineAlreadyExistException : Exception
- {
- public TimelineAlreadyExistException() : base(Resources.Services.Exception.TimelineAlreadyExistException) { }
- public TimelineAlreadyExistException(string name) : base(Resources.Services.Exception.TimelineAlreadyExistException) { Name = name; }
- public TimelineAlreadyExistException(string name, Exception inner) : base(Resources.Services.Exception.TimelineAlreadyExistException, inner) { Name = name; }
- protected TimelineAlreadyExistException(
- System.Runtime.Serialization.SerializationInfo info,
- System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
-
- public string? Name { get; set; }
- }
-}
diff --git a/Timeline/Services/TimelineMemberOperationUserException.cs b/Timeline/Services/TimelineMemberOperationUserException.cs deleted file mode 100644 index 543ee160..00000000 --- a/Timeline/Services/TimelineMemberOperationUserException.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System;
-using System.Globalization;
-
-namespace Timeline.Services
-{
- [Serializable]
- public class TimelineMemberOperationUserException : Exception
- {
- public enum MemberOperation
- {
- Add,
- Remove
- }
-
- public TimelineMemberOperationUserException() : base(Resources.Services.Exception.TimelineMemberOperationException) { }
- public TimelineMemberOperationUserException(string message) : base(message) { }
- public TimelineMemberOperationUserException(string message, Exception inner) : base(message, inner) { }
- protected TimelineMemberOperationUserException(
- System.Runtime.Serialization.SerializationInfo info,
- System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
-
- public TimelineMemberOperationUserException(int index, MemberOperation operation, string username, Exception inner)
- : base(MakeMessage(operation, index), inner) { Operation = operation; Index = index; Username = username; }
-
- private static string MakeMessage(MemberOperation operation, int index) => string.Format(CultureInfo.CurrentCulture,
- Resources.Services.Exception.TimelineMemberOperationExceptionDetail, operation, index);
-
- public MemberOperation? Operation { get; set; }
-
- /// <summary>
- /// The index of the member on which the operation failed.
- /// </summary>
- public int? Index { get; set; }
-
- public string? Username { get; set; }
- }
-}
diff --git a/Timeline/Services/TimelineNameBadFormatException.cs b/Timeline/Services/TimelineNameBadFormatException.cs deleted file mode 100644 index 5120a175..00000000 --- a/Timeline/Services/TimelineNameBadFormatException.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System;
-
-namespace Timeline.Services
-{
- [Serializable]
- public class TimelineNameBadFormatException : Exception
- {
- public TimelineNameBadFormatException()
- : base(Resources.Services.Exception.TimelineNameBadFormatException) { }
- public TimelineNameBadFormatException(string name)
- : base(Resources.Services.Exception.TimelineNameBadFormatException) { Name = name; }
- public TimelineNameBadFormatException(string name, Exception inner)
- : base(Resources.Services.Exception.TimelineNameBadFormatException, inner) { Name = name; }
-
- protected TimelineNameBadFormatException(
- System.Runtime.Serialization.SerializationInfo info,
- System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
-
- public string? Name { get; set; }
- }
-}
diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs index f43d2de5..89936aa2 100644 --- a/Timeline/Services/TimelineService.cs +++ b/Timeline/Services/TimelineService.cs @@ -1,13 +1,15 @@ -using Microsoft.EntityFrameworkCore;
+using AutoMapper;
+using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Timeline.Entities;
-using Timeline.Models;
using Timeline.Models.Http;
using Timeline.Models.Validation;
+using static Timeline.Resources.Services.TimelineService;
namespace Timeline.Services
{
@@ -28,12 +30,7 @@ namespace Timeline.Services /// <param name="name">Username or the timeline name. See remarks of <see cref="IBaseTimelineService"/>.</param>
/// <returns>A list of all posts.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> is null.</exception>
- /// <exception cref="TimelineNameBadFormatException">
- /// Thrown when timeline name is of bad format.
- /// For normal timeline, it means name is an empty string.
- /// For personal timeline, it means the username is of bad format,
- /// the inner exception should be a <see cref="UsernameBadFormatException"/>.
- /// </exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service).</exception>
/// <exception cref="TimelineNotExistException">
/// Thrown when timeline does not exist.
/// For normal timeline, it means the name does not exist.
@@ -46,26 +43,20 @@ namespace Timeline.Services /// Create a new post in timeline.
/// </summary>
/// <param name="name">Username or the timeline name. See remarks of <ssee cref="IBaseTimelineService"/>.</param>
- /// <param name="author">The author's username.</param>
+ /// <param name="authorId">The author's id.</param>
/// <param name="content">The content.</param>
/// <param name="time">The time of the post. If null, then use current time.</param>
/// <returns>The info of the created post.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> or <paramref name="author"/> or <paramref name="content"/> is null.</exception>
- /// <exception cref="TimelineNameBadFormatException">
- /// Thrown when timeline name is of bad format.
- /// For normal timeline, it means name is an empty string.
- /// For personal timeline, it means the username is of bad format,
- /// the inner exception should be a <see cref="UsernameBadFormatException"/>.
- /// </exception>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> or <paramref name="content"/> is null.</exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service).</exception>
/// <exception cref="TimelineNotExistException">
/// Thrown when timeline does not exist.
/// For normal timeline, it means the name does not exist.
/// For personal timeline, it means the user of that username does not exist
/// and the inner exception should be a <see cref="UserNotExistException"/>.
/// </exception>
- /// <exception cref="UsernameBadFormatException">Thrown if <paramref name="author"/> is of bad format.</exception>
- /// <exception cref="UserNotExistException">Thrown if <paramref name="author"/> does not exist.</exception>
- Task<TimelinePostCreateResponse> CreatePost(string name, string author, string content, DateTime? time);
+ /// <exception cref="UserNotExistException">Thrown if user with <paramref name="authorId"/> does not exist.</exception>
+ Task<TimelinePostInfo> CreatePost(string name, long authorId, string content, DateTime? time);
/// <summary>
/// Delete a post
@@ -73,12 +64,7 @@ namespace Timeline.Services /// <param name="name">Username or the timeline name. See remarks of <see cref="IBaseTimelineService"/>.</param>
/// <param name="id">The id of the post to delete.</param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> or <paramref name="username"/> is null.</exception>
- /// <exception cref="TimelineNameBadFormatException">
- /// Thrown when timeline name is of bad format.
- /// For normal timeline, it means name is an empty string.
- /// For personal timeline, it means the username is of bad format,
- /// the inner exception should be a <see cref="UsernameBadFormatException"/>.
- /// </exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service).</exception>
/// <exception cref="TimelineNotExistException">
/// Thrown when timeline does not exist.
/// For normal timeline, it means the name does not exist.
@@ -100,19 +86,14 @@ namespace Timeline.Services /// <param name="name">Username or the timeline name. See remarks of <see cref="IBaseTimelineService"/>.</param>
/// <param name="newProperties">The new properties. Null member means not to change.</param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> or <paramref name="newProperties"/> is null.</exception>
- /// <exception cref="TimelineNameBadFormatException">
- /// Thrown when timeline name is of bad format.
- /// For normal timeline, it means name is an empty string.
- /// For personal timeline, it means the username is of bad format,
- /// the inner exception should be a <see cref="UsernameBadFormatException"/>.
- /// </exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service).</exception>
/// <exception cref="TimelineNotExistException">
/// Thrown when timeline does not exist.
/// For normal timeline, it means the name does not exist.
/// For personal timeline, it means the user of that username does not exist
/// and the inner exception should be a <see cref="UserNotExistException"/>.
/// </exception>
- Task ChangeProperty(string name, TimelinePropertyChangeRequest newProperties);
+ Task ChangeProperty(string name, TimelinePatchRequest newProperties);
/// <summary>
/// Remove members to a timeline.
@@ -121,24 +102,16 @@ namespace Timeline.Services /// <param name="add">A list of usernames of members to add. May be null.</param>
/// <param name="remove">A list of usernames of members to remove. May be null.</param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> is null.</exception>
- /// <exception cref="TimelineNameBadFormatException">
- /// Thrown when timeline name is of bad format.
- /// For normal timeline, it means name is an empty string.
- /// For personal timeline, it means the username is of bad format,
- /// the inner exception should be a <see cref="UsernameBadFormatException"/>.
- /// </exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service).</exception>
+ /// <exception cref="ArgumentException">Thrown when names in <paramref name="add"/> or <paramref name="remove"/> is not a valid username.</exception>
/// <exception cref="TimelineNotExistException">
/// Thrown when timeline does not exist.
/// For normal timeline, it means the name does not exist.
/// For personal timeline, it means the user of that username does not exist
/// and the inner exception should be a <see cref="UserNotExistException"/>.
/// </exception>
- /// <exception cref="TimelineMemberOperationUserException">
- /// Thrown when an exception occurs on the user list.
- /// The inner exception is <see cref="UsernameBadFormatException"/>
- /// when one of the username is invalid.
- /// The inner exception is <see cref="UserNotExistException"/>
- /// when one of the user to change does not exist.
+ /// <exception cref="UserNotExistException">
+ /// Thrown when one of the user to change does not exist.
/// </exception>
/// <remarks>
/// Operating on a username that is of bad format or does not exist always throws.
@@ -153,42 +126,30 @@ namespace Timeline.Services /// Verify whether a visitor has the permission to read a timeline.
/// </summary>
/// <param name="name">Username or the timeline name. See remarks of <see cref="IBaseTimelineService"/>.</param>
- /// <param name="username">The user to check on. Null means visitor without account.</param>
+ /// <param name="visitorId">The id of the user to check on. Null means visitor without account.</param>
/// <returns>True if can read, false if can't read.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> is null.</exception>
- /// <exception cref="TimelineNameBadFormatException">
- /// Thrown when timeline name is of bad format.
- /// For normal timeline, it means name is an empty string.
- /// For personal timeline, it means the username is of bad format,
- /// the inner exception should be a <see cref="UsernameBadFormatException"/>.
- /// </exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service).</exception>
/// <exception cref="TimelineNotExistException">
/// Thrown when timeline does not exist.
/// For normal timeline, it means the name does not exist.
/// For personal timeline, it means the user of that username does not exist
/// and the inner exception should be a <see cref="UserNotExistException"/>.
/// </exception>
- /// <exception cref="UsernameBadFormatException">
- /// Thrown when <paramref name="username"/> is of bad format.
- /// </exception>
- /// <exception cref="UserNotExistException">
- /// Thrown when <paramref name="username"/> does not exist.
- /// </exception>
- Task<bool> HasReadPermission(string name, string? username);
+ /// <remarks>
+ /// This method does not check whether visitor is administrator.
+ /// Return false if user with visitor id does not exist.
+ /// </remarks>
+ Task<bool> HasReadPermission(string name, long? visitorId);
/// <summary>
/// Verify whether a user has the permission to modify a post.
/// </summary>
/// <param name="name">Username or the timeline name. See remarks of <see cref="IBaseTimelineService"/>.</param>
- /// <param name="username">The user to check on.</param>
+ /// <param name="modifierId">The id of the user to check on.</param>
/// <returns>True if can modify, false if can't modify.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> or <paramref name="username"/> is null.</exception>
- /// <exception cref="TimelineNameBadFormatException">
- /// Thrown when timeline name is of bad format.
- /// For normal timeline, it means name is an empty string.
- /// For personal timeline, it means the username is of bad format,
- /// the inner exception should be a <see cref="UsernameBadFormatException"/>.
- /// </exception>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> is null.</exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service).</exception>
/// <exception cref="TimelineNotExistException">
/// Thrown when timeline does not exist.
/// For normal timeline, it means the name does not exist.
@@ -198,47 +159,32 @@ namespace Timeline.Services /// <exception cref="TimelinePostNotExistException">
/// Thrown when the post with given id does not exist or is deleted already.
/// </exception>
- /// <exception cref="UsernameBadFormatException">
- /// Thrown when <paramref name="username"/> is of bad format.
- /// </exception>
- /// <exception cref="UserNotExistException">
- /// Thrown when <paramref name="username"/> does not exist.
- /// </exception>
/// <remarks>
/// This method does not check whether the user is administrator.
/// It only checks whether he is the author of the post or the owner of the timeline.
+ /// Return false when user with modifier id does not exist.
/// </remarks>
- Task<bool> HasPostModifyPermission(string name, long id, string username);
+ Task<bool> HasPostModifyPermission(string name, long id, long modifierId);
/// <summary>
/// Verify whether a user is member of a timeline.
/// </summary>
/// <param name="name">Username or the timeline name. See remarks of <see cref="IBaseTimelineService"/>.</param>
- /// <param name="username">The user to check on.</param>
+ /// <param name="userId">The id of user to check on.</param>
/// <returns>True if it is a member, false if not.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> or <paramref name="username"/> is null.</exception>
- /// <exception cref="TimelineNameBadFormatException">
- /// Thrown when timeline name is of bad format.
- /// For normal timeline, it means name is an empty string.
- /// For personal timeline, it means the username is of bad format,
- /// the inner exception should be a <see cref="UsernameBadFormatException"/>.
- /// </exception>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> is null.</exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service).</exception>
/// <exception cref="TimelineNotExistException">
/// Thrown when timeline does not exist.
/// For normal timeline, it means the name does not exist.
/// For personal timeline, it means the user of that username does not exist
/// and the inner exception should be a <see cref="UserNotExistException"/>.
/// </exception>
- /// <exception cref="UsernameBadFormatException">
- /// Thrown when <paramref name="username"/> is not a valid username.
- /// </exception>
- /// <exception cref="UserNotExistException">
- /// Thrown when user <paramref name="username"/> does not exist.
- /// </exception>
/// <remarks>
/// Timeline owner is also considered as a member.
+ /// Return false when user with user id does not exist.
/// </remarks>
- Task<bool> IsMemberOf(string name, string username);
+ Task<bool> IsMemberOf(string name, long userId);
}
/// <summary>
@@ -252,7 +198,7 @@ namespace Timeline.Services /// <param name="name">The name of the timeline.</param>
/// <returns>The timeline info.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> is null.</exception>
- /// <exception cref="TimelineNameBadFormatException">
+ /// <exception cref="ArgumentException">
/// Thrown when timeline name is invalid. Currently it means it is an empty string.
/// </exception>
/// <exception cref="TimelineNotExistException">
@@ -264,20 +210,12 @@ namespace Timeline.Services /// Create a timeline.
/// </summary>
/// <param name="name">The name of the timeline.</param>
- /// <param name="owner">The owner of the timeline.</param>
+ /// <param name="owner">The id of owner of the timeline.</param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> or <paramref name="owner"/> is null.</exception>
- /// <exception cref="TimelineNameBadFormatException">
- /// Thrown when timeline name is invalid. Currently it means it is an empty string.
- /// </exception>
- /// <exception cref="TimelineAlreadyExistException">
- /// Thrown when the timeline already exists.
- /// </exception>
- /// <exception cref="UsernameBadFormatException">
- /// Thrown when the username of the owner is not valid.
- /// </exception>
- /// <exception cref="UserNotExistException">
- /// Thrown when the owner user does not exist.</exception>
- Task CreateTimeline(string name, string owner);
+ /// <exception cref="ArgumentException">Thrown when timeline name is invalid. Currently it means it is an empty string.</exception>
+ /// <exception cref="ConflictException">Thrown when the timeline already exists.</exception>
+ /// <exception cref="UserNotExistException">Thrown when the owner user does not exist.</exception>
+ Task CreateTimeline(string name, long owner);
}
public interface IPersonalTimelineService : IBaseTimelineService
@@ -290,8 +228,8 @@ namespace Timeline.Services /// <exception cref="ArgumentNullException">
/// Thrown when <paramref name="username"/> is null.
/// </exception>
- /// <exception cref="TimelineNameBadFormatException">
- /// Thrown when <paramref name="username"/> is of bad format. Inner exception MUST be <see cref="UsernameBadFormatException"/>.
+ /// <exception cref="ArgumentException">
+ /// Thrown when <paramref name="username"/> is of bad format.
/// </exception>
/// <exception cref="TimelineNotExistException">
/// Thrown when the user does not exist. Inner exception MUST be <see cref="UserNotExistException"/>.
@@ -301,10 +239,12 @@ namespace Timeline.Services public abstract class BaseTimelineService : IBaseTimelineService
{
- protected BaseTimelineService(ILoggerFactory loggerFactory, DatabaseContext database, IClock clock)
+ protected BaseTimelineService(ILoggerFactory loggerFactory, DatabaseContext database, IUserService userService, IMapper mapper, IClock clock)
{
Clock = clock;
Database = database;
+ UserService = userService;
+ Mapper = mapper;
}
protected IClock Clock { get; }
@@ -313,6 +253,10 @@ namespace Timeline.Services protected DatabaseContext Database { get; }
+ protected IUserService UserService { get; }
+
+ protected IMapper Mapper { get; }
+
/// <summary>
/// Find the timeline id by the name.
/// For details, see remarks.
@@ -320,12 +264,7 @@ namespace Timeline.Services /// <param name="name">The username or the timeline name. See remarks.</param>
/// <returns>The id of the timeline entity.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> is null.</exception>
- /// <exception cref="TimelineNameBadFormatException">
- /// Thrown when timeline name is of bad format.
- /// For normal timeline, it means name is an empty string.
- /// For personal timeline, it means the username is of bad format,
- /// the inner exception should be a <see cref="UsernameBadFormatException"/>.
- /// </exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service).</exception>
/// <exception cref="TimelineNotExistException">
/// Thrown when timeline does not exist.
/// For normal timeline, it means the name does not exist.
@@ -347,66 +286,60 @@ namespace Timeline.Services if (name == null)
throw new ArgumentNullException(nameof(name));
+
var timelineId = await FindTimelineId(name);
var postEntities = await Database.TimelinePosts.OrderBy(p => p.Time).Where(p => p.TimelineId == timelineId && p.Content != null).ToListAsync();
+
var posts = new List<TimelinePostInfo>();
foreach (var entity in postEntities)
{
- posts.Add(new TimelinePostInfo
+ if (entity.Content != null) // otherwise it is deleted
{
- Id = entity.Id,
- Content = entity.Content,
- Author = (await Database.Users.Where(u => u.Id == entity.AuthorId).Select(u => new { u.Username }).SingleAsync()).Name,
- Time = entity.Time
- });
+ var author = Mapper.Map<UserInfo>(UserService.GetUserById(entity.AuthorId));
+ posts.Add(new TimelinePostInfo
+ {
+ Id = entity.Id,
+ Content = entity.Content,
+ Author = author,
+ Time = entity.Time,
+ LastUpdated = entity.LastUpdated
+ });
+ }
}
return posts;
}
- public async Task<TimelinePostCreateResponse> CreatePost(string name, string author, string content, DateTime? time)
+ public async Task<TimelinePostInfo> CreatePost(string name, long authorId, string content, DateTime? time)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
- if (author == null)
- throw new ArgumentNullException(nameof(author));
if (content == null)
throw new ArgumentNullException(nameof(content));
- {
- var (result, message) = UsernameValidator.Validate(author);
- if (!result)
- {
- throw new UsernameBadFormatException(author, message);
- }
- }
-
var timelineId = await FindTimelineId(name);
-
- var authorEntity = Database.Users.Where(u => u.Username == author).Select(u => new { u.Id }).SingleOrDefault();
- if (authorEntity == null)
- {
- throw new UserNotExistException(author);
- }
- var authorId = authorEntity.Id;
+ var author = Mapper.Map<UserInfo>(await UserService.GetUserById(authorId));
var currentTime = Clock.GetCurrentTime();
+ var finalTime = time ?? currentTime;
var postEntity = new TimelinePostEntity
{
Content = content,
AuthorId = authorId,
TimelineId = timelineId,
- Time = time ?? currentTime,
+ Time = finalTime,
LastUpdated = currentTime
};
-
Database.TimelinePosts.Add(postEntity);
await Database.SaveChangesAsync();
- return new TimelinePostCreateResponse
+ return new TimelinePostInfo
{
Id = postEntity.Id,
- Time = postEntity.Time
+ Content = content,
+ Author = author,
+ Time = finalTime,
+ LastUpdated = currentTime
};
}
@@ -426,7 +359,7 @@ namespace Timeline.Services await Database.SaveChangesAsync();
}
- public async Task ChangeProperty(string name, TimelinePropertyChangeRequest newProperties)
+ public async Task ChangeProperty(string name, TimelinePatchRequest newProperties)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
@@ -455,27 +388,23 @@ namespace Timeline.Services if (name == null)
throw new ArgumentNullException(nameof(name));
- // remove duplication and check the format of each username.
- // Return a username->index map.
- Dictionary<string, int>? RemoveDuplicateAndCheckFormat(IList<string>? list, TimelineMemberOperationUserException.MemberOperation operation)
+ List<string>? RemoveDuplicateAndCheckFormat(IList<string>? list, string paramName)
{
if (list != null)
{
- Dictionary<string, int> result = new Dictionary<string, int>();
+ List<string> result = new List<string>();
var count = list.Count;
for (var index = 0; index < count; index++)
{
var username = list[index];
- if (result.ContainsKey(username))
+ if (result.Contains(username))
{
continue;
}
var (validationResult, message) = UsernameValidator.Validate(username);
if (!validationResult)
- throw new TimelineMemberOperationUserException(
- index, operation, username,
- new UsernameBadFormatException(username, message));
- result.Add(username, index);
+ throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, ExceptionChangeMemberUsernameBadFormat, index), nameof(paramName));
+ result.Add(username);
}
return result;
}
@@ -484,13 +413,13 @@ namespace Timeline.Services return null;
}
}
- var simplifiedAdd = RemoveDuplicateAndCheckFormat(add, TimelineMemberOperationUserException.MemberOperation.Add);
- var simplifiedRemove = RemoveDuplicateAndCheckFormat(remove, TimelineMemberOperationUserException.MemberOperation.Remove);
+ var simplifiedAdd = RemoveDuplicateAndCheckFormat(add, nameof(add));
+ var simplifiedRemove = RemoveDuplicateAndCheckFormat(remove, nameof(remove));
// remove those both in add and remove
if (simplifiedAdd != null && simplifiedRemove != null)
{
- var usersToClean = simplifiedRemove.Keys.Where(u => simplifiedAdd.ContainsKey(u));
+ var usersToClean = simplifiedRemove.Where(u => simplifiedAdd.Contains(u)).ToList();
foreach (var u in usersToClean)
{
simplifiedAdd.Remove(u);
@@ -500,26 +429,20 @@ namespace Timeline.Services var timelineId = await FindTimelineId(name);
- async Task<List<long>?> CheckExistenceAndGetId(Dictionary<string, int>? map, TimelineMemberOperationUserException.MemberOperation operation)
+ async Task<List<long>?> CheckExistenceAndGetId(List<string>? list)
{
- if (map == null)
+ if (list == null)
return null;
List<long> result = new List<long>();
- foreach (var (username, index) in map)
+ foreach (var username in list)
{
- var user = await Database.Users.Where(u => u.Username == username).Select(u => new { u.Id }).SingleOrDefaultAsync();
- if (user == null)
- {
- throw new TimelineMemberOperationUserException(index, operation, username,
- new UserNotExistException(username));
- }
- result.Add(user.Id);
+ result.Add(await UserService.GetUserIdByUsername(username));
}
return result;
}
- var userIdsAdd = await CheckExistenceAndGetId(simplifiedAdd, TimelineMemberOperationUserException.MemberOperation.Add);
- var userIdsRemove = await CheckExistenceAndGetId(simplifiedRemove, TimelineMemberOperationUserException.MemberOperation.Remove);
+ var userIdsAdd = await CheckExistenceAndGetId(simplifiedAdd);
+ var userIdsRemove = await CheckExistenceAndGetId(simplifiedRemove);
if (userIdsAdd != null)
{
@@ -536,30 +459,11 @@ namespace Timeline.Services await Database.SaveChangesAsync();
}
- public async Task<bool> HasReadPermission(string name, string? username)
+ public async Task<bool> HasReadPermission(string name, long? visitorId)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
- long? userId = null;
- if (username != null)
- {
- var (result, message) = UsernameValidator.Validate(username);
- if (!result)
- {
- throw new UsernameBadFormatException(username);
- }
-
- var user = await Database.Users.Where(u => u.Username == username).Select(u => new { u.Id }).SingleOrDefaultAsync();
-
- if (user == null)
- {
- throw new UserNotExistException(username);
- }
-
- userId = user.Id;
- }
-
var timelineId = await FindTimelineId(name);
var timelineEntity = await Database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.Visibility }).SingleAsync();
@@ -567,43 +471,24 @@ namespace Timeline.Services if (timelineEntity.Visibility == TimelineVisibility.Public)
return true;
- if (timelineEntity.Visibility == TimelineVisibility.Register && username != null)
+ if (timelineEntity.Visibility == TimelineVisibility.Register && visitorId != null)
return true;
- if (userId == null)
+ if (visitorId == null)
{
return false;
}
else
{
- var memberEntity = await Database.TimelineMembers.Where(m => m.UserId == userId && m.TimelineId == timelineId).SingleOrDefaultAsync();
+ var memberEntity = await Database.TimelineMembers.Where(m => m.UserId == visitorId && m.TimelineId == timelineId).SingleOrDefaultAsync();
return memberEntity != null;
}
}
- public async Task<bool> HasPostModifyPermission(string name, long id, string username)
+ public async Task<bool> HasPostModifyPermission(string name, long id, long modifierId)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
- if (username == null)
- throw new ArgumentNullException(nameof(username));
-
- {
- var (result, message) = UsernameValidator.Validate(username);
- if (!result)
- {
- throw new UsernameBadFormatException(username);
- }
- }
-
- var user = await Database.Users.Where(u => u.Username == username).Select(u => new { u.Id }).SingleOrDefaultAsync();
-
- if (user == null)
- {
- throw new UserNotExistException(username);
- }
-
- var userId = user.Id;
var timelineId = await FindTimelineId(name);
@@ -614,32 +499,13 @@ namespace Timeline.Services if (postEntity == null)
throw new TimelinePostNotExistException(id);
- return timelineEntity.OwnerId == userId || postEntity.AuthorId == userId;
+ return timelineEntity.OwnerId == modifierId || postEntity.AuthorId == modifierId;
}
- public async Task<bool> IsMemberOf(string name, string username)
+ public async Task<bool> IsMemberOf(string name, long userId)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
- if (username == null)
- throw new ArgumentNullException(nameof(username));
-
- {
- var (result, message) = UsernameValidator.Validate(username);
- if (!result)
- {
- throw new UsernameBadFormatException(username);
- }
- }
-
- var user = await Database.Users.Where(u => u.Username == username).Select(u => new { u.Id }).SingleOrDefaultAsync();
-
- if (user == null)
- {
- throw new UserNotExistException(username);
- }
-
- var userId = user.Id;
var timelineId = await FindTimelineId(name);
@@ -648,38 +514,33 @@ namespace Timeline.Services if (userId == timelineEntity.OwnerId)
return true;
- var timelineMemberEntity = await Database.TimelineMembers.Where(m => m.TimelineId == timelineId && m.UserId == userId).SingleOrDefaultAsync();
-
- return timelineMemberEntity != null;
+ return await Database.TimelineMembers.AnyAsync(m => m.TimelineId == timelineId && m.UserId == userId);
}
}
public class PersonalTimelineService : BaseTimelineService, IPersonalTimelineService
{
- public PersonalTimelineService(ILoggerFactory loggerFactory, DatabaseContext database, IClock clock)
- : base(loggerFactory, database, clock)
+ public PersonalTimelineService(ILoggerFactory loggerFactory, DatabaseContext database, IUserService userService, IMapper mapper, IClock clock)
+ : base(loggerFactory, database, userService, mapper, clock)
{
}
protected override async Task<long> FindTimelineId(string name)
{
+ long userId;
+ try
{
- var (result, message) = UsernameValidator.Validate(name);
- if (!result)
- {
- throw new TimelineNameBadFormatException(name, new UsernameBadFormatException(name, message));
- }
+ userId = await UserService.GetUserIdByUsername(name);
}
-
- var userEntity = await Database.Users.Where(u => u.Username == name).Select(u => new { u.Id }).SingleOrDefaultAsync();
-
- if (userEntity == null)
+ catch (ArgumentException e)
{
- throw new TimelineNotExistException(name, new UserNotExistException(name));
+ throw new ArgumentException(ExceptionFindTimelineUsernameBadFormat, nameof(name), e);
+ }
+ catch (UserNotExistException e)
+ {
+ throw new TimelineNotExistException(name, e);
}
-
- var userId = userEntity.Id;
var timelineEntity = await Database.Timelines.Where(t => t.OwnerId == userId && t.Name == null).Select(t => new { t.Id }).SingleOrDefaultAsync();
@@ -715,16 +576,20 @@ namespace Timeline.Services var timelineMemberEntities = await Database.TimelineMembers.Where(m => m.TimelineId == timelineId).Select(m => new { m.UserId }).ToListAsync();
- var memberUsernameTasks = timelineMemberEntities.Select(m => Database.Users.Where(u => u.Id == m.UserId).Select(u => u.Username).SingleAsync()).ToArray();
+ var owner = Mapper.Map<UserInfo>(await UserService.GetUserById(timelineEntity.OwnerId));
- var memberUsernames = await Task.WhenAll(memberUsernameTasks);
+ var members = new List<UserInfo>();
+ foreach (var memberEntity in timelineMemberEntities)
+ {
+ members.Add(Mapper.Map<UserInfo>(await UserService.GetUserById(memberEntity.UserId)));
+ }
return new BaseTimelineInfo
{
Description = timelineEntity.Description ?? "",
- Owner = username,
+ Owner = owner,
Visibility = timelineEntity.Visibility,
- Members = memberUsernames.ToList()
+ Members = members
};
}
diff --git a/Timeline/Services/User.cs b/Timeline/Services/User.cs index f63a374e..09a472e5 100644 --- a/Timeline/Services/User.cs +++ b/Timeline/Services/User.cs @@ -1,14 +1,9 @@ -using Microsoft.AspNetCore.Mvc;
-using System;
-using Timeline.Controllers;
-
-namespace Timeline.Services
+namespace Timeline.Services
{
public class User
{
public string? Username { get; set; }
public string? Nickname { get; set; }
- public string? AvatarUrl { get; set; }
#region adminsecret
public bool? Administrator { get; set; }
@@ -20,30 +15,4 @@ namespace Timeline.Services public long? Version { get; set; }
#endregion secret
}
-
- public static class UserExtensions
- {
- public static User EraseSecretAndFinalFill(this User user, IUrlHelper urlHelper, bool adminstrator)
- {
- if (user == null)
- throw new ArgumentNullException(nameof(user));
-
- var result = new User
- {
- Username = user.Username,
- Nickname = user.Nickname,
- AvatarUrl = urlHelper.ActionLink(action: nameof(UserAvatarController.Get), controller: nameof(UserAvatarController), values: new
- {
- user.Username
- })
- };
-
- if (adminstrator)
- {
- result.Administrator = user.Administrator;
- }
-
- return result;
- }
- }
}
diff --git a/Timeline/Services/UserAvatarService.cs b/Timeline/Services/UserAvatarService.cs index ac7dd857..39b408e6 100644 --- a/Timeline/Services/UserAvatarService.cs +++ b/Timeline/Services/UserAvatarService.cs @@ -11,7 +11,6 @@ using System.Linq; using System.Threading.Tasks;
using Timeline.Entities;
using Timeline.Helpers;
-using Timeline.Models.Validation;
namespace Timeline.Services
{
@@ -61,36 +60,27 @@ namespace Timeline.Services public interface IUserAvatarService
{
/// <summary>
- /// Get the etag of a user's avatar.
+ /// Get the etag of a user's avatar. Warning: This method does not check the user existence.
/// </summary>
- /// <param name="username">The username of the user to get avatar etag of.</param>
+ /// <param name="id">The id of the user to get avatar etag of.</param>
/// <returns>The etag.</returns>
- /// <exception cref="ArgumentNullException">Thrown if <paramref name="username"/> is null.</exception>
- /// <exception cref="UsernameBadFormatException">Thrown if the <paramref name="username"/> is of bad format.</exception>
- /// <exception cref="UserNotExistException">Thrown if the user does not exist.</exception>
- Task<string> GetAvatarETag(string username);
+ Task<string> GetAvatarETag(long id);
/// <summary>
- /// Get avatar of a user. If the user has no avatar set, a default one is returned.
+ /// Get avatar of a user. If the user has no avatar set, a default one is returned. Warning: This method does not check the user existence.
/// </summary>
- /// <param name="username">The username of the user to get avatar of.</param>
+ /// <param name="id">The id of the user to get avatar of.</param>
/// <returns>The avatar info.</returns>
- /// <exception cref="ArgumentNullException">Thrown if <paramref name="username"/> is null.</exception>
- /// <exception cref="UsernameBadFormatException">Thrown if the <paramref name="username"/> is of bad format.</exception>
- /// <exception cref="UserNotExistException">Thrown if the user does not exist.</exception>
- Task<AvatarInfo> GetAvatar(string username);
+ Task<AvatarInfo> GetAvatar(long id);
/// <summary>
- /// Set avatar for a user.
+ /// Set avatar for a user. Warning: This method does not check the user existence.
/// </summary>
- /// <param name="username">The username of the user to set avatar for.</param>
+ /// <param name="id">The id of the user to set avatar for.</param>
/// <param name="avatar">The avatar. Can be null to delete the saved avatar.</param>
- /// <exception cref="ArgumentNullException">Throw if <paramref name="username"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown if any field in <paramref name="avatar"/> is null when <paramref name="avatar"/> is not null.</exception>
- /// <exception cref="UsernameBadFormatException">Thrown if the <paramref name="username"/> is of bad format.</exception>
- /// <exception cref="UserNotExistException">Thrown if the user does not exist.</exception>
/// <exception cref="AvatarFormatException">Thrown if avatar is of bad format.</exception>
- Task SetAvatar(string username, Avatar? avatar);
+ Task SetAvatar(long id, Avatar? avatar);
}
// TODO! : Make this configurable.
@@ -104,7 +94,6 @@ namespace Timeline.Services private DateTime _cacheLastModified;
private string _cacheETag = default!;
- [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "DI.")]
public DefaultUserAvatarProvider(IWebHostEnvironment environment, IETagGenerator eTagGenerator)
{
_avatarPath = Path.Combine(environment.ContentRootPath, "default-avatar.png");
@@ -195,22 +184,18 @@ namespace Timeline.Services _clock = clock;
}
- public async Task<string> GetAvatarETag(string username)
+ public async Task<string> GetAvatarETag(long id)
{
- var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username);
-
- var eTag = (await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.ETag }).SingleOrDefaultAsync())?.ETag;
+ var eTag = (await _database.UserAvatars.Where(a => a.UserId == id).Select(a => new { a.ETag }).SingleOrDefaultAsync())?.ETag;
if (eTag == null)
return await _defaultUserAvatarProvider.GetDefaultAvatarETag();
else
return eTag;
}
- public async Task<AvatarInfo> GetAvatar(string username)
+ public async Task<AvatarInfo> GetAvatar(long id)
{
- var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username);
-
- var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.Type, a.Data, a.LastModified }).SingleOrDefaultAsync();
+ var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == id).Select(a => new { a.Type, a.Data, a.LastModified }).SingleOrDefaultAsync();
if (avatarEntity != null)
{
@@ -240,7 +225,7 @@ namespace Timeline.Services return defaultAvatar;
}
- public async Task SetAvatar(string username, Avatar? avatar)
+ public async Task SetAvatar(long id, Avatar? avatar)
{
if (avatar != null)
{
@@ -250,8 +235,7 @@ namespace Timeline.Services throw new ArgumentException(Resources.Services.UserAvatarService.ExceptionAvatarTypeNullOrEmpty, nameof(avatar));
}
- var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username);
- var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == userId).SingleOrDefaultAsync();
+ var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == id).SingleOrDefaultAsync();
if (avatar == null)
{
@@ -281,7 +265,7 @@ namespace Timeline.Services avatarEntity.Data = avatar.Data;
avatarEntity.ETag = await _eTagGenerator.Generate(avatar.Data);
avatarEntity.LastModified = _clock.GetCurrentTime();
- avatarEntity.UserId = userId;
+ avatarEntity.UserId = id;
if (create)
{
_database.UserAvatars.Add(avatarEntity);
diff --git a/Timeline/Services/UserRoleConvert.cs b/Timeline/Services/UserRoleConvert.cs index 4fa4a7b8..f27ee1bb 100644 --- a/Timeline/Services/UserRoleConvert.cs +++ b/Timeline/Services/UserRoleConvert.cs @@ -5,7 +5,6 @@ using Timeline.Entities; namespace Timeline.Services
{
- [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "No need.")]
public static class UserRoleConvert
{
public const string UserRole = UserRoles.User;
diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs index ff2306c5..1197bb73 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -44,6 +44,16 @@ namespace Timeline.Services Task<User> GetUserByUsername(string username);
/// <summary>
+ /// Get the user id of given username.
+ /// </summary>
+ /// <param name="username">Username of the user.</param>
+ /// <returns>The id of the user.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="username"/> is null.</exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="username"/> is of bad format.</exception>
+ /// <exception cref="UserNotExistException">Thrown when the user with given username does not exist.</exception>
+ Task<long> GetUserIdByUsername(string username);
+
+ /// <summary>
/// List all users.
/// </summary>
/// <returns>The user info of users.</returns>
@@ -57,7 +67,7 @@ namespace Timeline.Services /// <returns>The id of the new user.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="info"/>is null.</exception>
/// <exception cref="ArgumentException">Thrown when some fields in <paramref name="info"/> is bad.</exception>
- /// <exception cref="ConfictException">Thrown when a user with given username already exists.</exception>
+ /// <exception cref="ConflictException">Thrown when a user with given username already exists.</exception>
/// <remarks>
/// <see cref="User.Username"/> must not be null and must be a valid username.
/// <see cref="User.Password"/> must not be null or empty.
@@ -78,13 +88,12 @@ namespace Timeline.Services /// Only <see cref="User.Username"/>, <see cref="User.Administrator"/>, <see cref="User.Password"/> and <see cref="User.Nickname"/> will be used.
/// If null, then not change.
/// Other fields are ignored.
- /// After modified, even if nothing is changed, version will increase.
+ /// Version will increase if password is changed.
///
/// <see cref="User.Username"/> must be a valid username if set.
/// <see cref="User.Password"/> can't be empty if set.
/// <see cref="User.Nickname"/> must be a valid nickname if set.
///
- /// Note: Whether <see cref="User.Version"/> is set or not, version will increase and not set to the specified value if there is one.
/// </remarks>
/// <seealso cref="ModifyUser(string, User)"/>
Task ModifyUser(long id, User? info);
@@ -97,6 +106,7 @@ namespace Timeline.Services /// <exception cref="ArgumentNullException">Thrown when <paramref name="username"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="username"/> is of bad format or some fields in <paramref name="info"/> is bad.</exception>
/// <exception cref="UserNotExistException">Thrown when user with given id does not exist.</exception>
+ /// <exception cref="ConflictException">Thrown when user with the newusername already exist.</exception>
/// <remarks>
/// Only <see cref="User.Administrator"/>, <see cref="User.Password"/> and <see cref="User.Nickname"/> will be used.
/// If null, then not change.
@@ -184,7 +194,7 @@ namespace Timeline.Services private static void ThrowUsernameConflict()
{
- throw new ConfictException(ExceptionUsernameConflict);
+ throw new ConflictException(ExceptionUsernameConflict);
}
private static User CreateUserFromEntity(UserEntity entity)
@@ -245,6 +255,21 @@ namespace Timeline.Services return CreateUserFromEntity(entity);
}
+ public async Task<long> GetUserIdByUsername(string username)
+ {
+ if (username == null)
+ throw new ArgumentNullException(nameof(username));
+
+ CheckUsernameFormat(username, nameof(username));
+
+ var entity = await _databaseContext.Users.Where(user => user.Username == username).Select(u => new { u.Id }).SingleOrDefaultAsync();
+
+ if (entity == null)
+ throw new UserNotExistException(username);
+
+ return entity.Id;
+ }
+
public async Task<User[]> GetUsers()
{
var entities = await _databaseContext.Users.ToArrayAsync();
@@ -325,6 +350,7 @@ namespace Timeline.Services if (password != null)
{
entity.Password = _passwordService.HashPassword(password);
+ entity.Version += 1;
}
var administrator = info.Administrator;
@@ -339,8 +365,6 @@ namespace Timeline.Services entity.Nickname = nickname;
}
}
-
- entity.Version += 1;
}
diff --git a/Timeline/Services/UserTokenService.cs b/Timeline/Services/UserTokenService.cs index c246fdff..cf7286f4 100644 --- a/Timeline/Services/UserTokenService.cs +++ b/Timeline/Services/UserTokenService.cs @@ -49,7 +49,6 @@ namespace Timeline.Services private readonly JwtSecurityTokenHandler _tokenHandler = new JwtSecurityTokenHandler();
private SymmetricSecurityKey _tokenSecurityKey;
- [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "<Pending>")]
public JwtUserTokenService(IOptionsMonitor<JwtConfig> jwtConfig, IClock clock)
{
_jwtConfig = jwtConfig;
diff --git a/Timeline/Startup.cs b/Timeline/Startup.cs index 091a16e5..998b5c44 100644 --- a/Timeline/Startup.cs +++ b/Timeline/Startup.cs @@ -1,3 +1,4 @@ +using AutoMapper;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpOverrides;
@@ -47,7 +48,6 @@ namespace Timeline });
services.Configure<JwtConfig>(Configuration.GetSection(nameof(JwtConfig)));
- var jwtConfig = Configuration.GetSection(nameof(JwtConfig)).Get<JwtConfig>();
services.AddAuthentication(AuthenticationConstants.Scheme)
.AddScheme<MyAuthenticationOptions, MyAuthenticationHandler>(AuthenticationConstants.Scheme, AuthenticationConstants.DisplayName, o => { });
services.AddAuthorization();
@@ -75,16 +75,14 @@ namespace Timeline });
}
- services.AddLocalization(options =>
- {
- options.ResourcesPath = "Resources";
- });
+ services.AddAutoMapper(GetType().Assembly);
+
+ services.AddTransient<IClock, Clock>();
+ services.AddTransient<IPasswordService, PasswordService>();
services.AddScoped<IUserService, UserService>();
services.AddScoped<IUserTokenService, JwtUserTokenService>();
services.AddScoped<IUserTokenManager, UserTokenManager>();
- services.AddTransient<IPasswordService, PasswordService>();
- services.AddTransient<IClock, Clock>();
services.AddUserAvatarService();
services.AddScoped<IPersonalTimelineService, PersonalTimelineService>();
diff --git a/Timeline/Timeline.csproj b/Timeline/Timeline.csproj index 82b45094..25d73068 100644 --- a/Timeline/Timeline.csproj +++ b/Timeline/Timeline.csproj @@ -15,6 +15,8 @@ </ItemGroup>
<ItemGroup>
+ <PackageReference Include="AutoMapper" Version="9.0.0" />
+ <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="7.0.0" />
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -26,14 +28,14 @@ <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="3.1.0" />
+ <PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="3.1.1" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql.Design" Version="1.1.2" />
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.0-dev002868" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="5.6.0" />
</ItemGroup>
<ItemGroup>
- <ProjectReference Include="..\Timeline.ErrorCodes\Timeline.ErrorCodes.csproj" />
+ <ProjectReference Include="..\Timeline.ErrorCodes\Timeline.ErrorCodes.csproj" />
</ItemGroup>
<ItemGroup>
@@ -42,11 +44,6 @@ <AutoGen>True</AutoGen>
<DependentUpon>AuthHandler.resx</DependentUpon>
</Compile>
- <Compile Update="Resources\Controllers\Testing\TestingI18nController.Designer.cs">
- <DesignTime>True</DesignTime>
- <AutoGen>True</AutoGen>
- <DependentUpon>TestingI18nController.resx</DependentUpon>
- </Compile>
<Compile Update="Resources\Controllers\TimelineController.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
@@ -73,9 +70,9 @@ <DependentUpon>Filters.resx</DependentUpon>
</Compile>
<Compile Update="Resources\Messages.Designer.cs">
- <DesignTime>True</DesignTime>
- <AutoGen>True</AutoGen>
- <DependentUpon>Messages.resx</DependentUpon>
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>Messages.resx</DependentUpon>
</Compile>
<Compile Update="Resources\Models\Http\Common.Designer.cs">
<DesignTime>True</DesignTime>
@@ -83,9 +80,9 @@ <DependentUpon>Common.resx</DependentUpon>
</Compile>
<Compile Update="Resources\Models\Validation\NicknameValidator.Designer.cs">
- <DesignTime>True</DesignTime>
- <AutoGen>True</AutoGen>
- <DependentUpon>NicknameValidator.resx</DependentUpon>
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>NicknameValidator.resx</DependentUpon>
</Compile>
<Compile Update="Resources\Models\Validation\UsernameValidator.Designer.cs">
<DesignTime>True</DesignTime>
@@ -102,20 +99,15 @@ <AutoGen>True</AutoGen>
<DependentUpon>Exception.resx</DependentUpon>
</Compile>
- <Compile Update="Resources\Services\UserAvatarService.Designer.cs">
- <DesignTime>True</DesignTime>
- <AutoGen>True</AutoGen>
- <DependentUpon>UserAvatarService.resx</DependentUpon>
- </Compile>
- <Compile Update="Resources\Services\UserCache.Designer.cs">
+ <Compile Update="Resources\Services\TimelineService.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
- <DependentUpon>UserCache.resx</DependentUpon>
+ <DependentUpon>TimelineService.resx</DependentUpon>
</Compile>
- <Compile Update="Resources\Services\UserDetailService.Designer.cs">
+ <Compile Update="Resources\Services\UserAvatarService.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
- <DependentUpon>UserDetailService.resx</DependentUpon>
+ <DependentUpon>UserAvatarService.resx</DependentUpon>
</Compile>
<Compile Update="Resources\Services\UserService.Designer.cs">
<DesignTime>True</DesignTime>
@@ -129,10 +121,6 @@ <Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>AuthHandler.Designer.cs</LastGenOutput>
</EmbeddedResource>
- <EmbeddedResource Update="Resources\Controllers\Testing\TestingI18nController.resx">
- <Generator>ResXFileCodeGenerator</Generator>
- <LastGenOutput>TestingI18nController.Designer.cs</LastGenOutput>
- </EmbeddedResource>
<EmbeddedResource Update="Resources\Controllers\TimelineController.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>TimelineController.Designer.cs</LastGenOutput>
@@ -155,16 +143,16 @@ <LastGenOutput>Filters.Designer.cs</LastGenOutput>
</EmbeddedResource>
<EmbeddedResource Update="Resources\Messages.resx">
- <Generator>ResXFileCodeGenerator</Generator>
- <LastGenOutput>Messages.Designer.cs</LastGenOutput>
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>Messages.Designer.cs</LastGenOutput>
</EmbeddedResource>
<EmbeddedResource Update="Resources\Models\Http\Common.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Common.Designer.cs</LastGenOutput>
</EmbeddedResource>
<EmbeddedResource Update="Resources\Models\Validation\NicknameValidator.resx">
- <Generator>ResXFileCodeGenerator</Generator>
- <LastGenOutput>NicknameValidator.Designer.cs</LastGenOutput>
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>NicknameValidator.Designer.cs</LastGenOutput>
</EmbeddedResource>
<EmbeddedResource Update="Resources\Models\Validation\UsernameValidator.resx">
<Generator>ResXFileCodeGenerator</Generator>
@@ -178,17 +166,13 @@ <Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Exception.Designer.cs</LastGenOutput>
</EmbeddedResource>
- <EmbeddedResource Update="Resources\Services\UserAvatarService.resx">
- <Generator>ResXFileCodeGenerator</Generator>
- <LastGenOutput>UserAvatarService.Designer.cs</LastGenOutput>
- </EmbeddedResource>
- <EmbeddedResource Update="Resources\Services\UserCache.resx">
+ <EmbeddedResource Update="Resources\Services\TimelineService.resx">
<Generator>ResXFileCodeGenerator</Generator>
- <LastGenOutput>UserCache.Designer.cs</LastGenOutput>
+ <LastGenOutput>TimelineService.Designer.cs</LastGenOutput>
</EmbeddedResource>
- <EmbeddedResource Update="Resources\Services\UserDetailService.resx">
+ <EmbeddedResource Update="Resources\Services\UserAvatarService.resx">
<Generator>ResXFileCodeGenerator</Generator>
- <LastGenOutput>UserDetailService.Designer.cs</LastGenOutput>
+ <LastGenOutput>UserAvatarService.Designer.cs</LastGenOutput>
</EmbeddedResource>
<EmbeddedResource Update="Resources\Services\UserService.resx">
<Generator>ResXFileCodeGenerator</Generator>
|