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/Entities/TimelineEntity.cs | 46 +++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 Timeline/Entities/TimelineEntity.cs (limited to 'Timeline/Entities/TimelineEntity.cs') diff --git a/Timeline/Entities/TimelineEntity.cs b/Timeline/Entities/TimelineEntity.cs new file mode 100644 index 00000000..f4c7045d --- /dev/null +++ b/Timeline/Entities/TimelineEntity.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Timeline.Entities +{ + public enum TimelineVisibility + { + Public, + Private + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "This is entity object.")] + [Table("timelines")] + public class TimelineEntity + { + [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + + /// + /// If null, then this timeline is a personal timeline. + /// + [Column("name")] + public string? Name { get; set; } + + [Column("description")] + public string? Description { get; set; } + + [Column("owner")] + public long OwnerId { get; set; } + + [ForeignKey(nameof(OwnerId))] + public User Owner { get; set; } = default!; + + [Column("visibility")] + public TimelineVisibility Visibility { get; set; } + + [Column("create_time")] + public DateTime CreateTime { get; set; } + + public List Members { get; set; } = default!; + + public List Posts { get; set; } = default!; + } +} -- 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/Entities/TimelineEntity.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 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/Entities/TimelineEntity.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