diff --git a/src/LinkDotNet.Blog.Web/App.razor b/src/LinkDotNet.Blog.Web/App.razor index 6b1f7d27..074d2b2f 100644 --- a/src/LinkDotNet.Blog.Web/App.razor +++ b/src/LinkDotNet.Blog.Web/App.razor @@ -1,13 +1,14 @@ @using LinkDotNet.Blog.Web.Features.Home.Components + - - - - - - - - - - + + + + + + + + + + diff --git a/src/LinkDotNet.Blog.Web/Features/Bookmarks/BookmarkService.cs b/src/LinkDotNet.Blog.Web/Features/Bookmarks/BookmarkService.cs new file mode 100644 index 00000000..189fc28b --- /dev/null +++ b/src/LinkDotNet.Blog.Web/Features/Bookmarks/BookmarkService.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using LinkDotNet.Blog.Domain; +using LinkDotNet.Blog.Infrastructure.Persistence.Sql; +using LinkDotNet.Blog.Web.Features.Services; +using Microsoft.EntityFrameworkCore; + +namespace LinkDotNet.Blog.Web.Features.Bookmarks; + +public class BookmarkService : IBookmarkService +{ + private readonly ILocalStorageService localStorageService; + + public BookmarkService(ILocalStorageService localStorageService) + { + this.localStorageService = localStorageService; + } + + public async Task IsBookmarked(string postId) + { + ArgumentException.ThrowIfNullOrEmpty(postId); + await InitializeIfNotExists(); + var bookmarks = await localStorageService.GetItemAsync>("bookmarks"); + + return bookmarks.Contains(postId); + } + + public async Task> GetBookmarkedPostIds() + { + await InitializeIfNotExists(); + return await localStorageService.GetItemAsync>("bookmarks"); + } + + public async Task SetBookmark(string postId, bool isBookmarked) + { + ArgumentException.ThrowIfNullOrEmpty(postId); + await InitializeIfNotExists(); + + var bookmarks = await localStorageService.GetItemAsync>("bookmarks"); + + if (!isBookmarked) + { + bookmarks.Remove(postId); + } + else + { + bookmarks.Add(postId); + } + + await localStorageService.SetItemAsync("bookmarks", bookmarks); + + } + + private async Task InitializeIfNotExists() + { + if (!(await localStorageService.ContainKeyAsync("bookmarks"))) + { + await localStorageService.SetItemAsync("bookmarks", new List()); + } + } +} diff --git a/src/LinkDotNet.Blog.Web/Features/Bookmarks/Bookmarks.razor b/src/LinkDotNet.Blog.Web/Features/Bookmarks/Bookmarks.razor new file mode 100644 index 00000000..8ffcef82 --- /dev/null +++ b/src/LinkDotNet.Blog.Web/Features/Bookmarks/Bookmarks.razor @@ -0,0 +1,46 @@ +@page "/bookmarks" +@using LinkDotNet.Blog.Domain +@using LinkDotNet.Blog.Infrastructure.Persistence +@inject IBookmarkService BookmarkService +@inject IRepository BlogPostRepository; + +
+

Bookmarks

+ @if (bookmarkedPosts.Count <= 0) + { + + } + else + { + @foreach (var post in bookmarkedPosts) + { + + } + } +
+ +@code { + private IReadOnlyList bookmarkedPosts = []; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + var ids = await BookmarkService.GetBookmarkedPostIds(); + + if (ids.Any()) + { + bookmarkedPosts = await BlogPostRepository.GetAllByProjectionAsync(post => post, post => ids.Contains(post.Id)); + StateHasChanged(); + } + } + } + +} diff --git a/src/LinkDotNet.Blog.Web/Features/Bookmarks/Components/BookmarkButton.razor b/src/LinkDotNet.Blog.Web/Features/Bookmarks/Components/BookmarkButton.razor new file mode 100644 index 00000000..0b1800b9 --- /dev/null +++ b/src/LinkDotNet.Blog.Web/Features/Bookmarks/Components/BookmarkButton.razor @@ -0,0 +1,17 @@ + + +@code { + [Parameter] public bool IsBookmarked { get; set; } + [Parameter] public EventCallback Bookmarked { get; set; } + + private async Task OnBookmarkClicked() + { + await Bookmarked.InvokeAsync(); + } +} diff --git a/src/LinkDotNet.Blog.Web/Features/Bookmarks/IBookmarkService.cs b/src/LinkDotNet.Blog.Web/Features/Bookmarks/IBookmarkService.cs new file mode 100644 index 00000000..35e98179 --- /dev/null +++ b/src/LinkDotNet.Blog.Web/Features/Bookmarks/IBookmarkService.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace LinkDotNet.Blog.Web.Features.Bookmarks; + +public interface IBookmarkService +{ + public Task IsBookmarked(string postId); + public Task> GetBookmarkedPostIds(); + public Task SetBookmark(string postId, bool isBookmarked); +} diff --git a/src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor b/src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor index ed5e67a6..e1345977 100644 --- a/src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor +++ b/src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor @@ -1,4 +1,7 @@ @using LinkDotNet.Blog.Domain +@using LinkDotNet.Blog.Web.Features.Bookmarks +@using LinkDotNet.Blog.Web.Features.Bookmarks.Components +@inject IBookmarkService BookmarkService
@@ -33,11 +36,13 @@
-

