From 60cf54d80e62ea9180660e5a951c569f90bc4636 Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 26 Nov 2020 20:02:03 +0800 Subject: feat: Add highlight timeline entity and service. --- BackEnd/Timeline/Services/UserService.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) (limited to 'BackEnd/Timeline/Services/UserService.cs') diff --git a/BackEnd/Timeline/Services/UserService.cs b/BackEnd/Timeline/Services/UserService.cs index 2c5644cd..76c24666 100644 --- a/BackEnd/Timeline/Services/UserService.cs +++ b/BackEnd/Timeline/Services/UserService.cs @@ -38,6 +38,13 @@ namespace Timeline.Services /// Thrown when password is wrong. Task VerifyCredential(string username, string password); + /// + /// Check if a user exists. + /// + /// The id of the user. + /// True if exists. Otherwise false. + Task CheckUserExistence(long id); + /// /// Try to get a user by id. /// @@ -188,6 +195,11 @@ namespace Timeline.Services return await CreateUserFromEntity(entity); } + public async Task CheckUserExistence(long id) + { + return await _databaseContext.Users.AnyAsync(u => u.Id == id); + } + public async Task GetUser(long id) { var user = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync(); -- cgit v1.2.3 From fb67fe839e742e65f024472c36c0976b3317d95c Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 26 Nov 2020 21:04:42 +0800 Subject: refactor: ... --- BackEnd/Timeline.Tests/GlobalSuppressions.cs | 25 +- .../Services/TimelinePostServiceTest.cs | 200 +++++++ .../Timeline.Tests/Services/TimelineServiceTest.cs | 170 +----- .../Services/UserDeleteServiceTest.cs | 4 +- BackEnd/Timeline/Controllers/TimelineController.cs | 18 +- BackEnd/Timeline/Services/BasicTimelineService.cs | 122 +++++ BackEnd/Timeline/Services/BasicUserService.cs | 66 +++ .../Timeline/Services/HighlightTimelineService.cs | 4 +- BackEnd/Timeline/Services/TimelinePostService.cs | 493 +++++++++++++++++ BackEnd/Timeline/Services/TimelineService.cs | 588 +-------------------- BackEnd/Timeline/Services/UserDeleteService.cs | 8 +- BackEnd/Timeline/Services/UserService.cs | 41 +- BackEnd/Timeline/Startup.cs | 18 +- 13 files changed, 939 insertions(+), 818 deletions(-) create mode 100644 BackEnd/Timeline.Tests/Services/TimelinePostServiceTest.cs create mode 100644 BackEnd/Timeline/Services/BasicTimelineService.cs create mode 100644 BackEnd/Timeline/Services/BasicUserService.cs create mode 100644 BackEnd/Timeline/Services/TimelinePostService.cs (limited to 'BackEnd/Timeline/Services/UserService.cs') diff --git a/BackEnd/Timeline.Tests/GlobalSuppressions.cs b/BackEnd/Timeline.Tests/GlobalSuppressions.cs index 0f873033..da9481f1 100644 --- a/BackEnd/Timeline.Tests/GlobalSuppressions.cs +++ b/BackEnd/Timeline.Tests/GlobalSuppressions.cs @@ -3,14 +3,17 @@ // Project-level suppressions either have no target or are given // a specific target and scoped to a namespace, type, member, etc. -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "This is not a UI application.")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Tests name have underscores.")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Test may catch all exceptions.")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1034:Nested types should not be visible", Justification = "Test classes can be nested.")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "This is redundant.")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1063:Implement IDisposable Correctly", Justification = "Test classes do not need to implement it that way.")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA1816:Dispose methods should call SuppressFinalize", Justification = "Test classes do not need to implement it that way.")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2234:Pass system uri objects instead of strings", Justification = "I really don't understand this rule.")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "Tests do not need make strings resources.")] -[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.")] +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "This is not a UI application.")] +[assembly: SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Tests name have underscores.")] +[assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Test may catch all exceptions.")] +[assembly: SuppressMessage("Design", "CA1034:Nested types should not be visible", Justification = "Test classes can be nested.")] +[assembly: SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "This is redundant.")] +[assembly: SuppressMessage("Design", "CA1063:Implement IDisposable Correctly", Justification = "Test classes do not need to implement it that way.")] +[assembly: SuppressMessage("Usage", "CA1816:Dispose methods should call SuppressFinalize", Justification = "Test classes do not need to implement it that way.")] +[assembly: SuppressMessage("Usage", "CA2234:Pass system uri objects instead of strings", Justification = "I really don't understand this rule.")] +[assembly: SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "Tests do not need make strings resources.")] +[assembly: SuppressMessage("Design", "CA1054:Uri parameters should not be strings", Justification = "That's unnecessary.")] +[assembly: SuppressMessage("Design", "CA1056:Uri properties should not be strings", Justification = "That's unnecessary.")] +[assembly: SuppressMessage("Design", "CA1001:Types that own disposable fields should be disposable", Justification = "We use another contract like IAsyncLifetime.")] diff --git a/BackEnd/Timeline.Tests/Services/TimelinePostServiceTest.cs b/BackEnd/Timeline.Tests/Services/TimelinePostServiceTest.cs new file mode 100644 index 00000000..97512be5 --- /dev/null +++ b/BackEnd/Timeline.Tests/Services/TimelinePostServiceTest.cs @@ -0,0 +1,200 @@ +using FluentAssertions; +using Microsoft.Extensions.Logging.Abstractions; +using System; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Models; +using Timeline.Services; +using Timeline.Tests.Helpers; +using Xunit; + +namespace Timeline.Tests.Services +{ + public class TimelinePostServiceTest : DatabaseBasedTest + { + private readonly PasswordService _passwordService = new PasswordService(); + + private readonly ETagGenerator _eTagGenerator = new ETagGenerator(); + + private readonly ImageValidator _imageValidator = new ImageValidator(); + + private readonly TestClock _clock = new TestClock(); + + private DataManager _dataManager = default!; + + private UserPermissionService _userPermissionService = default!; + + private UserService _userService = default!; + + private TimelineService _timelineService = default!; + + private TimelinePostService _timelinePostService = default!; + + private UserDeleteService _userDeleteService = default!; + + protected override void OnDatabaseCreated() + { + _dataManager = new DataManager(Database, _eTagGenerator); + _userPermissionService = new UserPermissionService(Database); + _userService = new UserService(NullLogger.Instance, Database, _passwordService, _clock, _userPermissionService); + _timelineService = new TimelineService(Database, _userService, _clock); + _timelinePostService = new TimelinePostService(NullLogger.Instance, Database, _timelineService, _userService, _dataManager, _imageValidator, _clock); + _userDeleteService = new UserDeleteService(NullLogger.Instance, Database, _timelinePostService); + } + + protected override void BeforeDatabaseDestroy() + { + _eTagGenerator.Dispose(); + } + + [Theory] + [InlineData("@user")] + [InlineData("tl")] + public async Task GetPosts_ModifiedSince(string timelineName) + { + _clock.ForwardCurrentTime(); + + var userId = await _userService.GetUserIdByUsername("user"); + + var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); + if (!isPersonal) + await _timelineService.CreateTimeline(timelineName, userId); + + var postContentList = new string[] { "a", "b", "c", "d" }; + + DateTime testPoint = new DateTime(); + + foreach (var (content, index) in postContentList.Select((v, i) => (v, i))) + { + var t = _clock.ForwardCurrentTime(); + if (index == 1) + testPoint = t; + await _timelinePostService.CreateTextPost(timelineName, userId, content, null); + } + + var posts = await _timelinePostService.GetPosts(timelineName, testPoint); + posts.Should().HaveCount(3) + .And.Subject.Select(p => ((TextTimelinePostContent)p.Content!).Text).Should().Equal(postContentList.Skip(1)); + } + + [Theory] + [InlineData("@user")] + [InlineData("tl")] + public async Task GetPosts_IncludeDeleted(string timelineName) + { + var userId = await _userService.GetUserIdByUsername("user"); + + var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); + if (!isPersonal) + await _timelineService.CreateTimeline(timelineName, userId); + + var postContentList = new string[] { "a", "b", "c", "d" }; + + foreach (var content in postContentList) + { + await _timelinePostService.CreateTextPost(timelineName, userId, content, null); + } + + var posts = await _timelinePostService.GetPosts(timelineName); + posts.Should().HaveCount(4); + posts.Select(p => p.Deleted).Should().Equal(Enumerable.Repeat(false, posts.Count)); + posts.Select(p => ((TextTimelinePostContent)p.Content!).Text).Should().Equal(postContentList); + + foreach (var id in new long[] { posts[0].Id, posts[2].Id }) + { + await _timelinePostService.DeletePost(timelineName, id); + } + + posts = await _timelinePostService.GetPosts(timelineName); + posts.Should().HaveCount(2); + posts.Select(p => p.Deleted).Should().Equal(Enumerable.Repeat(false, posts.Count)); + posts.Select(p => ((TextTimelinePostContent)p.Content!).Text).Should().Equal(new string[] { "b", "d" }); + + posts = await _timelinePostService.GetPosts(timelineName, includeDeleted: true); + posts.Should().HaveCount(4); + posts.Select(p => p.Deleted).Should().Equal(new bool[] { true, false, true, false }); + posts.Where(p => !p.Deleted).Select(p => ((TextTimelinePostContent)p.Content!).Text).Should().Equal(new string[] { "b", "d" }); + } + + [Theory] + [InlineData("@admin")] + [InlineData("tl")] + public async Task GetPosts_ModifiedSince_UsernameChange(string timelineName) + { + var time1 = _clock.ForwardCurrentTime(); + + var userId = await _userService.GetUserIdByUsername("user"); + + var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); + if (!isPersonal) + await _timelineService.CreateTimeline(timelineName, userId); + + var postContentList = new string[] { "a", "b", "c", "d" }; + + foreach (var (content, index) in postContentList.Select((v, i) => (v, i))) + { + await _timelinePostService.CreateTextPost(timelineName, userId, content, null); + } + + var time2 = _clock.ForwardCurrentTime(); + + { + var posts = await _timelinePostService.GetPosts(timelineName, time2); + posts.Should().HaveCount(0); + } + + { + await _userService.ModifyUser(userId, new ModifyUserParams { Nickname = "haha" }); + var posts = await _timelinePostService.GetPosts(timelineName, time2); + posts.Should().HaveCount(0); + } + + { + await _userService.ModifyUser(userId, new ModifyUserParams { Username = "haha" }); + var posts = await _timelinePostService.GetPosts(timelineName, time2); + posts.Should().HaveCount(4); + } + } + + [Theory] + [InlineData("@admin")] + [InlineData("tl")] + public async Task GetPosts_ModifiedSince_UserDelete(string timelineName) + { + var time1 = _clock.ForwardCurrentTime(); + + var userId = await _userService.GetUserIdByUsername("user"); + var adminId = await _userService.GetUserIdByUsername("admin"); + + var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); + if (!isPersonal) + await _timelineService.CreateTimeline(timelineName, adminId); + + var postContentList = new string[] { "a", "b", "c", "d" }; + + foreach (var (content, index) in postContentList.Select((v, i) => (v, i))) + { + await _timelinePostService.CreateTextPost(timelineName, userId, content, null); + } + + var time2 = _clock.ForwardCurrentTime(); + + { + var posts = await _timelinePostService.GetPosts(timelineName, time2); + posts.Should().HaveCount(0); + } + + await _userDeleteService.DeleteUser("user"); + + { + var posts = await _timelinePostService.GetPosts(timelineName, time2); + posts.Should().HaveCount(0); + } + + { + var posts = await _timelinePostService.GetPosts(timelineName, time2, true); + posts.Should().HaveCount(4); + } + } + } +} diff --git a/BackEnd/Timeline.Tests/Services/TimelineServiceTest.cs b/BackEnd/Timeline.Tests/Services/TimelineServiceTest.cs index 5f2c20e8..fac0b6f3 100644 --- a/BackEnd/Timeline.Tests/Services/TimelineServiceTest.cs +++ b/BackEnd/Timeline.Tests/Services/TimelineServiceTest.cs @@ -2,7 +2,6 @@ using Microsoft.Extensions.Logging.Abstractions; using System; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using Timeline.Models; using Timeline.Services; @@ -12,38 +11,23 @@ using Xunit; namespace Timeline.Tests.Services { - public class TimelineServiceTest : DatabaseBasedTest, IDisposable + public class TimelineServiceTest : DatabaseBasedTest { private readonly PasswordService _passwordService = new PasswordService(); - private readonly ETagGenerator _eTagGenerator = new ETagGenerator(); - - private readonly ImageValidator _imageValidator = new ImageValidator(); - private readonly TestClock _clock = new TestClock(); - private DataManager _dataManager = default!; - private UserPermissionService _userPermissionService = default!; private UserService _userService = default!; private TimelineService _timelineService = default!; - private UserDeleteService _userDeleteService = default!; - protected override void OnDatabaseCreated() { - _dataManager = new DataManager(Database, _eTagGenerator); _userPermissionService = new UserPermissionService(Database); _userService = new UserService(NullLogger.Instance, Database, _passwordService, _clock, _userPermissionService); - _timelineService = new TimelineService(NullLogger.Instance, Database, _dataManager, _userService, _imageValidator, _clock); - _userDeleteService = new UserDeleteService(NullLogger.Instance, Database, _timelineService); - } - - public void Dispose() - { - _eTagGenerator.Dispose(); + _timelineService = new TimelineService(Database, _userService, _clock); } [Theory] @@ -109,156 +93,6 @@ namespace Timeline.Tests.Services await GetAndCheck(); } - [Theory] - [InlineData("@user")] - [InlineData("tl")] - public async Task GetPosts_ModifiedSince(string timelineName) - { - _clock.ForwardCurrentTime(); - - var userId = await _userService.GetUserIdByUsername("user"); - - var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); - if (!isPersonal) - await _timelineService.CreateTimeline(timelineName, userId); - - var postContentList = new string[] { "a", "b", "c", "d" }; - - DateTime testPoint = new DateTime(); - - foreach (var (content, index) in postContentList.Select((v, i) => (v, i))) - { - var t = _clock.ForwardCurrentTime(); - if (index == 1) - testPoint = t; - await _timelineService.CreateTextPost(timelineName, userId, content, null); - } - - var posts = await _timelineService.GetPosts(timelineName, testPoint); - posts.Should().HaveCount(3) - .And.Subject.Select(p => ((TextTimelinePostContent)p.Content!).Text).Should().Equal(postContentList.Skip(1)); - } - - [Theory] - [InlineData("@user")] - [InlineData("tl")] - public async Task GetPosts_IncludeDeleted(string timelineName) - { - var userId = await _userService.GetUserIdByUsername("user"); - - var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); - if (!isPersonal) - await _timelineService.CreateTimeline(timelineName, userId); - - var postContentList = new string[] { "a", "b", "c", "d" }; - - foreach (var content in postContentList) - { - await _timelineService.CreateTextPost(timelineName, userId, content, null); - } - - var posts = await _timelineService.GetPosts(timelineName); - posts.Should().HaveCount(4); - posts.Select(p => p.Deleted).Should().Equal(Enumerable.Repeat(false, posts.Count)); - posts.Select(p => ((TextTimelinePostContent)p.Content!).Text).Should().Equal(postContentList); - - foreach (var id in new long[] { posts[0].Id, posts[2].Id }) - { - await _timelineService.DeletePost(timelineName, id); - } - - posts = await _timelineService.GetPosts(timelineName); - posts.Should().HaveCount(2); - posts.Select(p => p.Deleted).Should().Equal(Enumerable.Repeat(false, posts.Count)); - posts.Select(p => ((TextTimelinePostContent)p.Content!).Text).Should().Equal(new string[] { "b", "d" }); - - posts = await _timelineService.GetPosts(timelineName, includeDeleted: true); - posts.Should().HaveCount(4); - posts.Select(p => p.Deleted).Should().Equal(new bool[] { true, false, true, false }); - posts.Where(p => !p.Deleted).Select(p => ((TextTimelinePostContent)p.Content!).Text).Should().Equal(new string[] { "b", "d" }); - } - - [Theory] - [InlineData("@admin")] - [InlineData("tl")] - public async Task GetPosts_ModifiedSince_UsernameChange(string timelineName) - { - var time1 = _clock.ForwardCurrentTime(); - - var userId = await _userService.GetUserIdByUsername("user"); - - var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); - if (!isPersonal) - await _timelineService.CreateTimeline(timelineName, userId); - - var postContentList = new string[] { "a", "b", "c", "d" }; - - foreach (var (content, index) in postContentList.Select((v, i) => (v, i))) - { - await _timelineService.CreateTextPost(timelineName, userId, content, null); - } - - var time2 = _clock.ForwardCurrentTime(); - - { - var posts = await _timelineService.GetPosts(timelineName, time2); - posts.Should().HaveCount(0); - } - - { - await _userService.ModifyUser(userId, new ModifyUserParams { Nickname = "haha" }); - var posts = await _timelineService.GetPosts(timelineName, time2); - posts.Should().HaveCount(0); - } - - { - await _userService.ModifyUser(userId, new ModifyUserParams { Username = "haha" }); - var posts = await _timelineService.GetPosts(timelineName, time2); - posts.Should().HaveCount(4); - } - } - - [Theory] - [InlineData("@admin")] - [InlineData("tl")] - public async Task GetPosts_ModifiedSince_UserDelete(string timelineName) - { - var time1 = _clock.ForwardCurrentTime(); - - var userId = await _userService.GetUserIdByUsername("user"); - var adminId = await _userService.GetUserIdByUsername("admin"); - - var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); - if (!isPersonal) - await _timelineService.CreateTimeline(timelineName, adminId); - - var postContentList = new string[] { "a", "b", "c", "d" }; - - foreach (var (content, index) in postContentList.Select((v, i) => (v, i))) - { - await _timelineService.CreateTextPost(timelineName, userId, content, null); - } - - var time2 = _clock.ForwardCurrentTime(); - - { - var posts = await _timelineService.GetPosts(timelineName, time2); - posts.Should().HaveCount(0); - } - - await _userDeleteService.DeleteUser("user"); - - { - var posts = await _timelineService.GetPosts(timelineName, time2); - posts.Should().HaveCount(0); - } - - { - var posts = await _timelineService.GetPosts(timelineName, time2, true); - posts.Should().HaveCount(4); - } - } - [Theory] [InlineData("@admin")] [InlineData("tl")] diff --git a/BackEnd/Timeline.Tests/Services/UserDeleteServiceTest.cs b/BackEnd/Timeline.Tests/Services/UserDeleteServiceTest.cs index be11564e..59c0a9af 100644 --- a/BackEnd/Timeline.Tests/Services/UserDeleteServiceTest.cs +++ b/BackEnd/Timeline.Tests/Services/UserDeleteServiceTest.cs @@ -10,12 +10,12 @@ namespace Timeline.Tests.Services { public class UserDeleteServiceTest : DatabaseBasedTest { - private readonly Mock _mockTimelineService = new Mock(); + private readonly Mock _mockTimelinePostService = new Mock(); private UserDeleteService _service = default!; protected override void OnDatabaseCreated() { - _service = new UserDeleteService(NullLogger.Instance, Database, _mockTimelineService.Object); + _service = new UserDeleteService(NullLogger.Instance, Database, _mockTimelinePostService.Object); } [Fact] diff --git a/BackEnd/Timeline/Controllers/TimelineController.cs b/BackEnd/Timeline/Controllers/TimelineController.cs index 45060b5d..0ffadc50 100644 --- a/BackEnd/Timeline/Controllers/TimelineController.cs +++ b/BackEnd/Timeline/Controllers/TimelineController.cs @@ -29,17 +29,19 @@ namespace Timeline.Controllers private readonly IUserService _userService; private readonly ITimelineService _service; + private readonly ITimelinePostService _postService; private readonly IMapper _mapper; /// /// /// - public TimelineController(ILogger logger, IUserService userService, ITimelineService service, IMapper mapper) + public TimelineController(ILogger logger, IUserService userService, ITimelineService service, ITimelinePostService timelinePostService, IMapper mapper) { _logger = logger; _userService = userService; _service = service; + _postService = timelinePostService; _mapper = mapper; } @@ -187,7 +189,7 @@ namespace Timeline.Controllers return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } - List posts = await _service.GetPosts(name, modifiedSince, includeDeleted ?? false); + List posts = await _postService.GetPosts(name, modifiedSince, includeDeleted ?? false); var result = _mapper.Map>(posts); return result; @@ -217,9 +219,9 @@ namespace Timeline.Controllers try { - return await DataCacheHelper.GenerateActionResult(this, () => _service.GetPostDataETag(name, id), async () => + return await DataCacheHelper.GenerateActionResult(this, () => _postService.GetPostDataETag(name, id), async () => { - var data = await _service.GetPostData(name, id); + var data = await _postService.GetPostData(name, id); return data; }); } @@ -264,7 +266,7 @@ namespace Timeline.Controllers { return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_TextContentTextRequired)); } - post = await _service.CreateTextPost(name, id, text, body.Time); + post = await _postService.CreateTextPost(name, id, text, body.Time); } else if (content.Type == TimelinePostContentTypes.Image) { @@ -285,7 +287,7 @@ namespace Timeline.Controllers try { - post = await _service.CreateImagePost(name, id, data, body.Time); + post = await _postService.CreateImagePost(name, id, data, body.Time); } catch (ImageException) { @@ -315,13 +317,13 @@ namespace Timeline.Controllers [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task> PostDelete([FromRoute][GeneralTimelineName] string name, [FromRoute] long id) { - if (!UserHasAllTimelineManagementPermission && !await _service.HasPostModifyPermission(name, id, this.GetUserId())) + if (!UserHasAllTimelineManagementPermission && !await _postService.HasPostModifyPermission(name, id, this.GetUserId())) { return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } try { - await _service.DeletePost(name, id); + await _postService.DeletePost(name, id); return CommonDeleteResponse.Delete(); } catch (TimelinePostNotExistException) diff --git a/BackEnd/Timeline/Services/BasicTimelineService.cs b/BackEnd/Timeline/Services/BasicTimelineService.cs new file mode 100644 index 00000000..0d9f64a9 --- /dev/null +++ b/BackEnd/Timeline/Services/BasicTimelineService.cs @@ -0,0 +1,122 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Entities; +using Timeline.Models; +using Timeline.Models.Validation; +using Timeline.Services.Exceptions; + +namespace Timeline.Services +{ + /// + /// This service provide some basic timeline functions, which should be used internally for other services. + /// + public interface IBasicTimelineService + { + /// + /// Get the timeline id by name. + /// + /// Timeline name. + /// Id of the timeline. + /// Thrown when is null. + /// Throw when is of bad format. + /// + /// Thrown when timeline with name does not exist. + /// If it is a personal timeline, then inner exception is . + /// + /// + /// If name is of personal timeline and the timeline does not exist, it will be created if user exists. + /// If the user does not exist, will be thrown with as inner exception. + /// + Task GetTimelineIdByName(string timelineName); + } + + + public class BasicTimelineService : IBasicTimelineService + { + private readonly DatabaseContext _database; + + private readonly IBasicUserService _basicUserService; + private readonly IClock _clock; + + private readonly GeneralTimelineNameValidator _generalTimelineNameValidator = new GeneralTimelineNameValidator(); + + public BasicTimelineService(DatabaseContext database, IBasicUserService basicUserService, IClock clock) + { + _database = database; + _basicUserService = basicUserService; + _clock = clock; + } + + protected TimelineEntity CreateNewTimelineEntity(string? name, long ownerId) + { + var currentTime = _clock.GetCurrentTime(); + + return new TimelineEntity + { + Name = name, + NameLastModified = currentTime, + OwnerId = ownerId, + Visibility = TimelineVisibility.Register, + CreateTime = currentTime, + LastModified = currentTime, + CurrentPostLocalId = 0, + Members = new List() + }; + } + + public async Task GetTimelineIdByName(string timelineName) + { + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); + + if (!_generalTimelineNameValidator.Validate(timelineName, out var message)) + throw new ArgumentException(message); + + timelineName = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); + + if (isPersonal) + { + long userId; + try + { + userId = await _basicUserService.GetUserIdByUsername(timelineName); + } + catch (UserNotExistException e) + { + throw new TimelineNotExistException(timelineName, e); + } + + var timelineEntity = await _database.Timelines.Where(t => t.OwnerId == userId && t.Name == null).Select(t => new { t.Id }).SingleOrDefaultAsync(); + + if (timelineEntity != null) + { + return timelineEntity.Id; + } + else + { + var newTimelineEntity = CreateNewTimelineEntity(null, userId); + _database.Timelines.Add(newTimelineEntity); + await _database.SaveChangesAsync(); + + return newTimelineEntity.Id; + } + } + else + { + var timelineEntity = await _database.Timelines.Where(t => t.Name == timelineName).Select(t => new { t.Id }).SingleOrDefaultAsync(); + + if (timelineEntity == null) + { + throw new TimelineNotExistException(timelineName); + } + else + { + return timelineEntity.Id; + } + } + } + } +} diff --git a/BackEnd/Timeline/Services/BasicUserService.cs b/BackEnd/Timeline/Services/BasicUserService.cs new file mode 100644 index 00000000..fbbb6677 --- /dev/null +++ b/BackEnd/Timeline/Services/BasicUserService.cs @@ -0,0 +1,66 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Entities; +using Timeline.Models.Validation; +using Timeline.Services.Exceptions; + +namespace Timeline.Services +{ + /// + /// This service provide some basic user features, which should be used internally for other services. + /// + public interface IBasicUserService + { + /// + /// Check if a user exists. + /// + /// The id of the user. + /// True if exists. Otherwise false. + Task CheckUserExistence(long id); + + /// + /// Get the user id of given username. + /// + /// Username of the user. + /// The id of the user. + /// Thrown when is null. + /// Thrown when is of bad format. + /// Thrown when the user with given username does not exist. + Task GetUserIdByUsername(string username); + } + + public class BasicUserService : IBasicUserService + { + private readonly DatabaseContext _database; + + private readonly UsernameValidator _usernameValidator = new UsernameValidator(); + + public BasicUserService(DatabaseContext database) + { + _database = database; + } + + public async Task CheckUserExistence(long id) + { + return await _database.Users.AnyAsync(u => u.Id == id); + } + + public async Task GetUserIdByUsername(string username) + { + if (username == null) + throw new ArgumentNullException(nameof(username)); + + if (!_usernameValidator.Validate(username, out var message)) + throw new ArgumentException(message); + + var entity = await _database.Users.Where(user => user.Username == username).Select(u => new { u.Id }).SingleOrDefaultAsync(); + + if (entity == null) + throw new UserNotExistException(username); + + return entity.Id; + } + } +} diff --git a/BackEnd/Timeline/Services/HighlightTimelineService.cs b/BackEnd/Timeline/Services/HighlightTimelineService.cs index 7528d9b0..88ad4a4b 100644 --- a/BackEnd/Timeline/Services/HighlightTimelineService.cs +++ b/BackEnd/Timeline/Services/HighlightTimelineService.cs @@ -43,10 +43,10 @@ namespace Timeline.Services public class HighlightTimelineService : IHighlightTimelineService { private readonly DatabaseContext _database; - private readonly IUserService _userService; + private readonly IBasicUserService _userService; private readonly ITimelineService _timelineService; - public HighlightTimelineService(DatabaseContext database, IUserService userService, ITimelineService timelineService) + public HighlightTimelineService(DatabaseContext database, IBasicUserService userService, ITimelineService timelineService) { _database = database; _userService = userService; diff --git a/BackEnd/Timeline/Services/TimelinePostService.cs b/BackEnd/Timeline/Services/TimelinePostService.cs new file mode 100644 index 00000000..36fcdbca --- /dev/null +++ b/BackEnd/Timeline/Services/TimelinePostService.cs @@ -0,0 +1,493 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Entities; +using Timeline.Helpers; +using Timeline.Models; +using Timeline.Services.Exceptions; +using SixLabors.ImageSharp; +using static Timeline.Resources.Services.TimelineService; +using Microsoft.Extensions.Logging; + +namespace Timeline.Services +{ + public class PostData : ICacheableData + { +#pragma warning disable CA1819 // Properties should not return arrays + public byte[] Data { get; set; } = default!; +#pragma warning restore CA1819 // Properties should not return arrays + public string Type { get; set; } = default!; + public string ETag { get; set; } = default!; + public DateTime? LastModified { get; set; } // TODO: Why nullable? + } + + public interface ITimelinePostService + { + /// + /// Get all the posts in the timeline. + /// + /// The name of the timeline. + /// The time that posts have been modified since. + /// Whether include deleted posts. + /// A list of all posts. + /// Thrown when is null. + /// Throw when is of bad format. + /// + /// Thrown when timeline with name does not exist. + /// If it is a personal timeline, then inner exception is . + /// + Task> GetPosts(string timelineName, DateTime? modifiedSince = null, bool includeDeleted = false); + + /// + /// Get the etag of data of a post. + /// + /// The name of the timeline of the post. + /// The id of the post. + /// The etag of the data. + /// Thrown when is null. + /// Throw when is of bad format. + /// + /// Thrown when timeline with name does not exist. + /// If it is a personal timeline, then inner exception is . + /// + /// Thrown when post of does not exist or has been deleted. + /// Thrown when post has no data. + /// + Task GetPostDataETag(string timelineName, long postId); + + /// + /// Get the data of a post. + /// + /// The name of the timeline of the post. + /// The id of the post. + /// The etag of the data. + /// Thrown when is null. + /// Throw when is of bad format. + /// + /// Thrown when timeline with name does not exist. + /// If it is a personal timeline, then inner exception is . + /// + /// Thrown when post of does not exist or has been deleted. + /// Thrown when post has no data. + /// + Task GetPostData(string timelineName, long postId); + + /// + /// Create a new text post in timeline. + /// + /// The name of the timeline to create post against. + /// The author's user id. + /// The content text. + /// The time of the post. If null, then current time is used. + /// The info of the created post. + /// Thrown when or is null. + /// Throw when is of bad format. + /// + /// Thrown when timeline with name does not exist. + /// If it is a personal timeline, then inner exception is . + /// + /// Thrown if user of does not exist. + Task CreateTextPost(string timelineName, long authorId, string text, DateTime? time); + + /// + /// Create a new image post in timeline. + /// + /// The name of the timeline to create post against. + /// The author's user id. + /// The image data. + /// The time of the post. If null, then use current time. + /// The info of the created post. + /// Thrown when or is null. + /// Throw when is of bad format. + /// + /// Thrown when timeline with name does not exist. + /// If it is a personal timeline, then inner exception is . + /// + /// Thrown if user of does not exist. + /// Thrown if data is not a image. Validated by . + Task CreateImagePost(string timelineName, long authorId, byte[] imageData, DateTime? time); + + /// + /// Delete a post. + /// + /// The name of the timeline to delete post against. + /// The id of the post to delete. + /// Thrown when is null. + /// Throw when is of bad format. + /// + /// Thrown when timeline with name does not exist. + /// If it is a personal timeline, then inner exception is . + /// + /// Thrown when the post with given id does not exist or is deleted already. + /// + /// First use to check the permission. + /// + Task DeletePost(string timelineName, long postId); + + /// + /// Delete all posts of the given user. Used when delete a user. + /// + /// The id of the user. + Task DeleteAllPostsOfUser(long userId); + + /// + /// Verify whether a user has the permission to modify a post. + /// + /// The name of the timeline. + /// The id of the post. + /// The id of the user to check on. + /// True if you want it to throw . Default false. + /// True if can modify, false if can't modify. + /// Thrown when is null. + /// Throw when is of bad format. + /// + /// Thrown when timeline with name does not exist. + /// If it is a personal timeline, then inner exception is . + /// + /// Thrown when the post with given id does not exist or is deleted already and is true. + /// + /// Unless is true, this method should return true if the post does not exist. + /// If the post is deleted, its author info still exists, so it is checked as the post is not deleted unless is true. + /// 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. + /// + Task HasPostModifyPermission(string timelineName, long postId, long modifierId, bool throwOnPostNotExist = false); + } + + public class TimelinePostService : ITimelinePostService + { + private readonly ILogger _logger; + private readonly DatabaseContext _database; + private readonly IBasicTimelineService _basicTimelineService; + private readonly IUserService _userService; + private readonly IDataManager _dataManager; + private readonly IImageValidator _imageValidator; + private readonly IClock _clock; + + public TimelinePostService(ILogger logger, DatabaseContext database, IBasicTimelineService basicTimelineService, IUserService userService, IDataManager dataManager, IImageValidator imageValidator, IClock clock) + { + _logger = logger; + _database = database; + _basicTimelineService = basicTimelineService; + _userService = userService; + _dataManager = dataManager; + _imageValidator = imageValidator; + _clock = clock; + } + + private async Task MapTimelinePostFromEntity(TimelinePostEntity entity, string timelineName) + { + User? author = entity.AuthorId.HasValue ? await _userService.GetUser(entity.AuthorId.Value) : null; + + ITimelinePostContent? content = null; + + if (entity.Content != null) + { + var type = entity.ContentType; + + content = type switch + { + TimelinePostContentTypes.Text => new TextTimelinePostContent(entity.Content), + TimelinePostContentTypes.Image => new ImageTimelinePostContent(entity.Content), + _ => throw new DatabaseCorruptedException(string.Format(CultureInfo.InvariantCulture, ExceptionDatabaseUnknownContentType, type)) + }; + } + + return new TimelinePost( + id: entity.LocalId, + author: author, + content: content, + time: entity.Time, + lastUpdated: entity.LastUpdated, + timelineName: timelineName + ); + } + + public async Task> GetPosts(string timelineName, DateTime? modifiedSince = null, bool includeDeleted = false) + { + modifiedSince = modifiedSince?.MyToUtc(); + + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); + + var timelineId = await _basicTimelineService.GetTimelineIdByName(timelineName); + IQueryable query = _database.TimelinePosts.Where(p => p.TimelineId == timelineId); + + if (!includeDeleted) + { + query = query.Where(p => p.Content != null); + } + + if (modifiedSince.HasValue) + { + query = query.Include(p => p.Author).Where(p => p.LastUpdated >= modifiedSince || (p.Author != null && p.Author.UsernameChangeTime >= modifiedSince)); + } + + query = query.OrderBy(p => p.Time); + + var postEntities = await query.ToListAsync(); + + var posts = new List(); + foreach (var entity in postEntities) + { + posts.Add(await MapTimelinePostFromEntity(entity, timelineName)); + } + return posts; + } + + public async Task GetPostDataETag(string timelineName, long postId) + { + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); + + var timelineId = await _basicTimelineService.GetTimelineIdByName(timelineName); + + var postEntity = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync(); + + if (postEntity == null) + throw new TimelinePostNotExistException(timelineName, postId, false); + + if (postEntity.Content == null) + throw new TimelinePostNotExistException(timelineName, postId, true); + + if (postEntity.ContentType != TimelinePostContentTypes.Image) + throw new TimelinePostNoDataException(ExceptionGetDataNonImagePost); + + var tag = postEntity.Content; + + return tag; + } + + public async Task GetPostData(string timelineName, long postId) + { + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); + + var timelineId = await _basicTimelineService.GetTimelineIdByName(timelineName); + var postEntity = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync(); + + if (postEntity == null) + throw new TimelinePostNotExistException(timelineName, postId, false); + + if (postEntity.Content == null) + throw new TimelinePostNotExistException(timelineName, postId, true); + + if (postEntity.ContentType != TimelinePostContentTypes.Image) + throw new TimelinePostNoDataException(ExceptionGetDataNonImagePost); + + var tag = postEntity.Content; + + byte[] data; + + try + { + data = await _dataManager.GetEntry(tag); + } + catch (InvalidOperationException e) + { + throw new DatabaseCorruptedException(ExceptionGetDataDataEntryNotExist, e); + } + + if (postEntity.ExtraContent == null) + { + _logger.LogWarning(LogGetDataNoFormat); + var format = Image.DetectFormat(data); + postEntity.ExtraContent = format.DefaultMimeType; + await _database.SaveChangesAsync(); + } + + return new PostData + { + Data = data, + Type = postEntity.ExtraContent, + ETag = tag, + LastModified = postEntity.LastUpdated + }; + } + + public async Task CreateTextPost(string timelineName, long authorId, string text, DateTime? time) + { + time = time?.MyToUtc(); + + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); + if (text == null) + throw new ArgumentNullException(nameof(text)); + + var timelineId = await _basicTimelineService.GetTimelineIdByName(timelineName); + var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync(); + + var author = await _userService.GetUser(authorId); + + var currentTime = _clock.GetCurrentTime(); + var finalTime = time ?? currentTime; + + timelineEntity.CurrentPostLocalId += 1; + + var postEntity = new TimelinePostEntity + { + LocalId = timelineEntity.CurrentPostLocalId, + ContentType = TimelinePostContentTypes.Text, + Content = text, + AuthorId = authorId, + TimelineId = timelineId, + Time = finalTime, + LastUpdated = currentTime + }; + _database.TimelinePosts.Add(postEntity); + await _database.SaveChangesAsync(); + + + return new TimelinePost( + id: postEntity.LocalId, + content: new TextTimelinePostContent(text), + time: finalTime, + author: author, + lastUpdated: currentTime, + timelineName: timelineName + ); + } + + public async Task CreateImagePost(string timelineName, long authorId, byte[] data, DateTime? time) + { + time = time?.MyToUtc(); + + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); + if (data == null) + throw new ArgumentNullException(nameof(data)); + + var timelineId = await _basicTimelineService.GetTimelineIdByName(timelineName); + var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync(); + + var author = await _userService.GetUser(authorId); + + var imageFormat = await _imageValidator.Validate(data); + + var imageFormatText = imageFormat.DefaultMimeType; + + var tag = await _dataManager.RetainEntry(data); + + var currentTime = _clock.GetCurrentTime(); + var finalTime = time ?? currentTime; + + timelineEntity.CurrentPostLocalId += 1; + + var postEntity = new TimelinePostEntity + { + LocalId = timelineEntity.CurrentPostLocalId, + ContentType = TimelinePostContentTypes.Image, + Content = tag, + ExtraContent = imageFormatText, + AuthorId = authorId, + TimelineId = timelineId, + Time = finalTime, + LastUpdated = currentTime + }; + _database.TimelinePosts.Add(postEntity); + await _database.SaveChangesAsync(); + + return new TimelinePost( + id: postEntity.LocalId, + content: new ImageTimelinePostContent(tag), + time: finalTime, + author: author, + lastUpdated: currentTime, + timelineName: timelineName + ); + } + + public async Task DeletePost(string timelineName, long id) + { + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); + + var timelineId = await _basicTimelineService.GetTimelineIdByName(timelineName); + + var post = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == id).SingleOrDefaultAsync(); + + if (post == null) + throw new TimelinePostNotExistException(timelineName, id, false); + + if (post.Content == null) + throw new TimelinePostNotExistException(timelineName, id, true); + + string? dataTag = null; + + if (post.ContentType == TimelinePostContentTypes.Image) + { + dataTag = post.Content; + } + + post.Content = null; + post.LastUpdated = _clock.GetCurrentTime(); + + await _database.SaveChangesAsync(); + + if (dataTag != null) + { + await _dataManager.FreeEntry(dataTag); + } + } + + public async Task DeleteAllPostsOfUser(long userId) + { + var posts = await _database.TimelinePosts.Where(p => p.AuthorId == userId).ToListAsync(); + + var now = _clock.GetCurrentTime(); + + var dataTags = new List(); + + foreach (var post in posts) + { + if (post.Content != null) + { + if (post.ContentType == TimelinePostContentTypes.Image) + { + dataTags.Add(post.Content); + } + post.Content = null; + } + post.LastUpdated = now; + } + + await _database.SaveChangesAsync(); + + foreach (var dataTag in dataTags) + { + await _dataManager.FreeEntry(dataTag); + } + } + + public async Task HasPostModifyPermission(string timelineName, long postId, long modifierId, bool throwOnPostNotExist = false) + { + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); + + var timelineId = await _basicTimelineService.GetTimelineIdByName(timelineName); + + var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync(); + + var postEntity = await _database.TimelinePosts.Where(p => p.Id == postId).Select(p => new { p.Content, p.AuthorId }).SingleOrDefaultAsync(); + + if (postEntity == null) + { + if (throwOnPostNotExist) + throw new TimelinePostNotExistException(timelineName, postId, false); + else + return true; + } + + if (postEntity.Content == null && throwOnPostNotExist) + { + throw new TimelinePostNotExistException(timelineName, postId, true); + } + + return timelineEntity.OwnerId == modifierId || postEntity.AuthorId == modifierId; + } + } +} diff --git a/BackEnd/Timeline/Services/TimelineService.cs b/BackEnd/Timeline/Services/TimelineService.cs index f8c729bf..f943f8b4 100644 --- a/BackEnd/Timeline/Services/TimelineService.cs +++ b/BackEnd/Timeline/Services/TimelineService.cs @@ -51,20 +51,10 @@ namespace Timeline.Services public long UserId { get; set; } } - public class PostData : ICacheableData - { -#pragma warning disable CA1819 // Properties should not return arrays - public byte[] Data { get; set; } = default!; -#pragma warning restore CA1819 // Properties should not return arrays - public string Type { get; set; } = default!; - public string ETag { get; set; } = default!; - public DateTime? LastModified { get; set; } // TODO: Why nullable? - } - /// /// This define the interface of both personal timeline and ordinary timeline. /// - public interface ITimelineService + public interface ITimelineService : IBasicTimelineService { /// /// Get the timeline last modified time (not include name change). @@ -79,19 +69,6 @@ namespace Timeline.Services /// Task GetTimelineLastModifiedTime(string timelineName); - /// - /// Get the timeline id by name. - /// - /// Timeline name. - /// Id of the timeline. - /// Thrown when is null. - /// Throw when is of bad format. - /// - /// Thrown when timeline with name does not exist. - /// If it is a personal timeline, then inner exception is . - /// - Task GetTimelineIdByName(string timelineName); - /// /// Get the timeline unique id. /// @@ -139,112 +116,7 @@ namespace Timeline.Services /// Task ChangeProperty(string timelineName, TimelineChangePropertyRequest newProperties); - /// - /// Get all the posts in the timeline. - /// - /// The name of the timeline. - /// The time that posts have been modified since. - /// Whether include deleted posts. - /// A list of all posts. - /// Thrown when is null. - /// Throw when is of bad format. - /// - /// Thrown when timeline with name does not exist. - /// If it is a personal timeline, then inner exception is . - /// - Task> GetPosts(string timelineName, DateTime? modifiedSince = null, bool includeDeleted = false); - /// - /// Get the etag of data of a post. - /// - /// The name of the timeline of the post. - /// The id of the post. - /// The etag of the data. - /// Thrown when is null. - /// Throw when is of bad format. - /// - /// Thrown when timeline with name does not exist. - /// If it is a personal timeline, then inner exception is . - /// - /// Thrown when post of does not exist or has been deleted. - /// Thrown when post has no data. - /// - Task GetPostDataETag(string timelineName, long postId); - - /// - /// Get the data of a post. - /// - /// The name of the timeline of the post. - /// The id of the post. - /// The etag of the data. - /// Thrown when is null. - /// Throw when is of bad format. - /// - /// Thrown when timeline with name does not exist. - /// If it is a personal timeline, then inner exception is . - /// - /// Thrown when post of does not exist or has been deleted. - /// Thrown when post has no data. - /// - Task GetPostData(string timelineName, long postId); - - /// - /// Create a new text post in timeline. - /// - /// The name of the timeline to create post against. - /// The author's user id. - /// The content text. - /// The time of the post. If null, then current time is used. - /// The info of the created post. - /// Thrown when or is null. - /// Throw when is of bad format. - /// - /// Thrown when timeline with name does not exist. - /// If it is a personal timeline, then inner exception is . - /// - /// Thrown if user of does not exist. - Task CreateTextPost(string timelineName, long authorId, string text, DateTime? time); - - /// - /// Create a new image post in timeline. - /// - /// The name of the timeline to create post against. - /// The author's user id. - /// The image data. - /// The time of the post. If null, then use current time. - /// The info of the created post. - /// Thrown when or is null. - /// Throw when is of bad format. - /// - /// Thrown when timeline with name does not exist. - /// If it is a personal timeline, then inner exception is . - /// - /// Thrown if user of does not exist. - /// Thrown if data is not a image. Validated by . - Task CreateImagePost(string timelineName, long authorId, byte[] imageData, DateTime? time); - - /// - /// Delete a post. - /// - /// The name of the timeline to delete post against. - /// The id of the post to delete. - /// Thrown when is null. - /// Throw when is of bad format. - /// - /// Thrown when timeline with name does not exist. - /// If it is a personal timeline, then inner exception is . - /// - /// Thrown when the post with given id does not exist or is deleted already. - /// - /// First use to check the permission. - /// - Task DeletePost(string timelineName, long postId); - - /// - /// Delete all posts of the given user. Used when delete a user. - /// - /// The id of the user. - Task DeleteAllPostsOfUser(long userId); /// /// Change member of timeline. @@ -305,29 +177,6 @@ namespace Timeline.Services /// Task HasReadPermission(string timelineName, long? visitorId); - /// - /// Verify whether a user has the permission to modify a post. - /// - /// The name of the timeline. - /// The id of the post. - /// The id of the user to check on. - /// True if you want it to throw . Default false. - /// True if can modify, false if can't modify. - /// Thrown when is null. - /// Throw when is of bad format. - /// - /// Thrown when timeline with name does not exist. - /// If it is a personal timeline, then inner exception is . - /// - /// Thrown when the post with given id does not exist or is deleted already and is true. - /// - /// Unless is true, this method should return true if the post does not exist. - /// If the post is deleted, its author info still exists, so it is checked as the post is not deleted unless is true. - /// 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. - /// - Task HasPostModifyPermission(string timelineName, long postId, long modifierId, bool throwOnPostNotExist = false); /// /// Verify whether a user is member of a timeline. @@ -395,28 +244,20 @@ namespace Timeline.Services Task ChangeTimelineName(string oldTimelineName, string newTimelineName); } - public class TimelineService : ITimelineService + public class TimelineService : BasicTimelineService, ITimelineService { - public TimelineService(ILogger logger, DatabaseContext database, IDataManager dataManager, IUserService userService, IImageValidator imageValidator, IClock clock) + public TimelineService(DatabaseContext database, IUserService userService, IClock clock) + : base(database, userService, clock) { - _logger = logger; _database = database; - _dataManager = dataManager; _userService = userService; - _imageValidator = imageValidator; _clock = clock; } - private readonly ILogger _logger; - private readonly DatabaseContext _database; - private readonly IDataManager _dataManager; - private readonly IUserService _userService; - private readonly IImageValidator _imageValidator; - private readonly IClock _clock; private readonly UsernameValidator _usernameValidator = new UsernameValidator(); @@ -459,122 +300,12 @@ namespace Timeline.Services }; } - private async Task MapTimelinePostFromEntity(TimelinePostEntity entity, string timelineName) - { - User? author = entity.AuthorId.HasValue ? await _userService.GetUser(entity.AuthorId.Value) : null; - - ITimelinePostContent? content = null; - - if (entity.Content != null) - { - var type = entity.ContentType; - - content = type switch - { - TimelinePostContentTypes.Text => new TextTimelinePostContent(entity.Content), - TimelinePostContentTypes.Image => new ImageTimelinePostContent(entity.Content), - _ => throw new DatabaseCorruptedException(string.Format(CultureInfo.InvariantCulture, ExceptionDatabaseUnknownContentType, type)) - }; - } - - return new TimelinePost( - id: entity.LocalId, - author: author, - content: content, - time: entity.Time, - lastUpdated: entity.LastUpdated, - timelineName: timelineName - ); - } - - private TimelineEntity CreateNewTimelineEntity(string? name, long ownerId) - { - var currentTime = _clock.GetCurrentTime(); - - return new TimelineEntity - { - Name = name, - NameLastModified = currentTime, - OwnerId = ownerId, - Visibility = TimelineVisibility.Register, - CreateTime = currentTime, - LastModified = currentTime, - CurrentPostLocalId = 0, - Members = new List() - }; - } - - - - // Get timeline id by name. If it is a personal timeline and it does not exist, it will be created. - // - // This method will check the name format and if it is invalid, ArgumentException is thrown. - // - // For personal timeline, if the user does not exist, TimelineNotExistException will be thrown with UserNotExistException as inner exception. - // For ordinary timeline, if the timeline does not exist, TimelineNotExistException will be thrown. - // - // It follows all timeline-related function common interface contracts. - private async Task FindTimelineId(string timelineName) - { - timelineName = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); - - if (isPersonal) - { - long userId; - try - { - userId = await _userService.GetUserIdByUsername(timelineName); - } - catch (ArgumentException e) - { - throw new ArgumentException(ExceptionFindTimelineUsernameBadFormat, nameof(timelineName), e); - } - catch (UserNotExistException e) - { - throw new TimelineNotExistException(timelineName, e); - } - - var timelineEntity = await _database.Timelines.Where(t => t.OwnerId == userId && t.Name == null).Select(t => new { t.Id }).SingleOrDefaultAsync(); - - if (timelineEntity != null) - { - return timelineEntity.Id; - } - else - { - var newTimelineEntity = CreateNewTimelineEntity(null, userId); - _database.Timelines.Add(newTimelineEntity); - await _database.SaveChangesAsync(); - - return newTimelineEntity.Id; - } - } - else - { - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - - ValidateTimelineName(timelineName, nameof(timelineName)); - - var timelineEntity = await _database.Timelines.Where(t => t.Name == timelineName).Select(t => new { t.Id }).SingleOrDefaultAsync(); - - if (timelineEntity == null) - { - throw new TimelineNotExistException(timelineName); - } - else - { - return timelineEntity.Id; - } - } - } - public async Task GetTimelineLastModifiedTime(string timelineName) { if (timelineName == null) throw new ArgumentNullException(nameof(timelineName)); - var timelineId = await FindTimelineId(timelineName); + var timelineId = await GetTimelineIdByName(timelineName); var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.LastModified }).SingleAsync(); @@ -586,31 +317,19 @@ namespace Timeline.Services if (timelineName == null) throw new ArgumentNullException(nameof(timelineName)); - var timelineId = await FindTimelineId(timelineName); + var timelineId = await GetTimelineIdByName(timelineName); var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.UniqueId }).SingleAsync(); return timelineEntity.UniqueId; } - public async Task GetTimelineIdByName(string timelineName) - { - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - - var timelineId = await FindTimelineId(timelineName); - - var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.Id }).SingleAsync(); - - return timelineEntity.Id; - } - public async Task GetTimeline(string timelineName) { if (timelineName == null) throw new ArgumentNullException(nameof(timelineName)); - var timelineId = await FindTimelineId(timelineName); + var timelineId = await GetTimelineIdByName(timelineName); var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Include(t => t.Members).SingleAsync(); @@ -627,262 +346,6 @@ namespace Timeline.Services return await MapTimelineFromEntity(timelineEntity); } - public async Task> GetPosts(string timelineName, DateTime? modifiedSince = null, bool includeDeleted = false) - { - modifiedSince = modifiedSince?.MyToUtc(); - - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - - var timelineId = await FindTimelineId(timelineName); - IQueryable query = _database.TimelinePosts.Where(p => p.TimelineId == timelineId); - - if (!includeDeleted) - { - query = query.Where(p => p.Content != null); - } - - if (modifiedSince.HasValue) - { - query = query.Include(p => p.Author).Where(p => p.LastUpdated >= modifiedSince || (p.Author != null && p.Author.UsernameChangeTime >= modifiedSince)); - } - - query = query.OrderBy(p => p.Time); - - var postEntities = await query.ToListAsync(); - - var posts = new List(); - foreach (var entity in postEntities) - { - posts.Add(await MapTimelinePostFromEntity(entity, timelineName)); - } - return posts; - } - - public async Task GetPostDataETag(string timelineName, long postId) - { - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - - var timelineId = await FindTimelineId(timelineName); - - var postEntity = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync(); - - if (postEntity == null) - throw new TimelinePostNotExistException(timelineName, postId, false); - - if (postEntity.Content == null) - throw new TimelinePostNotExistException(timelineName, postId, true); - - if (postEntity.ContentType != TimelinePostContentTypes.Image) - throw new TimelinePostNoDataException(ExceptionGetDataNonImagePost); - - var tag = postEntity.Content; - - return tag; - } - - public async Task GetPostData(string timelineName, long postId) - { - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - - var timelineId = await FindTimelineId(timelineName); - var postEntity = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync(); - - if (postEntity == null) - throw new TimelinePostNotExistException(timelineName, postId, false); - - if (postEntity.Content == null) - throw new TimelinePostNotExistException(timelineName, postId, true); - - if (postEntity.ContentType != TimelinePostContentTypes.Image) - throw new TimelinePostNoDataException(ExceptionGetDataNonImagePost); - - var tag = postEntity.Content; - - byte[] data; - - try - { - data = await _dataManager.GetEntry(tag); - } - catch (InvalidOperationException e) - { - throw new DatabaseCorruptedException(ExceptionGetDataDataEntryNotExist, e); - } - - if (postEntity.ExtraContent == null) - { - _logger.LogWarning(LogGetDataNoFormat); - var format = Image.DetectFormat(data); - postEntity.ExtraContent = format.DefaultMimeType; - await _database.SaveChangesAsync(); - } - - return new PostData - { - Data = data, - Type = postEntity.ExtraContent, - ETag = tag, - LastModified = postEntity.LastUpdated - }; - } - - public async Task CreateTextPost(string timelineName, long authorId, string text, DateTime? time) - { - time = time?.MyToUtc(); - - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - if (text == null) - throw new ArgumentNullException(nameof(text)); - - var timelineId = await FindTimelineId(timelineName); - var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync(); - - var author = await _userService.GetUser(authorId); - - var currentTime = _clock.GetCurrentTime(); - var finalTime = time ?? currentTime; - - timelineEntity.CurrentPostLocalId += 1; - - var postEntity = new TimelinePostEntity - { - LocalId = timelineEntity.CurrentPostLocalId, - ContentType = TimelinePostContentTypes.Text, - Content = text, - AuthorId = authorId, - TimelineId = timelineId, - Time = finalTime, - LastUpdated = currentTime - }; - _database.TimelinePosts.Add(postEntity); - await _database.SaveChangesAsync(); - - - return new TimelinePost( - id: postEntity.LocalId, - content: new TextTimelinePostContent(text), - time: finalTime, - author: author, - lastUpdated: currentTime, - timelineName: timelineName - ); - } - - public async Task CreateImagePost(string timelineName, long authorId, byte[] data, DateTime? time) - { - time = time?.MyToUtc(); - - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - if (data == null) - throw new ArgumentNullException(nameof(data)); - - var timelineId = await FindTimelineId(timelineName); - var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync(); - - var author = await _userService.GetUser(authorId); - - var imageFormat = await _imageValidator.Validate(data); - - var imageFormatText = imageFormat.DefaultMimeType; - - var tag = await _dataManager.RetainEntry(data); - - var currentTime = _clock.GetCurrentTime(); - var finalTime = time ?? currentTime; - - timelineEntity.CurrentPostLocalId += 1; - - var postEntity = new TimelinePostEntity - { - LocalId = timelineEntity.CurrentPostLocalId, - ContentType = TimelinePostContentTypes.Image, - Content = tag, - ExtraContent = imageFormatText, - AuthorId = authorId, - TimelineId = timelineId, - Time = finalTime, - LastUpdated = currentTime - }; - _database.TimelinePosts.Add(postEntity); - await _database.SaveChangesAsync(); - - return new TimelinePost( - id: postEntity.LocalId, - content: new ImageTimelinePostContent(tag), - time: finalTime, - author: author, - lastUpdated: currentTime, - timelineName: timelineName - ); - } - - public async Task DeletePost(string timelineName, long id) - { - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - - var timelineId = await FindTimelineId(timelineName); - - var post = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == id).SingleOrDefaultAsync(); - - if (post == null) - throw new TimelinePostNotExistException(timelineName, id, false); - - if (post.Content == null) - throw new TimelinePostNotExistException(timelineName, id, true); - - string? dataTag = null; - - if (post.ContentType == TimelinePostContentTypes.Image) - { - dataTag = post.Content; - } - - post.Content = null; - post.LastUpdated = _clock.GetCurrentTime(); - - await _database.SaveChangesAsync(); - - if (dataTag != null) - { - await _dataManager.FreeEntry(dataTag); - } - } - - public async Task DeleteAllPostsOfUser(long userId) - { - var posts = await _database.TimelinePosts.Where(p => p.AuthorId == userId).ToListAsync(); - - var now = _clock.GetCurrentTime(); - - var dataTags = new List(); - - foreach (var post in posts) - { - if (post.Content != null) - { - if (post.ContentType == TimelinePostContentTypes.Image) - { - dataTags.Add(post.Content); - } - post.Content = null; - } - post.LastUpdated = now; - } - - await _database.SaveChangesAsync(); - - foreach (var dataTag in dataTags) - { - await _dataManager.FreeEntry(dataTag); - } - } - public async Task ChangeProperty(string timelineName, TimelineChangePropertyRequest newProperties) { if (timelineName == null) @@ -890,7 +353,7 @@ namespace Timeline.Services if (newProperties == null) throw new ArgumentNullException(nameof(newProperties)); - var timelineId = await FindTimelineId(timelineName); + var timelineId = await GetTimelineIdByName(timelineName); var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync(); @@ -976,7 +439,7 @@ namespace Timeline.Services if (simplifiedAdd == null && simplifiedRemove == null) return; - var timelineId = await FindTimelineId(timelineName); + var timelineId = await GetTimelineIdByName(timelineName); async Task?> CheckExistenceAndGetId(List? list) { @@ -1016,7 +479,7 @@ namespace Timeline.Services if (timelineName == null) throw new ArgumentNullException(nameof(timelineName)); - var timelineId = await FindTimelineId(timelineName); + var timelineId = await GetTimelineIdByName(timelineName); var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync(); return userId == timelineEntity.OwnerId; @@ -1027,7 +490,7 @@ namespace Timeline.Services if (timelineName == null) throw new ArgumentNullException(nameof(timelineName)); - var timelineId = await FindTimelineId(timelineName); + var timelineId = await GetTimelineIdByName(timelineName); var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.Visibility }).SingleAsync(); if (timelineEntity.Visibility == TimelineVisibility.Public) @@ -1047,39 +510,12 @@ namespace Timeline.Services } } - public async Task HasPostModifyPermission(string timelineName, long postId, long modifierId, bool throwOnPostNotExist = false) - { - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - - var timelineId = await FindTimelineId(timelineName); - - var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync(); - - var postEntity = await _database.TimelinePosts.Where(p => p.Id == postId).Select(p => new { p.Content, p.AuthorId }).SingleOrDefaultAsync(); - - if (postEntity == null) - { - if (throwOnPostNotExist) - throw new TimelinePostNotExistException(timelineName, postId, false); - else - return true; - } - - if (postEntity.Content == null && throwOnPostNotExist) - { - throw new TimelinePostNotExistException(timelineName, postId, true); - } - - return timelineEntity.OwnerId == modifierId || postEntity.AuthorId == modifierId; - } - public async Task IsMemberOf(string timelineName, long userId) { if (timelineName == null) throw new ArgumentNullException(nameof(timelineName)); - var timelineId = await FindTimelineId(timelineName); + var timelineId = await GetTimelineIdByName(timelineName); var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync(); diff --git a/BackEnd/Timeline/Services/UserDeleteService.cs b/BackEnd/Timeline/Services/UserDeleteService.cs index 5365313b..a4e77abc 100644 --- a/BackEnd/Timeline/Services/UserDeleteService.cs +++ b/BackEnd/Timeline/Services/UserDeleteService.cs @@ -31,15 +31,15 @@ namespace Timeline.Services private readonly DatabaseContext _databaseContext; - private readonly ITimelineService _timelineService; + private readonly ITimelinePostService _timelinePostService; private readonly UsernameValidator _usernameValidator = new UsernameValidator(); - public UserDeleteService(ILogger logger, DatabaseContext databaseContext, ITimelineService timelineService) + public UserDeleteService(ILogger logger, DatabaseContext databaseContext, ITimelinePostService timelinePostService) { _logger = logger; _databaseContext = databaseContext; - _timelineService = timelineService; + _timelinePostService = timelinePostService; } public async Task DeleteUser(string username) @@ -59,7 +59,7 @@ namespace Timeline.Services if (user.Id == 1) throw new InvalidOperationOnRootUserException("Can't delete root user."); - await _timelineService.DeleteAllPostsOfUser(user.Id); + await _timelinePostService.DeleteAllPostsOfUser(user.Id); _databaseContext.Users.Remove(user); diff --git a/BackEnd/Timeline/Services/UserService.cs b/BackEnd/Timeline/Services/UserService.cs index 76c24666..cded3ff1 100644 --- a/BackEnd/Timeline/Services/UserService.cs +++ b/BackEnd/Timeline/Services/UserService.cs @@ -24,7 +24,7 @@ namespace Timeline.Services public string? Nickname { get; set; } } - public interface IUserService + public interface IUserService : IBasicUserService { /// /// Try to verify the given username and password. @@ -38,12 +38,6 @@ namespace Timeline.Services /// Thrown when password is wrong. Task VerifyCredential(string username, string password); - /// - /// Check if a user exists. - /// - /// The id of the user. - /// True if exists. Otherwise false. - Task CheckUserExistence(long id); /// /// Try to get a user by id. @@ -53,15 +47,6 @@ namespace Timeline.Services /// Thrown when the user with given id does not exist. Task GetUser(long id); - /// - /// Get the user id of given username. - /// - /// Username of the user. - /// The id of the user. - /// Thrown when is null. - /// Thrown when is of bad format. - /// Thrown when the user with given username does not exist. - Task GetUserIdByUsername(string username); /// /// List all users. @@ -106,7 +91,7 @@ namespace Timeline.Services Task ChangePassword(long id, string oldPassword, string newPassword); } - public class UserService : IUserService + public class UserService : BasicUserService, IUserService { private readonly ILogger _logger; private readonly IClock _clock; @@ -119,7 +104,7 @@ namespace Timeline.Services private readonly UsernameValidator _usernameValidator = new UsernameValidator(); private readonly NicknameValidator _nicknameValidator = new NicknameValidator(); - public UserService(ILogger logger, DatabaseContext databaseContext, IPasswordService passwordService, IClock clock, IUserPermissionService userPermissionService) + public UserService(ILogger logger, DatabaseContext databaseContext, IPasswordService passwordService, IClock clock, IUserPermissionService userPermissionService) : base(databaseContext) { _logger = logger; _clock = clock; @@ -195,11 +180,6 @@ namespace Timeline.Services return await CreateUserFromEntity(entity); } - public async Task CheckUserExistence(long id) - { - return await _databaseContext.Users.AnyAsync(u => u.Id == id); - } - public async Task GetUser(long id) { var user = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync(); @@ -210,21 +190,6 @@ namespace Timeline.Services return await CreateUserFromEntity(user); } - public async Task 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> GetUsers() { List result = new(); diff --git a/BackEnd/Timeline/Startup.cs b/BackEnd/Timeline/Startup.cs index 532c63d0..63e2a0dd 100644 --- a/BackEnd/Timeline/Startup.cs +++ b/BackEnd/Timeline/Startup.cs @@ -72,34 +72,34 @@ namespace Timeline services.AddAuthentication(AuthenticationConstants.Scheme) .AddScheme(AuthenticationConstants.Scheme, AuthenticationConstants.DisplayName, o => { }); services.AddAuthorization(); - services.AddSingleton(); - services.AddSingleton(); + services.TryAddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddAutoMapper(GetType().Assembly); services.AddTransient(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddTransient(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); - - services.AddScoped(); - services.AddScoped(); - - services.AddScoped(); - services.AddUserAvatarService(); + services.AddScoped(); services.AddScoped(); + services.AddScoped(); - services.TryAddSingleton(); services.AddDbContext((services, options) => { -- cgit v1.2.3 From ce85af942c64fc3bad4e4502d683f730c882de96 Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 26 Nov 2020 23:43:11 +0800 Subject: refactor: ... --- .../Services/HighlightTimelineServiceTest.cs | 6 +- BackEnd/Timeline/Controllers/TokenController.cs | 4 +- BackEnd/Timeline/Controllers/UserController.cs | 6 +- BackEnd/Timeline/Services/TimelinePostService.cs | 4 +- BackEnd/Timeline/Services/TimelineService.cs | 3 - BackEnd/Timeline/Services/UserCredentialService.cs | 104 +++++++++++++++++++++ BackEnd/Timeline/Services/UserService.cs | 68 -------------- BackEnd/Timeline/Services/UserTokenManager.cs | 7 +- BackEnd/Timeline/Startup.cs | 2 +- 9 files changed, 120 insertions(+), 84 deletions(-) create mode 100644 BackEnd/Timeline/Services/UserCredentialService.cs (limited to 'BackEnd/Timeline/Services/UserService.cs') diff --git a/BackEnd/Timeline.Tests/Services/HighlightTimelineServiceTest.cs b/BackEnd/Timeline.Tests/Services/HighlightTimelineServiceTest.cs index 950fa974..a4cd983d 100644 --- a/BackEnd/Timeline.Tests/Services/HighlightTimelineServiceTest.cs +++ b/BackEnd/Timeline.Tests/Services/HighlightTimelineServiceTest.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Timeline.Services; +using Timeline.Services; namespace Timeline.Tests.Services { diff --git a/BackEnd/Timeline/Controllers/TokenController.cs b/BackEnd/Timeline/Controllers/TokenController.cs index 8f2ca600..41ec21e6 100644 --- a/BackEnd/Timeline/Controllers/TokenController.cs +++ b/BackEnd/Timeline/Controllers/TokenController.cs @@ -22,6 +22,7 @@ namespace Timeline.Controllers [ProducesErrorResponseType(typeof(CommonResponse))] public class TokenController : Controller { + private readonly IUserCredentialService _userCredentialService; private readonly IUserTokenManager _userTokenManager; private readonly ILogger _logger; private readonly IClock _clock; @@ -29,8 +30,9 @@ namespace Timeline.Controllers private readonly IMapper _mapper; /// - public TokenController(IUserTokenManager userTokenManager, ILogger logger, IClock clock, IMapper mapper) + public TokenController(IUserCredentialService userCredentialService, IUserTokenManager userTokenManager, ILogger logger, IClock clock, IMapper mapper) { + _userCredentialService = userCredentialService; _userTokenManager = userTokenManager; _logger = logger; _clock = clock; diff --git a/BackEnd/Timeline/Controllers/UserController.cs b/BackEnd/Timeline/Controllers/UserController.cs index 8edae139..626a116f 100644 --- a/BackEnd/Timeline/Controllers/UserController.cs +++ b/BackEnd/Timeline/Controllers/UserController.cs @@ -26,15 +26,17 @@ namespace Timeline.Controllers { private readonly ILogger _logger; private readonly IUserService _userService; + private readonly IUserCredentialService _userCredentialService; private readonly IUserPermissionService _userPermissionService; private readonly IUserDeleteService _userDeleteService; private readonly IMapper _mapper; /// - public UserController(ILogger logger, IUserService userService, IUserPermissionService userPermissionService, IUserDeleteService userDeleteService, IMapper mapper) + public UserController(ILogger logger, IUserService userService, IUserCredentialService userCredentialService, IUserPermissionService userPermissionService, IUserDeleteService userDeleteService, IMapper mapper) { _logger = logger; _userService = userService; + _userCredentialService = userCredentialService; _userPermissionService = userPermissionService; _userDeleteService = userDeleteService; _mapper = mapper; @@ -190,7 +192,7 @@ namespace Timeline.Controllers { try { - await _userService.ChangePassword(this.GetUserId(), request.OldPassword, request.NewPassword); + await _userCredentialService.ChangePassword(this.GetUserId(), request.OldPassword, request.NewPassword); return Ok(); } catch (BadPasswordException e) diff --git a/BackEnd/Timeline/Services/TimelinePostService.cs b/BackEnd/Timeline/Services/TimelinePostService.cs index 36fcdbca..a1176a68 100644 --- a/BackEnd/Timeline/Services/TimelinePostService.cs +++ b/BackEnd/Timeline/Services/TimelinePostService.cs @@ -1,4 +1,6 @@ using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using SixLabors.ImageSharp; using System; using System.Collections.Generic; using System.Globalization; @@ -8,9 +10,7 @@ using Timeline.Entities; using Timeline.Helpers; using Timeline.Models; using Timeline.Services.Exceptions; -using SixLabors.ImageSharp; using static Timeline.Resources.Services.TimelineService; -using Microsoft.Extensions.Logging; namespace Timeline.Services { diff --git a/BackEnd/Timeline/Services/TimelineService.cs b/BackEnd/Timeline/Services/TimelineService.cs index f943f8b4..b8ec354a 100644 --- a/BackEnd/Timeline/Services/TimelineService.cs +++ b/BackEnd/Timeline/Services/TimelineService.cs @@ -1,13 +1,10 @@ using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using SixLabors.ImageSharp; using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading.Tasks; using Timeline.Entities; -using Timeline.Helpers; using Timeline.Models; using Timeline.Models.Validation; using Timeline.Services.Exceptions; diff --git a/BackEnd/Timeline/Services/UserCredentialService.cs b/BackEnd/Timeline/Services/UserCredentialService.cs new file mode 100644 index 00000000..e5c3581b --- /dev/null +++ b/BackEnd/Timeline/Services/UserCredentialService.cs @@ -0,0 +1,104 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Entities; +using Timeline.Helpers; +using Timeline.Models; +using Timeline.Models.Validation; +using Timeline.Services.Exceptions; + +namespace Timeline.Services +{ + public interface IUserCredentialService + { + /// + /// Try to verify the given username and password. + /// + /// The username of the user to verify. + /// The password of the user to verify. + /// User id. + /// Thrown when or is null. + /// Thrown when is of bad format or is empty. + /// Thrown when the user with given username does not exist. + /// Thrown when password is wrong. + Task VerifyCredential(string username, string password); + + /// + /// Try to change a user's password with old password. + /// + /// The id of user to change password of. + /// Old password. + /// New password. + /// Thrown if or is null. + /// Thrown if or is empty. + /// Thrown if the user with given username does not exist. + /// Thrown if the old password is wrong. + Task ChangePassword(long id, string oldPassword, string newPassword); + } + + public class UserCredentialService : IUserCredentialService + { + private readonly ILogger _logger; + private readonly DatabaseContext _database; + private readonly IPasswordService _passwordService; + + private readonly UsernameValidator _usernameValidator = new UsernameValidator(); + + public UserCredentialService(ILogger logger, DatabaseContext database, IPasswordService passwordService) + { + _logger = logger; + _database = database; + _passwordService = passwordService; + } + + public async Task VerifyCredential(string username, string password) + { + if (username == null) + throw new ArgumentNullException(nameof(username)); + if (password == null) + throw new ArgumentNullException(nameof(password)); + if (!_usernameValidator.Validate(username, out var message)) + throw new ArgumentException(message); + if (password.Length == 0) + throw new ArgumentException("Password can't be empty."); + + var entity = await _database.Users.Where(u => u.Username == username).Select(u => new { u.Id, u.Password }).SingleOrDefaultAsync(); + + if (entity == null) + throw new UserNotExistException(username); + + if (!_passwordService.VerifyPassword(entity.Password, password)) + throw new BadPasswordException(password); + + return entity.Id; + } + + public async Task ChangePassword(long id, string oldPassword, string newPassword) + { + if (oldPassword == null) + throw new ArgumentNullException(nameof(oldPassword)); + if (newPassword == null) + throw new ArgumentNullException(nameof(newPassword)); + if (oldPassword.Length == 0) + throw new ArgumentException("Old password can't be empty."); + if (newPassword.Length == 0) + throw new ArgumentException("New password can't be empty."); + + var entity = await _database.Users.Where(u => u.Id == id).SingleOrDefaultAsync(); + + if (entity == null) + throw new UserNotExistException(id); + + if (!_passwordService.VerifyPassword(entity.Password, oldPassword)) + throw new BadPasswordException(oldPassword); + + entity.Password = _passwordService.HashPassword(newPassword); + entity.Version += 1; + await _database.SaveChangesAsync(); + _logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseUpdate, ("Id", id), ("Operation", "Change password"))); + } + } +} diff --git a/BackEnd/Timeline/Services/UserService.cs b/BackEnd/Timeline/Services/UserService.cs index cded3ff1..9395cc52 100644 --- a/BackEnd/Timeline/Services/UserService.cs +++ b/BackEnd/Timeline/Services/UserService.cs @@ -26,19 +26,6 @@ namespace Timeline.Services public interface IUserService : IBasicUserService { - /// - /// Try to verify the given username and password. - /// - /// The username of the user to verify. - /// The password of the user to verify. - /// The user info and auth info. - /// Thrown when or is null. - /// Thrown when is of bad format or is empty. - /// Thrown when the user with given username does not exist. - /// Thrown when password is wrong. - Task VerifyCredential(string username, string password); - - /// /// Try to get a user by id. /// @@ -47,7 +34,6 @@ namespace Timeline.Services /// Thrown when the user with given id does not exist. Task GetUser(long id); - /// /// List all users. /// @@ -77,18 +63,6 @@ namespace Timeline.Services /// Version will increase if password is changed. /// Task ModifyUser(long id, ModifyUserParams? param); - - /// - /// Try to change a user's password with old password. - /// - /// The id of user to change password of. - /// Old password. - /// New password. - /// Thrown if or is null. - /// Thrown if or is empty. - /// Thrown if the user with given username does not exist. - /// Thrown if the old password is wrong. - Task ChangePassword(long id, string oldPassword, string newPassword); } public class UserService : BasicUserService, IUserService @@ -159,26 +133,7 @@ namespace Timeline.Services }; } - public async Task VerifyCredential(string username, string password) - { - if (username == null) - throw new ArgumentNullException(nameof(username)); - if (password == null) - throw new ArgumentNullException(nameof(password)); - - CheckUsernameFormat(username, nameof(username)); - CheckPasswordFormat(password, nameof(password)); - - var entity = await _databaseContext.Users.Where(u => u.Username == username).SingleOrDefaultAsync(); - - if (entity == null) - throw new UserNotExistException(username); - if (!_passwordService.VerifyPassword(entity.Password, password)) - throw new BadPasswordException(password); - - return await CreateUserFromEntity(entity); - } public async Task GetUser(long id) { @@ -288,28 +243,5 @@ namespace Timeline.Services return await CreateUserFromEntity(entity); } - - public async Task ChangePassword(long id, string oldPassword, string newPassword) - { - if (oldPassword == null) - throw new ArgumentNullException(nameof(oldPassword)); - if (newPassword == null) - throw new ArgumentNullException(nameof(newPassword)); - CheckPasswordFormat(oldPassword, nameof(oldPassword)); - CheckPasswordFormat(newPassword, nameof(newPassword)); - - var entity = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync(); - - if (entity == null) - throw new UserNotExistException(id); - - if (!_passwordService.VerifyPassword(entity.Password, oldPassword)) - throw new BadPasswordException(oldPassword); - - entity.Password = _passwordService.HashPassword(newPassword); - entity.Version += 1; - await _databaseContext.SaveChangesAsync(); - _logger.LogInformation(Log.Format(LogDatabaseUpdate, ("Id", id), ("Operation", "Change password"))); - } } } diff --git a/BackEnd/Timeline/Services/UserTokenManager.cs b/BackEnd/Timeline/Services/UserTokenManager.cs index 09ecd19c..831329e6 100644 --- a/BackEnd/Timeline/Services/UserTokenManager.cs +++ b/BackEnd/Timeline/Services/UserTokenManager.cs @@ -45,13 +45,15 @@ namespace Timeline.Services { private readonly ILogger _logger; private readonly IUserService _userService; + private readonly IUserCredentialService _userCredentialService; private readonly IUserTokenService _userTokenService; private readonly IClock _clock; - public UserTokenManager(ILogger logger, IUserService userService, IUserTokenService userTokenService, IClock clock) + public UserTokenManager(ILogger logger, IUserService userService, IUserCredentialService userCredentialService, IUserTokenService userTokenService, IClock clock) { _logger = logger; _userService = userService; + _userCredentialService = userCredentialService; _userTokenService = userTokenService; _clock = clock; } @@ -65,7 +67,8 @@ namespace Timeline.Services if (password == null) throw new ArgumentNullException(nameof(password)); - var user = await _userService.VerifyCredential(username, password); + var userId = await _userCredentialService.VerifyCredential(username, password); + var user = await _userService.GetUser(userId); var token = _userTokenService.GenerateToken(new UserTokenInfo { Id = user.Id, Version = user.Version, ExpireAt = expireAt }); return new UserTokenCreateResult { Token = token, User = user }; diff --git a/BackEnd/Timeline/Startup.cs b/BackEnd/Timeline/Startup.cs index 63e2a0dd..bf34f9e2 100644 --- a/BackEnd/Timeline/Startup.cs +++ b/BackEnd/Timeline/Startup.cs @@ -90,6 +90,7 @@ namespace Timeline services.AddTransient(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -100,7 +101,6 @@ namespace Timeline services.AddScoped(); services.AddScoped(); - services.AddDbContext((services, options) => { var pathProvider = services.GetRequiredService(); -- cgit v1.2.3 From 7883590a7a0c5c5c4e5cff6290e26c64e1258a25 Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 27 Nov 2020 00:07:09 +0800 Subject: refactor: ... --- .../HttpClientTimelineExtensions.cs | 8 +- .../IntegratedTests/HttpClientUserExtensions.cs | 4 +- .../IntegratedTests/IntegratedTestBase.cs | 4 +- .../Timeline.Tests/IntegratedTests/TimelineTest.cs | 150 ++++++++++----------- .../Timeline.Tests/IntegratedTests/TokenTest.cs | 24 ++-- BackEnd/Timeline.Tests/IntegratedTests/UserTest.cs | 66 ++++----- .../Timeline.Tests/Services/TimelineServiceTest.cs | 2 +- BackEnd/Timeline/Controllers/TimelineController.cs | 32 ++--- BackEnd/Timeline/Controllers/TokenController.cs | 12 +- BackEnd/Timeline/Controllers/UserController.cs | 12 +- BackEnd/Timeline/Models/Http/Timeline.cs | 45 +++---- BackEnd/Timeline/Models/Http/TimelineController.cs | 21 ++- BackEnd/Timeline/Models/Http/TokenController.cs | 20 +-- BackEnd/Timeline/Models/Http/User.cs | 105 +++++++++++++++ BackEnd/Timeline/Models/Http/UserController.cs | 18 +-- BackEnd/Timeline/Models/Http/UserInfo.cs | 105 --------------- BackEnd/Timeline/Models/Timeline.cs | 98 -------------- BackEnd/Timeline/Models/TimelineInfo.cs | 130 ++++++++++++++++++ BackEnd/Timeline/Models/User.cs | 21 --- BackEnd/Timeline/Models/UserInfo.cs | 48 +++++++ .../Timeline/Services/HighlightTimelineService.cs | 7 +- BackEnd/Timeline/Services/TimelinePostService.cs | 24 ++-- BackEnd/Timeline/Services/TimelineService.cs | 54 ++++---- BackEnd/Timeline/Services/UserCredentialService.cs | 2 - BackEnd/Timeline/Services/UserService.cs | 45 +++---- BackEnd/Timeline/Services/UserTokenManager.cs | 6 +- 26 files changed, 561 insertions(+), 502 deletions(-) create mode 100644 BackEnd/Timeline/Models/Http/User.cs delete mode 100644 BackEnd/Timeline/Models/Http/UserInfo.cs delete mode 100644 BackEnd/Timeline/Models/Timeline.cs create mode 100644 BackEnd/Timeline/Models/TimelineInfo.cs delete mode 100644 BackEnd/Timeline/Models/User.cs create mode 100644 BackEnd/Timeline/Models/UserInfo.cs (limited to 'BackEnd/Timeline/Services/UserService.cs') diff --git a/BackEnd/Timeline.Tests/IntegratedTests/HttpClientTimelineExtensions.cs b/BackEnd/Timeline.Tests/IntegratedTests/HttpClientTimelineExtensions.cs index 8e48ccbf..ac60ce7c 100644 --- a/BackEnd/Timeline.Tests/IntegratedTests/HttpClientTimelineExtensions.cs +++ b/BackEnd/Timeline.Tests/IntegratedTests/HttpClientTimelineExtensions.cs @@ -6,11 +6,11 @@ namespace Timeline.Tests.IntegratedTests { public static class HttpClientTimelineExtensions { - public static Task GetTimelineAsync(this HttpClient client, string timelineName) - => client.TestGetAsync($"timelines/{timelineName}"); + public static Task GetTimelineAsync(this HttpClient client, string timelineName) + => client.TestGetAsync($"timelines/{timelineName}"); - public static Task PatchTimelineAsync(this HttpClient client, string timelineName, TimelinePatchRequest body) - => client.TestPatchAsync($"timelines/{timelineName}", body); + public static Task PatchTimelineAsync(this HttpClient client, string timelineName, HttpTimelinePatchRequest body) + => client.TestPatchAsync($"timelines/{timelineName}", body); public static Task PutTimelineMemberAsync(this HttpClient client, string timelineName, string memberUsername) => client.TestPutAsync($"timelines/{timelineName}/members/{memberUsername}"); diff --git a/BackEnd/Timeline.Tests/IntegratedTests/HttpClientUserExtensions.cs b/BackEnd/Timeline.Tests/IntegratedTests/HttpClientUserExtensions.cs index 81787eef..7ca62e38 100644 --- a/BackEnd/Timeline.Tests/IntegratedTests/HttpClientUserExtensions.cs +++ b/BackEnd/Timeline.Tests/IntegratedTests/HttpClientUserExtensions.cs @@ -6,7 +6,7 @@ namespace Timeline.Tests.IntegratedTests { public static class HttpClientUserExtensions { - public static Task GetUserAsync(this HttpClient client, string username) - => client.TestGetAsync($"users/{username}"); + public static Task GetUserAsync(this HttpClient client, string username) + => client.TestGetAsync($"users/{username}"); } } diff --git a/BackEnd/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs b/BackEnd/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs index e426ac98..82aed24e 100644 --- a/BackEnd/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs +++ b/BackEnd/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs @@ -103,8 +103,8 @@ namespace Timeline.Tests.IntegratedTests public async Task CreateClientWithCredential(string username, string password, bool setApiBase = true) { var client = await CreateDefaultClient(setApiBase); - var res = await client.TestPostAsync("token/create", - new CreateTokenRequest { Username = username, Password = password }); + var res = await client.TestPostAsync("token/create", + new HttpCreateTokenRequest { Username = username, Password = password }); var token = res.Token; client.DefaultRequestHeaders.Add("Authorization", "Bearer " + token); return client; diff --git a/BackEnd/Timeline.Tests/IntegratedTests/TimelineTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/TimelineTest.cs index 9845e1b1..12dd2b8d 100644 --- a/BackEnd/Timeline.Tests/IntegratedTests/TimelineTest.cs +++ b/BackEnd/Timeline.Tests/IntegratedTests/TimelineTest.cs @@ -19,20 +19,20 @@ namespace Timeline.Tests.IntegratedTests { public static class TimelineHelper { - public static TimelinePostContentInfo TextPostContent(string text) + public static HttpTimelinePostContent TextPostContent(string text) { - return new TimelinePostContentInfo + return new HttpTimelinePostContent { Type = "text", Text = text }; } - public static TimelinePostCreateRequest TextPostCreateRequest(string text, DateTime? time = null) + public static HttpTimelinePostCreateRequest TextPostCreateRequest(string text, DateTime? time = null) { - return new TimelinePostCreateRequest + return new HttpTimelinePostCreateRequest { - Content = new TimelinePostCreateRequestContent + Content = new HttpTimelinePostCreateRequestContent { Type = "text", Text = text @@ -72,7 +72,7 @@ namespace Timeline.Tests.IntegratedTests { - var body = await client.TestGetAsync("timelines/@user1"); + var body = await client.TestGetAsync("timelines/@user1"); body.Owner.Should().BeEquivalentTo(await client.GetUserAsync("user1")); body.Visibility.Should().Be(TimelineVisibility.Register); body.Description.Should().Be(""); @@ -84,7 +84,7 @@ namespace Timeline.Tests.IntegratedTests } { - var body = await client.TestGetAsync("timelines/t1"); + var body = await client.TestGetAsync("timelines/t1"); body.Owner.Should().BeEquivalentTo(await client.GetUserAsync("user1")); body.Visibility.Should().Be(TimelineVisibility.Register); body.Description.Should().Be(""); @@ -101,7 +101,7 @@ namespace Timeline.Tests.IntegratedTests { using var client = await CreateDefaultClient(); - var result = new List + var result = new List { await client.GetTimelineAsync("@user1") }; @@ -112,7 +112,7 @@ namespace Timeline.Tests.IntegratedTests } - var body = await client.TestGetAsync>("timelines"); + var body = await client.TestGetAsync>("timelines"); body.Should().BeEquivalentTo(result); } @@ -127,14 +127,14 @@ namespace Timeline.Tests.IntegratedTests await client.TestGetAssertInvalidModelAsync("timelines?visibility=aaa"); } - var testResultRelate = new List(); - var testResultOwn = new List(); - var testResultJoin = new List(); - var testResultOwnPrivate = new List(); - var testResultRelatePublic = new List(); - var testResultRelateRegister = new List(); - var testResultJoinPrivate = new List(); - var testResultPublic = new List(); + var testResultRelate = new List(); + var testResultOwn = new List(); + var testResultJoin = new List(); + var testResultOwnPrivate = new List(); + var testResultRelatePublic = new List(); + var testResultRelateRegister = new List(); + var testResultJoinPrivate = new List(); + var testResultPublic = new List(); { using var client = await CreateClientAsUser(); @@ -185,8 +185,8 @@ namespace Timeline.Tests.IntegratedTests { using var client = await CreateClientAs(3); - await client.PatchTimelineAsync("@user3", new TimelinePatchRequest { Visibility = TimelineVisibility.Private }); - await client.PatchTimelineAsync("t3", new TimelinePatchRequest { Visibility = TimelineVisibility.Register }); + await client.PatchTimelineAsync("@user3", new HttpTimelinePatchRequest { Visibility = TimelineVisibility.Private }); + await client.PatchTimelineAsync("t3", new HttpTimelinePatchRequest { Visibility = TimelineVisibility.Register }); { var timeline = await client.GetTimelineAsync("@user3"); @@ -206,9 +206,9 @@ namespace Timeline.Tests.IntegratedTests { using var client = await CreateDefaultClient(); - async Task TestAgainst(string url, List against) + async Task TestAgainst(string url, List against) { - var body = await client.TestGetAsync>(url); + var body = await client.TestGetAsync>(url); body.Should().BeEquivalentTo(against); } @@ -236,7 +236,7 @@ namespace Timeline.Tests.IntegratedTests await client.TestPostAssertInvalidModelAsync("timelines", new TimelineCreateRequest { Name = "!!!" }); { - var body = await client.TestPostAsync("timelines", new TimelineCreateRequest { Name = "aaa" }); + var body = await client.TestPostAsync("timelines", new TimelineCreateRequest { Name = "aaa" }); body.Should().BeEquivalentTo(await client.GetTimelineAsync("aaa")); } @@ -342,7 +342,7 @@ namespace Timeline.Tests.IntegratedTests var timelineName = generator(1); - async Task AssertMembers(List members) + async Task AssertMembers(List members) { var body = await client.GetTimelineAsync(timelineName); body.Members.Should().NotBeNull().And.BeEquivalentTo(members); @@ -358,7 +358,7 @@ namespace Timeline.Tests.IntegratedTests await client.TestPutAssertErrorAsync($"timelines/{timelineName}/members/usernotexist", errorCode: ErrorCodes.TimelineController.MemberPut_NotExist); await AssertEmptyMembers(); await client.PutTimelineMemberAsync(timelineName, "user2"); - await AssertMembers(new List { await client.GetUserAsync("user2") }); + await AssertMembers(new List { await client.GetUserAsync("user2") }); await client.DeleteTimelineMemberAsync(timelineName, "user2", true); await AssertEmptyMembers(); await client.DeleteTimelineMemberAsync(timelineName, "aaa", false); @@ -462,7 +462,7 @@ namespace Timeline.Tests.IntegratedTests async Task CreatePost(int userNumber) { using var client = await CreateClientAs(userNumber); - var body = await client.TestPostAsync($"timelines/{generator(1)}/posts", TimelineHelper.TextPostCreateRequest("aaa")); + var body = await client.TestPostAsync($"timelines/{generator(1)}/posts", TimelineHelper.TextPostCreateRequest("aaa")); return body.Id; } @@ -515,28 +515,28 @@ namespace Timeline.Tests.IntegratedTests using var client = await CreateClientAsUser(); { - var body = await client.TestGetAsync>($"timelines/{generator(1)}/posts"); + var body = await client.TestGetAsync>($"timelines/{generator(1)}/posts"); body.Should().BeEmpty(); } const string mockContent = "aaa"; - TimelinePostInfo createRes; + HttpTimelinePost createRes; { - var body = await client.TestPostAsync($"timelines/{generator(1)}/posts", TimelineHelper.TextPostCreateRequest(mockContent)); + var body = await client.TestPostAsync($"timelines/{generator(1)}/posts", TimelineHelper.TextPostCreateRequest(mockContent)); body.Content.Should().BeEquivalentTo(TimelineHelper.TextPostContent(mockContent)); body.Author.Should().BeEquivalentTo(await client.GetUserAsync("user1")); body.Deleted.Should().BeFalse(); createRes = body; } { - var body = await client.TestGetAsync>($"timelines/{generator(1)}/posts"); + var body = await client.TestGetAsync>($"timelines/{generator(1)}/posts"); body.Should().BeEquivalentTo(createRes); } const string mockContent2 = "bbb"; var mockTime2 = DateTime.UtcNow.AddDays(-1); - TimelinePostInfo createRes2; + HttpTimelinePost createRes2; { - var body = await client.TestPostAsync($"timelines/{generator(1)}/posts", TimelineHelper.TextPostCreateRequest(mockContent2, mockTime2)); + var body = await client.TestPostAsync($"timelines/{generator(1)}/posts", TimelineHelper.TextPostCreateRequest(mockContent2, mockTime2)); body.Should().NotBeNull(); body.Content.Should().BeEquivalentTo(TimelineHelper.TextPostContent(mockContent2)); body.Author.Should().BeEquivalentTo(await client.GetUserAsync("user1")); @@ -545,7 +545,7 @@ namespace Timeline.Tests.IntegratedTests createRes2 = body; } { - var body = await client.TestGetAsync>($"timelines/{generator(1)}/posts"); + var body = await client.TestGetAsync>($"timelines/{generator(1)}/posts"); body.Should().BeEquivalentTo(createRes, createRes2); } { @@ -554,7 +554,7 @@ namespace Timeline.Tests.IntegratedTests await client.TestDeleteAsync($"timelines/{generator(1)}/posts/30000", false); } { - var body = await client.TestGetAsync>($"timelines/{generator(1)}/posts"); + var body = await client.TestGetAsync>($"timelines/{generator(1)}/posts"); body.Should().BeEquivalentTo(createRes2); } } @@ -567,7 +567,7 @@ namespace Timeline.Tests.IntegratedTests async Task CreatePost(DateTime time) { - var body = await client.TestPostAsync($"timelines/{generator(1)}/posts", TimelineHelper.TextPostCreateRequest("aaa", time)); + var body = await client.TestPostAsync($"timelines/{generator(1)}/posts", TimelineHelper.TextPostCreateRequest("aaa", time)); return body.Id; } @@ -577,7 +577,7 @@ namespace Timeline.Tests.IntegratedTests var id2 = await CreatePost(now); { - var body = await client.TestGetAsync>($"timelines/{generator(1)}/posts"); + var body = await client.TestGetAsync>($"timelines/{generator(1)}/posts"); body.Select(p => p.Id).Should().Equal(id1, id2, id0); } } @@ -588,15 +588,15 @@ namespace Timeline.Tests.IntegratedTests { using var client = await CreateClientAsUser(); var postUrl = $"timelines/{generator(1)}/posts"; - await client.TestPostAssertInvalidModelAsync(postUrl, new TimelinePostCreateRequest { Content = null! }); - await client.TestPostAssertInvalidModelAsync(postUrl, new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = null! } }); - await client.TestPostAssertInvalidModelAsync(postUrl, new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = "hahaha" } }); - await client.TestPostAssertInvalidModelAsync(postUrl, new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = "text", Text = null } }); - await client.TestPostAssertInvalidModelAsync(postUrl, new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = "image", Data = null } }); + await client.TestPostAssertInvalidModelAsync(postUrl, new HttpTimelinePostCreateRequest { Content = null! }); + await client.TestPostAssertInvalidModelAsync(postUrl, new HttpTimelinePostCreateRequest { Content = new HttpTimelinePostCreateRequestContent { Type = null! } }); + await client.TestPostAssertInvalidModelAsync(postUrl, new HttpTimelinePostCreateRequest { Content = new HttpTimelinePostCreateRequestContent { Type = "hahaha" } }); + await client.TestPostAssertInvalidModelAsync(postUrl, new HttpTimelinePostCreateRequest { Content = new HttpTimelinePostCreateRequestContent { Type = "text", Text = null } }); + await client.TestPostAssertInvalidModelAsync(postUrl, new HttpTimelinePostCreateRequest { Content = new HttpTimelinePostCreateRequestContent { Type = "image", Data = null } }); // image not base64 - await client.TestPostAssertInvalidModelAsync(postUrl, new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = "image", Data = "!!!" } }); + await client.TestPostAssertInvalidModelAsync(postUrl, new HttpTimelinePostCreateRequest { Content = new HttpTimelinePostCreateRequestContent { Type = "image", Data = "!!!" } }); // image base64 not image - await client.TestPostAssertInvalidModelAsync(postUrl, new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = "image", Data = Convert.ToBase64String(new byte[] { 0x01, 0x02, 0x03 }) } }); + await client.TestPostAssertInvalidModelAsync(postUrl, new HttpTimelinePostCreateRequest { Content = new HttpTimelinePostCreateRequestContent { Type = "image", Data = Convert.ToBase64String(new byte[] { 0x01, 0x02, 0x03 }) } }); } [Theory] @@ -608,7 +608,7 @@ namespace Timeline.Tests.IntegratedTests long postId; string postImageUrl; - void AssertPostContent(TimelinePostContentInfo content) + void AssertPostContent(HttpTimelinePostContent content) { content.Type.Should().Be(TimelinePostContentTypes.Image); content.Url.Should().EndWith($"timelines/{generator(1)}/posts/{postId}/data"); @@ -618,10 +618,10 @@ namespace Timeline.Tests.IntegratedTests using var client = await CreateClientAsUser(); { - var body = await client.TestPostAsync($"timelines/{generator(1)}/posts", - new TimelinePostCreateRequest + var body = await client.TestPostAsync($"timelines/{generator(1)}/posts", + new HttpTimelinePostCreateRequest { - Content = new TimelinePostCreateRequestContent + Content = new HttpTimelinePostCreateRequestContent { Type = TimelinePostContentTypes.Image, Data = Convert.ToBase64String(imageData) @@ -633,7 +633,7 @@ namespace Timeline.Tests.IntegratedTests } { - var body = await client.TestGetAsync>($"timelines/{generator(1)}/posts"); + var body = await client.TestGetAsync>($"timelines/{generator(1)}/posts"); body.Should().HaveCount(1); var post = body[0]; post.Id.Should().Be(postId); @@ -655,7 +655,7 @@ namespace Timeline.Tests.IntegratedTests await client.TestDeleteAsync($"timelines/{generator(1)}/posts/{postId}", false); { - var body = await client.TestGetAsync>($"timelines/{generator(1)}/posts"); + var body = await client.TestGetAsync>($"timelines/{generator(1)}/posts"); body.Should().BeEmpty(); } @@ -677,7 +677,7 @@ namespace Timeline.Tests.IntegratedTests long postId; { - var body = await client.TestPostAsync($"timelines/{generator(1)}/posts", TimelineHelper.TextPostCreateRequest("aaa")); + var body = await client.TestPostAsync($"timelines/{generator(1)}/posts", TimelineHelper.TextPostCreateRequest("aaa")); postId = body.Id; } @@ -726,12 +726,12 @@ namespace Timeline.Tests.IntegratedTests using var client = await CreateClientAsUser(); var postContentList = new List { "a", "b", "c", "d" }; - var posts = new List(); + var posts = new List(); foreach (var content in postContentList) { - var post = await client.TestPostAsync($"timelines/{generator(1)}/posts", - new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Text = content, Type = TimelinePostContentTypes.Text } }); + var post = await client.TestPostAsync($"timelines/{generator(1)}/posts", + new HttpTimelinePostCreateRequest { Content = new HttpTimelinePostCreateRequestContent { Text = content, Type = TimelinePostContentTypes.Text } }); posts.Add(post); await Task.Delay(1000); } @@ -739,7 +739,7 @@ namespace Timeline.Tests.IntegratedTests await client.TestDeleteAsync($"timelines/{generator(1)}/posts/{posts[2].Id}", true); { - var body = await client.TestGetAsync>($"timelines/{generator(1)}/posts?modifiedSince={posts[1].LastUpdated.ToString("s", CultureInfo.InvariantCulture) }"); + var body = await client.TestGetAsync>($"timelines/{generator(1)}/posts?modifiedSince={posts[1].LastUpdated.ToString("s", CultureInfo.InvariantCulture) }"); body.Should().HaveCount(2) .And.Subject.Select(p => p.Content!.Text).Should().Equal("b", "d"); } @@ -752,12 +752,12 @@ namespace Timeline.Tests.IntegratedTests using var client = await CreateClientAsUser(); var postContentList = new List { "a", "b", "c", "d" }; - var posts = new List(); + var posts = new List(); foreach (var content in postContentList) { - var body = await client.TestPostAsync($"timelines/{generator(1)}/posts", - new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Text = content, Type = TimelinePostContentTypes.Text } }); + var body = await client.TestPostAsync($"timelines/{generator(1)}/posts", + new HttpTimelinePostCreateRequest { Content = new HttpTimelinePostCreateRequestContent { Text = content, Type = TimelinePostContentTypes.Text } }); posts.Add(body); } @@ -767,7 +767,7 @@ namespace Timeline.Tests.IntegratedTests } { - posts = await client.TestGetAsync>($"timelines/{generator(1)}/posts?includeDeleted=true"); + posts = await client.TestGetAsync>($"timelines/{generator(1)}/posts?includeDeleted=true"); posts.Should().HaveCount(4); posts.Select(p => p.Deleted).Should().Equal(true, false, true, false); posts.Select(p => p.Content == null).Should().Equal(true, false, true, false); @@ -781,12 +781,12 @@ namespace Timeline.Tests.IntegratedTests using var client = await CreateClientAsUser(); var postContentList = new List { "a", "b", "c", "d" }; - var posts = new List(); + var posts = new List(); foreach (var (content, index) in postContentList.Select((v, i) => (v, i))) { - var post = await client.TestPostAsync($"timelines/{generator(1)}/posts", - new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Text = content, Type = TimelinePostContentTypes.Text } }); + var post = await client.TestPostAsync($"timelines/{generator(1)}/posts", + new HttpTimelinePostCreateRequest { Content = new HttpTimelinePostCreateRequestContent { Text = content, Type = TimelinePostContentTypes.Text } }); posts.Add(post); await Task.Delay(1000); } @@ -795,7 +795,7 @@ namespace Timeline.Tests.IntegratedTests { - posts = await client.TestGetAsync>($"timelines/{generator(1)}/posts?modifiedSince={posts[1].LastUpdated.ToString("s", CultureInfo.InvariantCulture)}&includeDeleted=true"); + posts = await client.TestGetAsync>($"timelines/{generator(1)}/posts?modifiedSince={posts[1].LastUpdated.ToString("s", CultureInfo.InvariantCulture)}&includeDeleted=true"); posts.Should().HaveCount(3); posts.Select(p => p.Deleted).Should().Equal(false, true, false); posts.Select(p => p.Content == null).Should().Equal(false, true, false); @@ -809,7 +809,7 @@ namespace Timeline.Tests.IntegratedTests using var client = await CreateClientAsUser(); DateTime lastModifiedTime; - TimelineInfo timeline; + HttpTimeline timeline; string uniqueId; { @@ -830,7 +830,7 @@ namespace Timeline.Tests.IntegratedTests { - var body = await client.TestGetAsync($"timelines/{generator(1)}", + var body = await client.TestGetAsync($"timelines/{generator(1)}", headerSetup: (headers, _) => { headers.IfModifiedSince = lastModifiedTime.AddSeconds(-1); @@ -843,7 +843,7 @@ namespace Timeline.Tests.IntegratedTests } { - var body = await client.TestGetAsync($"timelines/{generator(1)}?ifModifiedSince={lastModifiedTime.AddSeconds(-1).ToString("s", CultureInfo.InvariantCulture) }"); + var body = await client.TestGetAsync($"timelines/{generator(1)}?ifModifiedSince={lastModifiedTime.AddSeconds(-1).ToString("s", CultureInfo.InvariantCulture) }"); body.Should().BeEquivalentTo(timeline); } @@ -853,7 +853,7 @@ namespace Timeline.Tests.IntegratedTests { var testUniqueId = (uniqueId[0] == 'a' ? "b" : "a") + uniqueId[1..]; - var body = await client.TestGetAsync($"timelines/{generator(1)}?ifModifiedSince={lastModifiedTime.AddSeconds(1).ToString("s", CultureInfo.InvariantCulture) }&checkUniqueId={testUniqueId}"); + var body = await client.TestGetAsync($"timelines/{generator(1)}?ifModifiedSince={lastModifiedTime.AddSeconds(1).ToString("s", CultureInfo.InvariantCulture) }&checkUniqueId={testUniqueId}"); body.Should().BeEquivalentTo(timeline); } } @@ -870,7 +870,7 @@ namespace Timeline.Tests.IntegratedTests } { - var body = await client.PatchTimelineAsync(generator(1), new TimelinePatchRequest { Title = "atitle" }); + var body = await client.PatchTimelineAsync(generator(1), new HttpTimelinePatchRequest { Title = "atitle" }); body.Title.Should().Be("atitle"); } @@ -885,26 +885,26 @@ namespace Timeline.Tests.IntegratedTests { { using var client = await CreateDefaultClient(); - await client.TestPostAssertUnauthorizedAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "t1", NewName = "tttttttt" }); + await client.TestPostAssertUnauthorizedAsync("timelineop/changename", new HttpTimelineChangeNameRequest { OldName = "t1", NewName = "tttttttt" }); } { using var client = await CreateClientAs(2); - await client.TestPostAssertForbiddenAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "t1", NewName = "tttttttt" }); + await client.TestPostAssertForbiddenAsync("timelineop/changename", new HttpTimelineChangeNameRequest { OldName = "t1", NewName = "tttttttt" }); } using (var client = await CreateClientAsUser()) { - await client.TestPostAssertInvalidModelAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "!!!", NewName = "tttttttt" }); - await client.TestPostAssertInvalidModelAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "ttt", NewName = "!!!!" }); - await client.TestPostAssertErrorAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "ttttt", NewName = "tttttttt" }, errorCode: ErrorCodes.TimelineController.NotExist); + await client.TestPostAssertInvalidModelAsync("timelineop/changename", new HttpTimelineChangeNameRequest { OldName = "!!!", NewName = "tttttttt" }); + await client.TestPostAssertInvalidModelAsync("timelineop/changename", new HttpTimelineChangeNameRequest { OldName = "ttt", NewName = "!!!!" }); + await client.TestPostAssertErrorAsync("timelineop/changename", new HttpTimelineChangeNameRequest { OldName = "ttttt", NewName = "tttttttt" }, errorCode: ErrorCodes.TimelineController.NotExist); - await client.TestPostAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "t1", NewName = "newt" }); + await client.TestPostAsync("timelineop/changename", new HttpTimelineChangeNameRequest { OldName = "t1", NewName = "newt" }); await client.TestGetAsync("timelines/t1", expectedStatusCode: HttpStatusCode.NotFound); { - var body = await client.TestGetAsync("timelines/newt"); + var body = await client.TestGetAsync("timelines/newt"); body.Name.Should().Be("newt"); } } @@ -920,9 +920,9 @@ namespace Timeline.Tests.IntegratedTests string etag; { - var body = await client.TestPostAsync($"timelines/{generator(1)}/posts", new TimelinePostCreateRequest + var body = await client.TestPostAsync($"timelines/{generator(1)}/posts", new HttpTimelinePostCreateRequest { - Content = new TimelinePostCreateRequestContent + Content = new HttpTimelinePostCreateRequestContent { Type = TimelinePostContentTypes.Image, Data = Convert.ToBase64String(ImageHelper.CreatePngWithSize(100, 50)) diff --git a/BackEnd/Timeline.Tests/IntegratedTests/TokenTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/TokenTest.cs index a5208618..fdf1af99 100644 --- a/BackEnd/Timeline.Tests/IntegratedTests/TokenTest.cs +++ b/BackEnd/Timeline.Tests/IntegratedTests/TokenTest.cs @@ -14,9 +14,9 @@ namespace Timeline.Tests.IntegratedTests private const string CreateTokenUrl = "token/create"; private const string VerifyTokenUrl = "token/verify"; - private static async Task CreateUserTokenAsync(HttpClient client, string username, string password, int? expireOffset = null) + private static async Task CreateUserTokenAsync(HttpClient client, string username, string password, int? expireOffset = null) { - return await client.TestPostAsync(CreateTokenUrl, new CreateTokenRequest { Username = username, Password = password, Expire = expireOffset }); + return await client.TestPostAsync(CreateTokenUrl, new HttpCreateTokenRequest { Username = username, Password = password, Expire = expireOffset }); } public static IEnumerable CreateToken_InvalidModel_Data() @@ -32,7 +32,7 @@ namespace Timeline.Tests.IntegratedTests public async Task CreateToken_InvalidModel(string username, string password, int expire) { using var client = await CreateDefaultClient(); - await client.TestPostAssertInvalidModelAsync(CreateTokenUrl, new CreateTokenRequest + await client.TestPostAssertInvalidModelAsync(CreateTokenUrl, new HttpCreateTokenRequest { Username = username, Password = password, @@ -52,7 +52,7 @@ namespace Timeline.Tests.IntegratedTests { using var client = await CreateDefaultClient(); await client.TestPostAssertErrorAsync(CreateTokenUrl, - new CreateTokenRequest { Username = username, Password = password }, + new HttpCreateTokenRequest { Username = username, Password = password }, errorCode: ErrorCodes.TokenController.Create_BadCredential); } @@ -60,8 +60,8 @@ namespace Timeline.Tests.IntegratedTests public async Task CreateToken_Success() { using var client = await CreateDefaultClient(); - var body = await client.TestPostAsync(CreateTokenUrl, - new CreateTokenRequest { Username = "user1", Password = "user1pw" }); + var body = await client.TestPostAsync(CreateTokenUrl, + new HttpCreateTokenRequest { Username = "user1", Password = "user1pw" }); body.Token.Should().NotBeNullOrWhiteSpace(); body.User.Should().BeEquivalentTo(await client.GetUserAsync("user1")); } @@ -70,7 +70,7 @@ namespace Timeline.Tests.IntegratedTests public async Task VerifyToken_InvalidModel() { using var client = await CreateDefaultClient(); - await client.TestPostAssertInvalidModelAsync(VerifyTokenUrl, new VerifyTokenRequest { Token = null! }); + await client.TestPostAssertInvalidModelAsync(VerifyTokenUrl, new HttpVerifyTokenRequest { Token = null! }); } [Fact] @@ -78,7 +78,7 @@ namespace Timeline.Tests.IntegratedTests { using var client = await CreateDefaultClient(); await client.TestPostAssertErrorAsync(VerifyTokenUrl, - new VerifyTokenRequest { Token = "bad token hahaha" }, + new HttpVerifyTokenRequest { Token = "bad token hahaha" }, errorCode: ErrorCodes.TokenController.Verify_BadFormat); } @@ -97,7 +97,7 @@ namespace Timeline.Tests.IntegratedTests } await client.TestPostAssertErrorAsync(VerifyTokenUrl, - new VerifyTokenRequest { Token = token }, + new HttpVerifyTokenRequest { Token = token }, errorCode: ErrorCodes.TokenController.Verify_OldVersion); } @@ -114,7 +114,7 @@ namespace Timeline.Tests.IntegratedTests } await client.TestPostAssertErrorAsync(VerifyTokenUrl, - new VerifyTokenRequest { Token = token }, + new HttpVerifyTokenRequest { Token = token }, errorCode: ErrorCodes.TokenController.Verify_UserNotExist); } @@ -141,8 +141,8 @@ namespace Timeline.Tests.IntegratedTests { using var client = await CreateDefaultClient(); var createTokenResult = await CreateUserTokenAsync(client, "user1", "user1pw"); - var body = await client.TestPostAsync(VerifyTokenUrl, - new VerifyTokenRequest { Token = createTokenResult.Token }); + var body = await client.TestPostAsync(VerifyTokenUrl, + new HttpVerifyTokenRequest { Token = createTokenResult.Token }); body.User.Should().BeEquivalentTo(await client.GetUserAsync("user1")); } } diff --git a/BackEnd/Timeline.Tests/IntegratedTests/UserTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/UserTest.cs index e0ebf635..56dbf92a 100644 --- a/BackEnd/Timeline.Tests/IntegratedTests/UserTest.cs +++ b/BackEnd/Timeline.Tests/IntegratedTests/UserTest.cs @@ -12,7 +12,7 @@ namespace Timeline.Tests.IntegratedTests public async Task UserListShouldHaveUniqueId() { using var client = await CreateDefaultClient(); - foreach (var user in await client.TestGetAsync>("users")) + foreach (var user in await client.TestGetAsync>("users")) { user.UniqueId.Should().NotBeNullOrWhiteSpace(); } @@ -22,14 +22,14 @@ namespace Timeline.Tests.IntegratedTests public async Task GetList() { using var client = await CreateDefaultClient(); - await client.TestGetAsync>("users"); + await client.TestGetAsync>("users"); } [Fact] public async Task Get() { using var client = await CreateDefaultClient(); - var user = await client.TestGetAsync($"users/admin"); + var user = await client.TestGetAsync($"users/admin"); user.Username.Should().Be("admin"); user.Nickname.Should().Be("administrator"); user.UniqueId.Should().NotBeNullOrEmpty(); @@ -55,8 +55,8 @@ namespace Timeline.Tests.IntegratedTests { using var client = await CreateClientAsUser(); { - var body = await client.TestPatchAsync("users/user1", - new UserPatchRequest { Nickname = "aaa" }); + var body = await client.TestPatchAsync("users/user1", + new HttpUserPatchRequest { Nickname = "aaa" }); body.Nickname.Should().Be("aaa"); } @@ -73,8 +73,8 @@ namespace Timeline.Tests.IntegratedTests using var userClient = await CreateClientAsUser(); { - var body = await client.TestPatchAsync("users/user1", - new UserPatchRequest + var body = await client.TestPatchAsync("users/user1", + new HttpUserPatchRequest { Username = "newuser", Password = "newpw", @@ -91,7 +91,7 @@ namespace Timeline.Tests.IntegratedTests { var token = userClient.DefaultRequestHeaders.Authorization!.Parameter!; // Token should expire. - await userClient.TestPostAssertErrorAsync("token/verify", new VerifyTokenRequest() { Token = token }); + await userClient.TestPostAssertErrorAsync("token/verify", new HttpVerifyTokenRequest() { Token = token }); } { @@ -104,26 +104,26 @@ namespace Timeline.Tests.IntegratedTests public async Task Patch_NotExist() { using var client = await CreateClientAsAdministrator(); - await client.TestPatchAssertNotFoundAsync("users/usernotexist", new UserPatchRequest { }, errorCode: ErrorCodes.UserCommon.NotExist); + await client.TestPatchAssertNotFoundAsync("users/usernotexist", new HttpUserPatchRequest { }, errorCode: ErrorCodes.UserCommon.NotExist); } [Fact] public async Task Patch_InvalidModel() { using var client = await CreateClientAsAdministrator(); - await client.TestPatchAssertInvalidModelAsync("users/aaa!a", new UserPatchRequest { }); + await client.TestPatchAssertInvalidModelAsync("users/aaa!a", new HttpUserPatchRequest { }); } public static IEnumerable 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) } }; + yield return new[] { new HttpUserPatchRequest { Username = "aaa!a" } }; + yield return new[] { new HttpUserPatchRequest { Password = "" } }; + yield return new[] { new HttpUserPatchRequest { Nickname = new string('a', 50) } }; } [Theory] [MemberData(nameof(Patch_InvalidModel_Body_Data))] - public async Task Patch_InvalidModel_Body(UserPatchRequest body) + public async Task Patch_InvalidModel_Body(HttpUserPatchRequest body) { using var client = await CreateClientAsAdministrator(); await client.TestPatchAssertInvalidModelAsync("users/user1", body); @@ -133,35 +133,35 @@ namespace Timeline.Tests.IntegratedTests public async Task Patch_UsernameConflict() { using var client = await CreateClientAsAdministrator(); - await client.TestPatchAssertErrorAsync("users/user1", new UserPatchRequest { Username = "admin" }, errorCode: ErrorCodes.UserController.UsernameConflict); + await client.TestPatchAssertErrorAsync("users/user1", new HttpUserPatchRequest { Username = "admin" }, errorCode: ErrorCodes.UserController.UsernameConflict); } [Fact] public async Task Patch_NoAuth_Unauthorized() { using var client = await CreateDefaultClient(); - await client.TestPatchAssertUnauthorizedAsync("users/user1", new UserPatchRequest { Nickname = "aaa" }); + await client.TestPatchAssertUnauthorizedAsync("users/user1", new HttpUserPatchRequest { Nickname = "aaa" }); } [Fact] public async Task Patch_User_Forbid() { using var client = await CreateClientAsUser(); - await client.TestPatchAssertForbiddenAsync("users/admin", new UserPatchRequest { Nickname = "aaa" }); + await client.TestPatchAssertForbiddenAsync("users/admin", new HttpUserPatchRequest { Nickname = "aaa" }); } [Fact] public async Task Patch_Username_Forbid() { using var client = await CreateClientAsUser(); - await client.TestPatchAssertForbiddenAsync("users/user1", new UserPatchRequest { Username = "aaa" }); + await client.TestPatchAssertForbiddenAsync("users/user1", new HttpUserPatchRequest { Username = "aaa" }); } [Fact] public async Task Patch_Password_Forbid() { using var client = await CreateClientAsUser(); - await client.TestPatchAssertForbiddenAsync("users/user1", new UserPatchRequest { Password = "aaa" }); + await client.TestPatchAssertForbiddenAsync("users/user1", new HttpUserPatchRequest { Password = "aaa" }); } [Fact] @@ -214,7 +214,7 @@ namespace Timeline.Tests.IntegratedTests { using var client = await CreateClientAsAdministrator(); { - var body = await client.TestPostAsync(createUserUrl, new CreateUserRequest + var body = await client.TestPostAsync(createUserUrl, new HttpCreateUserRequest { Username = "aaa", Password = "bbb", @@ -233,15 +233,15 @@ namespace Timeline.Tests.IntegratedTests public static IEnumerable Op_CreateUser_InvalidModel_Data() { - yield return new[] { new CreateUserRequest { Username = "aaa" } }; - yield return new[] { new CreateUserRequest { Password = "bbb" } }; - yield return new[] { new CreateUserRequest { Username = "a!a", Password = "bbb" } }; - yield return new[] { new CreateUserRequest { Username = "aaa", Password = "" } }; + yield return new[] { new HttpCreateUserRequest { Username = "aaa" } }; + yield return new[] { new HttpCreateUserRequest { Password = "bbb" } }; + yield return new[] { new HttpCreateUserRequest { Username = "a!a", Password = "bbb" } }; + yield return new[] { new HttpCreateUserRequest { Username = "aaa", Password = "" } }; } [Theory] [MemberData(nameof(Op_CreateUser_InvalidModel_Data))] - public async Task Op_CreateUser_InvalidModel(CreateUserRequest body) + public async Task Op_CreateUser_InvalidModel(HttpCreateUserRequest body) { using var client = await CreateClientAsAdministrator(); await client.TestPostAssertInvalidModelAsync(createUserUrl, body); @@ -251,7 +251,7 @@ namespace Timeline.Tests.IntegratedTests public async Task Op_CreateUser_UsernameConflict() { using var client = await CreateClientAsAdministrator(); - await client.TestPostAssertErrorAsync(createUserUrl, new CreateUserRequest + await client.TestPostAssertErrorAsync(createUserUrl, new HttpCreateUserRequest { Username = "user1", Password = "bbb", @@ -262,7 +262,7 @@ namespace Timeline.Tests.IntegratedTests public async Task Op_CreateUser_NoAuth_Unauthorized() { using var client = await CreateDefaultClient(); - await client.TestPostAssertUnauthorizedAsync(createUserUrl, new CreateUserRequest + await client.TestPostAssertUnauthorizedAsync(createUserUrl, new HttpCreateUserRequest { Username = "aaa", Password = "bbb", @@ -273,7 +273,7 @@ namespace Timeline.Tests.IntegratedTests public async Task Op_CreateUser_User_Forbid() { using var client = await CreateClientAsUser(); - await client.TestPostAssertForbiddenAsync(createUserUrl, new CreateUserRequest + await client.TestPostAssertForbiddenAsync(createUserUrl, new HttpCreateUserRequest { Username = "aaa", Password = "bbb", @@ -286,8 +286,8 @@ namespace Timeline.Tests.IntegratedTests public async Task Op_ChangePassword() { using var client = await CreateClientAsUser(); - await client.TestPostAsync(changePasswordUrl, new ChangePasswordRequest { OldPassword = "user1pw", NewPassword = "newpw" }); - await client.TestPatchAssertUnauthorizedAsync("users/user1", new UserPatchRequest { }); + await client.TestPostAsync(changePasswordUrl, new HttpChangePasswordRequest { OldPassword = "user1pw", NewPassword = "newpw" }); + await client.TestPatchAssertUnauthorizedAsync("users/user1", new HttpUserPatchRequest { }); (await CreateClientWithCredential("user1", "newpw")).Dispose(); } @@ -303,21 +303,21 @@ namespace Timeline.Tests.IntegratedTests { using var client = await CreateClientAsUser(); await client.TestPostAssertInvalidModelAsync(changePasswordUrl, - new ChangePasswordRequest { OldPassword = oldPassword, NewPassword = newPassword }); + new HttpChangePasswordRequest { OldPassword = oldPassword, NewPassword = newPassword }); } [Fact] public async Task Op_ChangePassword_BadOldPassword() { using var client = await CreateClientAsUser(); - await client.TestPostAssertErrorAsync(changePasswordUrl, new ChangePasswordRequest { OldPassword = "???", NewPassword = "???" }, errorCode: ErrorCodes.UserController.ChangePassword_BadOldPassword); + await client.TestPostAssertErrorAsync(changePasswordUrl, new HttpChangePasswordRequest { OldPassword = "???", NewPassword = "???" }, errorCode: ErrorCodes.UserController.ChangePassword_BadOldPassword); } [Fact] public async Task Op_ChangePassword_NoAuth_Unauthorized() { using var client = await CreateDefaultClient(); - await client.TestPostAssertUnauthorizedAsync(changePasswordUrl, new ChangePasswordRequest { OldPassword = "???", NewPassword = "???" }); + await client.TestPostAssertUnauthorizedAsync(changePasswordUrl, new HttpChangePasswordRequest { OldPassword = "???", NewPassword = "???" }); } } } diff --git a/BackEnd/Timeline.Tests/Services/TimelineServiceTest.cs b/BackEnd/Timeline.Tests/Services/TimelineServiceTest.cs index fac0b6f3..98f03066 100644 --- a/BackEnd/Timeline.Tests/Services/TimelineServiceTest.cs +++ b/BackEnd/Timeline.Tests/Services/TimelineServiceTest.cs @@ -67,7 +67,7 @@ namespace Timeline.Tests.Services { var initTime = _clock.ForwardCurrentTime(); - void Check(Models.Timeline timeline) + void Check(TimelineInfo timeline) { timeline.NameLastModified.Should().Be(initTime); timeline.LastModified.Should().Be(_clock.GetCurrentTime()); diff --git a/BackEnd/Timeline/Controllers/TimelineController.cs b/BackEnd/Timeline/Controllers/TimelineController.cs index 0ffadc50..27b4b7a7 100644 --- a/BackEnd/Timeline/Controllers/TimelineController.cs +++ b/BackEnd/Timeline/Controllers/TimelineController.cs @@ -57,7 +57,7 @@ namespace Timeline.Controllers [HttpGet("timelines")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] - public async Task>> TimelineList([FromQuery][Username] string? relate, [FromQuery][RegularExpression("(own)|(join)")] string? relateType, [FromQuery] string? visibility) + public async Task>> TimelineList([FromQuery][Username] string? relate, [FromQuery][RegularExpression("(own)|(join)")] string? relateType, [FromQuery] string? visibility) { List? visibilityFilter = null; if (visibility != null) @@ -109,7 +109,7 @@ namespace Timeline.Controllers } var timelines = await _service.GetTimelines(relationship, visibilityFilter); - var result = _mapper.Map>(timelines); + var result = _mapper.Map>(timelines); return result; } @@ -125,7 +125,7 @@ namespace Timeline.Controllers [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status304NotModified)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task> TimelineGet([FromRoute][GeneralTimelineName] string name, [FromQuery] string? checkUniqueId, [FromQuery(Name = "ifModifiedSince")] DateTime? queryIfModifiedSince, [FromHeader(Name = "If-Modified-Since")] DateTime? headerIfModifiedSince) + public async Task> TimelineGet([FromRoute][GeneralTimelineName] string name, [FromQuery] string? checkUniqueId, [FromQuery(Name = "ifModifiedSince")] DateTime? queryIfModifiedSince, [FromHeader(Name = "If-Modified-Since")] DateTime? headerIfModifiedSince) { DateTime? ifModifiedSince = null; if (queryIfModifiedSince.HasValue) @@ -166,7 +166,7 @@ namespace Timeline.Controllers else { var timeline = await _service.GetTimeline(name); - var result = _mapper.Map(timeline); + var result = _mapper.Map(timeline); return result; } } @@ -182,16 +182,16 @@ namespace Timeline.Controllers [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> PostListGet([FromRoute][GeneralTimelineName] string name, [FromQuery] DateTime? modifiedSince, [FromQuery] bool? includeDeleted) + public async Task>> PostListGet([FromRoute][GeneralTimelineName] string name, [FromQuery] DateTime? modifiedSince, [FromQuery] bool? includeDeleted) { if (!UserHasAllTimelineManagementPermission && !await _service.HasReadPermission(name, this.GetOptionalUserId())) { return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } - List posts = await _postService.GetPosts(name, modifiedSince, includeDeleted ?? false); + List posts = await _postService.GetPosts(name, modifiedSince, includeDeleted ?? false); - var result = _mapper.Map>(posts); + var result = _mapper.Map>(posts); return result; } @@ -247,7 +247,7 @@ namespace Timeline.Controllers [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task> PostPost([FromRoute][GeneralTimelineName] string name, [FromBody] TimelinePostCreateRequest body) + public async Task> PostPost([FromRoute][GeneralTimelineName] string name, [FromBody] HttpTimelinePostCreateRequest body) { var id = this.GetUserId(); if (!UserHasAllTimelineManagementPermission && !await _service.IsMemberOf(name, id)) @@ -257,7 +257,7 @@ namespace Timeline.Controllers var content = body.Content; - TimelinePost post; + TimelinePostInfo post; if (content.Type == TimelinePostContentTypes.Text) { @@ -299,7 +299,7 @@ namespace Timeline.Controllers return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ContentUnknownType)); } - var result = _mapper.Map(post); + var result = _mapper.Map(post); return result; } @@ -344,7 +344,7 @@ namespace Timeline.Controllers [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task> TimelinePatch([FromRoute][GeneralTimelineName] string name, [FromBody] TimelinePatchRequest body) + public async Task> TimelinePatch([FromRoute][GeneralTimelineName] string name, [FromBody] HttpTimelinePatchRequest body) { if (!UserHasAllTimelineManagementPermission && !(await _service.HasManagePermission(name, this.GetUserId()))) { @@ -352,7 +352,7 @@ namespace Timeline.Controllers } await _service.ChangeProperty(name, _mapper.Map(body)); var timeline = await _service.GetTimeline(name); - var result = _mapper.Map(timeline); + var result = _mapper.Map(timeline); return result; } @@ -423,14 +423,14 @@ namespace Timeline.Controllers [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] - public async Task> TimelineCreate([FromBody] TimelineCreateRequest body) + public async Task> TimelineCreate([FromBody] TimelineCreateRequest body) { var userId = this.GetUserId(); try { var timeline = await _service.CreateTimeline(body.Name, userId); - var result = _mapper.Map(timeline); + var result = _mapper.Map(timeline); return result; } catch (EntityAlreadyExistException e) when (e.EntityName == EntityNames.Timeline) @@ -474,7 +474,7 @@ namespace Timeline.Controllers [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task> TimelineOpChangeName([FromBody] TimelineChangeNameRequest body) + public async Task> TimelineOpChangeName([FromBody] HttpTimelineChangeNameRequest body) { if (!UserHasAllTimelineManagementPermission && !(await _service.HasManagePermission(body.OldName, this.GetUserId()))) { @@ -484,7 +484,7 @@ namespace Timeline.Controllers try { var timeline = await _service.ChangeTimelineName(body.OldName, body.NewName); - return Ok(_mapper.Map(timeline)); + return Ok(_mapper.Map(timeline)); } catch (EntityAlreadyExistException) { diff --git a/BackEnd/Timeline/Controllers/TokenController.cs b/BackEnd/Timeline/Controllers/TokenController.cs index 41ec21e6..c801b8cc 100644 --- a/BackEnd/Timeline/Controllers/TokenController.cs +++ b/BackEnd/Timeline/Controllers/TokenController.cs @@ -47,7 +47,7 @@ namespace Timeline.Controllers [AllowAnonymous] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] - public async Task> Create([FromBody] CreateTokenRequest request) + public async Task> Create([FromBody] HttpCreateTokenRequest request) { void LogFailure(string reason, Exception? e = null) { @@ -71,10 +71,10 @@ namespace Timeline.Controllers ("Username", request.Username), ("Expire At", expireTime?.ToString(CultureInfo.CurrentCulture.DateTimeFormat) ?? "default") )); - return Ok(new CreateTokenResponse + return Ok(new HttpCreateTokenResponse { Token = result.Token, - User = _mapper.Map(result.User) + User = _mapper.Map(result.User) }); } catch (UserNotExistException e) @@ -97,7 +97,7 @@ namespace Timeline.Controllers [AllowAnonymous] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] - public async Task> Verify([FromBody] VerifyTokenRequest request) + public async Task> Verify([FromBody] HttpVerifyTokenRequest request) { void LogFailure(string reason, Exception? e = null, params (string, object?)[] otherProperties) { @@ -113,9 +113,9 @@ namespace Timeline.Controllers var result = await _userTokenManager.VerifyToken(request.Token); _logger.LogInformation(Log.Format(LogVerifySuccess, ("Username", result.Username), ("Token", request.Token))); - return Ok(new VerifyTokenResponse + return Ok(new HttpVerifyTokenResponse { - User = _mapper.Map(result) + User = _mapper.Map(result) }); } catch (UserTokenTimeExpireException e) diff --git a/BackEnd/Timeline/Controllers/UserController.cs b/BackEnd/Timeline/Controllers/UserController.cs index 626a116f..3727da36 100644 --- a/BackEnd/Timeline/Controllers/UserController.cs +++ b/BackEnd/Timeline/Controllers/UserController.cs @@ -42,7 +42,7 @@ namespace Timeline.Controllers _mapper = mapper; } - private UserInfo ConvertToUserInfo(User user) => _mapper.Map(user); + private HttpUser ConvertToUserInfo(UserInfo user) => _mapper.Map(user); private bool UserHasUserManagementPermission => this.UserHasPermission(UserPermission.UserManagement); @@ -52,7 +52,7 @@ namespace Timeline.Controllers /// All user list. [HttpGet("users")] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> List() + public async Task> List() { var users = await _userService.GetUsers(); var result = users.Select(u => ConvertToUserInfo(u)).ToArray(); @@ -67,7 +67,7 @@ namespace Timeline.Controllers [HttpGet("users/{username}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task> Get([FromRoute][Username] string username) + public async Task> Get([FromRoute][Username] string username) { try { @@ -94,7 +94,7 @@ namespace Timeline.Controllers [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task> Patch([FromBody] UserPatchRequest body, [FromRoute][Username] string username) + public async Task> Patch([FromBody] HttpUserPatchRequest body, [FromRoute][Username] string username) { if (UserHasUserManagementPermission) { @@ -168,7 +168,7 @@ namespace Timeline.Controllers [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task> CreateUser([FromBody] CreateUserRequest body) + public async Task> CreateUser([FromBody] HttpCreateUserRequest body) { try { @@ -188,7 +188,7 @@ namespace Timeline.Controllers [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] - public async Task ChangePassword([FromBody] ChangePasswordRequest request) + public async Task ChangePassword([FromBody] HttpChangePasswordRequest request) { try { diff --git a/BackEnd/Timeline/Models/Http/Timeline.cs b/BackEnd/Timeline/Models/Http/Timeline.cs index a81b33f5..8e3831e1 100644 --- a/BackEnd/Timeline/Models/Http/Timeline.cs +++ b/BackEnd/Timeline/Models/Http/Timeline.cs @@ -11,7 +11,7 @@ namespace Timeline.Models.Http /// /// Info of post content. /// - public class TimelinePostContentInfo + public class HttpTimelinePostContent { /// /// Type of the post content. @@ -34,7 +34,7 @@ namespace Timeline.Models.Http /// /// Info of a post. /// - public class TimelinePostInfo + public class HttpTimelinePost { /// /// Post id. @@ -43,7 +43,7 @@ namespace Timeline.Models.Http /// /// Content of the post. May be null if post is deleted. /// - public TimelinePostContentInfo? Content { get; set; } + public HttpTimelinePostContent? Content { get; set; } /// /// True if post is deleted. /// @@ -55,7 +55,7 @@ namespace Timeline.Models.Http /// /// The author. May be null if the user has been deleted. /// - public UserInfo? Author { get; set; } = default!; + public HttpUser? Author { get; set; } = default!; /// /// Last updated time. /// @@ -65,7 +65,7 @@ namespace Timeline.Models.Http /// /// Info of a timeline. /// - public class TimelineInfo + public class HttpTimeline { /// /// Unique id. @@ -90,7 +90,7 @@ namespace Timeline.Models.Http /// /// Owner of the timeline. /// - public UserInfo Owner { get; set; } = default!; + public HttpUser Owner { get; set; } = default!; /// /// Visibility of the timeline. /// @@ -99,7 +99,7 @@ namespace Timeline.Models.Http /// /// Members of timeline. /// - public List Members { get; set; } = default!; + public List Members { get; set; } = default!; #pragma warning restore CA2227 // Collection properties should be read only /// /// Create time of timeline. @@ -114,14 +114,14 @@ namespace Timeline.Models.Http /// /// Related links. /// - public TimelineInfoLinks _links { get; set; } = default!; + public HttpTimelineLinks _links { get; set; } = default!; #pragma warning restore CA1707 // Identifiers should not contain underscores } /// /// Related links for timeline. /// - public class TimelineInfoLinks + public class HttpTimelineLinks { /// /// Self. @@ -133,23 +133,23 @@ namespace Timeline.Models.Http public string Posts { get; set; } = default!; } - public class TimelineInfoLinksValueResolver : IValueResolver + public class HttpTimelineLinksValueResolver : IValueResolver { private readonly IActionContextAccessor _actionContextAccessor; private readonly IUrlHelperFactory _urlHelperFactory; - public TimelineInfoLinksValueResolver(IActionContextAccessor actionContextAccessor, IUrlHelperFactory urlHelperFactory) + public HttpTimelineLinksValueResolver(IActionContextAccessor actionContextAccessor, IUrlHelperFactory urlHelperFactory) { _actionContextAccessor = actionContextAccessor; _urlHelperFactory = urlHelperFactory; } - public TimelineInfoLinks Resolve(Timeline source, TimelineInfo destination, TimelineInfoLinks destMember, ResolutionContext context) + public HttpTimelineLinks Resolve(TimelineInfo source, HttpTimeline destination, HttpTimelineLinks destMember, ResolutionContext context) { var actionContext = _actionContextAccessor.AssertActionContextForUrlFill(); var urlHelper = _urlHelperFactory.GetUrlHelper(actionContext); - return new TimelineInfoLinks + return new HttpTimelineLinks { Self = urlHelper.ActionLink(nameof(TimelineController.TimelineGet), nameof(TimelineController)[0..^nameof(Controller).Length], new { source.Name }), Posts = urlHelper.ActionLink(nameof(TimelineController.PostListGet), nameof(TimelineController)[0..^nameof(Controller).Length], new { source.Name }) @@ -157,18 +157,18 @@ namespace Timeline.Models.Http } } - public class TimelinePostContentResolver : IValueResolver + public class HttpTimelinePostContentResolver : IValueResolver { private readonly IActionContextAccessor _actionContextAccessor; private readonly IUrlHelperFactory _urlHelperFactory; - public TimelinePostContentResolver(IActionContextAccessor actionContextAccessor, IUrlHelperFactory urlHelperFactory) + public HttpTimelinePostContentResolver(IActionContextAccessor actionContextAccessor, IUrlHelperFactory urlHelperFactory) { _actionContextAccessor = actionContextAccessor; _urlHelperFactory = urlHelperFactory; } - public TimelinePostContentInfo? Resolve(TimelinePost source, TimelinePostInfo destination, TimelinePostContentInfo? destMember, ResolutionContext context) + public HttpTimelinePostContent? Resolve(TimelinePostInfo source, HttpTimelinePost destination, HttpTimelinePostContent? destMember, ResolutionContext context) { var actionContext = _actionContextAccessor.AssertActionContextForUrlFill(); var urlHelper = _urlHelperFactory.GetUrlHelper(actionContext); @@ -182,7 +182,7 @@ namespace Timeline.Models.Http if (sourceContent is TextTimelinePostContent textContent) { - return new TimelinePostContentInfo + return new HttpTimelinePostContent { Type = TimelinePostContentTypes.Text, Text = textContent.Text @@ -190,7 +190,7 @@ namespace Timeline.Models.Http } else if (sourceContent is ImageTimelinePostContent imageContent) { - return new TimelinePostContentInfo + return new HttpTimelinePostContent { Type = TimelinePostContentTypes.Image, Url = urlHelper.ActionLink( @@ -207,13 +207,12 @@ namespace Timeline.Models.Http } } - public class TimelineInfoAutoMapperProfile : Profile + public class HttpTimelineAutoMapperProfile : Profile { - public TimelineInfoAutoMapperProfile() + public HttpTimelineAutoMapperProfile() { - CreateMap().ForMember(u => u._links, opt => opt.MapFrom()); - CreateMap().ForMember(p => p.Content, opt => opt.MapFrom()); - CreateMap(); + CreateMap().ForMember(u => u._links, opt => opt.MapFrom()); + CreateMap().ForMember(p => p.Content, opt => opt.MapFrom()); } } } diff --git a/BackEnd/Timeline/Models/Http/TimelineController.cs b/BackEnd/Timeline/Models/Http/TimelineController.cs index 7bd141ed..42a926fd 100644 --- a/BackEnd/Timeline/Models/Http/TimelineController.cs +++ b/BackEnd/Timeline/Models/Http/TimelineController.cs @@ -1,4 +1,5 @@ -using System; +using AutoMapper; +using System; using System.ComponentModel.DataAnnotations; using Timeline.Models.Validation; @@ -7,7 +8,7 @@ namespace Timeline.Models.Http /// /// Content of post create request. /// - public class TimelinePostCreateRequestContent + public class HttpTimelinePostCreateRequestContent { /// /// Type of post content. @@ -24,13 +25,13 @@ namespace Timeline.Models.Http public string? Data { get; set; } } - public class TimelinePostCreateRequest + public class HttpTimelinePostCreateRequest { /// /// Content of the new post. /// [Required] - public TimelinePostCreateRequestContent Content { get; set; } = default!; + public HttpTimelinePostCreateRequestContent Content { get; set; } = default!; /// /// Time of the post. If not set, current time will be used. @@ -54,7 +55,7 @@ namespace Timeline.Models.Http /// /// Patch timeline request model. /// - public class TimelinePatchRequest + public class HttpTimelinePatchRequest { /// /// New title. Null for not change. @@ -75,7 +76,7 @@ namespace Timeline.Models.Http /// /// Change timeline name request model. /// - public class TimelineChangeNameRequest + public class HttpTimelineChangeNameRequest { /// /// Old name of timeline. @@ -90,4 +91,12 @@ namespace Timeline.Models.Http [TimelineName] public string NewName { get; set; } = default!; } + + public class HttpTimelineControllerAutoMapperProfile : Profile + { + public HttpTimelineControllerAutoMapperProfile() + { + CreateMap(); + } + } } diff --git a/BackEnd/Timeline/Models/Http/TokenController.cs b/BackEnd/Timeline/Models/Http/TokenController.cs index a42c44e5..a5cbba14 100644 --- a/BackEnd/Timeline/Models/Http/TokenController.cs +++ b/BackEnd/Timeline/Models/Http/TokenController.cs @@ -4,9 +4,9 @@ using Timeline.Controllers; namespace Timeline.Models.Http { /// - /// Request model for . + /// Request model for . /// - public class CreateTokenRequest + public class HttpCreateTokenRequest { /// /// The username. @@ -24,9 +24,9 @@ namespace Timeline.Models.Http } /// - /// Response model for . + /// Response model for . /// - public class CreateTokenResponse + public class HttpCreateTokenResponse { /// /// The token created. @@ -35,13 +35,13 @@ namespace Timeline.Models.Http /// /// The user owning the token. /// - public UserInfo User { get; set; } = default!; + public HttpUser User { get; set; } = default!; } /// - /// Request model for . + /// Request model for . /// - public class VerifyTokenRequest + public class HttpVerifyTokenRequest { /// /// The token to verify. @@ -50,13 +50,13 @@ namespace Timeline.Models.Http } /// - /// Response model for . + /// Response model for . /// - public class VerifyTokenResponse + public class HttpVerifyTokenResponse { /// /// The user owning the token. /// - public UserInfo User { get; set; } = default!; + public HttpUser User { get; set; } = default!; } } diff --git a/BackEnd/Timeline/Models/Http/User.cs b/BackEnd/Timeline/Models/Http/User.cs new file mode 100644 index 00000000..bdb40b9f --- /dev/null +++ b/BackEnd/Timeline/Models/Http/User.cs @@ -0,0 +1,105 @@ +using AutoMapper; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using System.Collections.Generic; +using Timeline.Controllers; +using Timeline.Services; + +namespace Timeline.Models.Http +{ + /// + /// Info of a user. + /// + public class HttpUser + { + /// + /// Unique id. + /// + public string UniqueId { get; set; } = default!; + /// + /// Username. + /// + public string Username { get; set; } = default!; + /// + /// Nickname. + /// + public string Nickname { get; set; } = default!; +#pragma warning disable CA2227 // Collection properties should be read only + /// + /// The permissions of the user. + /// + public List Permissions { get; set; } = default!; +#pragma warning restore CA2227 // Collection properties should be read only +#pragma warning disable CA1707 // Identifiers should not contain underscores + /// + /// Related links. + /// + public HttpUserLinks _links { get; set; } = default!; +#pragma warning restore CA1707 // Identifiers should not contain underscores + } + + /// + /// Related links for user. + /// + public class HttpUserLinks + { + /// + /// Self. + /// + public string Self { get; set; } = default!; + /// + /// Avatar url. + /// + public string Avatar { get; set; } = default!; + /// + /// Personal timeline url. + /// + public string Timeline { get; set; } = default!; + } + + public class HttpUserPermissionsValueConverter : ITypeConverter> + { + public List Convert(UserPermissions source, List destination, ResolutionContext context) + { + return source.ToStringList(); + } + } + + public class HttpUserLinksValueResolver : IValueResolver + { + private readonly IActionContextAccessor _actionContextAccessor; + private readonly IUrlHelperFactory _urlHelperFactory; + + public HttpUserLinksValueResolver(IActionContextAccessor actionContextAccessor, IUrlHelperFactory urlHelperFactory) + { + _actionContextAccessor = actionContextAccessor; + _urlHelperFactory = urlHelperFactory; + } + + public HttpUserLinks Resolve(UserInfo source, HttpUser destination, HttpUserLinks destMember, ResolutionContext context) + { + var actionContext = _actionContextAccessor.AssertActionContextForUrlFill(); + var urlHelper = _urlHelperFactory.GetUrlHelper(actionContext); + + var result = new HttpUserLinks + { + Self = urlHelper.ActionLink(nameof(UserController.Get), nameof(UserController)[0..^nameof(Controller).Length], new { destination.Username }), + Avatar = urlHelper.ActionLink(nameof(UserAvatarController.Get), nameof(UserAvatarController)[0..^nameof(Controller).Length], new { destination.Username }), + Timeline = urlHelper.ActionLink(nameof(TimelineController.TimelineGet), nameof(TimelineController)[0..^nameof(Controller).Length], new { Name = "@" + destination.Username }) + }; + return result; + } + } + + public class HttpUserAutoMapperProfile : Profile + { + public HttpUserAutoMapperProfile() + { + CreateMap>() + .ConvertUsing(); + CreateMap() + .ForMember(u => u._links, opt => opt.MapFrom()); + } + } +} diff --git a/BackEnd/Timeline/Models/Http/UserController.cs b/BackEnd/Timeline/Models/Http/UserController.cs index 92a63874..1b4d09ec 100644 --- a/BackEnd/Timeline/Models/Http/UserController.cs +++ b/BackEnd/Timeline/Models/Http/UserController.cs @@ -7,9 +7,9 @@ using Timeline.Services; namespace Timeline.Models.Http { /// - /// Request model for . + /// Request model for . /// - public class UserPatchRequest + public class HttpUserPatchRequest { /// /// New username. Null if not change. Need to be administrator. @@ -31,9 +31,9 @@ namespace Timeline.Models.Http } /// - /// Request model for . + /// Request model for . /// - public class CreateUserRequest + public class HttpCreateUserRequest { /// /// Username of the new user. @@ -49,9 +49,9 @@ namespace Timeline.Models.Http } /// - /// Request model for . + /// Request model for . /// - public class ChangePasswordRequest + public class HttpChangePasswordRequest { /// /// Old password. @@ -66,11 +66,11 @@ namespace Timeline.Models.Http public string NewPassword { get; set; } = default!; } - public class UserControllerAutoMapperProfile : Profile + public class HttpUserControllerModelAutoMapperProfile : Profile { - public UserControllerAutoMapperProfile() + public HttpUserControllerModelAutoMapperProfile() { - CreateMap(); + CreateMap(); } } } diff --git a/BackEnd/Timeline/Models/Http/UserInfo.cs b/BackEnd/Timeline/Models/Http/UserInfo.cs deleted file mode 100644 index 0f865172..00000000 --- a/BackEnd/Timeline/Models/Http/UserInfo.cs +++ /dev/null @@ -1,105 +0,0 @@ -using AutoMapper; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.AspNetCore.Mvc.Routing; -using System.Collections.Generic; -using Timeline.Controllers; -using Timeline.Services; - -namespace Timeline.Models.Http -{ - /// - /// Info of a user. - /// - public class UserInfo - { - /// - /// Unique id. - /// - public string UniqueId { get; set; } = default!; - /// - /// Username. - /// - public string Username { get; set; } = default!; - /// - /// Nickname. - /// - public string Nickname { get; set; } = default!; -#pragma warning disable CA2227 // Collection properties should be read only - /// - /// The permissions of the user. - /// - public List Permissions { get; set; } = default!; -#pragma warning restore CA2227 // Collection properties should be read only -#pragma warning disable CA1707 // Identifiers should not contain underscores - /// - /// Related links. - /// - public UserInfoLinks _links { get; set; } = default!; -#pragma warning restore CA1707 // Identifiers should not contain underscores - } - - /// - /// Related links for user. - /// - public class UserInfoLinks - { - /// - /// Self. - /// - public string Self { get; set; } = default!; - /// - /// Avatar url. - /// - public string Avatar { get; set; } = default!; - /// - /// Personal timeline url. - /// - public string Timeline { get; set; } = default!; - } - - public class UserPermissionsValueConverter : ITypeConverter> - { - public List Convert(UserPermissions source, List destination, ResolutionContext context) - { - return source.ToStringList(); - } - } - - public class UserInfoLinksValueResolver : IValueResolver - { - private readonly IActionContextAccessor _actionContextAccessor; - private readonly IUrlHelperFactory _urlHelperFactory; - - public UserInfoLinksValueResolver(IActionContextAccessor actionContextAccessor, IUrlHelperFactory urlHelperFactory) - { - _actionContextAccessor = actionContextAccessor; - _urlHelperFactory = urlHelperFactory; - } - - public UserInfoLinks Resolve(User source, UserInfo destination, UserInfoLinks destMember, ResolutionContext context) - { - var actionContext = _actionContextAccessor.AssertActionContextForUrlFill(); - var urlHelper = _urlHelperFactory.GetUrlHelper(actionContext); - - var result = new UserInfoLinks - { - Self = urlHelper.ActionLink(nameof(UserController.Get), nameof(UserController)[0..^nameof(Controller).Length], new { destination.Username }), - Avatar = urlHelper.ActionLink(nameof(UserAvatarController.Get), nameof(UserAvatarController)[0..^nameof(Controller).Length], new { destination.Username }), - Timeline = urlHelper.ActionLink(nameof(TimelineController.TimelineGet), nameof(TimelineController)[0..^nameof(Controller).Length], new { Name = "@" + destination.Username }) - }; - return result; - } - } - - public class UserInfoAutoMapperProfile : Profile - { - public UserInfoAutoMapperProfile() - { - CreateMap>() - .ConvertUsing(); - CreateMap() - .ForMember(u => u._links, opt => opt.MapFrom()); - } - } -} diff --git a/BackEnd/Timeline/Models/Timeline.cs b/BackEnd/Timeline/Models/Timeline.cs deleted file mode 100644 index a5987577..00000000 --- a/BackEnd/Timeline/Models/Timeline.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Timeline.Models -{ - public enum TimelineVisibility - { - /// - /// All people including those without accounts. - /// - Public, - /// - /// Only people signed in. - /// - Register, - /// - /// Only member. - /// - Private - } - - public static class TimelinePostContentTypes - { - public const string Text = "text"; - public const string Image = "image"; - } - - public interface ITimelinePostContent - { - public string Type { get; } - } - - public class TextTimelinePostContent : ITimelinePostContent - { - public TextTimelinePostContent(string text) { Text = text; } - - public string Type { get; } = TimelinePostContentTypes.Text; - public string Text { get; set; } - } - - public class ImageTimelinePostContent : ITimelinePostContent - { - public ImageTimelinePostContent(string dataTag) { DataTag = dataTag; } - - public string Type { get; } = TimelinePostContentTypes.Image; - - /// - /// The tag of the data. The tag of the entry in DataManager. Also the etag (not quoted). - /// - public string DataTag { get; set; } - } - - public class TimelinePost - { - public TimelinePost(long id, ITimelinePostContent? content, DateTime time, User? author, DateTime lastUpdated, string timelineName) - { - Id = id; - Content = content; - Time = time; - Author = author; - LastUpdated = lastUpdated; - TimelineName = timelineName; - } - - public long Id { get; set; } - public ITimelinePostContent? Content { get; set; } - public bool Deleted => Content == null; - public DateTime Time { get; set; } - public User? Author { get; set; } - public DateTime LastUpdated { get; set; } - public string TimelineName { get; set; } - } - -#pragma warning disable CA1724 // Type names should not match namespaces - public class Timeline -#pragma warning restore CA1724 // Type names should not match namespaces - { - public string UniqueID { get; set; } = default!; - public string Name { get; set; } = default!; - public DateTime NameLastModified { get; set; } = default!; - public string Title { get; set; } = default!; - public string Description { get; set; } = default!; - public User Owner { get; set; } = default!; - public TimelineVisibility Visibility { get; set; } -#pragma warning disable CA2227 // Collection properties should be read only - public List Members { get; set; } = default!; -#pragma warning restore CA2227 // Collection properties should be read only - public DateTime CreateTime { get; set; } = default!; - public DateTime LastModified { get; set; } = default!; - } - - public class TimelineChangePropertyRequest - { - public string? Title { get; set; } - public string? Description { get; set; } - public TimelineVisibility? Visibility { get; set; } - } -} diff --git a/BackEnd/Timeline/Models/TimelineInfo.cs b/BackEnd/Timeline/Models/TimelineInfo.cs new file mode 100644 index 00000000..440f6b81 --- /dev/null +++ b/BackEnd/Timeline/Models/TimelineInfo.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; + +namespace Timeline.Models +{ + public enum TimelineVisibility + { + /// + /// All people including those without accounts. + /// + Public, + /// + /// Only people signed in. + /// + Register, + /// + /// Only member. + /// + Private + } + + public static class TimelinePostContentTypes + { + public const string Text = "text"; + public const string Image = "image"; + } + + public interface ITimelinePostContent + { + public string Type { get; } + } + + public class TextTimelinePostContent : ITimelinePostContent + { + public TextTimelinePostContent(string text) { Text = text; } + + public string Type { get; } = TimelinePostContentTypes.Text; + public string Text { get; set; } + } + + public class ImageTimelinePostContent : ITimelinePostContent + { + public ImageTimelinePostContent(string dataTag) { DataTag = dataTag; } + + public string Type { get; } = TimelinePostContentTypes.Image; + + /// + /// The tag of the data. The tag of the entry in DataManager. Also the etag (not quoted). + /// + public string DataTag { get; set; } + } + + public record TimelinePostInfo + { + public TimelinePostInfo() + { + + } + + public TimelinePostInfo(long id, ITimelinePostContent? content, DateTime time, UserInfo? author, DateTime lastUpdated, string timelineName) + { + Id = id; + Content = content; + Time = time; + Author = author; + LastUpdated = lastUpdated; + TimelineName = timelineName; + } + + public long Id { get; set; } + public ITimelinePostContent? Content { get; set; } + public bool Deleted => Content == null; + public DateTime Time { get; set; } + public UserInfo? Author { get; set; } + public DateTime LastUpdated { get; set; } + public string TimelineName { get; set; } = default!; + } + + public record TimelineInfo + { + public TimelineInfo() + { + + } + + public TimelineInfo( + string uniqueId, + string name, + DateTime nameLastModified, + string title, + string description, + UserInfo owner, + TimelineVisibility visibility, + List members, + DateTime createTime, + DateTime lastModified) + { + UniqueId = uniqueId; + Name = name; + NameLastModified = nameLastModified; + Title = title; + Description = description; + Owner = owner; + Visibility = visibility; + Members = members; + CreateTime = createTime; + LastModified = lastModified; + } + + public string UniqueId { get; set; } = default!; + public string Name { get; set; } = default!; + public DateTime NameLastModified { get; set; } = default!; + public string Title { get; set; } = default!; + public string Description { get; set; } = default!; + public UserInfo Owner { get; set; } = default!; + public TimelineVisibility Visibility { get; set; } +#pragma warning disable CA2227 // Collection properties should be read only + public List Members { get; set; } = default!; +#pragma warning restore CA2227 // Collection properties should be read only + public DateTime CreateTime { get; set; } = default!; + public DateTime LastModified { get; set; } = default!; + } + + public class TimelineChangePropertyRequest + { + public string? Title { get; set; } + public string? Description { get; set; } + public TimelineVisibility? Visibility { get; set; } + } +} diff --git a/BackEnd/Timeline/Models/User.cs b/BackEnd/Timeline/Models/User.cs deleted file mode 100644 index ae2afe85..00000000 --- a/BackEnd/Timeline/Models/User.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using Timeline.Services; - -namespace Timeline.Models -{ - public record User - { - public long Id { get; set; } - public string UniqueId { get; set; } = default!; - - public string Username { get; set; } = default!; - public string Nickname { get; set; } = default!; - - public UserPermissions Permissions { get; set; } = default!; - - public DateTime UsernameChangeTime { get; set; } - public DateTime CreateTime { get; set; } - public DateTime LastModified { get; set; } - public long Version { get; set; } - } -} diff --git a/BackEnd/Timeline/Models/UserInfo.cs b/BackEnd/Timeline/Models/UserInfo.cs new file mode 100644 index 00000000..058cc590 --- /dev/null +++ b/BackEnd/Timeline/Models/UserInfo.cs @@ -0,0 +1,48 @@ +using System; +using Timeline.Services; + +namespace Timeline.Models +{ + public record UserInfo + { + public UserInfo() + { + + } + + public UserInfo( + long id, + string uniqueId, + string username, + string nickname, + UserPermissions permissions, + DateTime usernameChangeTime, + DateTime createTime, + DateTime lastModified, + long version) + { + Id = id; + UniqueId = uniqueId; + Username = username; + Nickname = nickname; + Permissions = permissions; + UsernameChangeTime = usernameChangeTime; + CreateTime = createTime; + LastModified = lastModified; + Version = version; + } + + public long Id { get; set; } + public string UniqueId { get; set; } = default!; + + public string Username { get; set; } = default!; + public string Nickname { get; set; } = default!; + + public UserPermissions Permissions { get; set; } = default!; + + public DateTime UsernameChangeTime { get; set; } + public DateTime CreateTime { get; set; } + public DateTime LastModified { get; set; } + public long Version { get; set; } + } +} diff --git a/BackEnd/Timeline/Services/HighlightTimelineService.cs b/BackEnd/Timeline/Services/HighlightTimelineService.cs index 88ad4a4b..619bc33e 100644 --- a/BackEnd/Timeline/Services/HighlightTimelineService.cs +++ b/BackEnd/Timeline/Services/HighlightTimelineService.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Timeline.Entities; +using Timeline.Models; using Timeline.Services.Exceptions; namespace Timeline.Services @@ -14,7 +15,7 @@ namespace Timeline.Services /// Get all highlight timelines. /// /// A list of all highlight timelines. - Task> GetHighlightTimelines(); + Task> GetHighlightTimelines(); /// /// Add a timeline to highlight list. @@ -73,11 +74,11 @@ namespace Timeline.Services await _database.SaveChangesAsync(); } - public async Task> GetHighlightTimelines() + public async Task> GetHighlightTimelines() { var entities = await _database.HighlightTimelines.Select(t => new { t.Id }).ToListAsync(); - var result = new List(); + var result = new List(); foreach (var entity in entities) { diff --git a/BackEnd/Timeline/Services/TimelinePostService.cs b/BackEnd/Timeline/Services/TimelinePostService.cs index a1176a68..35513a36 100644 --- a/BackEnd/Timeline/Services/TimelinePostService.cs +++ b/BackEnd/Timeline/Services/TimelinePostService.cs @@ -39,7 +39,7 @@ namespace Timeline.Services /// Thrown when timeline with name does not exist. /// If it is a personal timeline, then inner exception is . /// - Task> GetPosts(string timelineName, DateTime? modifiedSince = null, bool includeDeleted = false); + Task> GetPosts(string timelineName, DateTime? modifiedSince = null, bool includeDeleted = false); /// /// Get the etag of data of a post. @@ -90,7 +90,7 @@ namespace Timeline.Services /// If it is a personal timeline, then inner exception is . /// /// Thrown if user of does not exist. - Task CreateTextPost(string timelineName, long authorId, string text, DateTime? time); + Task CreateTextPost(string timelineName, long authorId, string text, DateTime? time); /// /// Create a new image post in timeline. @@ -108,7 +108,7 @@ namespace Timeline.Services /// /// Thrown if user of does not exist. /// Thrown if data is not a image. Validated by . - Task CreateImagePost(string timelineName, long authorId, byte[] imageData, DateTime? time); + Task CreateImagePost(string timelineName, long authorId, byte[] imageData, DateTime? time); /// /// Delete a post. @@ -179,9 +179,9 @@ namespace Timeline.Services _clock = clock; } - private async Task MapTimelinePostFromEntity(TimelinePostEntity entity, string timelineName) + private async Task MapTimelinePostFromEntity(TimelinePostEntity entity, string timelineName) { - User? author = entity.AuthorId.HasValue ? await _userService.GetUser(entity.AuthorId.Value) : null; + UserInfo? author = entity.AuthorId.HasValue ? await _userService.GetUser(entity.AuthorId.Value) : null; ITimelinePostContent? content = null; @@ -197,7 +197,7 @@ namespace Timeline.Services }; } - return new TimelinePost( + return new TimelinePostInfo( id: entity.LocalId, author: author, content: content, @@ -207,7 +207,7 @@ namespace Timeline.Services ); } - public async Task> GetPosts(string timelineName, DateTime? modifiedSince = null, bool includeDeleted = false) + public async Task> GetPosts(string timelineName, DateTime? modifiedSince = null, bool includeDeleted = false) { modifiedSince = modifiedSince?.MyToUtc(); @@ -231,7 +231,7 @@ namespace Timeline.Services var postEntities = await query.ToListAsync(); - var posts = new List(); + var posts = new List(); foreach (var entity in postEntities) { posts.Add(await MapTimelinePostFromEntity(entity, timelineName)); @@ -309,7 +309,7 @@ namespace Timeline.Services }; } - public async Task CreateTextPost(string timelineName, long authorId, string text, DateTime? time) + public async Task CreateTextPost(string timelineName, long authorId, string text, DateTime? time) { time = time?.MyToUtc(); @@ -342,7 +342,7 @@ namespace Timeline.Services await _database.SaveChangesAsync(); - return new TimelinePost( + return new TimelinePostInfo( id: postEntity.LocalId, content: new TextTimelinePostContent(text), time: finalTime, @@ -352,7 +352,7 @@ namespace Timeline.Services ); } - public async Task CreateImagePost(string timelineName, long authorId, byte[] data, DateTime? time) + public async Task CreateImagePost(string timelineName, long authorId, byte[] data, DateTime? time) { time = time?.MyToUtc(); @@ -391,7 +391,7 @@ namespace Timeline.Services _database.TimelinePosts.Add(postEntity); await _database.SaveChangesAsync(); - return new TimelinePost( + return new TimelinePostInfo( id: postEntity.LocalId, content: new ImageTimelinePostContent(tag), time: finalTime, diff --git a/BackEnd/Timeline/Services/TimelineService.cs b/BackEnd/Timeline/Services/TimelineService.cs index b8ec354a..b65b3cf4 100644 --- a/BackEnd/Timeline/Services/TimelineService.cs +++ b/BackEnd/Timeline/Services/TimelineService.cs @@ -90,7 +90,7 @@ namespace Timeline.Services /// Thrown when timeline with name does not exist. /// If it is a personal timeline, then inner exception is . /// - Task GetTimeline(string timelineName); + Task GetTimeline(string timelineName); /// /// Get timeline by id. @@ -98,7 +98,7 @@ namespace Timeline.Services /// Id of timeline. /// The timeline. /// Thrown when timeline with given id does not exist. - Task GetTimelineById(long id); + Task GetTimelineById(long id); /// /// Set the properties of a timeline. @@ -113,8 +113,6 @@ namespace Timeline.Services /// Task ChangeProperty(string timelineName, TimelineChangePropertyRequest newProperties); - - /// /// Change member of timeline. /// @@ -174,7 +172,6 @@ namespace Timeline.Services /// Task HasReadPermission(string timelineName, long? visitorId); - /// /// Verify whether a user is member of a timeline. /// @@ -202,7 +199,7 @@ namespace Timeline.Services /// /// If user with related user id does not exist, empty list will be returned. /// - Task> GetTimelines(TimelineUserRelationship? relate = null, List? visibility = null); + Task> GetTimelines(TimelineUserRelationship? relate = null, List? visibility = null); /// /// Create a timeline. @@ -214,7 +211,7 @@ namespace Timeline.Services /// Thrown when timeline name is invalid. /// Thrown when the timeline already exists. /// Thrown when the owner user does not exist. - Task CreateTimeline(string timelineName, long ownerId); + Task CreateTimeline(string timelineName, long ownerId); /// /// Delete a timeline. @@ -238,7 +235,7 @@ namespace Timeline.Services /// /// You can only change name of general timeline. /// - Task ChangeTimelineName(string oldTimelineName, string newTimelineName); + Task ChangeTimelineName(string oldTimelineName, string newTimelineName); } public class TimelineService : BasicTimelineService, ITimelineService @@ -270,11 +267,11 @@ namespace Timeline.Services } /// Remember to include Members when query. - private async Task MapTimelineFromEntity(TimelineEntity entity) + private async Task MapTimelineFromEntity(TimelineEntity entity) { var owner = await _userService.GetUser(entity.OwnerId); - var members = new List(); + var members = new List(); foreach (var memberEntity in entity.Members) { members.Add(await _userService.GetUser(memberEntity.UserId)); @@ -282,19 +279,18 @@ namespace Timeline.Services var name = entity.Name ?? ("@" + owner.Username); - return new Models.Timeline - { - UniqueID = entity.UniqueId, - Name = name, - NameLastModified = entity.NameLastModified, - Title = string.IsNullOrEmpty(entity.Title) ? name : entity.Title, - Description = entity.Description ?? "", - Owner = owner, - Visibility = entity.Visibility, - Members = members, - CreateTime = entity.CreateTime, - LastModified = entity.LastModified - }; + return new TimelineInfo( + entity.UniqueId, + name, + entity.NameLastModified, + string.IsNullOrEmpty(entity.Title) ? name : entity.Title, + entity.Description ?? "", + owner, + entity.Visibility, + members, + entity.CreateTime, + entity.LastModified + ); } public async Task GetTimelineLastModifiedTime(string timelineName) @@ -321,7 +317,7 @@ namespace Timeline.Services return timelineEntity.UniqueId; } - public async Task GetTimeline(string timelineName) + public async Task GetTimeline(string timelineName) { if (timelineName == null) throw new ArgumentNullException(nameof(timelineName)); @@ -333,7 +329,7 @@ namespace Timeline.Services return await MapTimelineFromEntity(timelineEntity); } - public async Task GetTimelineById(long id) + public async Task GetTimelineById(long id) { var timelineEntity = await _database.Timelines.Where(t => t.Id == id).Include(t => t.Members).SingleOrDefaultAsync(); @@ -522,7 +518,7 @@ namespace Timeline.Services return await _database.TimelineMembers.AnyAsync(m => m.TimelineId == timelineId && m.UserId == userId); } - public async Task> GetTimelines(TimelineUserRelationship? relate = null, List? visibility = null) + public async Task> GetTimelines(TimelineUserRelationship? relate = null, List? visibility = null) { List entities; @@ -556,7 +552,7 @@ namespace Timeline.Services } } - var result = new List(); + var result = new List(); foreach (var entity in entities) { @@ -566,7 +562,7 @@ namespace Timeline.Services return result; } - public async Task CreateTimeline(string name, long owner) + public async Task CreateTimeline(string name, long owner) { if (name == null) throw new ArgumentNullException(nameof(name)); @@ -604,7 +600,7 @@ namespace Timeline.Services await _database.SaveChangesAsync(); } - public async Task ChangeTimelineName(string oldTimelineName, string newTimelineName) + public async Task ChangeTimelineName(string oldTimelineName, string newTimelineName) { if (oldTimelineName == null) throw new ArgumentNullException(nameof(oldTimelineName)); diff --git a/BackEnd/Timeline/Services/UserCredentialService.cs b/BackEnd/Timeline/Services/UserCredentialService.cs index e5c3581b..8aeef9ef 100644 --- a/BackEnd/Timeline/Services/UserCredentialService.cs +++ b/BackEnd/Timeline/Services/UserCredentialService.cs @@ -1,12 +1,10 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using System; -using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Timeline.Entities; using Timeline.Helpers; -using Timeline.Models; using Timeline.Models.Validation; using Timeline.Services.Exceptions; diff --git a/BackEnd/Timeline/Services/UserService.cs b/BackEnd/Timeline/Services/UserService.cs index 9395cc52..96068e44 100644 --- a/BackEnd/Timeline/Services/UserService.cs +++ b/BackEnd/Timeline/Services/UserService.cs @@ -32,13 +32,13 @@ namespace Timeline.Services /// The id of the user. /// The user info. /// Thrown when the user with given id does not exist. - Task GetUser(long id); + Task GetUser(long id); /// /// List all users. /// /// The user info of users. - Task> GetUsers(); + Task> GetUsers(); /// /// Create a user with given info. @@ -49,7 +49,7 @@ namespace Timeline.Services /// Thrown when or is null. /// Thrown when or is of bad format. /// Thrown when a user with given username already exists. - Task CreateUser(string username, string password); + Task CreateUser(string username, string password); /// /// Modify a user. @@ -62,7 +62,7 @@ namespace Timeline.Services /// /// Version will increase if password is changed. /// - Task ModifyUser(long id, ModifyUserParams? param); + Task ModifyUser(long id, ModifyUserParams? param); } public class UserService : BasicUserService, IUserService @@ -116,26 +116,23 @@ namespace Timeline.Services throw new EntityAlreadyExistException(EntityNames.User, ExceptionUsernameConflict); } - private async Task CreateUserFromEntity(UserEntity entity) + private async Task CreateUserFromEntity(UserEntity entity) { var permission = await _userPermissionService.GetPermissionsOfUserAsync(entity.Id); - return new User - { - UniqueId = entity.UniqueId, - Username = entity.Username, - Permissions = permission, - Nickname = string.IsNullOrEmpty(entity.Nickname) ? entity.Username : entity.Nickname, - Id = entity.Id, - Version = entity.Version, - CreateTime = entity.CreateTime, - UsernameChangeTime = entity.UsernameChangeTime, - LastModified = entity.LastModified - }; + return new UserInfo( + entity.Id, + entity.UniqueId, + entity.Username, + string.IsNullOrEmpty(entity.Nickname) ? entity.Username : entity.Nickname, + permission, + entity.UsernameChangeTime, + entity.CreateTime, + entity.LastModified, + entity.Version + ); } - - - public async Task GetUser(long id) + public async Task GetUser(long id) { var user = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync(); @@ -145,9 +142,9 @@ namespace Timeline.Services return await CreateUserFromEntity(user); } - public async Task> GetUsers() + public async Task> GetUsers() { - List result = new(); + List result = new(); foreach (var entity in await _databaseContext.Users.ToArrayAsync()) { result.Add(await CreateUserFromEntity(entity)); @@ -155,7 +152,7 @@ namespace Timeline.Services return result; } - public async Task CreateUser(string username, string password) + public async Task CreateUser(string username, string password) { if (username == null) throw new ArgumentNullException(nameof(username)); @@ -183,7 +180,7 @@ namespace Timeline.Services return await CreateUserFromEntity(newEntity); } - public async Task ModifyUser(long id, ModifyUserParams? param) + public async Task ModifyUser(long id, ModifyUserParams? param) { if (param != null) { diff --git a/BackEnd/Timeline/Services/UserTokenManager.cs b/BackEnd/Timeline/Services/UserTokenManager.cs index 831329e6..b887b987 100644 --- a/BackEnd/Timeline/Services/UserTokenManager.cs +++ b/BackEnd/Timeline/Services/UserTokenManager.cs @@ -10,7 +10,7 @@ namespace Timeline.Services public class UserTokenCreateResult { public string Token { get; set; } = default!; - public User User { get; set; } = default!; + public UserInfo User { get; set; } = default!; } public interface IUserTokenManager @@ -38,7 +38,7 @@ namespace Timeline.Services /// Thrown when the token is of bad version. /// Thrown when the token is of bad format. /// Thrown when the user specified by the token does not exist. Usually the user had been deleted after the token was issued. - public Task VerifyToken(string token); + public Task VerifyToken(string token); } public class UserTokenManager : IUserTokenManager @@ -75,7 +75,7 @@ namespace Timeline.Services } - public async Task VerifyToken(string token) + public async Task VerifyToken(string token) { if (token == null) throw new ArgumentNullException(nameof(token)); -- cgit v1.2.3 From 5e4cdbbfa75ce021dfbfd8e1f2d38370a702285e Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 27 Nov 2020 01:26:23 +0800 Subject: ... --- BackEnd/Timeline.Tests/Helpers/TestDatabase.cs | 2 +- .../Services/HighlightTimelineServiceTest.cs | 35 ++++++++++++++++++---- .../Services/TimelinePostServiceTest.cs | 2 +- .../Timeline.Tests/Services/TimelineServiceTest.cs | 2 +- .../Timeline/Entities/HighlightTimelineEntity.cs | 8 +++-- BackEnd/Timeline/Models/TimelineInfo.cs | 4 +-- BackEnd/Timeline/Models/UserInfo.cs | 2 +- .../Timeline/Services/HighlightTimelineService.cs | 6 ++-- BackEnd/Timeline/Services/UserPermissionService.cs | 33 ++++++++++++++++++-- BackEnd/Timeline/Services/UserService.cs | 6 ++-- 10 files changed, 79 insertions(+), 21 deletions(-) (limited to 'BackEnd/Timeline/Services/UserService.cs') diff --git a/BackEnd/Timeline.Tests/Helpers/TestDatabase.cs b/BackEnd/Timeline.Tests/Helpers/TestDatabase.cs index 74db74aa..a71c2208 100644 --- a/BackEnd/Timeline.Tests/Helpers/TestDatabase.cs +++ b/BackEnd/Timeline.Tests/Helpers/TestDatabase.cs @@ -35,7 +35,7 @@ namespace Timeline.Tests.Helpers if (_createUser) { var passwordService = new PasswordService(); - var userService = new UserService(NullLogger.Instance, context, passwordService, new Clock(), new UserPermissionService(context)); + var userService = new UserService(NullLogger.Instance, context, passwordService, new UserPermissionService(context), new Clock()); var admin = await userService.CreateUser("admin", "adminpw"); await userService.ModifyUser(admin.Id, new ModifyUserParams() { Nickname = "administrator" }); diff --git a/BackEnd/Timeline.Tests/Services/HighlightTimelineServiceTest.cs b/BackEnd/Timeline.Tests/Services/HighlightTimelineServiceTest.cs index a4cd983d..8ba26613 100644 --- a/BackEnd/Timeline.Tests/Services/HighlightTimelineServiceTest.cs +++ b/BackEnd/Timeline.Tests/Services/HighlightTimelineServiceTest.cs @@ -1,18 +1,43 @@ -using Timeline.Services; +using FluentAssertions; +using Microsoft.Extensions.Logging.Abstractions; +using System.Threading.Tasks; +using Timeline.Services; +using Timeline.Tests.Helpers; +using Xunit; namespace Timeline.Tests.Services { public class HighlightTimelineServiceTest : DatabaseBasedTest { - private UserService _userService; - private TimelineService _timelineService; + private readonly TestClock _clock = new TestClock(); + private UserService _userService = default!; + private TimelineService _timelineService = default!; - private HighlightTimelineService _service; + private HighlightTimelineService _service = default!; protected override void OnDatabaseCreated() { - + _userService = new UserService(NullLogger.Instance, Database, new PasswordService(), new UserPermissionService(Database), _clock); + _timelineService = new TimelineService(Database, _userService, _clock); + _service = new HighlightTimelineService(Database, _userService, _timelineService, _clock); } + [Fact] + public async Task Should_Work() + { + { + var ht = await _service.GetHighlightTimelines(); + ht.Should().BeEmpty(); + } + + var userId = await _userService.GetUserIdByUsername("user"); + await _timelineService.CreateTimeline("tl", userId); + await _service.AddHighlightTimeline("tl", userId); + + { + var ht = await _service.GetHighlightTimelines(); + ht.Should().HaveCount(1).And.BeEquivalentTo(await _timelineService.GetTimeline("tl")); + } + } } } diff --git a/BackEnd/Timeline.Tests/Services/TimelinePostServiceTest.cs b/BackEnd/Timeline.Tests/Services/TimelinePostServiceTest.cs index 97512be5..7771ae0b 100644 --- a/BackEnd/Timeline.Tests/Services/TimelinePostServiceTest.cs +++ b/BackEnd/Timeline.Tests/Services/TimelinePostServiceTest.cs @@ -36,7 +36,7 @@ namespace Timeline.Tests.Services { _dataManager = new DataManager(Database, _eTagGenerator); _userPermissionService = new UserPermissionService(Database); - _userService = new UserService(NullLogger.Instance, Database, _passwordService, _clock, _userPermissionService); + _userService = new UserService(NullLogger.Instance, Database, _passwordService, _userPermissionService, _clock); _timelineService = new TimelineService(Database, _userService, _clock); _timelinePostService = new TimelinePostService(NullLogger.Instance, Database, _timelineService, _userService, _dataManager, _imageValidator, _clock); _userDeleteService = new UserDeleteService(NullLogger.Instance, Database, _timelinePostService); diff --git a/BackEnd/Timeline.Tests/Services/TimelineServiceTest.cs b/BackEnd/Timeline.Tests/Services/TimelineServiceTest.cs index 98f03066..70f54ede 100644 --- a/BackEnd/Timeline.Tests/Services/TimelineServiceTest.cs +++ b/BackEnd/Timeline.Tests/Services/TimelineServiceTest.cs @@ -26,7 +26,7 @@ namespace Timeline.Tests.Services protected override void OnDatabaseCreated() { _userPermissionService = new UserPermissionService(Database); - _userService = new UserService(NullLogger.Instance, Database, _passwordService, _clock, _userPermissionService); + _userService = new UserService(NullLogger.Instance, Database, _passwordService, _userPermissionService, _clock); _timelineService = new TimelineService(Database, _userService, _clock); } diff --git a/BackEnd/Timeline/Entities/HighlightTimelineEntity.cs b/BackEnd/Timeline/Entities/HighlightTimelineEntity.cs index 0a38c8a6..3378a175 100644 --- a/BackEnd/Timeline/Entities/HighlightTimelineEntity.cs +++ b/BackEnd/Timeline/Entities/HighlightTimelineEntity.cs @@ -1,10 +1,11 @@ -using System.ComponentModel.DataAnnotations; +using System; +using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace Timeline.Entities { [Table("highlight_timelines")] - public record HighlightTimelineEntity + public class HighlightTimelineEntity { [Key, Column("id"), DatabaseGenerated(DatabaseGeneratedOption.Identity)] public long Id { get; set; } @@ -20,5 +21,8 @@ namespace Timeline.Entities [ForeignKey(nameof(OperatorId))] public UserEntity? Operator { get; set; } + + [Column("add_time")] + public DateTime AddTime { get; set; } } } diff --git a/BackEnd/Timeline/Models/TimelineInfo.cs b/BackEnd/Timeline/Models/TimelineInfo.cs index 440f6b81..649af274 100644 --- a/BackEnd/Timeline/Models/TimelineInfo.cs +++ b/BackEnd/Timeline/Models/TimelineInfo.cs @@ -50,7 +50,7 @@ namespace Timeline.Models public string DataTag { get; set; } } - public record TimelinePostInfo + public class TimelinePostInfo { public TimelinePostInfo() { @@ -76,7 +76,7 @@ namespace Timeline.Models public string TimelineName { get; set; } = default!; } - public record TimelineInfo + public class TimelineInfo { public TimelineInfo() { diff --git a/BackEnd/Timeline/Models/UserInfo.cs b/BackEnd/Timeline/Models/UserInfo.cs index 058cc590..e8d57def 100644 --- a/BackEnd/Timeline/Models/UserInfo.cs +++ b/BackEnd/Timeline/Models/UserInfo.cs @@ -3,7 +3,7 @@ using Timeline.Services; namespace Timeline.Models { - public record UserInfo + public class UserInfo { public UserInfo() { diff --git a/BackEnd/Timeline/Services/HighlightTimelineService.cs b/BackEnd/Timeline/Services/HighlightTimelineService.cs index 619bc33e..0f4e5488 100644 --- a/BackEnd/Timeline/Services/HighlightTimelineService.cs +++ b/BackEnd/Timeline/Services/HighlightTimelineService.cs @@ -46,12 +46,14 @@ namespace Timeline.Services private readonly DatabaseContext _database; private readonly IBasicUserService _userService; private readonly ITimelineService _timelineService; + private readonly IClock _clock; - public HighlightTimelineService(DatabaseContext database, IBasicUserService userService, ITimelineService timelineService) + public HighlightTimelineService(DatabaseContext database, IBasicUserService userService, ITimelineService timelineService, IClock clock) { _database = database; _userService = userService; _timelineService = timelineService; + _clock = clock; } public async Task AddHighlightTimeline(string timelineName, long? operatorId) @@ -70,7 +72,7 @@ namespace Timeline.Services if (alreadyIs) return; - _database.HighlightTimelines.Add(new HighlightTimelineEntity { TimelineId = timelineId, OperatorId = operatorId }); + _database.HighlightTimelines.Add(new HighlightTimelineEntity { TimelineId = timelineId, OperatorId = operatorId, AddTime = _clock.GetCurrentTime() }); await _database.SaveChangesAsync(); } diff --git a/BackEnd/Timeline/Services/UserPermissionService.cs b/BackEnd/Timeline/Services/UserPermissionService.cs index 9683000a..bd7cd6aa 100644 --- a/BackEnd/Timeline/Services/UserPermissionService.cs +++ b/BackEnd/Timeline/Services/UserPermissionService.cs @@ -28,7 +28,7 @@ namespace Timeline.Services /// /// Represents a user's permissions. /// - public class UserPermissions : IEnumerable + public class UserPermissions : IEnumerable, IEquatable { public static UserPermissions AllPermissions { get; } = new UserPermissions(Enum.GetValues()); @@ -49,10 +49,10 @@ namespace Timeline.Services public UserPermissions(IEnumerable permissions) { if (permissions == null) throw new ArgumentNullException(nameof(permissions)); - _permissions = new HashSet(permissions); + _permissions = new SortedSet(permissions); } - private readonly HashSet _permissions = new(); + private readonly SortedSet _permissions = new(); /// /// Check if a permission is contained in the list. @@ -108,6 +108,33 @@ namespace Timeline.Services { return ((IEnumerable)_permissions).GetEnumerator(); } + + public bool Equals(UserPermissions? other) + { + if (other == null) + return false; + + return _permissions.SequenceEqual(other._permissions); + } + + public override bool Equals(object? obj) + { + return Equals(obj as UserPermissions); + } + + public override int GetHashCode() + { + int result = 0; + foreach (var permission in Enum.GetValues()) + { + if (_permissions.Contains(permission)) + { + result += 1; + } + result <<= 1; + } + return result; + } } public interface IUserPermissionService diff --git a/BackEnd/Timeline/Services/UserService.cs b/BackEnd/Timeline/Services/UserService.cs index 96068e44..c99e86b0 100644 --- a/BackEnd/Timeline/Services/UserService.cs +++ b/BackEnd/Timeline/Services/UserService.cs @@ -17,7 +17,7 @@ namespace Timeline.Services /// /// Null means not change. /// - public record ModifyUserParams + public class ModifyUserParams { public string? Username { get; set; } public string? Password { get; set; } @@ -78,13 +78,13 @@ namespace Timeline.Services private readonly UsernameValidator _usernameValidator = new UsernameValidator(); private readonly NicknameValidator _nicknameValidator = new NicknameValidator(); - public UserService(ILogger logger, DatabaseContext databaseContext, IPasswordService passwordService, IClock clock, IUserPermissionService userPermissionService) : base(databaseContext) + public UserService(ILogger logger, DatabaseContext databaseContext, IPasswordService passwordService, IUserPermissionService userPermissionService, IClock clock) : base(databaseContext) { _logger = logger; - _clock = clock; _databaseContext = databaseContext; _passwordService = passwordService; _userPermissionService = userPermissionService; + _clock = clock; } private void CheckUsernameFormat(string username, string? paramName) -- cgit v1.2.3