From cb2d9949ecc1d3c4b10295eb715a9293d493c5e2 Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Sun, 3 Nov 2019 00:28:21 +0800 Subject: Design the entity model primarily. --- Timeline/Services/TimelineService.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 Timeline/Services/TimelineService.cs (limited to 'Timeline/Services/TimelineService.cs') diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs new file mode 100644 index 00000000..e6a1e845 --- /dev/null +++ b/Timeline/Services/TimelineService.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Timeline.Services +{ + public interface ITimelineService + { + // TODO: Design this. + } +} -- cgit v1.2.3 From 27719fa2a00e041cdb957182812508464d19a9d7 Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Sun, 3 Nov 2019 16:51:10 +0800 Subject: WIP: Design the timeline service interface. --- Timeline/Entities/TimelineMemberEntity.cs | 6 +- Timeline/Models/Timeline.cs | 42 +++++ Timeline/Resources/Services/Exception.Designer.cs | 36 ++++ Timeline/Resources/Services/Exception.resx | 12 ++ .../Services/TimelineNameBadFormatException.cs | 21 +++ Timeline/Services/TimelineNotExistException.cs | 19 ++ Timeline/Services/TimelineService.cs | 191 ++++++++++++++++++++- 7 files changed, 320 insertions(+), 7 deletions(-) create mode 100644 Timeline/Models/Timeline.cs create mode 100644 Timeline/Services/TimelineNameBadFormatException.cs create mode 100644 Timeline/Services/TimelineNotExistException.cs (limited to 'Timeline/Services/TimelineService.cs') diff --git a/Timeline/Entities/TimelineMemberEntity.cs b/Timeline/Entities/TimelineMemberEntity.cs index 4631dc89..c8961013 100644 --- a/Timeline/Entities/TimelineMemberEntity.cs +++ b/Timeline/Entities/TimelineMemberEntity.cs @@ -1,9 +1,5 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using System.Linq; -using System.Threading.Tasks; namespace Timeline.Entities { diff --git a/Timeline/Models/Timeline.cs b/Timeline/Models/Timeline.cs new file mode 100644 index 00000000..26012878 --- /dev/null +++ b/Timeline/Models/Timeline.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Entities; + +namespace Timeline.Models +{ + public class TimelinePostInfo + { + public long Id { get; set; } + + public string? Content { get; set; } + + public DateTime Time { get; set; } + + /// + /// The username of the author. + /// + public string Author { get; set; } = default!; + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "This is a DTO class.")] + public class BaseTimelineInfo + { + public string? Description { get; set; } + + /// + /// The username of the owner. + /// + public string Owner { get; set; } = default!; + + public TimelineVisibility Visibility { get; set; } + + public List Members { get; set; } = default!; + } + + public class TimelineInfo : BaseTimelineInfo + { + public string Name { get; set; } = default!; + } +} diff --git a/Timeline/Resources/Services/Exception.Designer.cs b/Timeline/Resources/Services/Exception.Designer.cs index ddf60f45..a5785cb6 100644 --- a/Timeline/Resources/Services/Exception.Designer.cs +++ b/Timeline/Resources/Services/Exception.Designer.cs @@ -267,6 +267,42 @@ namespace Timeline.Resources.Services { } } + /// + /// Looks up a localized string similar to An exception happened when add or remove member on timeline.. + /// + internal static string TimelineMemberOperationException { + get { + return ResourceManager.GetString("TimelineMemberOperationException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An exception happened when operating on the {} member on timeline.. + /// + internal static string TimelineMemberOperationExceptionIndex { + get { + return ResourceManager.GetString("TimelineMemberOperationExceptionIndex", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Timeline name is of bad format. If this is a personal timeline, it means the username is of bad format and inner exception should be a UsernameBadFormatException.. + /// + internal static string TimelineNameBadFormatException { + get { + return ResourceManager.GetString("TimelineNameBadFormatException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Timeline does not exist. If this is a personal timeline, it means the user does not exist and inner exception should be a UserNotExistException.. + /// + internal static string TimelineNotExistException { + get { + return ResourceManager.GetString("TimelineNotExistException", resourceCulture); + } + } + /// /// Looks up a localized string similar to The username is of bad format.. /// diff --git a/Timeline/Resources/Services/Exception.resx b/Timeline/Resources/Services/Exception.resx index 12bf9afb..e6622094 100644 --- a/Timeline/Resources/Services/Exception.resx +++ b/Timeline/Resources/Services/Exception.resx @@ -186,6 +186,18 @@ version claim is not a number. + + An exception happened when add or remove member on timeline. + + + An exception happened when operating on the {} member on timeline. + + + Timeline name is of bad format. If this is a personal timeline, it means the username is of bad format and inner exception should be a UsernameBadFormatException. + + + Timeline does not exist. If this is a personal timeline, it means the user does not exist and inner exception should be a UserNotExistException. + The username is of bad format. diff --git a/Timeline/Services/TimelineNameBadFormatException.cs b/Timeline/Services/TimelineNameBadFormatException.cs new file mode 100644 index 00000000..5120a175 --- /dev/null +++ b/Timeline/Services/TimelineNameBadFormatException.cs @@ -0,0 +1,21 @@ +using System; + +namespace Timeline.Services +{ + [Serializable] + public class TimelineNameBadFormatException : Exception + { + public TimelineNameBadFormatException() + : base(Resources.Services.Exception.TimelineNameBadFormatException) { } + public TimelineNameBadFormatException(string name) + : base(Resources.Services.Exception.TimelineNameBadFormatException) { Name = name; } + public TimelineNameBadFormatException(string name, Exception inner) + : base(Resources.Services.Exception.TimelineNameBadFormatException, inner) { Name = name; } + + protected TimelineNameBadFormatException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + public string? Name { get; set; } + } +} diff --git a/Timeline/Services/TimelineNotExistException.cs b/Timeline/Services/TimelineNotExistException.cs new file mode 100644 index 00000000..6dfd0bab --- /dev/null +++ b/Timeline/Services/TimelineNotExistException.cs @@ -0,0 +1,19 @@ +using System; + +namespace Timeline.Services +{ + [Serializable] + public class TimelineNotExistException : Exception + { + public TimelineNotExistException() : base(Resources.Services.Exception.TimelineNotExistException) { } + public TimelineNotExistException(string name) + : base(Resources.Services.Exception.TimelineNotExistException) { Name = name; } + public TimelineNotExistException(string name, Exception inner) + : base(Resources.Services.Exception.TimelineNotExistException, inner) { Name = name; } + protected TimelineNotExistException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + public string? Name { get; set; } + } +} diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs index e6a1e845..cf130a70 100644 --- a/Timeline/Services/TimelineService.cs +++ b/Timeline/Services/TimelineService.cs @@ -1,12 +1,199 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Threading.Tasks; +using Timeline.Entities; +using Timeline.Models; namespace Timeline.Services { - public interface ITimelineService + + [Serializable] + public class TimelineMemberOperationException : Exception + { + public TimelineMemberOperationException() : base(Resources.Services.Exception.TimelineMemberOperationException) { } + public TimelineMemberOperationException(string message) : base(message) { } + public TimelineMemberOperationException(string message, Exception inner) : base(message, inner) { } + protected TimelineMemberOperationException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + public TimelineMemberOperationException(int index, Exception inner) : base(MakeIndexMessage(index), inner) { Index = index; } + + private static string MakeIndexMessage(int index) => string.Format(CultureInfo.CurrentCulture, + Resources.Services.Exception.TimelineMemberOperationExceptionIndex, index); + + public int? Index { get; set; } + } + + /// + /// This define the common interface of both personal timeline + /// and normal timeline. + /// + /// + /// The "name" parameter in method means name of timeline in + /// while username of the owner + /// of the personal timeline in . + /// + public interface IBaseTimelineService + { + /// + /// Get all the posts in the timeline. + /// + /// Username or the timeline name. See remarks of . + /// A list of all posts. + /// Thrown when is null. + /// + /// Thrown when timeline name is of bad format. + /// For normal timeline, it means name is an empty string. + /// For personal timeline, it means the username is of bad format, + /// the inner exception should be a . + /// + /// + /// Thrown when timeline does not exist. + /// For normal timeline, it means the name does not exist. + /// For personal timeline, it means the user of that username does not exist + /// and the inner exception should be a . + /// + Task> GetPosts(string name); + + /// + /// Create a new post in timeline. + /// + /// Username or the timeline name. See remarks of . + /// The author's username. + /// The content. + /// The time of the post. If null, then use current time. + /// + /// Thrown when or or is null. + /// + /// Thrown when timeline name is of bad format. + /// For normal timeline, it means name is an empty string. + /// For personal timeline, it means the username is of bad format, + /// the inner exception should be a . + /// + /// + /// Thrown when timeline does not exist. + /// For normal timeline, it means the name does not exist. + /// For personal timeline, it means the user of that username does not exist + /// and the inner exception should be a . + /// + /// Thrown if is of bad format. + /// Thrown if does not exist. + Task Post(string name, string author, string content, DateTime? time); + + /// + /// Set the visibility permission of a timeline. + /// + /// Username or the timeline name. See remarks of . + /// The new visibility. + /// Thrown when is null. + /// + /// Thrown when timeline name is of bad format. + /// For normal timeline, it means name is an empty string. + /// For personal timeline, it means the username is of bad format, + /// the inner exception should be a . + /// + /// + /// Thrown when timeline does not exist. + /// For normal timeline, it means the name does not exist. + /// For personal timeline, it means the user of that username does not exist + /// and the inner exception should be a . + /// + Task SetVisibility(string name, TimelineVisibility visibility); + + /// + /// Set the description of a timeline. + /// + /// Username or the timeline name. See remarks of . + /// The new description. + /// Thrown when or is null. + /// + /// Thrown when timeline name is of bad format. + /// For normal timeline, it means name is an empty string. + /// For personal timeline, it means the username is of bad format, + /// the inner exception should be a . + /// + /// + /// Thrown when timeline does not exist. + /// For normal timeline, it means the name does not exist. + /// For personal timeline, it means the user of that username does not exist + /// and the inner exception should be a . + /// + Task SetDescription(string name, string description); + + /// + /// Add members to a timeline. + /// + /// Username or the timeline name. See remarks of . + /// A list of new members' usernames + /// Thrown when or is null. + /// + /// Thrown when timeline name is of bad format. + /// For normal timeline, it means name is an empty string. + /// For personal timeline, it means the username is of bad format, + /// the inner exception should be a . + /// + /// + /// Thrown when timeline does not exist. + /// For normal timeline, it means the name does not exist. + /// For personal timeline, it means the user of that username does not exist + /// and the inner exception should be a . + /// + /// + /// TODO! complete this documents. + /// + Task AddMember(string name, IList usernames); + + /// + /// Remove members to a timeline. + /// + /// Username or the timeline name. See remarks of . + /// A list of members' usernames + /// Thrown when is null. + /// + /// Thrown when timeline name is of bad format. + /// For normal timeline, it means name is an empty string. + /// For personal timeline, it means the username is of bad format, + /// the inner exception should be a . + /// + /// + /// Thrown when timeline does not exist. + /// For normal timeline, it means the name does not exist. + /// For personal timeline, it means the user of that username does not exist + /// and the inner exception should be a . + /// + Task RemoveMember(string name, IList usernames); + } + + /// + /// Service for normal timeline. + /// + public interface ITimelineService : IBaseTimelineService + { + /// + /// Get the timeline info. + /// + /// The name of the timeline. + /// The timeline info. + Task GetTimeline(string name); + + /// + /// Create a timeline. + /// + /// The name of the timeline. + /// The owner of the timeline. + Task CreateTimeline(string name, string owner); + } + + public interface IPersonalTimelineService : IBaseTimelineService { - // TODO: Design this. + /// + /// Get the timeline info. + /// + /// The username of the owner of the personal timeline. + /// The timeline info. + Task GetTimeline(string username); } } -- cgit v1.2.3 From 83cee23d8091c99bddbecdee2b9003794480b079 Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Mon, 4 Nov 2019 21:02:12 +0800 Subject: Complete designing timeline service though there may be many bugs in the design. --- Timeline/Resources/Services/Exception.Designer.cs | 18 +++++++ Timeline/Resources/Services/Exception.resx | 6 +++ Timeline/Services/TimelineAlreadyExistException.cs | 17 ++++++ .../TimelineMemberOperationUserException.cs | 28 ++++++++++ Timeline/Services/TimelineService.cs | 63 ++++++++++++++-------- .../Services/TimelineUserNotMemberException.cs | 15 ++++++ 6 files changed, 125 insertions(+), 22 deletions(-) create mode 100644 Timeline/Services/TimelineAlreadyExistException.cs create mode 100644 Timeline/Services/TimelineMemberOperationUserException.cs create mode 100644 Timeline/Services/TimelineUserNotMemberException.cs (limited to 'Timeline/Services/TimelineService.cs') diff --git a/Timeline/Resources/Services/Exception.Designer.cs b/Timeline/Resources/Services/Exception.Designer.cs index a5785cb6..1f6493cb 100644 --- a/Timeline/Resources/Services/Exception.Designer.cs +++ b/Timeline/Resources/Services/Exception.Designer.cs @@ -267,6 +267,15 @@ namespace Timeline.Resources.Services { } } + /// + /// Looks up a localized string similar to The timeline with that name already exists.. + /// + internal static string TimelineAlreadyExistException { + get { + return ResourceManager.GetString("TimelineAlreadyExistException", resourceCulture); + } + } + /// /// Looks up a localized string similar to An exception happened when add or remove member on timeline.. /// @@ -303,6 +312,15 @@ namespace Timeline.Resources.Services { } } + /// + /// Looks up a localized string similar to The use is not a member of the timeline.. + /// + internal static string TimelineUserNotMemberException { + get { + return ResourceManager.GetString("TimelineUserNotMemberException", resourceCulture); + } + } + /// /// Looks up a localized string similar to The username is of bad format.. /// diff --git a/Timeline/Resources/Services/Exception.resx b/Timeline/Resources/Services/Exception.resx index e6622094..3e9d3747 100644 --- a/Timeline/Resources/Services/Exception.resx +++ b/Timeline/Resources/Services/Exception.resx @@ -186,6 +186,9 @@ version claim is not a number. + + The timeline with that name already exists. + An exception happened when add or remove member on timeline. @@ -198,6 +201,9 @@ Timeline does not exist. If this is a personal timeline, it means the user does not exist and inner exception should be a UserNotExistException. + + The use is not a member of the timeline. + The username is of bad format. diff --git a/Timeline/Services/TimelineAlreadyExistException.cs b/Timeline/Services/TimelineAlreadyExistException.cs new file mode 100644 index 00000000..c2dea1f9 --- /dev/null +++ b/Timeline/Services/TimelineAlreadyExistException.cs @@ -0,0 +1,17 @@ +using System; + +namespace Timeline.Services +{ + [Serializable] + public class TimelineAlreadyExistException : Exception + { + public TimelineAlreadyExistException() : base(Resources.Services.Exception.TimelineAlreadyExistException) { } + public TimelineAlreadyExistException(string name) : base(Resources.Services.Exception.TimelineAlreadyExistException) { Name = name; } + public TimelineAlreadyExistException(string name, Exception inner) : base(Resources.Services.Exception.TimelineAlreadyExistException, inner) { Name = name; } + protected TimelineAlreadyExistException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + public string? Name { get; set; } + } +} diff --git a/Timeline/Services/TimelineMemberOperationUserException.cs b/Timeline/Services/TimelineMemberOperationUserException.cs new file mode 100644 index 00000000..998f1a6e --- /dev/null +++ b/Timeline/Services/TimelineMemberOperationUserException.cs @@ -0,0 +1,28 @@ +using System; +using System.Globalization; + +namespace Timeline.Services +{ + [Serializable] + public class TimelineMemberOperationUserException : Exception + { + public TimelineMemberOperationUserException() : base(Resources.Services.Exception.TimelineMemberOperationException) { } + public TimelineMemberOperationUserException(string message) : base(message) { } + public TimelineMemberOperationUserException(string message, Exception inner) : base(message, inner) { } + protected TimelineMemberOperationUserException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + public TimelineMemberOperationUserException(int index, string username, Exception inner) : base(MakeIndexMessage(index), inner) { Index = index; Username = username; } + + private static string MakeIndexMessage(int index) => string.Format(CultureInfo.CurrentCulture, + Resources.Services.Exception.TimelineMemberOperationExceptionIndex, index); + + /// + /// The index of the member on which the operation failed. + /// + public int? Index { get; set; } + + public string? Username { get; set; } + } +} diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs index cf130a70..9d309e7e 100644 --- a/Timeline/Services/TimelineService.cs +++ b/Timeline/Services/TimelineService.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Threading.Tasks; using Timeline.Entities; @@ -8,25 +7,6 @@ using Timeline.Models; namespace Timeline.Services { - - [Serializable] - public class TimelineMemberOperationException : Exception - { - public TimelineMemberOperationException() : base(Resources.Services.Exception.TimelineMemberOperationException) { } - public TimelineMemberOperationException(string message) : base(message) { } - public TimelineMemberOperationException(string message, Exception inner) : base(message, inner) { } - protected TimelineMemberOperationException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - public TimelineMemberOperationException(int index, Exception inner) : base(MakeIndexMessage(index), inner) { Index = index; } - - private static string MakeIndexMessage(int index) => string.Format(CultureInfo.CurrentCulture, - Resources.Services.Exception.TimelineMemberOperationExceptionIndex, index); - - public int? Index { get; set; } - } - /// /// This define the common interface of both personal timeline /// and normal timeline. @@ -141,8 +121,12 @@ namespace Timeline.Services /// For personal timeline, it means the user of that username does not exist /// and the inner exception should be a . /// - /// - /// TODO! complete this documents. + /// + /// Thrown when an exception occurs on users in the list. + /// The inner exception is + /// when one of the username is not valid. + /// The inner exception is + /// when one of the user does not exist. /// Task AddMember(string name, IList usernames); @@ -164,6 +148,13 @@ namespace Timeline.Services /// For personal timeline, it means the user of that username does not exist /// and the inner exception should be a . /// + /// + /// Thrown when an exception occurs on the user list. + /// The inner exception is + /// when one of the username is invalid. + /// The inner exception is + /// when one of the user is not a member of the timeline. + /// Task RemoveMember(string name, IList usernames); } @@ -177,6 +168,13 @@ namespace Timeline.Services /// /// The name of the timeline. /// The timeline info. + /// Thrown when is null. + /// + /// Thrown when timeline name is invalid. Currently it means it is an empty string. + /// + /// + /// Thrown when timeline with the name does not exist. + /// Task GetTimeline(string name); /// @@ -184,6 +182,18 @@ namespace Timeline.Services /// /// The name of the timeline. /// The owner of the timeline. + /// Thrown when or is null. + /// + /// Thrown when timeline name is invalid. Currently it means it is an empty string. + /// + /// + /// Thrown when the timeline already exists. + /// + /// + /// Thrown when the username of the owner is not valid. + /// + /// + /// Thrown when the owner user does not exist. Task CreateTimeline(string name, string owner); } @@ -194,6 +204,15 @@ namespace Timeline.Services /// /// The username of the owner of the personal timeline. /// The timeline info. + /// + /// Thrown when is null. + /// + /// + /// Thrown when is of bad format. Inner exception MUST be . + /// + /// + /// Thrown when the user does not exist. Inner exception MUST be . + /// Task GetTimeline(string username); } } diff --git a/Timeline/Services/TimelineUserNotMemberException.cs b/Timeline/Services/TimelineUserNotMemberException.cs new file mode 100644 index 00000000..260beb02 --- /dev/null +++ b/Timeline/Services/TimelineUserNotMemberException.cs @@ -0,0 +1,15 @@ +using System; + +namespace Timeline.Services +{ + [Serializable] + public class TimelineUserNotMemberException : Exception + { + public TimelineUserNotMemberException() : base(Resources.Services.Exception.TimelineUserNotMemberException) { } + public TimelineUserNotMemberException(string message) : base(message) { } + public TimelineUserNotMemberException(string message, Exception inner) : base(message, inner) { } + protected TimelineUserNotMemberException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + } +} -- cgit v1.2.3 From 2852190501ea6636f93d6bc06f8eb4a11cab1077 Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Mon, 4 Nov 2019 22:58:24 +0800 Subject: Add controller primarily and of course redesign the service accordingly. --- Timeline/Controllers/PersonalTimelineController.cs | 141 +++++++++++++++++++++ Timeline/Entities/TimelineEntity.cs | 10 ++ Timeline/Models/Http/Timeline.cs | 24 ++++ .../Controllers/TimelineController.Designer.cs | 90 +++++++++++++ .../Resources/Controllers/TimelineController.resx | 129 +++++++++++++++++++ .../Controllers/TimelineController.zh.resx | 126 ++++++++++++++++++ Timeline/Services/TimelineService.cs | 64 +++++++--- .../Services/TimelineUserNotMemberException.cs | 15 --- Timeline/Timeline.csproj | 9 ++ 9 files changed, 575 insertions(+), 33 deletions(-) create mode 100644 Timeline/Controllers/PersonalTimelineController.cs create mode 100644 Timeline/Models/Http/Timeline.cs create mode 100644 Timeline/Resources/Controllers/TimelineController.Designer.cs create mode 100644 Timeline/Resources/Controllers/TimelineController.resx create mode 100644 Timeline/Resources/Controllers/TimelineController.zh.resx delete mode 100644 Timeline/Services/TimelineUserNotMemberException.cs (limited to 'Timeline/Services/TimelineService.cs') diff --git a/Timeline/Controllers/PersonalTimelineController.cs b/Timeline/Controllers/PersonalTimelineController.cs new file mode 100644 index 00000000..1535a0b2 --- /dev/null +++ b/Timeline/Controllers/PersonalTimelineController.cs @@ -0,0 +1,141 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Auth; +using Timeline.Entities; +using Timeline.Filters; +using Timeline.Models; +using Timeline.Models.Http; +using Timeline.Models.Validation; +using Timeline.Services; +using static Timeline.Resources.Controllers.TimelineController; + +namespace Timeline +{ + public static partial class ErrorCodes + { + public static partial class Http + { + public static class Timeline // ccc = 004 + { + public const int PostsGetForbid = 10040101; + public const int PostsCreateForbid = 10040102; + } + } + } +} + +namespace Timeline.Controllers +{ + [ApiController] + public class PersonalTimelineController : Controller + { + private readonly IPersonalTimelineService _service; + + private bool IsAdmin() + { + if (User != null) + { + return User.IsAdministrator(); + } + return false; + } + + private string? GetAuthUsername() + { + if (User == null) + { + return null; + } + else + { + return User.Identity.Name; + } + } + + public PersonalTimelineController(IPersonalTimelineService service) + { + _service = service; + } + + [HttpGet("users/{username}/timeline")] + public async Task> TimelineGet([FromRoute][Username] string username) + { + return await _service.GetTimeline(username); + } + + [HttpGet("users/{username}/timeline/posts")] + public async Task>> PostsGet([FromRoute][Username] string username) + { + if (!IsAdmin() && !await _service.HasReadPermission(username, GetAuthUsername())) + { + return StatusCode(StatusCodes.Status403Forbidden, + new CommonResponse(ErrorCodes.Http.Timeline.PostsGetForbid, MessagePostsGetForbid)); + } + + return await _service.GetPosts(username); + } + + [HttpPost("user/{username}/timeline/posts/create")] + [Authorize] + public async Task PostsCreate([FromRoute][Username] string username, [FromBody] TimelinePostCreateRequest body) + { + if (!IsAdmin() && !await _service.IsMemberOf(username, GetAuthUsername()!)) + { + return StatusCode(StatusCodes.Status403Forbidden, + new CommonResponse(ErrorCodes.Http.Timeline.PostsCreateForbid, MessagePostsCreateForbid)); + } + + await _service.CreatePost(username, User.Identity.Name!, body.Content, body.Time); + return Ok(); + } + + [HttpPut("user/{username}/timeline/description")] + [Authorize] + [SelfOrAdmin] + public async Task TimelinePutDescription([FromRoute][Username] string username, [FromBody] string body) + { + await _service.SetDescription(username, body); + return Ok(); + } + + private static TimelineVisibility StringToVisibility(string s) + { + if ("public".Equals(s, StringComparison.InvariantCultureIgnoreCase)) + { + return TimelineVisibility.Public; + } + else if ("register".Equals(s, StringComparison.InvariantCultureIgnoreCase)) + { + return TimelineVisibility.Register; + } + else if ("private".Equals(s, StringComparison.InvariantCultureIgnoreCase)) + { + return TimelineVisibility.Private; + } + throw new ArgumentException(ExceptionStringToVisibility); + } + + [HttpPut("user/{username}/timeline/visibility")] + [Authorize] + [SelfOrAdmin] + public async Task TimelinePutVisibility([FromRoute][Username] string username, [FromBody][RegularExpression("public|register|private")] string body) + { + await _service.SetVisibility(username, StringToVisibility(body)); + return Ok(); + } + + [HttpPost("user/{username}/timeline/members/change")] + [Authorize] + [SelfOrAdmin] + public async Task TimelineMembersChange([FromRoute][Username] string username, [FromBody] TimelineMemberChangeRequest body) + { + //TODO! + } + } +} diff --git a/Timeline/Entities/TimelineEntity.cs b/Timeline/Entities/TimelineEntity.cs index f4c7045d..f5e22a54 100644 --- a/Timeline/Entities/TimelineEntity.cs +++ b/Timeline/Entities/TimelineEntity.cs @@ -7,7 +7,17 @@ namespace Timeline.Entities { public enum TimelineVisibility { + /// + /// All people including those without accounts. + /// Public, + /// + /// Only people signed in. + /// + Register, + /// + /// Only member. + /// Private } diff --git a/Timeline/Models/Http/Timeline.cs b/Timeline/Models/Http/Timeline.cs new file mode 100644 index 00000000..37de9e58 --- /dev/null +++ b/Timeline/Models/Http/Timeline.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace Timeline.Models.Http +{ + public class TimelinePostCreateRequest + { + [Required(AllowEmptyStrings = false)] + public string Content { get; set; } = default!; + + public DateTime? Time { get; set; } + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "This is a DTO class.")] + public class TimelineMemberChangeRequest + { + public List? Add { get; set; } + + public List? Remove { get; set; } + } +} diff --git a/Timeline/Resources/Controllers/TimelineController.Designer.cs b/Timeline/Resources/Controllers/TimelineController.Designer.cs new file mode 100644 index 00000000..1e56f651 --- /dev/null +++ b/Timeline/Resources/Controllers/TimelineController.Designer.cs @@ -0,0 +1,90 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Resources.Controllers { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class TimelineController { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal TimelineController() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Controllers.TimelineController", typeof(TimelineController).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to An unknown timeline visibility value. Can't convert it.. + /// + internal static string ExceptionStringToVisibility { + get { + return ResourceManager.GetString("ExceptionStringToVisibility", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You have no permission to create posts in the timeline.. + /// + internal static string MessagePostsCreateForbid { + get { + return ResourceManager.GetString("MessagePostsCreateForbid", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You have no permission to read posts of the timeline.. + /// + internal static string MessagePostsGetForbid { + get { + return ResourceManager.GetString("MessagePostsGetForbid", resourceCulture); + } + } + } +} diff --git a/Timeline/Resources/Controllers/TimelineController.resx b/Timeline/Resources/Controllers/TimelineController.resx new file mode 100644 index 00000000..420ac419 --- /dev/null +++ b/Timeline/Resources/Controllers/TimelineController.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + An unknown timeline visibility value. Can't convert it. + + + You have no permission to create posts in the timeline. + + + You have no permission to read posts of the timeline. + + \ No newline at end of file diff --git a/Timeline/Resources/Controllers/TimelineController.zh.resx b/Timeline/Resources/Controllers/TimelineController.zh.resx new file mode 100644 index 00000000..e22f44fa --- /dev/null +++ b/Timeline/Resources/Controllers/TimelineController.zh.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 你没有权限在这个时间线中创建消息。 + + + 你没有权限读取这个时间线消息。 + + \ No newline at end of file diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs index 9d309e7e..7fe32cac 100644 --- a/Timeline/Services/TimelineService.cs +++ b/Timeline/Services/TimelineService.cs @@ -61,7 +61,7 @@ namespace Timeline.Services /// /// Thrown if is of bad format. /// Thrown if does not exist. - Task Post(string name, string author, string content, DateTime? time); + Task CreatePost(string name, string author, string content, DateTime? time); /// /// Set the visibility permission of a timeline. @@ -104,11 +104,12 @@ namespace Timeline.Services Task SetDescription(string name, string description); /// - /// Add members to a timeline. + /// Remove members to a timeline. /// /// Username or the timeline name. See remarks of . - /// A list of new members' usernames - /// Thrown when or is null. + /// A list of usernames of members to add. May be null. + /// A list of usernames of members to remove. May be null. + /// Thrown when is null. /// /// Thrown when timeline name is of bad format. /// For normal timeline, it means name is an empty string. @@ -122,19 +123,26 @@ namespace Timeline.Services /// and the inner exception should be a . /// /// - /// Thrown when an exception occurs on users in the list. + /// Thrown when an exception occurs on the user list. /// The inner exception is - /// when one of the username is not valid. + /// when one of the username is invalid. /// The inner exception is - /// when one of the user does not exist. - /// - Task AddMember(string name, IList usernames); + /// when one of the user to add does not exist. + /// + /// + /// Operating on a username that is of bad format always throws. + /// Add a user that already is a member has no effects. + /// Remove a user that is not a member also has not effects. + /// Add a user that does not exist will throw . + /// But remove one does not throw. + /// + Task ChangeMember(string name, IList? add, IList? remove); /// - /// Remove members to a timeline. + /// Verify whether a visitor has the permission to read a timeline. /// /// Username or the timeline name. See remarks of . - /// A list of members' usernames + /// The user to check on. Null means visitor without account. /// Thrown when is null. /// /// Thrown when timeline name is of bad format. @@ -148,14 +156,34 @@ namespace Timeline.Services /// For personal timeline, it means the user of that username does not exist /// and the inner exception should be a . /// - /// - /// Thrown when an exception occurs on the user list. - /// The inner exception is - /// when one of the username is invalid. - /// The inner exception is - /// when one of the user is not a member of the timeline. + /// True if can read, false if can't read. + Task HasReadPermission(string name, string? username); + + /// + /// Verify whether a user is member of a timeline. + /// + /// Username or the timeline name. See remarks of . + /// The user to check on. + /// Thrown when or is null. + /// + /// Thrown when timeline name is of bad format. + /// For normal timeline, it means name is an empty string. + /// For personal timeline, it means the username is of bad format, + /// the inner exception should be a . + /// + /// + /// Thrown when timeline does not exist. + /// For normal timeline, it means the name does not exist. + /// For personal timeline, it means the user of that username does not exist + /// and the inner exception should be a . /// - Task RemoveMember(string name, IList usernames); + /// + /// Thrown when is not a valid username. + /// + /// + /// Thrown when user does not exist. + /// True if it is a member, false if not. + Task IsMemberOf(string name, string username); } /// diff --git a/Timeline/Services/TimelineUserNotMemberException.cs b/Timeline/Services/TimelineUserNotMemberException.cs deleted file mode 100644 index 260beb02..00000000 --- a/Timeline/Services/TimelineUserNotMemberException.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace Timeline.Services -{ - [Serializable] - public class TimelineUserNotMemberException : Exception - { - public TimelineUserNotMemberException() : base(Resources.Services.Exception.TimelineUserNotMemberException) { } - public TimelineUserNotMemberException(string message) : base(message) { } - public TimelineUserNotMemberException(string message, Exception inner) : base(message, inner) { } - protected TimelineUserNotMemberException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - } -} diff --git a/Timeline/Timeline.csproj b/Timeline/Timeline.csproj index bd195475..7e1dd4ef 100644 --- a/Timeline/Timeline.csproj +++ b/Timeline/Timeline.csproj @@ -49,6 +49,11 @@ True TestingI18nController.resx + + True + True + TimelineController.resx + True True @@ -119,6 +124,10 @@ ResXFileCodeGenerator TestingI18nController.Designer.cs + + ResXFileCodeGenerator + TimelineController.Designer.cs + Designer ResXFileCodeGenerator -- cgit v1.2.3 From 2f36e9a1c8d6db2a808f874134c9cb7d57c3ef16 Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Thu, 7 Nov 2019 22:06:06 +0800 Subject: Complete PersonalTimelineController and write attribute test. --- .../Controllers/PersonalTimelineControllerTest.cs | 111 +++++++++++++++++++++ Timeline/Controllers/PersonalTimelineController.cs | 86 +++++++++------- Timeline/Filters/Timeline.cs | 47 +++++++++ Timeline/Models/Http/Timeline.cs | 21 ++++ .../Controllers/TimelineController.Designer.cs | 27 +++++ .../Resources/Controllers/TimelineController.resx | 9 ++ .../Controllers/TimelineController.zh.resx | 6 ++ Timeline/Resources/Filters.Designer.cs | 18 ++++ Timeline/Resources/Filters.resx | 6 ++ Timeline/Resources/Filters.zh.resx | 6 ++ Timeline/Resources/Services/Exception.Designer.cs | 15 ++- Timeline/Resources/Services/Exception.resx | 7 +- .../TimelineMemberOperationUserException.cs | 15 ++- Timeline/Services/TimelinePostNotExistException.cs | 23 +++++ Timeline/Services/TimelineService.cs | 62 ++++++++++-- 15 files changed, 406 insertions(+), 53 deletions(-) create mode 100644 Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs create mode 100644 Timeline/Filters/Timeline.cs create mode 100644 Timeline/Services/TimelinePostNotExistException.cs (limited to 'Timeline/Services/TimelineService.cs') diff --git a/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs b/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs new file mode 100644 index 00000000..d5c470ee --- /dev/null +++ b/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Controllers; +using Timeline.Services; +using Moq; +using Microsoft.Extensions.Logging.Abstractions; +using Xunit; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc; +using Timeline.Filters; +using Timeline.Tests.Helpers; +using Timeline.Models.Validation; +using System.Reflection; +using Microsoft.AspNetCore.Authorization; +using Timeline.Models.Http; + +namespace Timeline.Tests.Controllers +{ + public class PersonalTimelineControllerTest : IDisposable + { + private readonly Mock _service; + + private readonly PersonalTimelineController _controller; + + public PersonalTimelineControllerTest() + { + _service = new Mock(); + _controller = new PersonalTimelineController(NullLogger.Instance, _service.Object); + } + + public void Dispose() + { + _controller.Dispose(); + } + + [Fact] + public void AttributeTest() + { + static void AssertUsernameParameter(MethodInfo m) + { + m.GetParameter("username") + .Should().BeDecoratedWith() + .And.BeDecoratedWith(); + } + + static void AssertBodyParamter(MethodInfo m) + { + var p = m.GetParameter("body"); + p.Should().BeDecoratedWith(); + p.ParameterType.Should().Be(typeof(TBody)); + } + + var type = typeof(PersonalTimelineController); + type.Should().BeDecoratedWith(); + + { + var m = type.GetMethod(nameof(PersonalTimelineController.TimelineGet)); + m.Should().BeDecoratedWith() + .And.BeDecoratedWith(); + AssertUsernameParameter(m); + } + + { + var m = type.GetMethod(nameof(PersonalTimelineController.PostsGet)); + m.Should().BeDecoratedWith() + .And.BeDecoratedWith(); + AssertUsernameParameter(m); + } + + { + var m = type.GetMethod(nameof(PersonalTimelineController.TimelinePost)); + m.Should().BeDecoratedWith() + .And.BeDecoratedWith() + .And.BeDecoratedWith(); + AssertUsernameParameter(m); + AssertBodyParamter(m); + } + + { + var m = type.GetMethod(nameof(PersonalTimelineController.TimelinePostDelete)); + m.Should().BeDecoratedWith() + .And.BeDecoratedWith() + .And.BeDecoratedWith(); + AssertUsernameParameter(m); + AssertBodyParamter(m); + } + + { + var m = type.GetMethod(nameof(PersonalTimelineController.TimelineChangeProperty)); + m.Should().BeDecoratedWith() + .And.BeDecoratedWith() + .And.BeDecoratedWith() + .And.BeDecoratedWith(); + AssertUsernameParameter(m); + AssertBodyParamter(m); + } + + { + var m = type.GetMethod(nameof(PersonalTimelineController.TimelineChangeMember)); + m.Should().BeDecoratedWith() + .And.BeDecoratedWith() + .And.BeDecoratedWith() + .And.BeDecoratedWith(); + AssertUsernameParameter(m); + AssertBodyParamter(m); + } + } + } +} diff --git a/Timeline/Controllers/PersonalTimelineController.cs b/Timeline/Controllers/PersonalTimelineController.cs index 1535a0b2..f006ad47 100644 --- a/Timeline/Controllers/PersonalTimelineController.cs +++ b/Timeline/Controllers/PersonalTimelineController.cs @@ -1,13 +1,11 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using System; +using Microsoft.Extensions.Logging; using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; +using System.Globalization; using System.Threading.Tasks; using Timeline.Auth; -using Timeline.Entities; using Timeline.Filters; using Timeline.Models; using Timeline.Models.Http; @@ -25,6 +23,7 @@ namespace Timeline { public const int PostsGetForbid = 10040101; public const int PostsCreateForbid = 10040102; + public const int MemberAddNotExist = 10040201; } } } @@ -35,6 +34,8 @@ namespace Timeline.Controllers [ApiController] public class PersonalTimelineController : Controller { + private readonly ILogger _logger; + private readonly IPersonalTimelineService _service; private bool IsAdmin() @@ -58,18 +59,21 @@ namespace Timeline.Controllers } } - public PersonalTimelineController(IPersonalTimelineService service) + public PersonalTimelineController(ILogger logger, IPersonalTimelineService service) { + _logger = logger; _service = service; } [HttpGet("users/{username}/timeline")] + [CatchTimelineNotExistException] public async Task> TimelineGet([FromRoute][Username] string username) { return await _service.GetTimeline(username); } [HttpGet("users/{username}/timeline/posts")] + [CatchTimelineNotExistException] public async Task>> PostsGet([FromRoute][Username] string username) { if (!IsAdmin() && !await _service.HasReadPermission(username, GetAuthUsername())) @@ -81,9 +85,10 @@ namespace Timeline.Controllers return await _service.GetPosts(username); } - [HttpPost("user/{username}/timeline/posts/create")] + [HttpPost("user/{username}/timeline/postop/create")] [Authorize] - public async Task PostsCreate([FromRoute][Username] string username, [FromBody] TimelinePostCreateRequest body) + [CatchTimelineNotExistException] + public async Task> TimelinePost([FromRoute][Username] string username, [FromBody] TimelinePostCreateRequest body) { if (!IsAdmin() && !await _service.IsMemberOf(username, GetAuthUsername()!)) { @@ -91,51 +96,62 @@ namespace Timeline.Controllers new CommonResponse(ErrorCodes.Http.Timeline.PostsCreateForbid, MessagePostsCreateForbid)); } - await _service.CreatePost(username, User.Identity.Name!, body.Content, body.Time); - return Ok(); + var res = await _service.CreatePost(username, User.Identity.Name!, body.Content, body.Time); + return res; } - [HttpPut("user/{username}/timeline/description")] + [HttpPost("user/{username}/timeline/postop/delete")] [Authorize] - [SelfOrAdmin] - public async Task TimelinePutDescription([FromRoute][Username] string username, [FromBody] string body) - { - await _service.SetDescription(username, body); - return Ok(); - } - - private static TimelineVisibility StringToVisibility(string s) + [CatchTimelineNotExistException] + public async Task TimelinePostDelete([FromRoute][Username] string username, [FromBody] TimelinePostDeleteRequest body) { - if ("public".Equals(s, StringComparison.InvariantCultureIgnoreCase)) + var postId = body.Id!.Value; + if (!IsAdmin() && !await _service.HasPostModifyPermission(username, postId, GetAuthUsername()!)) { - return TimelineVisibility.Public; - } - else if ("register".Equals(s, StringComparison.InvariantCultureIgnoreCase)) - { - return TimelineVisibility.Register; - } - else if ("private".Equals(s, StringComparison.InvariantCultureIgnoreCase)) - { - return TimelineVisibility.Private; + return StatusCode(StatusCodes.Status403Forbidden, + new CommonResponse(ErrorCodes.Http.Timeline.PostsCreateForbid, MessagePostsCreateForbid)); } - throw new ArgumentException(ExceptionStringToVisibility); + await _service.DeletePost(username, postId); + return Ok(); } - [HttpPut("user/{username}/timeline/visibility")] + [HttpPost("user/{username}/timeline/op/property")] [Authorize] [SelfOrAdmin] - public async Task TimelinePutVisibility([FromRoute][Username] string username, [FromBody][RegularExpression("public|register|private")] string body) + [CatchTimelineNotExistException] + public async Task TimelineChangeProperty([FromRoute][Username] string username, [FromBody] TimelinePropertyChangeRequest body) { - await _service.SetVisibility(username, StringToVisibility(body)); + await _service.ChangeProperty(username, body); return Ok(); } - [HttpPost("user/{username}/timeline/members/change")] + [HttpPost("user/{username}/timeline/op/member")] [Authorize] [SelfOrAdmin] - public async Task TimelineMembersChange([FromRoute][Username] string username, [FromBody] TimelineMemberChangeRequest body) + [CatchTimelineNotExistException] + public async Task TimelineChangeMember([FromRoute][Username] string username, [FromBody] TimelineMemberChangeRequest body) { - //TODO! + try + { + await _service.ChangeMember(username, body.Add, body.Remove); + return Ok(); + } + catch (TimelineMemberOperationUserException e) + { + if (e.InnerException is UsernameBadFormatException) + { + return BadRequest(CommonResponse.InvalidModel( + string.Format(CultureInfo.CurrentCulture, MessageMemberUsernameBadFormat, e.Index, e.Operation))); + } + else if (e.InnerException is UserNotExistException) + { + return BadRequest(new CommonResponse(ErrorCodes.Http.Timeline.MemberAddNotExist, + string.Format(CultureInfo.CurrentCulture, MessageMemberUserNotExist, e.Index, e.Operation))); + } + + _logger.LogError(e, LogUnknownTimelineMemberOperationUserException); + throw; + } } } } diff --git a/Timeline/Filters/Timeline.cs b/Timeline/Filters/Timeline.cs new file mode 100644 index 00000000..7859d409 --- /dev/null +++ b/Timeline/Filters/Timeline.cs @@ -0,0 +1,47 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Timeline.Models.Http; +using Timeline.Services; +using static Timeline.Resources.Filters; + +namespace Timeline +{ + public static partial class ErrorCodes + { + public static partial class Http + { + public static partial class Filter // bxx = 1xx + { + public static class Timeline // bbb = 102 + { + public const int UserNotExist = 11020101; + public const int NameNotExist = 11020102; + } + } + } + } +} + +namespace Timeline.Filters +{ + public class CatchTimelineNotExistExceptionAttribute : ExceptionFilterAttribute + { + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")] + public override void OnException(ExceptionContext context) + { + if (context.Exception is TimelineNotExistException e) + { + if (e.InnerException is UserNotExistException) + { + context.Result = new BadRequestObjectResult( + new CommonResponse(ErrorCodes.Http.Filter.Timeline.UserNotExist, MessageTimelineNotExistUser)); + } + else + { + context.Result = new BadRequestObjectResult( + new CommonResponse(ErrorCodes.Http.Filter.Timeline.NameNotExist, MessageTimelineNotExist)); + } + } + } + } +} diff --git a/Timeline/Models/Http/Timeline.cs b/Timeline/Models/Http/Timeline.cs index 37de9e58..f676afa0 100644 --- a/Timeline/Models/Http/Timeline.cs +++ b/Timeline/Models/Http/Timeline.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; +using Timeline.Entities; namespace Timeline.Models.Http { @@ -14,6 +15,26 @@ namespace Timeline.Models.Http public DateTime? Time { get; set; } } + public class TimelinePostCreateResponse + { + public long Id { get; set; } + + public DateTime Time { get; set; } + } + + public class TimelinePostDeleteRequest + { + [Required] + public long? Id { get; set; } + } + + public class TimelinePropertyChangeRequest + { + public string? Description { get; set; } + + public TimelineVisibility? Visibility { get; set; } + } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "This is a DTO class.")] public class TimelineMemberChangeRequest { diff --git a/Timeline/Resources/Controllers/TimelineController.Designer.cs b/Timeline/Resources/Controllers/TimelineController.Designer.cs index 1e56f651..5a4209c3 100644 --- a/Timeline/Resources/Controllers/TimelineController.Designer.cs +++ b/Timeline/Resources/Controllers/TimelineController.Designer.cs @@ -69,6 +69,33 @@ namespace Timeline.Resources.Controllers { } } + /// + /// Looks up a localized string similar to An unknown TimelineMemberOperationUserException is thrown. Can't recognize its inner exception. It is rethrown.. + /// + internal static string LogUnknownTimelineMemberOperationUserException { + get { + return ResourceManager.GetString("LogUnknownTimelineMemberOperationUserException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The {0}-st username to do operation {1} on is of bad format.. + /// + internal static string MessageMemberUsernameBadFormat { + get { + return ResourceManager.GetString("MessageMemberUsernameBadFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The {0}-st user to do operation {1} on does not exist.. + /// + internal static string MessageMemberUserNotExist { + get { + return ResourceManager.GetString("MessageMemberUserNotExist", resourceCulture); + } + } + /// /// Looks up a localized string similar to You have no permission to create posts in the timeline.. /// diff --git a/Timeline/Resources/Controllers/TimelineController.resx b/Timeline/Resources/Controllers/TimelineController.resx index 420ac419..7e323164 100644 --- a/Timeline/Resources/Controllers/TimelineController.resx +++ b/Timeline/Resources/Controllers/TimelineController.resx @@ -120,6 +120,15 @@ An unknown timeline visibility value. Can't convert it. + + An unknown TimelineMemberOperationUserException is thrown. Can't recognize its inner exception. It is rethrown. + + + The {0}-st username to do operation {1} on is of bad format. + + + The {0}-st user to do operation {1} on does not exist. + You have no permission to create posts in the timeline. diff --git a/Timeline/Resources/Controllers/TimelineController.zh.resx b/Timeline/Resources/Controllers/TimelineController.zh.resx index e22f44fa..cacce5fa 100644 --- a/Timeline/Resources/Controllers/TimelineController.zh.resx +++ b/Timeline/Resources/Controllers/TimelineController.zh.resx @@ -117,6 +117,12 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + 第{0}个做{1}操作的用户名格式错误。 + + + 第{0}个做{1}操作的用户不存在。 + 你没有权限在这个时间线中创建消息。 diff --git a/Timeline/Resources/Filters.Designer.cs b/Timeline/Resources/Filters.Designer.cs index 3481e4ae..5576190d 100644 --- a/Timeline/Resources/Filters.Designer.cs +++ b/Timeline/Resources/Filters.Designer.cs @@ -123,6 +123,24 @@ namespace Timeline.Resources { } } + /// + /// Looks up a localized string similar to The requested timeline does not exist.. + /// + internal static string MessageTimelineNotExist { + get { + return ResourceManager.GetString("MessageTimelineNotExist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The requested personal timeline does not exist because the user does not exist.. + /// + internal static string MessageTimelineNotExistUser { + get { + return ResourceManager.GetString("MessageTimelineNotExistUser", resourceCulture); + } + } + /// /// Looks up a localized string similar to The user does not exist.. /// diff --git a/Timeline/Resources/Filters.resx b/Timeline/Resources/Filters.resx index b91d4612..7bfbc703 100644 --- a/Timeline/Resources/Filters.resx +++ b/Timeline/Resources/Filters.resx @@ -138,6 +138,12 @@ You can't access the resource unless you are the owner or administrator. + + The requested timeline does not exist. + + + The requested personal timeline does not exist because the user does not exist. + The user does not exist. diff --git a/Timeline/Resources/Filters.zh.resx b/Timeline/Resources/Filters.zh.resx index 159ac04a..36aac788 100644 --- a/Timeline/Resources/Filters.zh.resx +++ b/Timeline/Resources/Filters.zh.resx @@ -129,6 +129,12 @@ 你无权访问该资源除非你是资源的拥有者或者管理员。 + + 请求的时间线不存在。 + + + 请求的个人时间线不存在因为该用户不存在。 + 用户不存在。 diff --git a/Timeline/Resources/Services/Exception.Designer.cs b/Timeline/Resources/Services/Exception.Designer.cs index 1f6493cb..970c306d 100644 --- a/Timeline/Resources/Services/Exception.Designer.cs +++ b/Timeline/Resources/Services/Exception.Designer.cs @@ -286,11 +286,11 @@ namespace Timeline.Resources.Services { } /// - /// Looks up a localized string similar to An exception happened when operating on the {} member on timeline.. + /// Looks up a localized string similar to An exception happened when do operation {} on the {} member on timeline.. /// - internal static string TimelineMemberOperationExceptionIndex { + internal static string TimelineMemberOperationExceptionDetail { get { - return ResourceManager.GetString("TimelineMemberOperationExceptionIndex", resourceCulture); + return ResourceManager.GetString("TimelineMemberOperationExceptionDetail", resourceCulture); } } @@ -312,6 +312,15 @@ namespace Timeline.Resources.Services { } } + /// + /// Looks up a localized string similar to The timeline post does not exist. You can't do operation on it.. + /// + internal static string TimelinePostNotExistException { + get { + return ResourceManager.GetString("TimelinePostNotExistException", resourceCulture); + } + } + /// /// Looks up a localized string similar to The use is not a member of the timeline.. /// diff --git a/Timeline/Resources/Services/Exception.resx b/Timeline/Resources/Services/Exception.resx index 3e9d3747..c8f6676a 100644 --- a/Timeline/Resources/Services/Exception.resx +++ b/Timeline/Resources/Services/Exception.resx @@ -192,8 +192,8 @@ An exception happened when add or remove member on timeline. - - An exception happened when operating on the {} member on timeline. + + An exception happened when do operation {} on the {} member on timeline. Timeline name is of bad format. If this is a personal timeline, it means the username is of bad format and inner exception should be a UsernameBadFormatException. @@ -201,6 +201,9 @@ Timeline does not exist. If this is a personal timeline, it means the user does not exist and inner exception should be a UserNotExistException. + + The timeline post does not exist. You can't do operation on it. + The use is not a member of the timeline. diff --git a/Timeline/Services/TimelineMemberOperationUserException.cs b/Timeline/Services/TimelineMemberOperationUserException.cs index 998f1a6e..543ee160 100644 --- a/Timeline/Services/TimelineMemberOperationUserException.cs +++ b/Timeline/Services/TimelineMemberOperationUserException.cs @@ -6,6 +6,12 @@ namespace Timeline.Services [Serializable] public class TimelineMemberOperationUserException : Exception { + public enum MemberOperation + { + Add, + Remove + } + public TimelineMemberOperationUserException() : base(Resources.Services.Exception.TimelineMemberOperationException) { } public TimelineMemberOperationUserException(string message) : base(message) { } public TimelineMemberOperationUserException(string message, Exception inner) : base(message, inner) { } @@ -13,10 +19,13 @@ namespace Timeline.Services System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - public TimelineMemberOperationUserException(int index, string username, Exception inner) : base(MakeIndexMessage(index), inner) { Index = index; Username = username; } + public TimelineMemberOperationUserException(int index, MemberOperation operation, string username, Exception inner) + : base(MakeMessage(operation, index), inner) { Operation = operation; Index = index; Username = username; } + + private static string MakeMessage(MemberOperation operation, int index) => string.Format(CultureInfo.CurrentCulture, + Resources.Services.Exception.TimelineMemberOperationExceptionDetail, operation, index); - private static string MakeIndexMessage(int index) => string.Format(CultureInfo.CurrentCulture, - Resources.Services.Exception.TimelineMemberOperationExceptionIndex, index); + public MemberOperation? Operation { get; set; } /// /// The index of the member on which the operation failed. diff --git a/Timeline/Services/TimelinePostNotExistException.cs b/Timeline/Services/TimelinePostNotExistException.cs new file mode 100644 index 00000000..97e5d550 --- /dev/null +++ b/Timeline/Services/TimelinePostNotExistException.cs @@ -0,0 +1,23 @@ +using System; + +namespace Timeline.Services +{ + [Serializable] + public class TimelinePostNotExistException : Exception + { + public TimelinePostNotExistException() { } + public TimelinePostNotExistException(string message) : base(message) { } + public TimelinePostNotExistException(string message, Exception inner) : base(message, inner) { } + protected TimelinePostNotExistException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + public TimelinePostNotExistException(long id) : base(Resources.Services.Exception.TimelinePostNotExistException) { Id = id; } + + public TimelinePostNotExistException(long id, string message) : base(message) { Id = id; } + + public TimelinePostNotExistException(long id, string message, Exception inner) : base(message, inner) { Id = id; } + + public long Id { get; set; } + } +} diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs index 7fe32cac..28b1f91d 100644 --- a/Timeline/Services/TimelineService.cs +++ b/Timeline/Services/TimelineService.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading.Tasks; using Timeline.Entities; using Timeline.Models; +using Timeline.Models.Http; namespace Timeline.Services { @@ -45,7 +46,7 @@ namespace Timeline.Services /// The author's username. /// The content. /// The time of the post. If null, then use current time. - /// + /// The info of the created post. /// Thrown when or or is null. /// /// Thrown when timeline name is of bad format. @@ -61,14 +62,14 @@ namespace Timeline.Services /// /// Thrown if is of bad format. /// Thrown if does not exist. - Task CreatePost(string name, string author, string content, DateTime? time); + Task CreatePost(string name, string author, string content, DateTime? time); /// - /// Set the visibility permission of a timeline. + /// Delete a post /// /// Username or the timeline name. See remarks of . - /// The new visibility. - /// Thrown when is null. + /// The id of the post to delete. + /// Thrown when or is null. /// /// Thrown when timeline name is of bad format. /// For normal timeline, it means name is an empty string. @@ -81,14 +82,21 @@ namespace Timeline.Services /// For personal timeline, it means the user of that username does not exist /// and the inner exception should be a . /// - Task SetVisibility(string name, TimelineVisibility visibility); + /// + /// Thrown when the post with given id does not exist or is deleted already. + /// + /// + /// First use + /// to check the permission. + /// + Task DeletePost(string name, long id); /// - /// Set the description of a timeline. + /// Set the properties of a timeline. /// /// Username or the timeline name. See remarks of . - /// The new description. - /// Thrown when or is null. + /// The new properties. Null member means not to change. + /// Thrown when or is null. /// /// Thrown when timeline name is of bad format. /// For normal timeline, it means name is an empty string. @@ -101,7 +109,7 @@ namespace Timeline.Services /// For personal timeline, it means the user of that username does not exist /// and the inner exception should be a . /// - Task SetDescription(string name, string description); + Task ChangeProperty(string name, TimelinePropertyChangeRequest newProperties); /// /// Remove members to a timeline. @@ -159,6 +167,40 @@ namespace Timeline.Services /// True if can read, false if can't read. Task HasReadPermission(string name, string? username); + /// + /// Verify whether a user has the permission to modify a post. + /// + /// Username or the timeline name. See remarks of . + /// The user to check on. + /// True if can modify, false if can't modify. + /// Thrown when or is null. + /// + /// Thrown when timeline name is of bad format. + /// For normal timeline, it means name is an empty string. + /// For personal timeline, it means the username is of bad format, + /// the inner exception should be a . + /// + /// + /// Thrown when timeline does not exist. + /// For normal timeline, it means the name does not exist. + /// For personal timeline, it means the user of that username does not exist + /// and the inner exception should be a . + /// + /// + /// Thrown when the post with given id does not exist or is deleted already. + /// + /// + /// Thrown when is of bad format. + /// + /// + /// Thrown when does not exist. + /// + /// + /// 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. + /// + Task HasPostModifyPermission(string name, long id, string username); + /// /// Verify whether a user is member of a timeline. /// -- cgit v1.2.3 From 16b4720938ca42b777a10ba67d400531dcc1db35 Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Wed, 13 Nov 2019 22:55:31 +0800 Subject: WIP : Write timeline service. --- .../Controllers/PersonalTimelineControllerTest.cs | 2 +- Timeline/Controllers/PersonalTimelineController.cs | 4 +- Timeline/Entities/DatabaseContext.cs | 1 + Timeline/Services/TimelineService.cs | 336 ++++++++++++++++++++- Timeline/Services/UsernameBadFormatException.cs | 4 +- 5 files changed, 336 insertions(+), 11 deletions(-) (limited to 'Timeline/Services/TimelineService.cs') diff --git a/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs b/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs index aecd10af..a7cbb37e 100644 --- a/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs +++ b/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs @@ -367,7 +367,7 @@ namespace Timeline.Tests.Controllers }); result.Should().NotBeNull().And.BeAssignableTo() .Which.Value.Should().BeAssignableTo() - .Which.Code.Should().Be(ErrorCodes.Http.Timeline.MemberAddNotExist); + .Which.Code.Should().Be(ErrorCodes.Http.Timeline.ChangeMemberUserNotExist); _service.VerifyAll(); } diff --git a/Timeline/Controllers/PersonalTimelineController.cs b/Timeline/Controllers/PersonalTimelineController.cs index f0f4e4c2..af6a70f8 100644 --- a/Timeline/Controllers/PersonalTimelineController.cs +++ b/Timeline/Controllers/PersonalTimelineController.cs @@ -25,7 +25,7 @@ namespace Timeline public const int PostOperationCreateForbid = 10040102; public const int PostOperationDeleteForbid = 10040103; public const int PostOperationDeleteNotExist = 10040201; - public const int MemberAddNotExist = 10040301; + public const int ChangeMemberUserNotExist = 10040301; } } } @@ -156,7 +156,7 @@ namespace Timeline.Controllers } else if (e.InnerException is UserNotExistException) { - return BadRequest(new CommonResponse(ErrorCodes.Http.Timeline.MemberAddNotExist, + return BadRequest(new CommonResponse(ErrorCodes.Http.Timeline.ChangeMemberUserNotExist, string.Format(CultureInfo.CurrentCulture, MessageMemberUserNotExist, e.Index, e.Operation))); } diff --git a/Timeline/Entities/DatabaseContext.cs b/Timeline/Entities/DatabaseContext.cs index 19df32c6..123ae0f3 100644 --- a/Timeline/Entities/DatabaseContext.cs +++ b/Timeline/Entities/DatabaseContext.cs @@ -22,5 +22,6 @@ namespace Timeline.Entities public DbSet UserDetails { get; set; } = default!; public DbSet Timelines { get; set; } = default!; public DbSet TimelinePosts { get; set; } = default!; + public DbSet TimelineMembers { get; set; } = default!; } } diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs index 28b1f91d..eff0c3fc 100644 --- a/Timeline/Services/TimelineService.cs +++ b/Timeline/Services/TimelineService.cs @@ -1,10 +1,12 @@ -using System; +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.Http; +using Timeline.Models.Validation; namespace Timeline.Services { @@ -135,14 +137,14 @@ namespace Timeline.Services /// The inner exception is /// when one of the username is invalid. /// The inner exception is - /// when one of the user to add does not exist. + /// when one of the user to change does not exist. /// /// - /// Operating on a username that is of bad format always throws. + /// Operating on a username that is of bad format or does not exist always throws. /// Add a user that already is a member has no effects. /// Remove a user that is not a member also has not effects. - /// Add a user that does not exist will throw . - /// But remove one does not throw. + /// Add and remove an identical user results in no effects. + /// More than one same usernames are regarded as one. /// Task ChangeMember(string name, IList? add, IList? remove); @@ -151,6 +153,7 @@ namespace Timeline.Services /// /// Username or the timeline name. See remarks of . /// The user to check on. Null means visitor without account. + /// True if can read, false if can't read. /// Thrown when is null. /// /// Thrown when timeline name is of bad format. @@ -164,7 +167,12 @@ namespace Timeline.Services /// For personal timeline, it means the user of that username does not exist /// and the inner exception should be a . /// - /// True if can read, false if can't read. + /// + /// Thrown when is of bad format. + /// + /// + /// Thrown when does not exist. + /// Task HasReadPermission(string name, string? username); /// @@ -285,4 +293,320 @@ namespace Timeline.Services /// Task GetTimeline(string username); } + + public abstract class BaseTimelineService : IBaseTimelineService + { + protected BaseTimelineService(DatabaseContext database, IClock clock) + { + Clock = clock; + Database = database; + } + + protected IClock Clock { get; } + + protected UsernameValidator UsernameValidator { get; } = new UsernameValidator(); + + protected DatabaseContext Database { get; } + + /// + /// Find the timeline id by the name. + /// For details, see remarks. + /// + /// The username or the timeline name. See remarks. + /// The id of the timeline entity. + /// Thrown when is null. + /// + /// Thrown when timeline name is of bad format. + /// For normal timeline, it means name is an empty string. + /// For personal timeline, it means the username is of bad format, + /// the inner exception should be a . + /// + /// + /// Thrown when timeline does not exist. + /// For normal timeline, it means the name does not exist. + /// For personal timeline, it means the user of that username does not exist + /// and the inner exception should be a . + /// + /// + /// This is the common but different part for both types of timeline service. + /// For class that implements , this method should + /// find the timeline entity id by the given as the username of the owner. + /// For class that implements , this method should + /// find the timeline entity id by the given as the timeline name. + /// This method should be called by many other method that follows the contract. + /// + protected abstract Task FindTimelineId(string name); + + public async Task> GetPosts(string name) + { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + var timelineId = await FindTimelineId(name); + var postEntities = await Database.TimelinePosts.Where(p => p.TimelineId == timelineId).ToListAsync(); + var posts = new List(await Task.WhenAll(postEntities.Select(async p => new TimelinePostInfo + { + Id = p.Id, + Content = p.Content, + Author = (await Database.Users.Where(u => u.Id == p.AuthorId).Select(u => new { u.Name }).SingleAsync()).Name, + Time = p.Time + }))); + return posts; + } + + public async Task CreatePost(string name, string author, string content, DateTime? time) + { + if (name == null) + throw new ArgumentNullException(nameof(name)); + if (author == null) + throw new ArgumentNullException(nameof(author)); + if (content == null) + throw new ArgumentNullException(nameof(content)); + + { + var (result, message) = UsernameValidator.Validate(author); + if (!result) + { + throw new UsernameBadFormatException(author, message); + } + } + + var timelineId = await FindTimelineId(name); + + var authorEntity = Database.Users.Where(u => u.Name == author).Select(u => new { u.Id }).SingleOrDefault(); + if (authorEntity == null) + { + throw new UserNotExistException(author); + } + var authorId = authorEntity.Id; + + var currentTime = Clock.GetCurrentTime(); + + var postEntity = new TimelinePostEntity + { + Content = content, + AuthorId = authorId, + TimelineId = timelineId, + Time = time ?? currentTime, + LastUpdated = currentTime + }; + + Database.TimelinePosts.Add(postEntity); + await Database.SaveChangesAsync(); + + return new TimelinePostCreateResponse + { + Id = postEntity.Id, + Time = postEntity.Time + }; + } + + public async Task DeletePost(string name, long id) + { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + var timelineId = FindTimelineId(name); + + var post = await Database.TimelinePosts.Where(p => p.Id == id).SingleOrDefaultAsync(); + + if (post == null) + throw new TimelinePostNotExistException(id); + + Database.TimelinePosts.Remove(post); + await Database.SaveChangesAsync(); + } + + public async Task ChangeProperty(string name, TimelinePropertyChangeRequest newProperties) + { + if (name == null) + throw new ArgumentNullException(nameof(name)); + if (newProperties == null) + throw new ArgumentNullException(nameof(newProperties)); + + var timelineId = await FindTimelineId(name); + + var timelineEntity = await Database.Timelines.Where(t => t.Id == timelineId).SingleAsync(); + + if (newProperties.Description != null) + { + timelineEntity.Description = newProperties.Description; + } + + if (newProperties.Visibility.HasValue) + { + timelineEntity.Visibility = newProperties.Visibility.Value; + } + + await Database.SaveChangesAsync(); + } + + public async Task ChangeMember(string name, IList? add, IList? remove) + { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + // remove duplication and check the format of each username. + // Return a username->index map. + Dictionary? RemoveDuplicateAndCheckFormat(IList? list, TimelineMemberOperationUserException.MemberOperation operation) + { + if (list != null) + { + Dictionary result = new Dictionary(); + var count = 0; + for (var index = 0; index < count; index++) + { + var username = list[index]; + if (result.ContainsKey(username)) + { + continue; + } + var (validationResult, message) = UsernameValidator.Validate(username); + if (!validationResult) + throw new TimelineMemberOperationUserException( + index, operation, username, + new UsernameBadFormatException(username, message)); + result.Add(username, index); + } + return result; + } + else + { + return null; + } + } + var simplifiedAdd = RemoveDuplicateAndCheckFormat(add, TimelineMemberOperationUserException.MemberOperation.Add); + var simplifiedRemove = RemoveDuplicateAndCheckFormat(remove, TimelineMemberOperationUserException.MemberOperation.Remove); + + // remove those both in add and remove + if (simplifiedAdd != null && simplifiedRemove != null) + { + var usersToClean = simplifiedRemove.Keys.Where(u => simplifiedAdd.ContainsKey(u)); + foreach (var u in usersToClean) + { + simplifiedAdd.Remove(u); + simplifiedRemove.Remove(u); + } + } + + var timelineId = await FindTimelineId(name); + + async Task?> CheckExistenceAndGetId(Dictionary? map, TimelineMemberOperationUserException.MemberOperation operation) + { + if (map == null) + return null; + + List result = new List(); + foreach (var (username, index) in map) + { + var user = await Database.Users.Where(u => u.Name == username).Select(u => new { u.Id }).SingleOrDefaultAsync(); + if (user == null) + { + throw new TimelineMemberOperationUserException(index, operation, username, + new UserNotExistException(username)); + } + result.Add(user.Id); + } + return result; + } + var userIdsAdd = await CheckExistenceAndGetId(simplifiedAdd, TimelineMemberOperationUserException.MemberOperation.Add); + var userIdsRemove = await CheckExistenceAndGetId(simplifiedRemove, TimelineMemberOperationUserException.MemberOperation.Remove); + + if (userIdsAdd != null) + { + var membersToAdd = userIdsAdd.Select(id => new TimelineMemberEntity { UserId = id, TimelineId = timelineId }).ToList(); + Database.TimelineMembers.AddRange(membersToAdd); + } + + if (userIdsRemove != null) + { + var membersToRemove = await Database.TimelineMembers.Where(m => m.TimelineId == timelineId && userIdsRemove.Contains(m.UserId)).ToListAsync(); + Database.TimelineMembers.RemoveRange(membersToRemove); + } + + await Database.SaveChangesAsync(); + } + + public async Task HasReadPermission(string name, string? username) + { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + long? userId = null; + if (username != null) + { + var (result, message) = UsernameValidator.Validate(username); + if (!result) + { + throw new UsernameBadFormatException(username); + } + + var user = await Database.Users.Where(u => u.Name == username).Select(u => new { u.Id }).SingleOrDefaultAsync(); + + if (user == null) + { + throw new UserNotExistException(username); + } + + userId = user.Id; + } + + var timelineId = await FindTimelineId(name); + + var timelineEntity = await Database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.Visibility }).SingleAsync(); + + if (timelineEntity.Visibility == TimelineVisibility.Public) + return true; + + if (timelineEntity.Visibility == TimelineVisibility.Register && username != null) + return true; + + if (userId == null) + { + return false; + } + else + { + var memberEntity = await Database.TimelineMembers.Where(m => m.UserId == userId && m.TimelineId == timelineId).SingleOrDefaultAsync(); + return memberEntity != null; + } + } + + public async Task HasPostModifyPermission(string name, long id, string username) + { + if (name == null) + throw new ArgumentNullException(nameof(name)); + if (username == null) + throw new ArgumentNullException(nameof(username)); + + { + var (result, message) = UsernameValidator.Validate(username); + if (!result) + { + throw new UsernameBadFormatException(username); + } + } + + var user = await Database.Users.Where(u => u.Name == username).Select(u => new { u.Id }).SingleOrDefaultAsync(); + + if (user == null) + { + throw new UserNotExistException(username); + } + + var userId = user.Id; + + var timelineId = await FindTimelineId(name); + + var timelineEntity = await Database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync(); + + var postEntitu = await Database.Timelines. // TODO! + + if (timelineEntity.OwnerId == userId) + { + return true; + } + } + + } } diff --git a/Timeline/Services/UsernameBadFormatException.cs b/Timeline/Services/UsernameBadFormatException.cs index 04354d22..d82bf962 100644 --- a/Timeline/Services/UsernameBadFormatException.cs +++ b/Timeline/Services/UsernameBadFormatException.cs @@ -9,8 +9,8 @@ namespace Timeline.Services public class UsernameBadFormatException : Exception { public UsernameBadFormatException() : base(Resources.Services.Exception.UsernameBadFormatException) { } - public UsernameBadFormatException(string message) : base(message) { } - public UsernameBadFormatException(string message, Exception inner) : base(message, inner) { } + public UsernameBadFormatException(string username) : this() { Username = username; } + public UsernameBadFormatException(string username, Exception inner) : base(Resources.Services.Exception.UsernameBadFormatException, inner) { Username = username; } public UsernameBadFormatException(string username, string message) : base(message) { Username = username; } public UsernameBadFormatException(string username, string message, Exception inner) : base(message, inner) { Username = username; } -- cgit v1.2.3 From c2b107eda8a14f836d663638641a3e3f17373b5d Mon Sep 17 00:00:00 2001 From: crupest Date: Sun, 17 Nov 2019 18:56:27 +0800 Subject: Complete personal timeline service without tests. --- Timeline/Services/TimelineService.cs | 126 +++++++++++++++++++++++++++++++++-- 1 file changed, 121 insertions(+), 5 deletions(-) (limited to 'Timeline/Services/TimelineService.cs') diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs index eff0c3fc..494beb11 100644 --- a/Timeline/Services/TimelineService.cs +++ b/Timeline/Services/TimelineService.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Linq; @@ -214,6 +215,7 @@ namespace Timeline.Services /// /// Username or the timeline name. See remarks of . /// The user to check on. + /// True if it is a member, false if not. /// Thrown when or is null. /// /// Thrown when timeline name is of bad format. @@ -231,8 +233,11 @@ namespace Timeline.Services /// Thrown when is not a valid username. /// /// - /// Thrown when user does not exist. - /// True if it is a member, false if not. + /// Thrown when user does not exist. + /// + /// + /// Timeline owner is also considered as a member. + /// Task IsMemberOf(string name, string username); } @@ -296,7 +301,7 @@ namespace Timeline.Services public abstract class BaseTimelineService : IBaseTimelineService { - protected BaseTimelineService(DatabaseContext database, IClock clock) + protected BaseTimelineService(ILoggerFactory loggerFactory, DatabaseContext database, IClock clock) { Clock = clock; Database = database; @@ -600,13 +605,124 @@ namespace Timeline.Services var timelineEntity = await Database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync(); - var postEntitu = await Database.Timelines. // TODO! + var postEntity = await Database.Timelines.Where(p => p.Id == id).Select(p => new { p.OwnerId }).SingleOrDefaultAsync(); + + if (postEntity == null) + throw new TimelinePostNotExistException(id); + + return timelineEntity.OwnerId == userId || postEntity.OwnerId == userId; + } + + public async Task IsMemberOf(string name, string username) + { + if (name == null) + throw new ArgumentNullException(nameof(name)); + if (username == null) + throw new ArgumentNullException(nameof(username)); - if (timelineEntity.OwnerId == userId) { + var (result, message) = UsernameValidator.Validate(username); + if (!result) + { + throw new UsernameBadFormatException(username); + } + } + + var user = await Database.Users.Where(u => u.Name == username).Select(u => new { u.Id }).SingleOrDefaultAsync(); + + if (user == null) + { + throw new UserNotExistException(username); + } + + var userId = user.Id; + + var timelineId = await FindTimelineId(name); + + var timelineEntity = await Database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync(); + + if (userId == timelineEntity.OwnerId) return true; + + var timelineMemberEntity = await Database.TimelineMembers.Where(m => m.TimelineId == timelineId && m.UserId == userId).SingleOrDefaultAsync(); + + return timelineMemberEntity != null; + } + } + + public class PersonalTimelineService : BaseTimelineService, IPersonalTimelineService + { + public PersonalTimelineService(ILoggerFactory loggerFactory, DatabaseContext database, IClock clock) + : base(loggerFactory, database, clock) + { + + } + + protected override async Task FindTimelineId(string name) + { + { + var (result, message) = UsernameValidator.Validate(name); + if (!result) + { + throw new TimelineNameBadFormatException(name, new UsernameBadFormatException(name, message)); + } + } + + var userEntity = await Database.Users.Where(u => u.Name == name).Select(u => new { u.Id }).SingleOrDefaultAsync(); + + if (userEntity == null) + { + throw new TimelineNotExistException(name, new UserNotExistException(name)); + } + + var userId = userEntity.Id; + + 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 = new TimelineEntity + { + Name = null, + Description = null, + OwnerId = userId, + Visibility = TimelineVisibility.Register, + CreateTime = Clock.GetCurrentTime(), + }; + Database.Timelines.Add(newTimelineEntity); + await Database.SaveChangesAsync(); + + return newTimelineEntity.Id; } } + public async Task GetTimeline(string username) + { + if (username == null) + throw new ArgumentNullException(nameof(username)); + + var timelineId = await FindTimelineId(username); + + var timelineEntity = await Database.Timelines.Where(t => t.Id == timelineId).SingleAsync(); + + var timelineMemberEntities = await Database.TimelineMembers.Where(m => m.TimelineId == timelineId).Select(m => new { m.UserId }).ToListAsync(); + + var memberUsernameTasks = timelineMemberEntities.Select(m => Database.Users.Where(u => u.Id == m.UserId).Select(u => u.Name).SingleAsync()).ToArray(); + + var memberUsernames = await Task.WhenAll(memberUsernameTasks); + + return new BaseTimelineInfo + { + Description = timelineEntity.Description, + Owner = username, + Visibility = timelineEntity.Visibility, + Members = memberUsernames.ToList() + }; + } + } } -- cgit v1.2.3 From 06a5d9aae4a348ff93aeaa40ac3d3ae2e7354f0f Mon Sep 17 00:00:00 2001 From: crupest Date: Mon, 18 Nov 2019 19:29:37 +0800 Subject: Write tests and fix bugs found via tests. --- .../Controllers/PersonalTimelineControllerTest.cs | 2 +- Timeline.Tests/Helpers/ResponseAssertions.cs | 2 +- .../IntegratedTests/PersonalTimelineTest.cs | 135 +++++++++++++++++++++ Timeline/Entities/TimelineEntity.cs | 17 +-- Timeline/Models/Timeline.cs | 18 ++- Timeline/Resources/Services/Exception.Designer.cs | 2 +- Timeline/Resources/Services/Exception.resx | 2 +- Timeline/Services/TimelineService.cs | 2 +- 8 files changed, 157 insertions(+), 23 deletions(-) (limited to 'Timeline/Services/TimelineService.cs') diff --git a/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs b/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs index a7cbb37e..819017c2 100644 --- a/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs +++ b/Timeline.Tests/Controllers/PersonalTimelineControllerTest.cs @@ -309,7 +309,7 @@ namespace Timeline.Tests.Controllers var req = new TimelinePropertyChangeRequest { Description = "", - Visibility = Entities.TimelineVisibility.Private + Visibility = TimelineVisibility.Private }; _service.Setup(s => s.ChangeProperty(username, req)).Returns(Task.CompletedTask); var result = await _controller.TimelineChangeProperty(username, req); diff --git a/Timeline.Tests/Helpers/ResponseAssertions.cs b/Timeline.Tests/Helpers/ResponseAssertions.cs index 0e6f215b..6d764c68 100644 --- a/Timeline.Tests/Helpers/ResponseAssertions.cs +++ b/Timeline.Tests/Helpers/ResponseAssertions.cs @@ -88,7 +88,7 @@ namespace Timeline.Tests.Helpers return new AndWhichConstraint(this, null); } - var result = JsonConvert.DeserializeObject(body); + var result = JsonConvert.DeserializeObject(body); // TODO! catch and throw on bad format return new AndWhichConstraint(this, result); } } diff --git a/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs b/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs index 9629fc0a..aaa6215c 100644 --- a/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs +++ b/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Net.Http; using System.Text.Json; using System.Threading.Tasks; +using Timeline.Models; using Timeline.Models.Http; using Timeline.Tests.Helpers; using Timeline.Tests.Helpers.Authentication; @@ -22,6 +23,64 @@ namespace Timeline.Tests.IntegratedTests } + [Fact] + public async Task Member_Should_Work() + { + const string getUrl = "users/user/timeline"; + const string changeUrl = "users/user/timeline/op/member"; + using var client = await Factory.CreateClientAsUser(); + + async Task AssertMembers(IList members) + { + var res = await client.GetAsync(getUrl); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Members.Should().NotBeNull().And.BeEquivalentTo(members); + } + + async Task AssertEmptyMembers() + { + var res = await client.GetAsync(getUrl); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Members.Should().NotBeNull().And.BeEmpty(); + } + + await AssertEmptyMembers(); + { + var res = await client.PostAsJsonAsync(changeUrl, + new TimelineMemberChangeRequest { Add = new List { "admin", "usernotexist" } }); + res.Should().HaveStatusCode(400) + .And.HaveCommonBody() + .Which.Code.Should().Be(ErrorCodes.Http.Timeline.ChangeMemberUserNotExist); + } + { + var res = await client.PostAsJsonAsync(changeUrl, + new TimelineMemberChangeRequest { Remove = new List { "admin", "usernotexist" } }); + res.Should().HaveStatusCode(400) + .And.HaveCommonBody() + .Which.Code.Should().Be(ErrorCodes.Http.Timeline.ChangeMemberUserNotExist); + } + { + var res = await client.PostAsJsonAsync(changeUrl, + new TimelineMemberChangeRequest { Add = new List { "admin" }, Remove = new List { "admin" } }); + res.Should().HaveStatusCode(200); + await AssertEmptyMembers(); + } + { + var res = await client.PostAsJsonAsync(changeUrl, + new TimelineMemberChangeRequest { Add = new List { "admin" } }); + res.Should().HaveStatusCode(200); + await AssertMembers(new List { "admin" }); + } + { + var res = await client.PostAsJsonAsync(changeUrl, + new TimelineMemberChangeRequest { Remove = new List { "admin" } }); + res.Should().HaveStatusCode(200); + await AssertEmptyMembers(); + } + } + [Theory] [InlineData(AuthType.None, 200, 401, 401, 401, 401)] [InlineData(AuthType.User, 200, 200, 403, 200, 403)] @@ -58,5 +117,81 @@ namespace Timeline.Tests.IntegratedTests res.Should().HaveStatusCode(opMemberAdmin); } } + + [Fact] + public async Task Permission_GetPost() + { + const string userUrl = "users/user/timeline/posts"; + const string adminUrl = "users/admin/timeline/posts"; + { // default visibility is registered + { + using var client = Factory.CreateDefaultClient(); + var res = await client.GetAsync(userUrl); + res.Should().HaveStatusCode(403); + } + + { + using var client = await Factory.CreateClientAsUser(); + var res = await client.GetAsync(adminUrl); + res.Should().HaveStatusCode(200); + } + } + + { // change visibility to public + { + using var client = await Factory.CreateClientAsUser(); + var res = await client.PostAsJsonAsync("users/user/timeline/op/property", + new TimelinePropertyChangeRequest { Visibility = TimelineVisibility.Public }); + res.Should().HaveStatusCode(200); + } + { + using var client = Factory.CreateDefaultClient(); + var res = await client.GetAsync(userUrl); + res.Should().HaveStatusCode(200); + } + } + + { // change visibility to private + { + using var client = await Factory.CreateClientAsAdmin(); + { + var res = await client.PostAsJsonAsync("users/user/timeline/op/property", + new TimelinePropertyChangeRequest { Visibility = TimelineVisibility.Private }); + res.Should().HaveStatusCode(200); + } + { + var res = await client.PostAsJsonAsync("users/admin/timeline/op/property", + new TimelinePropertyChangeRequest { Visibility = TimelineVisibility.Private }); + res.Should().HaveStatusCode(200); + } + } + { + using var client = Factory.CreateDefaultClient(); + var res = await client.GetAsync(userUrl); + res.Should().HaveStatusCode(403); + } + { // user can't read admin's + using var client = await Factory.CreateClientAsUser(); + var res = await client.GetAsync(adminUrl); + res.Should().HaveStatusCode(403); + } + { // admin can read user's + using var client = await Factory.CreateClientAsAdmin(); + var res = await client.GetAsync(userUrl); + res.Should().HaveStatusCode(200); + } + { // add member + using var client = await Factory.CreateClientAsAdmin(); + var res = await client.PostAsJsonAsync("users/admin/timeline/op/member", + new TimelineMemberChangeRequest { Add = new List { "user" } }); + res.Should().HaveStatusCode(200); + } + { // now user can read admin's + using var client = await Factory.CreateClientAsUser(); + var res = await client.GetAsync(adminUrl); + res.Should().HaveStatusCode(200); + } + } + } } } diff --git a/Timeline/Entities/TimelineEntity.cs b/Timeline/Entities/TimelineEntity.cs index f5e22a54..9cacfcae 100644 --- a/Timeline/Entities/TimelineEntity.cs +++ b/Timeline/Entities/TimelineEntity.cs @@ -2,25 +2,10 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using Timeline.Models; namespace Timeline.Entities { - public enum TimelineVisibility - { - /// - /// All people including those without accounts. - /// - Public, - /// - /// Only people signed in. - /// - Register, - /// - /// Only member. - /// - Private - } - [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "This is entity object.")] [Table("timelines")] public class TimelineEntity diff --git a/Timeline/Models/Timeline.cs b/Timeline/Models/Timeline.cs index 26012878..85fefff5 100644 --- a/Timeline/Models/Timeline.cs +++ b/Timeline/Models/Timeline.cs @@ -1,11 +1,25 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Timeline.Entities; namespace Timeline.Models { + public enum TimelineVisibility + { + /// + /// All people including those without accounts. + /// + Public, + /// + /// Only people signed in. + /// + Register, + /// + /// Only member. + /// + Private + } + public class TimelinePostInfo { public long Id { get; set; } diff --git a/Timeline/Resources/Services/Exception.Designer.cs b/Timeline/Resources/Services/Exception.Designer.cs index 970c306d..1b46f9e9 100644 --- a/Timeline/Resources/Services/Exception.Designer.cs +++ b/Timeline/Resources/Services/Exception.Designer.cs @@ -286,7 +286,7 @@ namespace Timeline.Resources.Services { } /// - /// Looks up a localized string similar to An exception happened when do operation {} on the {} member on timeline.. + /// Looks up a localized string similar to An exception happened when do operation {0} on the {1} member on timeline.. /// internal static string TimelineMemberOperationExceptionDetail { get { diff --git a/Timeline/Resources/Services/Exception.resx b/Timeline/Resources/Services/Exception.resx index c8f6676a..1d9c0037 100644 --- a/Timeline/Resources/Services/Exception.resx +++ b/Timeline/Resources/Services/Exception.resx @@ -193,7 +193,7 @@ An exception happened when add or remove member on timeline. - An exception happened when do operation {} on the {} member on timeline. + An exception happened when do operation {0} on the {1} member on timeline. Timeline name is of bad format. If this is a personal timeline, it means the username is of bad format and inner exception should be a UsernameBadFormatException. diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs index 494beb11..1d199aae 100644 --- a/Timeline/Services/TimelineService.cs +++ b/Timeline/Services/TimelineService.cs @@ -458,7 +458,7 @@ namespace Timeline.Services if (list != null) { Dictionary result = new Dictionary(); - var count = 0; + var count = list.Count; for (var index = 0; index < count; index++) { var username = list[index]; -- cgit v1.2.3 From 0b77c676b81d7340560b22fc6609740df98e9141 Mon Sep 17 00:00:00 2001 From: crupest Date: Mon, 18 Nov 2019 19:43:33 +0800 Subject: Continue to write tests. --- .../IntegratedTests/PersonalTimelineTest.cs | 49 ++++++++++++++++++++++ Timeline/Models/Timeline.cs | 1 - Timeline/Services/TimelineService.cs | 2 +- 3 files changed, 50 insertions(+), 2 deletions(-) (limited to 'Timeline/Services/TimelineService.cs') diff --git a/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs b/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs index aaa6215c..2e5b86fa 100644 --- a/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs +++ b/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs @@ -23,6 +23,55 @@ namespace Timeline.Tests.IntegratedTests } + [Fact] + public async Task TimelineGet_Should_Work() + { + using var client = Factory.CreateDefaultClient(); + var res = await client.GetAsync("users/user/timeline"); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + body.Owner.Should().Be("user"); + body.Visibility.Should().Be(TimelineVisibility.Register); + body.Description.Should().Be(""); + body.Members.Should().NotBeNull().And.BeEmpty(); + } + + [Fact] + public async Task Description_Should_Work() + { + using var client = await Factory.CreateClientAsUser(); + + async Task AssertDescription(string description) + { + var res = await client.GetAsync("users/user/timeline"); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Description.Should().Be(description); + } + + const string mockDescription = "haha"; + + await AssertDescription(""); + { + var res = await client.PostAsJsonAsync("users/user/timeline/op/property", + new TimelinePropertyChangeRequest { Description = mockDescription }); + res.Should().HaveStatusCode(200); + await AssertDescription(mockDescription); + } + { + var res = await client.PostAsJsonAsync("users/user/timeline/op/property", + new TimelinePropertyChangeRequest { Description = null }); + res.Should().HaveStatusCode(200); + await AssertDescription(mockDescription); + } + { + var res = await client.PostAsJsonAsync("users/user/timeline/op/property", + new TimelinePropertyChangeRequest { Description = "" }); + res.Should().HaveStatusCode(200); + await AssertDescription(""); + } + } + [Fact] public async Task Member_Should_Work() { diff --git a/Timeline/Models/Timeline.cs b/Timeline/Models/Timeline.cs index 85fefff5..752c698d 100644 --- a/Timeline/Models/Timeline.cs +++ b/Timeline/Models/Timeline.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using Timeline.Entities; namespace Timeline.Models { diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs index 1d199aae..1e64353c 100644 --- a/Timeline/Services/TimelineService.cs +++ b/Timeline/Services/TimelineService.cs @@ -717,7 +717,7 @@ namespace Timeline.Services return new BaseTimelineInfo { - Description = timelineEntity.Description, + Description = timelineEntity.Description ?? "", Owner = username, Visibility = timelineEntity.Visibility, Members = memberUsernames.ToList() -- cgit v1.2.3 From 0ff98dd9274485994b11d9d00f4b647a5f2d42ac Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 19 Nov 2019 23:18:45 +0800 Subject: Complete integrated tests??? Fix bugs. --- Timeline.Tests/DatabaseTest.cs | 2 +- .../Authentication/AuthenticationExtensions.cs | 28 ++- Timeline.Tests/Helpers/TestApplication.cs | 22 +-- .../IntegratedTests/IntegratedTestBase.cs | 8 + .../IntegratedTests/PersonalTimelineTest.cs | 219 +++++++++++++++++++++ Timeline.Tests/Mock/Data/TestDatabase.cs | 70 +++++-- Timeline.Tests/Mock/Data/TestUsers.cs | 25 --- Timeline.Tests/Services/UserAvatarServiceTest.cs | 8 +- Timeline.Tests/Services/UserDetailServiceTest.cs | 6 +- Timeline/Services/TimelineService.cs | 4 +- 10 files changed, 320 insertions(+), 72 deletions(-) (limited to 'Timeline/Services/TimelineService.cs') diff --git a/Timeline.Tests/DatabaseTest.cs b/Timeline.Tests/DatabaseTest.cs index fc153c24..20f57c40 100644 --- a/Timeline.Tests/DatabaseTest.cs +++ b/Timeline.Tests/DatabaseTest.cs @@ -15,7 +15,7 @@ namespace Timeline.Tests public DatabaseTest() { _database = new TestDatabase(); - _context = _database.DatabaseContext; + _context = _database.Context; } public void Dispose() diff --git a/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs b/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs index 6a78be7a..4048bb73 100644 --- a/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs +++ b/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs @@ -1,5 +1,4 @@ using Microsoft.AspNetCore.Mvc.Testing; -using Newtonsoft.Json; using System; using System.Net.Http; using System.Threading.Tasks; @@ -22,9 +21,8 @@ namespace Timeline.Tests.Helpers.Authentication public static async Task CreateUserTokenAsync(this HttpClient client, string username, string password, int? expireOffset = null) { var response = await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest { Username = username, Password = password, Expire = expireOffset }); - response.Should().HaveStatusCode(200); - var result = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); - return result; + return response.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; } public static async Task CreateClientWithCredential(this WebApplicationFactory factory, string username, string password) where T : class @@ -35,14 +33,19 @@ namespace Timeline.Tests.Helpers.Authentication return client; } + public static Task CreateClientAs(this WebApplicationFactory factory, MockUser user) where T : class + { + return CreateClientWithCredential(factory, user.Username, user.Password); + } + public static Task CreateClientAsUser(this WebApplicationFactory factory) where T : class { - return factory.CreateClientWithCredential(MockUser.User.Username, MockUser.User.Password); + return factory.CreateClientAs(MockUser.User); } public static Task CreateClientAsAdmin(this WebApplicationFactory factory) where T : class { - return factory.CreateClientWithCredential(MockUser.Admin.Username, MockUser.Admin.Password); + return factory.CreateClientAs(MockUser.Admin); } public static Task CreateClientAs(this WebApplicationFactory factory, AuthType authType) where T : class @@ -55,5 +58,18 @@ namespace Timeline.Tests.Helpers.Authentication _ => throw new InvalidOperationException("Unknown auth type.") }; } + + public static MockUser GetMockUser(this AuthType authType) + { + return authType switch + { + AuthType.None => null, + AuthType.User => MockUser.User, + AuthType.Admin => MockUser.Admin, + _ => throw new InvalidOperationException("Unknown auth type.") + }; + } + + public static string GetUsername(this AuthType authType) => authType.GetMockUser().Username; } } diff --git a/Timeline.Tests/Helpers/TestApplication.cs b/Timeline.Tests/Helpers/TestApplication.cs index b0187a30..5862f452 100644 --- a/Timeline.Tests/Helpers/TestApplication.cs +++ b/Timeline.Tests/Helpers/TestApplication.cs @@ -10,26 +10,11 @@ namespace Timeline.Tests.Helpers { public class TestApplication : IDisposable { - public SqliteConnection DatabaseConnection { get; } = new SqliteConnection("Data Source=:memory:;"); + public TestDatabase Database { get; } = new TestDatabase(); public WebApplicationFactory Factory { get; } public TestApplication(WebApplicationFactory factory) { - // We should keep the connection, so the database is persisted but not recreate every time. - // See https://docs.microsoft.com/en-us/ef/core/miscellaneous/testing/sqlite#writing-tests . - DatabaseConnection.Open(); - - { - var options = new DbContextOptionsBuilder() - .UseSqlite(DatabaseConnection) - .Options; - - using (var context = new DatabaseContext(options)) - { - TestDatabase.InitDatabase(context); - }; - } - Factory = factory.WithWebHostBuilder(builder => { builder.ConfigureServices(services => @@ -37,7 +22,7 @@ namespace Timeline.Tests.Helpers services.AddEntityFrameworkSqlite(); services.AddDbContext(options => { - options.UseSqlite(DatabaseConnection); + options.UseSqlite(Database.Connection); }); }); }); @@ -45,8 +30,7 @@ namespace Timeline.Tests.Helpers public void Dispose() { - DatabaseConnection.Close(); - DatabaseConnection.Dispose(); + Database.Dispose(); } } } diff --git a/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs b/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs index c4d72faf..2dfaf82e 100644 --- a/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs +++ b/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Timeline.Tests.Helpers; +using Timeline.Tests.Mock.Data; using Xunit; namespace Timeline.Tests.IntegratedTests @@ -23,5 +24,12 @@ namespace Timeline.Tests.IntegratedTests { TestApp.Dispose(); } + + protected void CreateExtraMockUsers(int count) + { + TestApp.Database.CreateExtraMockUsers(count); + } + + protected IReadOnlyList ExtraMockUsers => TestApp.Database.ExtraMockUsers; } } diff --git a/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs b/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs index 2e5b86fa..705675f9 100644 --- a/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs +++ b/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs @@ -11,6 +11,7 @@ using Timeline.Models; using Timeline.Models.Http; using Timeline.Tests.Helpers; using Timeline.Tests.Helpers.Authentication; +using Timeline.Tests.Mock.Data; using Xunit; namespace Timeline.Tests.IntegratedTests @@ -242,5 +243,223 @@ namespace Timeline.Tests.IntegratedTests } } } + + + [Fact] + public async Task Permission_Post_Create() + { + CreateExtraMockUsers(1); + + using (var client = await Factory.CreateClientAsUser()) + { + var res = await client.PostAsJsonAsync("users/user/timeline/op/member", + new TimelineMemberChangeRequest { Add = new List { "user0" } }); + res.Should().HaveStatusCode(200); + } + + using (var client = Factory.CreateDefaultClient()) + { + { // no auth should get 401 + var res = await client.PostAsJsonAsync("users/user/timeline/postop/create", + new TimelinePostCreateRequest { Content = "aaa" }); + res.Should().HaveStatusCode(401); + } + } + + using (var client = await Factory.CreateClientAsUser()) + { + { // post self's + var res = await client.PostAsJsonAsync("users/user/timeline/postop/create", + new TimelinePostCreateRequest { Content = "aaa" }); + res.Should().HaveStatusCode(200); + } + { // post other not as a member should get 403 + var res = await client.PostAsJsonAsync("users/admin/timeline/postop/create", + new TimelinePostCreateRequest { Content = "aaa" }); + res.Should().HaveStatusCode(403); + } + } + + using (var client = await Factory.CreateClientAsAdmin()) + { + { // post as admin + var res = await client.PostAsJsonAsync("users/user/timeline/postop/create", + new TimelinePostCreateRequest { Content = "aaa" }); + res.Should().HaveStatusCode(200); + } + } + + using (var client = await Factory.CreateClientAs(ExtraMockUsers[0])) + { + { // post as member + var res = await client.PostAsJsonAsync("users/user/timeline/postop/create", + new TimelinePostCreateRequest { Content = "aaa" }); + res.Should().HaveStatusCode(200); + } + } + } + + [Fact] + public async Task Permission_Post_Delete() + { + CreateExtraMockUsers(2); + + async Task CreatePost(MockUser auth, string timeline) + { + using var client = await Factory.CreateClientAs(auth); + var res = await client.PostAsJsonAsync($"users/{timeline}/timeline/postop/create", + new TimelinePostCreateRequest { Content = "aaa" }); + return res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Id; + } + + using (var client = await Factory.CreateClientAsUser()) + { + var res = await client.PostAsJsonAsync("users/user/timeline/op/member", + new TimelineMemberChangeRequest { Add = new List { "user0", "user1" } }); + res.Should().HaveStatusCode(200); + } + + { // no auth should get 401 + using var client = Factory.CreateDefaultClient(); + var res = await client.PostAsJsonAsync("users/user/timeline/postop/delete", + new TimelinePostDeleteRequest { Id = 12 }); + res.Should().HaveStatusCode(401); + } + + { // self can delete self + var postId = await CreatePost(MockUser.User, "user"); + using var client = await Factory.CreateClientAsUser(); + var res = await client.PostAsJsonAsync("users/user/timeline/postop/delete", + new TimelinePostDeleteRequest { Id = postId }); + res.Should().HaveStatusCode(200); + } + + { // admin can delete any + var postId = await CreatePost(MockUser.User, "user"); + using var client = await Factory.CreateClientAsAdmin(); + var res = await client.PostAsJsonAsync("users/user/timeline/postop/delete", + new TimelinePostDeleteRequest { Id = postId }); + res.Should().HaveStatusCode(200); + } + + { // owner can delete other + var postId = await CreatePost(ExtraMockUsers[0], "user"); + using var client = await Factory.CreateClientAsUser(); + var res = await client.PostAsJsonAsync("users/user/timeline/postop/delete", + new TimelinePostDeleteRequest { Id = postId }); + res.Should().HaveStatusCode(200); + } + + { // author can delete self + var postId = await CreatePost(ExtraMockUsers[0], "user"); + using var client = await Factory.CreateClientAs(ExtraMockUsers[0]); + var res = await client.PostAsJsonAsync("users/user/timeline/postop/delete", + new TimelinePostDeleteRequest { Id = postId }); + res.Should().HaveStatusCode(200); + } + + { // otherwise is forbidden + var postId = await CreatePost(ExtraMockUsers[0], "user"); + using var client = await Factory.CreateClientAs(ExtraMockUsers[1]); + var res = await client.PostAsJsonAsync("users/user/timeline/postop/delete", + new TimelinePostDeleteRequest { Id = postId }); + res.Should().HaveStatusCode(403); + } + } + + [Fact] + public async Task Post_Op_Should_Work() + { + { + using var client = await Factory.CreateClientAsUser(); + { + var res = await client.GetAsync("users/user/timeline/posts"); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Should().NotBeNull().And.BeEmpty(); + } + { + var res = await client.PostAsJsonAsync("users/user/timeline/postop/create", + new TimelinePostCreateRequest { Content = null }); + res.Should().BeInvalidModel(); + } + const string mockContent = "aaa"; + TimelinePostCreateResponse createRes; + { + var res = await client.PostAsJsonAsync("users/user/timeline/postop/create", + new TimelinePostCreateRequest { Content = mockContent }); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which; + body.Should().NotBeNull(); + createRes = body; + } + { + var res = await client.GetAsync("users/user/timeline/posts"); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Should().NotBeNull().And.BeEquivalentTo( + new TimelinePostInfo + { + Id = createRes.Id, + Author = "user", + Content = mockContent, + Time = createRes.Time + }); + } + const string mockContent2 = "bbb"; + var mockTime2 = DateTime.Now.AddDays(-1); + TimelinePostCreateResponse createRes2; + { + var res = await client.PostAsJsonAsync("users/user/timeline/postop/create", + new TimelinePostCreateRequest { Content = mockContent2, Time = mockTime2 }); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which; + body.Should().NotBeNull(); + createRes2 = body; + } + { + var res = await client.GetAsync("users/user/timeline/posts"); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Should().NotBeNull().And.BeEquivalentTo( + new TimelinePostInfo + { + Id = createRes.Id, + Author = "user", + Content = mockContent, + Time = createRes.Time + }, + new TimelinePostInfo + { + Id = createRes2.Id, + Author = "user", + Content = mockContent2, + Time = createRes2.Time + }); + } + { + var res = await client.PostAsJsonAsync("users/user/timeline/postop/delete", + new TimelinePostDeleteRequest { Id = createRes.Id }); + res.Should().HaveStatusCode(200); + } + { + var res = await client.GetAsync("users/user/timeline/posts"); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Should().NotBeNull().And.BeEquivalentTo( + new TimelinePostInfo + { + Id = createRes2.Id, + Author = "user", + Content = mockContent2, + Time = createRes2.Time + }); + } + } + } } } diff --git a/Timeline.Tests/Mock/Data/TestDatabase.cs b/Timeline.Tests/Mock/Data/TestDatabase.cs index 1e662546..1f396177 100644 --- a/Timeline.Tests/Mock/Data/TestDatabase.cs +++ b/Timeline.Tests/Mock/Data/TestDatabase.cs @@ -1,42 +1,88 @@ using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using System; +using System.Collections.Generic; +using System.Linq; using Timeline.Entities; +using Timeline.Models; +using Timeline.Services; namespace Timeline.Tests.Mock.Data { public class TestDatabase : IDisposable { - public static void InitDatabase(DatabaseContext context) + // currently password service is thread safe, so we share a static one. + private static PasswordService PasswordService { get; } = new PasswordService(); + + private static User CreateEntityFromMock(MockUser user) + { + return new User + { + Name = user.Username, + EncryptedPassword = PasswordService.HashPassword(user.Password), + RoleString = UserRoleConvert.ToString(user.Administrator), + Avatar = null + }; + } + + private static IEnumerable CreateDefaultMockEntities() + { + // emmmmmmm. Never reuse the user instances because EF Core uses them, which will cause strange things. + yield return CreateEntityFromMock(MockUser.User); + yield return CreateEntityFromMock(MockUser.Admin); + } + + private static void InitDatabase(DatabaseContext context) { context.Database.EnsureCreated(); - context.Users.AddRange(MockUser.CreateMockEntities()); + context.Users.AddRange(CreateDefaultMockEntities()); context.SaveChanges(); } public TestDatabase() { - DatabaseConnection = new SqliteConnection("Data Source=:memory:;"); - DatabaseConnection.Open(); + Connection = new SqliteConnection("Data Source=:memory:;"); + Connection.Open(); var options = new DbContextOptionsBuilder() - .UseSqlite(DatabaseConnection) + .UseSqlite(Connection) .Options; - DatabaseContext = new DatabaseContext(options); + Context = new DatabaseContext(options); + + InitDatabase(Context); + } + + private List _extraMockUsers; + + public IReadOnlyList ExtraMockUsers => _extraMockUsers; + + public void CreateExtraMockUsers(int count) + { + if (count <= 0) + throw new ArgumentOutOfRangeException(nameof(count), count, "Additional user count must be bigger than 0."); + if (_extraMockUsers != null) + throw new InvalidOperationException("Already create mock users."); + + _extraMockUsers = new List(); + for (int i = 0; i < count; i++) + { + _extraMockUsers.Add(new MockUser($"user{i}", $"password", false)); + } - InitDatabase(DatabaseContext); + Context.AddRange(_extraMockUsers.Select(u => CreateEntityFromMock(u))); + Context.SaveChanges(); } public void Dispose() { - DatabaseContext.Dispose(); + Context.Dispose(); - DatabaseConnection.Close(); - DatabaseConnection.Dispose(); + Connection.Close(); + Connection.Dispose(); } - public SqliteConnection DatabaseConnection { get; } - public DatabaseContext DatabaseContext { get; } + public SqliteConnection Connection { get; } + public DatabaseContext Context { get; } } } diff --git a/Timeline.Tests/Mock/Data/TestUsers.cs b/Timeline.Tests/Mock/Data/TestUsers.cs index fa75236a..443d3cf9 100644 --- a/Timeline.Tests/Mock/Data/TestUsers.cs +++ b/Timeline.Tests/Mock/Data/TestUsers.cs @@ -1,8 +1,5 @@ -using System; using System.Collections.Generic; -using Timeline.Entities; using Timeline.Models; -using Timeline.Services; namespace Timeline.Tests.Mock.Data { @@ -24,27 +21,5 @@ namespace Timeline.Tests.Mock.Data public static MockUser Admin { get; } = new MockUser("admin", "adminpassword", true); public static IReadOnlyList UserInfoList { get; } = new List { User.Info, Admin.Info }; - - // emmmmmmm. Never reuse the user instances because EF Core uses them, which will cause strange things. - public static IEnumerable CreateMockEntities() - { - var passwordService = new PasswordService(); - User Create(MockUser user) - { - return new User - { - Name = user.Username, - EncryptedPassword = passwordService.HashPassword(user.Password), - RoleString = UserRoleConvert.ToString(user.Administrator), - Avatar = null - }; - } - - return new List - { - Create(User), - Create(Admin) - }; - } } } diff --git a/Timeline.Tests/Services/UserAvatarServiceTest.cs b/Timeline.Tests/Services/UserAvatarServiceTest.cs index cf3d2a0a..033a5e90 100644 --- a/Timeline.Tests/Services/UserAvatarServiceTest.cs +++ b/Timeline.Tests/Services/UserAvatarServiceTest.cs @@ -139,7 +139,7 @@ namespace Timeline.Tests.Services _database = new TestDatabase(); - _service = new UserAvatarService(NullLogger.Instance, _database.DatabaseContext, _mockDefaultAvatarProvider.Object, _mockValidator.Object, _mockETagGenerator.Object, _mockClock.Object); + _service = new UserAvatarService(NullLogger.Instance, _database.Context, _mockDefaultAvatarProvider.Object, _mockValidator.Object, _mockETagGenerator.Object, _mockClock.Object); } public void Dispose() @@ -171,7 +171,7 @@ namespace Timeline.Tests.Services string username = MockUser.User.Username; var mockAvatarEntity = CreateMockAvatarEntity("aaa"); { - var context = _database.DatabaseContext; + var context = _database.Context; var user = await context.Users.Where(u => u.Name == username).Include(u => u.Avatar).SingleAsync(); user.Avatar = mockAvatarEntity; await context.SaveChangesAsync(); @@ -205,7 +205,7 @@ namespace Timeline.Tests.Services string username = MockUser.User.Username; var mockAvatarEntity = CreateMockAvatarEntity("aaa"); { - var context = _database.DatabaseContext; + var context = _database.Context; var user = await context.Users.Where(u => u.Name == username).Include(u => u.Avatar).SingleAsync(); user.Avatar = mockAvatarEntity; await context.SaveChangesAsync(); @@ -237,7 +237,7 @@ namespace Timeline.Tests.Services { string username = MockUser.User.Username; - var user = await _database.DatabaseContext.Users.Where(u => u.Name == username).Include(u => u.Avatar).SingleAsync(); + var user = await _database.Context.Users.Where(u => u.Name == username).Include(u => u.Avatar).SingleAsync(); var avatar1 = CreateMockAvatar("aaa"); var avatar2 = CreateMockAvatar("bbb"); diff --git a/Timeline.Tests/Services/UserDetailServiceTest.cs b/Timeline.Tests/Services/UserDetailServiceTest.cs index c7037c6e..bddb1494 100644 --- a/Timeline.Tests/Services/UserDetailServiceTest.cs +++ b/Timeline.Tests/Services/UserDetailServiceTest.cs @@ -21,7 +21,7 @@ namespace Timeline.Tests.Services public UserDetailServiceTest() { _testDatabase = new TestDatabase(); - _service = new UserDetailService(_testDatabase.DatabaseContext, NullLogger.Instance); + _service = new UserDetailService(_testDatabase.Context, NullLogger.Instance); } public void Dispose() @@ -51,7 +51,7 @@ namespace Timeline.Tests.Services { const string nickname = "aaaaaa"; { - var context = _testDatabase.DatabaseContext; + var context = _testDatabase.Context; var userId = (await context.Users.Where(u => u.Name == MockUser.User.Username).Select(u => new { u.Id }).SingleAsync()).Id; context.UserDetails.Add(new UserDetail { @@ -84,7 +84,7 @@ namespace Timeline.Tests.Services public async Task SetNickname_ShouldWork() { var username = MockUser.User.Username; - var user = await _testDatabase.DatabaseContext.Users.Where(u => u.Name == username).Include(u => u.Detail).SingleAsync(); + var user = await _testDatabase.Context.Users.Where(u => u.Name == username).Include(u => u.Detail).SingleAsync(); var nickname1 = "nickname1"; var nickname2 = "nickname2"; diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs index 1e64353c..a2ff4098 100644 --- a/Timeline/Services/TimelineService.cs +++ b/Timeline/Services/TimelineService.cs @@ -605,12 +605,12 @@ namespace Timeline.Services var timelineEntity = await Database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync(); - var postEntity = await Database.Timelines.Where(p => p.Id == id).Select(p => new { p.OwnerId }).SingleOrDefaultAsync(); + var postEntity = await Database.TimelinePosts.Where(p => p.Id == id).Select(p => new { p.AuthorId }).SingleOrDefaultAsync(); if (postEntity == null) throw new TimelinePostNotExistException(id); - return timelineEntity.OwnerId == userId || postEntity.OwnerId == userId; + return timelineEntity.OwnerId == userId || postEntity.AuthorId == userId; } public async Task IsMemberOf(string name, string username) -- cgit v1.2.3 From 23b3f79f4d1b48eb4a230402b1ad88a8d5409d24 Mon Sep 17 00:00:00 2001 From: crupest Date: Wed, 20 Nov 2019 16:40:41 +0800 Subject: Change post content to null instead of delete the entity when deleting post. --- Timeline/Services/TimelineService.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'Timeline/Services/TimelineService.cs') diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs index a2ff4098..9ae9dc1c 100644 --- a/Timeline/Services/TimelineService.cs +++ b/Timeline/Services/TimelineService.cs @@ -348,7 +348,7 @@ namespace Timeline.Services throw new ArgumentNullException(nameof(name)); var timelineId = await FindTimelineId(name); - var postEntities = await Database.TimelinePosts.Where(p => p.TimelineId == timelineId).ToListAsync(); + var postEntities = await Database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.Content != null).ToListAsync(); var posts = new List(await Task.WhenAll(postEntities.Select(async p => new TimelinePostInfo { Id = p.Id, @@ -418,7 +418,9 @@ namespace Timeline.Services if (post == null) throw new TimelinePostNotExistException(id); - Database.TimelinePosts.Remove(post); + post.Content = null; + post.LastUpdated = Clock.GetCurrentTime(); + await Database.SaveChangesAsync(); } -- cgit v1.2.3 From ca87f6781a5b0e80989a66be338a699846c40f8d Mon Sep 17 00:00:00 2001 From: crupest Date: Wed, 20 Nov 2019 16:49:12 +0800 Subject: Get posts should return posts ordered by time. --- .../IntegratedTests/PersonalTimelineTest.cs | 29 +++++++++++++++++++++- Timeline/Services/TimelineService.cs | 2 +- 2 files changed, 29 insertions(+), 2 deletions(-) (limited to 'Timeline/Services/TimelineService.cs') diff --git a/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs b/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs index 43549d1a..483499fb 100644 --- a/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs +++ b/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Mvc.Testing; using System; using System.Collections.Generic; +using System.Linq; using System.Net.Http; using System.Threading.Tasks; using Timeline.Models; @@ -461,7 +462,33 @@ namespace Timeline.Tests.IntegratedTests Time = createRes2.Time }); } - // TODO! Add post not exist tests. + } + } + + [Fact] + public async Task GetPost_Should_Ordered() + { + using var client = await CreateClientAsUser(); + + async Task CreatePost(DateTime time) + { + var res = await client.PostAsJsonAsync("users/user/timeline/postop/create", + new TimelinePostCreateRequest { Content = "aaa", Time = time }); + return res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Id; + } + + var now = DateTime.Now; + var id0 = await CreatePost(now.AddDays(1)); + var id1 = await CreatePost(now.AddDays(-1)); + var id2 = await CreatePost(now); + + { + var res = await client.GetAsync("users/user/timeline/posts"); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Select(p => p.Id).Should().Equal(id1, id2, id0); } } } diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs index 9ae9dc1c..affcff2e 100644 --- a/Timeline/Services/TimelineService.cs +++ b/Timeline/Services/TimelineService.cs @@ -348,7 +348,7 @@ namespace Timeline.Services throw new ArgumentNullException(nameof(name)); var timelineId = await FindTimelineId(name); - var postEntities = await Database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.Content != null).ToListAsync(); + var postEntities = await Database.TimelinePosts.OrderBy(p => p.Time).Where(p => p.TimelineId == timelineId && p.Content != null).ToListAsync(); var posts = new List(await Task.WhenAll(postEntities.Select(async p => new TimelinePostInfo { Id = p.Id, -- cgit v1.2.3