@BlogPost.Title

-

+
+

@BlogPost.Title

+ +

@MarkdownConverter.ToMarkupString(BlogPost.ShortDescription)

- Read the whole article + Read the whole article

@@ -47,6 +52,8 @@ [Parameter, EditorRequired] public required BlogPost BlogPost { get; set; } + private bool isBookmarked = false; + [Parameter] public bool UseAlternativeStyle { get; set; } @@ -55,6 +62,13 @@ private string AltCssClass => UseAlternativeStyle ? "alt" : string.Empty; + private async Task ToggleBookmark() + { + isBookmarked = !isBookmarked; + await BookmarkService.SetBookmark(BlogPost.Id, isBookmarked); + StateHasChanged(); + } + public override Task SetParametersAsync(ParameterView parameters) { foreach (var parameter in parameters) @@ -75,4 +89,13 @@ return base.SetParametersAsync(ParameterView.Empty); } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + isBookmarked = await BookmarkService.IsBookmarked(BlogPost.Id); + StateHasChanged(); + } + } } diff --git a/src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor.css b/src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor.css index 173a7a5e..b0bb87ea 100644 --- a/src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor.css +++ b/src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor.css @@ -80,6 +80,11 @@ z-index: 1; } +.blog-card .description .header { + display: flex; + justify-content: space-between; +} + .blog-card .description h1 { line-height: 1; margin: 0 0 5px 0; diff --git a/src/LinkDotNet.Blog.Web/Features/Home/Components/NavMenu.razor b/src/LinkDotNet.Blog.Web/Features/Home/Components/NavMenu.razor index 27cf67bb..c2ca1329 100644 --- a/src/LinkDotNet.Blog.Web/Features/Home/Components/NavMenu.razor +++ b/src/LinkDotNet.Blog.Web/Features/Home/Components/NavMenu.razor @@ -23,9 +23,11 @@ - @BlogPost.ReadingTimeInMinutes minute read + @BlogPost.ReadingTimeInMinutes minute read +
+ +
@if (BlogPost.Tags is not null && BlogPost.Tags.Any()) {
@@ -110,6 +116,7 @@ else if (BlogPost is not null) private string OgDataImage => BlogPost!.PreviewImageUrlFallback ?? BlogPost.PreviewImageUrl; private string BlogPostCanoncialUrl => $"blogPost/{BlogPost?.Id}"; private IReadOnlyCollection shortCodes = []; + private bool isBookmarked; private BlogPost? BlogPost { get; set; } @@ -129,6 +136,12 @@ else if (BlogPost is not null) { await JsRuntime.InvokeVoidAsync("hljs.highlightAll"); _ = UserRecordService.StoreUserRecordAsync(); + + if (BlogPost is not null && firstRender) + { + isBookmarked = await BookmarkService.IsBookmarked(BlogPost.Id); + StateHasChanged(); + } } private MarkupString EnrichWithShortCodes(string content) @@ -154,4 +167,17 @@ else if (BlogPost is not null) BlogPost.Likes = hasLiked ? BlogPost.Likes + 1 : BlogPost.Likes - 1; await BlogPostRepository.StoreAsync(BlogPost); } + + private async Task BlogPostBookmarked() + { + if (BlogPost is null) + { + return; + } + + isBookmarked = !isBookmarked; + await BookmarkService.SetBookmark(BlogPost.Id, isBookmarked); + StateHasChanged(); + } + } diff --git a/src/LinkDotNet.Blog.Web/Pages/_Host.cshtml b/src/LinkDotNet.Blog.Web/Pages/_Host.cshtml index a9d61e28..9b8117f2 100644 --- a/src/LinkDotNet.Blog.Web/Pages/_Host.cshtml +++ b/src/LinkDotNet.Blog.Web/Pages/_Host.cshtml @@ -5,4 +5,4 @@ Layout = "_Layout"; } - \ No newline at end of file + diff --git a/src/LinkDotNet.Blog.Web/ServiceExtensions.cs b/src/LinkDotNet.Blog.Web/ServiceExtensions.cs index 6432c7d0..ef4a1c01 100644 --- a/src/LinkDotNet.Blog.Web/ServiceExtensions.cs +++ b/src/LinkDotNet.Blog.Web/ServiceExtensions.cs @@ -4,6 +4,7 @@ using Blazorise.Bootstrap5; using LinkDotNet.Blog.Web.Features.Admin.BlogPostEditor.Services; using LinkDotNet.Blog.Web.Features.Admin.Sitemap.Services; +using LinkDotNet.Blog.Web.Features.Bookmarks; using LinkDotNet.Blog.Web.Features.Services; using LinkDotNet.Blog.Web.RegistrationExtensions; using Microsoft.AspNetCore.Builder; @@ -19,6 +20,7 @@ public static IServiceCollection AddApplicationServices(this IServiceCollection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/src/LinkDotNet.Blog.Web/wwwroot/css/fonts/Blog.json b/src/LinkDotNet.Blog.Web/wwwroot/css/fonts/Blog.json index 6fa6177b..ba5db898 100644 --- a/src/LinkDotNet.Blog.Web/wwwroot/css/fonts/Blog.json +++ b/src/LinkDotNet.Blog.Web/wwwroot/css/fonts/Blog.json @@ -1929,7 +1929,10 @@ "ligatures": "bookmark, ribbon", "name": "bookmark", "id": 210, - "order": 0 + "order": 87, + "prevSize": 32, + "code": 59858, + "tempChar": "" }, { "ligatures": "bookmarks, ribbons", @@ -3150,7 +3153,7 @@ "order": 85, "prevSize": 32, "code": 60061, - "tempChar": "" + "tempChar": "" }, { "ligatures": "youtube2, brand22", @@ -13357,6 +13360,5 @@ "showCodes": true, "gridSize": 16 }, - "uid": -1, - "time": 1731146188893 + "uid": -1 } \ No newline at end of file diff --git a/src/LinkDotNet.Blog.Web/wwwroot/css/fonts/icons.woff b/src/LinkDotNet.Blog.Web/wwwroot/css/fonts/icons.woff index 40f27b1e..e6709dc1 100644 Binary files a/src/LinkDotNet.Blog.Web/wwwroot/css/fonts/icons.woff and b/src/LinkDotNet.Blog.Web/wwwroot/css/fonts/icons.woff differ diff --git a/src/LinkDotNet.Blog.Web/wwwroot/css/fonts/icons.woff2 b/src/LinkDotNet.Blog.Web/wwwroot/css/fonts/icons.woff2 index a4f1ee4c..4953dd28 100644 Binary files a/src/LinkDotNet.Blog.Web/wwwroot/css/fonts/icons.woff2 and b/src/LinkDotNet.Blog.Web/wwwroot/css/fonts/icons.woff2 differ diff --git a/src/LinkDotNet.Blog.Web/wwwroot/css/icons.css b/src/LinkDotNet.Blog.Web/wwwroot/css/icons.css index 1a65e5e8..fca2aad1 100644 --- a/src/LinkDotNet.Blog.Web/wwwroot/css/icons.css +++ b/src/LinkDotNet.Blog.Web/wwwroot/css/icons.css @@ -112,3 +112,6 @@ i { .bluesky:before { content: "\e902"; } +.bookmark:before { + content: "\e9d2"; +} diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/DraftBlogPost/DraftBlogPostPageTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/DraftBlogPost/DraftBlogPostPageTests.cs index cca5020c..b932a383 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/DraftBlogPost/DraftBlogPostPageTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/DraftBlogPost/DraftBlogPostPageTests.cs @@ -2,6 +2,7 @@ using LinkDotNet.Blog.Domain; using LinkDotNet.Blog.TestUtilities; using LinkDotNet.Blog.Web.Features.Admin.DraftBlogPost; + using LinkDotNet.Blog.Web.Features.Bookmarks; using LinkDotNet.Blog.Web.Features.Components; using Microsoft.Extensions.DependencyInjection; @@ -19,6 +20,7 @@ public async Task ShouldOnlyShowPublishedPosts() using var ctx = new BunitContext(); ctx.JSInterop.Mode = JSRuntimeMode.Loose; ctx.Services.AddScoped(_ => Repository); + ctx.Services.AddScoped(_ => Substitute.For()); var cut = ctx.Render(); cut.WaitForElement(".blog-card"); diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Bookmarks/BookmarksTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Bookmarks/BookmarksTests.cs new file mode 100644 index 00000000..88d67b82 --- /dev/null +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Bookmarks/BookmarksTests.cs @@ -0,0 +1,82 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using LinkDotNet.Blog.Domain; +using LinkDotNet.Blog.TestUtilities; +using LinkDotNet.Blog.Web.Features.Bookmarks; +using LinkDotNet.Blog.Web.Features.Bookmarks.Components; +using LinkDotNet.Blog.Web.Features.Components; +using Microsoft.Extensions.DependencyInjection; + +namespace LinkDotNet.Blog.IntegrationTests.Web.Features.Bookmarks; + +public class BookmarksTests : SqlDatabaseTestBase +{ + [Fact] + public async Task ShouldOnlyDisplayBookmarkedPosts() + { + // Arrange + using var ctx = new BunitContext(); + var bookmarkService = Substitute.For(); + var bookmarkedBlogPost = new BlogPostBuilder().WithTitle("Bookmarked Post").Build(); + var nonBookmarkedBlogPost = new BlogPostBuilder().WithTitle("Non-Bookmarked Post").Build(); + await Repository.StoreAsync(bookmarkedBlogPost); + await Repository.StoreAsync(nonBookmarkedBlogPost); + bookmarkService.GetBookmarkedPostIds().Returns(new List { bookmarkedBlogPost.Id }); + ctx.Services.AddScoped(_ => Repository); + ctx.Services.AddScoped(_ => bookmarkService); + + // Act + var cut = ctx.Render(); + + // Assert + cut.WaitForElement(".blog-card"); + var blogPosts = cut.FindComponents(); + + blogPosts.ShouldHaveSingleItem(); + blogPosts[0].Find(".description h1").TextContent.ShouldBe("Bookmarked Post"); + } + + [Fact] + public async Task ShouldRemoveBookmarkWhenButtonIsClicked() + { + // Arrange + using var ctx = new BunitContext(); + var bookmarkService = Substitute.For(); + var bookmarkedBlogPost = new BlogPostBuilder().WithTitle("Bookmarked Post").Build(); + await Repository.StoreAsync(bookmarkedBlogPost); + bookmarkService.GetBookmarkedPostIds().Returns(new List { bookmarkedBlogPost.Id }); + bookmarkService.IsBookmarked(bookmarkedBlogPost.Id).Returns(true); + ctx.Services.AddScoped(_ => Repository); + ctx.Services.AddScoped(_ => bookmarkService); + + // Act + var cut = ctx.Render(); + cut.WaitForElement(".blog-card"); + + // Find and click the bookmark button + var bookmarkButton = cut.FindComponent().Find("button"); + bookmarkButton.Click(); + + // Assert + await bookmarkService.Received(1).SetBookmark(bookmarkedBlogPost.Id, false); + } + + [Fact] + public void ShouldDisplayMessageWhenNoBookmarksExist() + { + // Arrange + using var ctx = new BunitContext(); + var bookmarkService = Substitute.For(); + bookmarkService.GetBookmarkedPostIds().Returns(new List()); + ctx.Services.AddScoped(_ => Repository); + ctx.Services.AddScoped(_ => bookmarkService); + + // Act + var cut = ctx.Render(); + + // Assert + cut.WaitForElement("p"); + cut.Find("p").TextContent.ShouldBe("You have no bookmarks"); + cut.FindComponents().Count.ShouldBe(0); + } +} diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Home/IndexTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Home/IndexTests.cs index b52f3ff3..a5a64d29 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Home/IndexTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Home/IndexTests.cs @@ -3,6 +3,7 @@ using LinkDotNet.Blog.Domain; using LinkDotNet.Blog.TestUtilities; using LinkDotNet.Blog.Web; +using LinkDotNet.Blog.Web.Features.Bookmarks; using LinkDotNet.Blog.Web.Features.Components; using LinkDotNet.Blog.Web.Features.Home; using LinkDotNet.Blog.Web.Features.Services; @@ -168,5 +169,6 @@ private void RegisterComponents(BunitContext ctx, string? profilePictureUri = nu ctx.Services.AddScoped(_ => Options.Create(CreateSampleAppConfiguration(profilePictureUri).ApplicationConfiguration)); ctx.Services.AddScoped(_ => Options.Create(CreateSampleAppConfiguration(profilePictureUri).Introduction)); ctx.Services.AddScoped(_ => Substitute.For()); + ctx.Services.AddScoped(_ => Substitute.For()); } } diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Search/SearchPageTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Search/SearchPageTests.cs index bab5d7e4..8813ba5f 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Search/SearchPageTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Search/SearchPageTests.cs @@ -1,6 +1,7 @@ using System.Threading.Tasks; using LinkDotNet.Blog.Domain; using LinkDotNet.Blog.TestUtilities; +using LinkDotNet.Blog.Web.Features.Bookmarks; using LinkDotNet.Blog.Web.Features.Components; using LinkDotNet.Blog.Web.Features.Search; using Microsoft.Extensions.DependencyInjection; @@ -58,5 +59,6 @@ public async Task ShouldUnescapeQuery() private void RegisterServices(BunitContext ctx) { ctx.Services.AddScoped(_ => Repository); + ctx.Services.AddScoped(_ => Substitute.For()); } } diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/SearchByTag/SearchByTagPageTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/SearchByTag/SearchByTagPageTests.cs index 325d039f..5628328e 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/SearchByTag/SearchByTagPageTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/SearchByTag/SearchByTagPageTests.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using LinkDotNet.Blog.Domain; using LinkDotNet.Blog.TestUtilities; +using LinkDotNet.Blog.Web.Features.Bookmarks; using LinkDotNet.Blog.Web.Features.SearchByTag; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; @@ -66,6 +67,7 @@ private async Task AddBlogPostWithTagAsync(string tag, bool isPublished = true) private void RegisterServices(BunitContext ctx) { ctx.Services.AddScoped(_ => Repository); + ctx.Services.AddScoped(_ => Substitute.For()); } private class PageTitleStub : ComponentBase diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs index 22923471..817c6d6f 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs @@ -4,6 +4,7 @@ using LinkDotNet.Blog.Infrastructure; using LinkDotNet.Blog.Infrastructure.Persistence; using LinkDotNet.Blog.TestUtilities; +using LinkDotNet.Blog.Web.Features.Bookmarks; using LinkDotNet.Blog.Web.Features.Components; using LinkDotNet.Blog.Web.Features.Services; using LinkDotNet.Blog.Web.Features.ShowBlogPost; @@ -148,5 +149,6 @@ private void RegisterComponents(BunitContext ctx, ILocalStorageService? localSto var shortCodeRepository = Substitute.For>(); shortCodeRepository.GetAllAsync().Returns(PagedList.Empty); ctx.Services.AddScoped(_ => shortCodeRepository); + ctx.Services.AddScoped(_ => Substitute.For()); } } \ No newline at end of file diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Bookmarks/BookmarkServiceTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Bookmarks/BookmarkServiceTests.cs new file mode 100644 index 00000000..903ad59e --- /dev/null +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Bookmarks/BookmarkServiceTests.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using LinkDotNet.Blog.Web.Features.Bookmarks; +using LinkDotNet.Blog.Web.Features.Services; +using NSubstitute.ReceivedExtensions; + +namespace LinkDotNet.Blog.UnitTests.Web.Features.Bookmarks; + +public class BookmarkServiceTests +{ + private readonly ILocalStorageService localStorageService; + private readonly IBookmarkService bookmarkService; + + public BookmarkServiceTests() + { + localStorageService = Substitute.For(); + bookmarkService = new BookmarkService(localStorageService); + } + + [Fact] + public async Task ShouldReturnIds() + { + localStorageService + .GetItemAsync>("bookmarks") + .Returns(x => ["1", "2", "3"]); + + var ids = await bookmarkService.GetBookmarkedPostIds(); + + ids.ShouldBe(["1", "2", "3"]); + ids.ShouldBeUnique(); + } + + [Fact] + public async Task ShouldReturnEmptyListWhenNoBookmarks() + { + localStorageService + .GetItemAsync>("bookmarks") + .Returns(x => []); + + var ids = await bookmarkService.GetBookmarkedPostIds(); + + ids.ShouldBeEmpty(); + } + + [Fact] + public async Task ShouldReturnTrueIfBookmarked() + { + localStorageService + .GetItemAsync>("bookmarks") + .Returns(x => ["1", "2", "3"]); + + var isBookmarked = await bookmarkService.IsBookmarked("1"); + + isBookmarked.ShouldBeTrue(); + } + + [Fact] + public async Task ShouldReturnFalseIfNoBookmarked() + { + localStorageService + .GetItemAsync>("bookmarks") + .Returns(x => ["1", "2", "3"]); + + var isBookmarked = await bookmarkService.IsBookmarked("4"); + + isBookmarked.ShouldBeFalse(); + } + + [Fact] + public async Task ShouldThrowArgumentExceptionWhenIdIsEmptyOrNull() + { + var id = string.Empty; + + await bookmarkService.SetBookmark(id, false) + .ShouldThrowAsync(); + + await bookmarkService.IsBookmarked(id) + .ShouldThrowAsync(); + } +} diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Components/ShortBlogPostTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Components/ShortBlogPostTests.cs index d70d755b..b3d4511d 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Components/ShortBlogPostTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Components/ShortBlogPostTests.cs @@ -1,7 +1,9 @@ using System; using System.Linq; using LinkDotNet.Blog.TestUtilities; +using LinkDotNet.Blog.Web.Features.Bookmarks; using LinkDotNet.Blog.Web.Features.Components; +using Microsoft.Extensions.DependencyInjection; namespace LinkDotNet.Blog.UnitTests.Web.Features.Components; @@ -10,6 +12,7 @@ public class ShortBlogPostTests : BunitContext [Fact] public void ShouldOpenBlogPost() { + Services.AddScoped(_ => Substitute.For()); var blogPost = new BlogPostBuilder().Build(); blogPost.Id = "SomeId"; var cut = Render( @@ -23,6 +26,7 @@ public void ShouldOpenBlogPost() [Fact] public void ShouldNavigateToEscapedTagSiteWhenClickingOnTag() { + Services.AddScoped(_ => Substitute.For()); var blogPost = new BlogPostBuilder().WithTags("Tag 1").Build(); var cut = Render( p => p.Add(c => c.BlogPost, blogPost)); @@ -35,6 +39,7 @@ public void ShouldNavigateToEscapedTagSiteWhenClickingOnTag() [Fact] public void WhenNoTagsAreGivenThenTagsAreNotShown() { + Services.AddScoped(_ => Substitute.For()); var blogPost = new BlogPostBuilder().Build(); var cut = Render( @@ -46,6 +51,7 @@ public void WhenNoTagsAreGivenThenTagsAreNotShown() [Fact] public void GivenBlogPostThatIsScheduled_ThenIndicating() { + Services.AddScoped(_ => Substitute.For()); var blogPost = new BlogPostBuilder().IsPublished(false).WithScheduledPublishDate(new DateTime(2099, 1, 1)) .Build(); @@ -58,6 +64,7 @@ public void GivenBlogPostThatIsScheduled_ThenIndicating() [Fact] public void GivenBlogPostThatIsNotPublishedAndNotScheduled_ThenIndicating() { + Services.AddScoped(_ => Substitute.For()); var blogPost = new BlogPostBuilder().IsPublished(false).Build(); var cut = Render( @@ -69,6 +76,7 @@ public void GivenBlogPostThatIsNotPublishedAndNotScheduled_ThenIndicating() [Fact] public void GivenBlogPostThatIsPublished_ThenNoDraft() { + Services.AddScoped(_ => Substitute.For()); var blogPost = new BlogPostBuilder().IsPublished(true).Build(); var cut = Render( diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs index d661af9b..98baee9d 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs @@ -5,6 +5,7 @@ using LinkDotNet.Blog.Infrastructure; using LinkDotNet.Blog.Infrastructure.Persistence; using LinkDotNet.Blog.TestUtilities; +using LinkDotNet.Blog.Web.Features.Bookmarks; using LinkDotNet.Blog.Web.Features.Components; using LinkDotNet.Blog.Web.Features.Services; using LinkDotNet.Blog.Web.Features.ShowBlogPost; @@ -31,6 +32,7 @@ public ShowBlogPostPageTests() Services.AddScoped(_ => Substitute.For()); Services.AddScoped(_ => Substitute.For()); Services.AddScoped(_ => Options.Create(new ApplicationConfigurationBuilder().Build())); + Services.AddScoped(_ => Substitute.For()); AddAuthorization(); ComponentFactories.Add(); ComponentFactories.Add();