From 6ac5ba10f9c417f63a4713612a0abf5b20deb099 Mon Sep 17 00:00:00 2001 From: crupest Date: Sat, 19 Dec 2020 20:08:49 +0800 Subject: feat: Bookmark timeline REST api. --- BackEnd/Timeline.ErrorCodes/ErrorCodes.cs | 5 + .../IntegratedTests/BookmarkTimelineTest.cs | 87 ++++++++++++++++ .../IntegratedTests/HighlightTimelineTest.cs | 1 + .../IntegratedTests/HttpClientTestExtensions.cs | 5 + .../Controllers/BookmarkTimelineController.cs | 114 +++++++++++++++++++++ .../Controllers/HighlightTimelineController.cs | 8 +- BackEnd/Timeline/Models/Http/BookmarkTimeline.cs | 23 +++++ BackEnd/Timeline/Models/Http/HighlightTimeline.cs | 6 ++ BackEnd/Timeline/Startup.cs | 1 + 9 files changed, 249 insertions(+), 1 deletion(-) create mode 100644 BackEnd/Timeline.Tests/IntegratedTests/BookmarkTimelineTest.cs create mode 100644 BackEnd/Timeline/Controllers/BookmarkTimelineController.cs create mode 100644 BackEnd/Timeline/Models/Http/BookmarkTimeline.cs (limited to 'BackEnd') diff --git a/BackEnd/Timeline.ErrorCodes/ErrorCodes.cs b/BackEnd/Timeline.ErrorCodes/ErrorCodes.cs index a8519216..c65bf26e 100644 --- a/BackEnd/Timeline.ErrorCodes/ErrorCodes.cs +++ b/BackEnd/Timeline.ErrorCodes/ErrorCodes.cs @@ -68,6 +68,11 @@ { public const int NonHighlight = 1_105_01_01; } + + public static class BookmarkTimelineController + { + public const int NonBookmark = 1_106_01_01; + } } } diff --git a/BackEnd/Timeline.Tests/IntegratedTests/BookmarkTimelineTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/BookmarkTimelineTest.cs new file mode 100644 index 00000000..e6ae178f --- /dev/null +++ b/BackEnd/Timeline.Tests/IntegratedTests/BookmarkTimelineTest.cs @@ -0,0 +1,87 @@ +using FluentAssertions; +using System.Collections.Generic; +using System.Threading.Tasks; +using Timeline.Models.Http; +using Xunit; + +namespace Timeline.Tests.IntegratedTests +{ + public class BookmarkTimelineTest : IntegratedTestBase + { + [Fact] + public async Task AuthTest() + { + using var client = await CreateDefaultClient(); + + await client.TestPutAssertUnauthorizedAsync("bookmarks/@user1"); + await client.TestDeleteAssertUnauthorizedAsync("bookmarks/@user1"); + await client.TestPostAssertUnauthorizedAsync("bookmarkop/move", new HttpBookmarkTimelineMoveRequest { Timeline = "aaa", NewPosition = 1 }); + } + + [Fact] + public async Task InvalidModel() + { + using var client = await CreateClientAsUser(); + + await client.TestPutAssertInvalidModelAsync("bookmarks/!!!"); + await client.TestDeleteAssertInvalidModelAsync("bookmarks/!!!"); + await client.TestPostAssertInvalidModelAsync("bookmarkop/move", new HttpBookmarkTimelineMoveRequest { Timeline = null!, NewPosition = 1 }); + await client.TestPostAssertInvalidModelAsync("bookmarkop/move", new HttpBookmarkTimelineMoveRequest { Timeline = "!!!", NewPosition = 1 }); + await client.TestPostAssertInvalidModelAsync("bookmarkop/move", new HttpBookmarkTimelineMoveRequest { Timeline = "aaa", NewPosition = null }); + } + + [Fact] + public async Task ShouldWork() + { + using var client = await CreateClientAsUser(); + await client.TestPostAsync("timelines", new TimelineCreateRequest { Name = "t1" }); + + + { + var h = await client.TestGetAsync>("bookmarks"); + h.Should().BeEmpty(); + } + + await client.TestPutAsync("bookmarks/@user1"); + + { + var h = await client.TestGetAsync>("bookmarks"); + h.Should().HaveCount(1); + h[0].Name.Should().Be("@user1"); + } + + await client.TestPutAsync("bookmarks/t1"); + + { + var h = await client.TestGetAsync>("bookmarks"); + h.Should().HaveCount(2); + h[0].Name.Should().Be("@user1"); + h[1].Name.Should().Be("t1"); + } + + await client.TestPostAsync("bookmarkop/move", new HttpHighlightTimelineMoveRequest { Timeline = "@user1", NewPosition = 2 }); + + { + var h = await client.TestGetAsync>("bookmarks"); + h.Should().HaveCount(2); + h[0].Name.Should().Be("t1"); + h[1].Name.Should().Be("@user1"); + } + + await client.TestDeleteAsync("bookmarks/@user1"); + + { + var h = await client.TestGetAsync>("bookmarks"); + h.Should().HaveCount(1); + h[0].Name.Should().Be("t1"); + } + + await client.TestDeleteAsync("bookmarks/t1"); + + { + var h = await client.TestGetAsync>("bookmarks"); + h.Should().BeEmpty(); + } + } + } +} diff --git a/BackEnd/Timeline.Tests/IntegratedTests/HighlightTimelineTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/HighlightTimelineTest.cs index d4b4d55d..63f40a1e 100644 --- a/BackEnd/Timeline.Tests/IntegratedTests/HighlightTimelineTest.cs +++ b/BackEnd/Timeline.Tests/IntegratedTests/HighlightTimelineTest.cs @@ -28,6 +28,7 @@ namespace Timeline.Tests.IntegratedTests await client.TestPutAssertInvalidModelAsync("highlights/!!!"); await client.TestDeleteAssertInvalidModelAsync("highlights/!!!"); await client.TestPostAssertInvalidModelAsync("highlightop/move", new HttpHighlightTimelineMoveRequest { Timeline = null!, NewPosition = 1 }); + await client.TestPostAssertInvalidModelAsync("highlightop/move", new HttpHighlightTimelineMoveRequest { Timeline = "!!!", NewPosition = 1 }); await client.TestPostAssertInvalidModelAsync("highlightop/move", new HttpHighlightTimelineMoveRequest { Timeline = "aaa", NewPosition = null }); } diff --git a/BackEnd/Timeline.Tests/IntegratedTests/HttpClientTestExtensions.cs b/BackEnd/Timeline.Tests/IntegratedTests/HttpClientTestExtensions.cs index ec517362..b219f092 100644 --- a/BackEnd/Timeline.Tests/IntegratedTests/HttpClientTestExtensions.cs +++ b/BackEnd/Timeline.Tests/IntegratedTests/HttpClientTestExtensions.cs @@ -192,6 +192,11 @@ namespace Timeline.Tests.IntegratedTests await client.TestJsonSendAssertUnauthorizedAsync(HttpMethod.Patch, url, jsonBody, errorCode, headerSetup); } + public static async Task TestPutAssertUnauthorizedAsync(this HttpClient client, string url, object? jsonBody = null, int? errorCode = null, HeaderSetup? headerSetup = null) + { + await client.TestJsonSendAssertUnauthorizedAsync(HttpMethod.Put, url, jsonBody, errorCode, headerSetup); + } + public static async Task TestDeleteAssertUnauthorizedAsync(this HttpClient client, string url, object? jsonBody = null, int? errorCode = null, HeaderSetup? headerSetup = null) { await client.TestJsonSendAssertUnauthorizedAsync(HttpMethod.Delete, url, jsonBody, errorCode, headerSetup); diff --git a/BackEnd/Timeline/Controllers/BookmarkTimelineController.cs b/BackEnd/Timeline/Controllers/BookmarkTimelineController.cs new file mode 100644 index 00000000..9dff95f3 --- /dev/null +++ b/BackEnd/Timeline/Controllers/BookmarkTimelineController.cs @@ -0,0 +1,114 @@ +using AutoMapper; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using System.Threading.Tasks; +using Timeline.Models.Http; +using Timeline.Models.Validation; +using Timeline.Services; +using Timeline.Services.Exceptions; + +namespace Timeline.Controllers +{ + /// + /// Api related to timeline bookmarks. + /// + [ApiController] + [ProducesErrorResponseType(typeof(CommonResponse))] + public class BookmarkTimelineController : Controller + { + private readonly IBookmarkTimelineService _service; + + private readonly IMapper _mapper; + + public BookmarkTimelineController(IBookmarkTimelineService service, IMapper mapper) + { + _service = service; + _mapper = mapper; + } + + /// + /// Get bookmark list in order. + /// + /// Bookmarks. + [HttpGet("bookmarks")] + [Authorize] + [ProducesResponseType(200)] + [ProducesResponseType(401)] + public async Task>> List() + { + var bookmarks = await _service.GetBookmarks(this.GetUserId()); + return Ok(_mapper.Map>(bookmarks)); + } + + /// + /// Add a bookmark. + /// + /// Timeline name. + [HttpPut("bookmarks/{timeline}")] + [Authorize] + [ProducesResponseType(200)] + [ProducesResponseType(400)] + [ProducesResponseType(401)] + public async Task Put([GeneralTimelineName] string timeline) + { + try + { + await _service.AddBookmark(this.GetUserId(), timeline); + return Ok(); + } + catch (TimelineNotExistException) + { + return BadRequest(ErrorResponse.TimelineController.NotExist()); + } + } + + /// + /// Remove a bookmark. + /// + /// Timeline name. + [HttpDelete("bookmarks/{timeline}")] + [Authorize] + [ProducesResponseType(200)] + [ProducesResponseType(400)] + [ProducesResponseType(401)] + public async Task Delete([GeneralTimelineName] string timeline) + { + try + { + await _service.RemoveBookmark(this.GetUserId(), timeline); + return Ok(); + } + catch (TimelineNotExistException) + { + return BadRequest(ErrorResponse.TimelineController.NotExist()); + } + } + + /// + /// Move a bookmark to new posisition. + /// + /// Request body. + [HttpPost("bookmarkop/move")] + [Authorize] + [ProducesResponseType(200)] + [ProducesResponseType(400)] + [ProducesResponseType(401)] + public async Task Move([FromBody] HttpBookmarkTimelineMoveRequest request) + { + try + { + await _service.MoveBookmark(this.GetUserId(), request.Timeline, request.NewPosition!.Value); + return Ok(); + } + catch (TimelineNotExistException) + { + return BadRequest(ErrorResponse.TimelineController.NotExist()); + } + catch (InvalidBookmarkException) + { + return BadRequest(new CommonResponse(ErrorCodes.BookmarkTimelineController.NonBookmark, "You can't move a non-bookmark timeline.")); + } + } + } +} diff --git a/BackEnd/Timeline/Controllers/HighlightTimelineController.cs b/BackEnd/Timeline/Controllers/HighlightTimelineController.cs index 0b6e1665..519d6161 100644 --- a/BackEnd/Timeline/Controllers/HighlightTimelineController.cs +++ b/BackEnd/Timeline/Controllers/HighlightTimelineController.cs @@ -46,6 +46,8 @@ namespace Timeline.Controllers [PermissionAuthorize(UserPermission.HighlightTimelineManagement)] [ProducesResponseType(200)] [ProducesResponseType(400)] + [ProducesResponseType(401)] + [ProducesResponseType(403)] public async Task Put([GeneralTimelineName] string timeline) { try @@ -67,6 +69,8 @@ namespace Timeline.Controllers [PermissionAuthorize(UserPermission.HighlightTimelineManagement)] [ProducesResponseType(200)] [ProducesResponseType(400)] + [ProducesResponseType(401)] + [ProducesResponseType(403)] public async Task Delete([GeneralTimelineName] string timeline) { try @@ -81,12 +85,14 @@ namespace Timeline.Controllers } /// - /// Move a highlight position. + /// Move a highlight to new position. /// [HttpPost("highlightop/move")] [PermissionAuthorize(UserPermission.HighlightTimelineManagement)] [ProducesResponseType(200)] [ProducesResponseType(400)] + [ProducesResponseType(401)] + [ProducesResponseType(403)] public async Task Move([FromBody] HttpHighlightTimelineMoveRequest body) { try diff --git a/BackEnd/Timeline/Models/Http/BookmarkTimeline.cs b/BackEnd/Timeline/Models/Http/BookmarkTimeline.cs new file mode 100644 index 00000000..14be1112 --- /dev/null +++ b/BackEnd/Timeline/Models/Http/BookmarkTimeline.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; +using Timeline.Models.Validation; + +namespace Timeline.Models.Http +{ + /// + /// Move bookmark timeline request body model. + /// + public class HttpBookmarkTimelineMoveRequest + { + /// + /// Timeline name. + /// + [GeneralTimelineName] + public string Timeline { get; set; } = default!; + + /// + /// New position, starting at 1. + /// + [Required] + public long? NewPosition { get; set; } + } +} diff --git a/BackEnd/Timeline/Models/Http/HighlightTimeline.cs b/BackEnd/Timeline/Models/Http/HighlightTimeline.cs index e5aed068..5af0e528 100644 --- a/BackEnd/Timeline/Models/Http/HighlightTimeline.cs +++ b/BackEnd/Timeline/Models/Http/HighlightTimeline.cs @@ -8,9 +8,15 @@ namespace Timeline.Models.Http /// public class HttpHighlightTimelineMoveRequest { + /// + /// Timeline name. + /// [GeneralTimelineName] public string Timeline { get; set; } = default!; + /// + /// New position, starting at 1. + /// [Required] public long? NewPosition { get; set; } } diff --git a/BackEnd/Timeline/Startup.cs b/BackEnd/Timeline/Startup.cs index d20fc54b..66c708ac 100644 --- a/BackEnd/Timeline/Startup.cs +++ b/BackEnd/Timeline/Startup.cs @@ -102,6 +102,7 @@ namespace Timeline services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddDbContext((services, options) => { -- cgit v1.2.3