From 80f11708de05ed2eeb1593c9158cb31bfa5ab40a Mon Sep 17 00:00:00 2001 From: Arnab Roy Chowdhury Date: Sat, 30 Aug 2025 13:35:46 +0530 Subject: [PATCH 01/19] Add author name in blog post entity --- src/LinkDotNet.Blog.Domain/BlogPost.cs | 7 ++++++- .../Persistence/Sql/Mapping/BlogPostConfiguration.cs | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/LinkDotNet.Blog.Domain/BlogPost.cs b/src/LinkDotNet.Blog.Domain/BlogPost.cs index 3a52b28e..9d0319f1 100644 --- a/src/LinkDotNet.Blog.Domain/BlogPost.cs +++ b/src/LinkDotNet.Blog.Domain/BlogPost.cs @@ -38,6 +38,8 @@ public sealed partial class BlogPost : Entity public string Slug => GenerateSlug(); + public string? AuthorName { get; private set; } + private string GenerateSlug() { if (string.IsNullOrWhiteSpace(Title)) @@ -92,7 +94,8 @@ public static BlogPost Create( DateTime? updatedDate = null, DateTime? scheduledPublishDate = null, IEnumerable? tags = null, - string? previewImageUrlFallback = null) + string? previewImageUrlFallback = null, + string? authorName = null) { if (scheduledPublishDate is not null && isPublished) { @@ -113,6 +116,7 @@ public static BlogPost Create( IsPublished = isPublished, Tags = tags?.Select(t => t.Trim()).ToImmutableArray() ?? [], ReadingTimeInMinutes = ReadingTimeCalculator.CalculateReadingTime(content), + AuthorName = authorName }; return blogPost; @@ -143,5 +147,6 @@ public void Update(BlogPost from) IsPublished = from.IsPublished; Tags = from.Tags; ReadingTimeInMinutes = from.ReadingTimeInMinutes; + AuthorName = from.AuthorName; } } diff --git a/src/LinkDotNet.Blog.Infrastructure/Persistence/Sql/Mapping/BlogPostConfiguration.cs b/src/LinkDotNet.Blog.Infrastructure/Persistence/Sql/Mapping/BlogPostConfiguration.cs index e4f1c03e..bdba5a05 100644 --- a/src/LinkDotNet.Blog.Infrastructure/Persistence/Sql/Mapping/BlogPostConfiguration.cs +++ b/src/LinkDotNet.Blog.Infrastructure/Persistence/Sql/Mapping/BlogPostConfiguration.cs @@ -21,6 +21,7 @@ public void Configure(EntityTypeBuilder builder) builder.Property(x => x.IsPublished).IsRequired(); builder.Property(x => x.Tags).HasMaxLength(2048); + builder.Property(x => x.AuthorName).HasMaxLength(256).IsRequired(false); builder.HasIndex(x => new { x.IsPublished, x.UpdatedDate }) .HasDatabaseName("IX_BlogPosts_IsPublished_UpdatedDate") From 7d6094eeead2f515ddb582cd4a97fc14f13ff633 Mon Sep 17 00:00:00 2001 From: Arnab Roy Chowdhury Date: Sat, 30 Aug 2025 13:37:49 +0530 Subject: [PATCH 02/19] Add IsMultiModeEnabled config --- src/LinkDotNet.Blog.Web/ApplicationConfiguration.cs | 2 ++ src/LinkDotNet.Blog.Web/appsettings.json | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/LinkDotNet.Blog.Web/ApplicationConfiguration.cs b/src/LinkDotNet.Blog.Web/ApplicationConfiguration.cs index 33274f8c..7b459e3e 100644 --- a/src/LinkDotNet.Blog.Web/ApplicationConfiguration.cs +++ b/src/LinkDotNet.Blog.Web/ApplicationConfiguration.cs @@ -23,4 +23,6 @@ public sealed record ApplicationConfiguration public bool ShowReadingIndicator { get; init; } public bool ShowSimilarPosts { get; init; } + + public bool IsMultiModeEnabled { get; init; } } diff --git a/src/LinkDotNet.Blog.Web/appsettings.json b/src/LinkDotNet.Blog.Web/appsettings.json index d61a5cf0..bfac91c2 100644 --- a/src/LinkDotNet.Blog.Web/appsettings.json +++ b/src/LinkDotNet.Blog.Web/appsettings.json @@ -46,5 +46,6 @@ "CdnEndpoint": "" }, "ShowReadingIndicator": true, - "ShowSimilarPosts": true + "ShowSimilarPosts": true, + "IsMultiModeEnabled": true } From 50b5525681c4ff43382eda44fde952b3d5e9ba18 Mon Sep 17 00:00:00 2001 From: Arnab Roy Chowdhury Date: Sat, 30 Aug 2025 13:39:44 +0530 Subject: [PATCH 03/19] Add author name in the claim at the time of login, if multi mode is enabled --- .../Authentication/Dummy/DummyLoginManager.cs | 6 ++-- .../Authentication/ILoginManager.cs | 4 +-- .../OpenIdConnect/AuthLoginManager.cs | 24 +++++++++++++--- src/LinkDotNet.Blog.Web/Pages/Login.cshtml | 28 +++++++++++++++++-- src/LinkDotNet.Blog.Web/Pages/Login.cshtml.cs | 16 +++++++++-- 5 files changed, 65 insertions(+), 13 deletions(-) diff --git a/src/LinkDotNet.Blog.Web/Authentication/Dummy/DummyLoginManager.cs b/src/LinkDotNet.Blog.Web/Authentication/Dummy/DummyLoginManager.cs index e499c888..fa6a02db 100644 --- a/src/LinkDotNet.Blog.Web/Authentication/Dummy/DummyLoginManager.cs +++ b/src/LinkDotNet.Blog.Web/Authentication/Dummy/DummyLoginManager.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; @@ -22,11 +22,11 @@ public async Task SignOutAsync(string redirectUri = "/") context.Response.Redirect(redirectUri); } - public async Task SignInAsync(string redirectUri) + public async Task SignInAsync(string redirectUri, string? authorName = null) { var claims = new[] { - new Claim(ClaimTypes.Name, "Dummy user"), + new Claim(ClaimTypes.Name, authorName ?? "Dummy user"), new Claim(ClaimTypes.NameIdentifier, Guid.NewGuid().ToString()), }; var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); diff --git a/src/LinkDotNet.Blog.Web/Authentication/ILoginManager.cs b/src/LinkDotNet.Blog.Web/Authentication/ILoginManager.cs index 624a5975..f3a31f18 100644 --- a/src/LinkDotNet.Blog.Web/Authentication/ILoginManager.cs +++ b/src/LinkDotNet.Blog.Web/Authentication/ILoginManager.cs @@ -1,10 +1,10 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; namespace LinkDotNet.Blog.Web.Authentication; public interface ILoginManager { - Task SignInAsync(string redirectUri); + Task SignInAsync(string redirectUri, string? authorName = null); Task SignOutAsync(string redirectUri = "/"); } diff --git a/src/LinkDotNet.Blog.Web/Authentication/OpenIdConnect/AuthLoginManager.cs b/src/LinkDotNet.Blog.Web/Authentication/OpenIdConnect/AuthLoginManager.cs index 716adbc9..d34160d6 100644 --- a/src/LinkDotNet.Blog.Web/Authentication/OpenIdConnect/AuthLoginManager.cs +++ b/src/LinkDotNet.Blog.Web/Authentication/OpenIdConnect/AuthLoginManager.cs @@ -1,4 +1,5 @@ using System; +using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; @@ -21,12 +22,27 @@ public AuthLoginManager(IHttpContextAccessor httpContextAccessor, IOptions + + Login + + + +
+

Login

+
+
+ + +
+ + Back to home +
+
+ + diff --git a/src/LinkDotNet.Blog.Web/Pages/Login.cshtml.cs b/src/LinkDotNet.Blog.Web/Pages/Login.cshtml.cs index 5b0f520b..694762ba 100644 --- a/src/LinkDotNet.Blog.Web/Pages/Login.cshtml.cs +++ b/src/LinkDotNet.Blog.Web/Pages/Login.cshtml.cs @@ -1,20 +1,32 @@ +using System; using System.Threading.Tasks; using LinkDotNet.Blog.Web.Authentication; using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Options; namespace LinkDotNet.Blog.Web.Pages; public sealed partial class LoginModel : PageModel { private readonly ILoginManager loginManager; + private readonly ApplicationConfiguration applicationConfiguration; - public LoginModel(ILoginManager loginManager) + public LoginModel(ILoginManager loginManager, IOptions options) { + ArgumentNullException.ThrowIfNull(options); + this.loginManager = loginManager; + applicationConfiguration = options.Value; } public async Task OnGet(string redirectUri) { - await loginManager.SignInAsync(redirectUri); + if (!applicationConfiguration.IsMultiModeEnabled) + { + await loginManager.SignInAsync(redirectUri); + } } + + public async Task OnPost(string redirectUri, string authorName) + => await loginManager.SignInAsync(redirectUri, authorName); } From d6f003b45b415fb619204684e09aedf445c045e2 Mon Sep 17 00:00:00 2001 From: Arnab Roy Chowdhury Date: Sat, 30 Aug 2025 13:43:44 +0530 Subject: [PATCH 04/19] Save author name in the db at the time of blog post create and update --- .../Components/CreateNewBlogPost.razor | 11 +++++++++++ .../BlogPostEditor/Components/CreateNewModel.cs | 13 +++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor index 9a2f97e7..fdc7fc93 100644 --- a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor +++ b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor @@ -2,12 +2,15 @@ @using LinkDotNet.Blog.Infrastructure @using LinkDotNet.Blog.Infrastructure.Persistence @using LinkDotNet.Blog.Web.Features.Services +@using Microsoft.AspNetCore.Http @using NCronJob @using System.Threading @inject IJSRuntime JSRuntime @inject ICacheInvalidator CacheInvalidator @inject IInstantJobRegistry InstantJobRegistry @inject IRepository ShortCodeRepository +@inject IOptions AppConfiguration +@inject IHttpContextAccessor HttpContextAccessor Creating new Blog Post @@ -276,9 +279,16 @@ private bool IsScheduled => model.ScheduledPublishDate.HasValue; + private string? authorName; + protected override async Task OnInitializedAsync() { shortCodes = await ShortCodeRepository.GetAllAsync(); + + if (AppConfiguration.Value.IsMultiModeEnabled) + { + authorName = HttpContextAccessor.HttpContext?.User.Identity?.Name; + } } protected override void OnParametersSet() @@ -322,6 +332,7 @@ private async Task OnValidBlogPostCreatedAsync() { canSubmit = false; + model.AuthorName = authorName; await OnBlogPostCreated.InvokeAsync(model.ToBlogPost()); if (model.ShouldInvalidateCache) { diff --git a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewModel.cs b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewModel.cs index d574c320..cca56134 100644 --- a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewModel.cs +++ b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewModel.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ComponentModel.DataAnnotations; using LinkDotNet.Blog.Domain; @@ -18,6 +18,7 @@ public sealed class CreateNewModel private string tags = string.Empty; private string previewImageUrlFallback = string.Empty; private DateTime? scheduledPublishDate; + private string? authorName; [Required] [MaxLength(256)] @@ -91,6 +92,12 @@ public bool ShouldInvalidateCache set => SetProperty(out shouldInvalidateCache, value); } + public string? AuthorName + { + get => authorName; + set => SetProperty(out authorName, value); + } + public bool IsDirty { get; private set; } public static CreateNewModel FromBlogPost(BlogPost blogPost) @@ -109,6 +116,7 @@ public static CreateNewModel FromBlogPost(BlogPost blogPost) originalUpdatedDate = blogPost.UpdatedDate, PreviewImageUrlFallback = blogPost.PreviewImageUrlFallback ?? string.Empty, scheduledPublishDate = blogPost.ScheduledPublishDate?.ToUniversalTime(), + authorName = blogPost.AuthorName, IsDirty = false, }; } @@ -131,7 +139,8 @@ public BlogPost ToBlogPost() updatedDate, scheduledPublishDate, tagList, - PreviewImageUrlFallback); + PreviewImageUrlFallback, + AuthorName); blogPost.Id = id; return blogPost; } From 51af4270d08e76b15b2e0f951fc48e2d0c60addf Mon Sep 17 00:00:00 2001 From: Arnab Roy Chowdhury Date: Sat, 30 Aug 2025 13:46:31 +0530 Subject: [PATCH 05/19] Show author name in the top menu if multi mode is enabled --- .../Home/Components/AccessControl.razor | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/LinkDotNet.Blog.Web/Features/Home/Components/AccessControl.razor b/src/LinkDotNet.Blog.Web/Features/Home/Components/AccessControl.razor index 83e4e2e2..92ab5cb3 100644 --- a/src/LinkDotNet.Blog.Web/Features/Home/Components/AccessControl.razor +++ b/src/LinkDotNet.Blog.Web/Features/Home/Components/AccessControl.razor @@ -1,4 +1,8 @@ - +@using Microsoft.AspNetCore.Http +@inject IOptions AppConfiguration +@inject IHttpContextAccessor HttpContextAccessor + + - + @@ -30,4 +34,14 @@ @code { [Parameter] public string CurrentUri { get; set; } = string.Empty; + + private string? authorName; + + protected override void OnInitialized() + { + if (AppConfiguration.Value.IsMultiModeEnabled) + { + authorName = HttpContextAccessor.HttpContext?.User.Identity?.Name; + } + } } From ecaa23dd65eea79f36fcdb6ff457029474d2f171 Mon Sep 17 00:00:00 2001 From: Arnab Roy Chowdhury Date: Sat, 30 Aug 2025 13:47:12 +0530 Subject: [PATCH 06/19] Update tests --- .../Sql/BlogPostRepositoryTests.cs | 61 ++++++++++++++--- .../CreateNewBlogPostPageTests.cs | 38 ++++++++++- .../BlogPostEditor/UpdateBlogPostPageTests.cs | 54 ++++++++++++++- .../Web/Shared/NavMenuTests.cs | 12 +++- .../ApplicationConfigurationBuilder.cs | 10 ++- .../BlogPostBuilder.cs | 12 +++- .../Domain/BlogPostTests.cs | 21 +++++- .../Web/ApplicationConfigurationTests.cs | 4 +- .../Components/CreateNewBlogPostTests.cs | 53 ++++++++++++++- .../Home/Components/AccessControlTests.cs | 56 +++++++++++++++- .../Web/Pages/LoginModelTests.cs | 67 +++++++++++++++++-- 11 files changed, 353 insertions(+), 35 deletions(-) diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Infrastructure/Persistence/Sql/BlogPostRepositoryTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Infrastructure/Persistence/Sql/BlogPostRepositoryTests.cs index 9fe8fd06..b07b379d 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Infrastructure/Persistence/Sql/BlogPostRepositoryTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Infrastructure/Persistence/Sql/BlogPostRepositoryTests.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using System.Threading.Tasks; using LinkDotNet.Blog.Domain; using LinkDotNet.Blog.Infrastructure.Persistence; @@ -11,10 +11,15 @@ namespace LinkDotNet.Blog.IntegrationTests.Infrastructure.Persistence.Sql; public sealed class BlogPostRepositoryTests : SqlDatabaseTestBase { - [Fact] - public async Task ShouldLoadBlogPost() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ShouldLoadBlogPost(bool isAuthorEnable) { - var blogPost = BlogPost.Create("Title", "Subtitle", "Content", "url", true, tags: new[] { "Tag 1", "Tag 2" }); + var blogPost = isAuthorEnable + ? BlogPost.Create("Title", "Subtitle", "Content", "url", true, tags: new[] { "Tag 1", "Tag 2" }, authorName: "Test Author") + : BlogPost.Create("Title", "Subtitle", "Content", "url", true, tags: new[] { "Tag 1", "Tag 2" }); + await DbContext.BlogPosts.AddAsync(blogPost, TestContext.Current.CancellationToken); await DbContext.SaveChangesAsync(TestContext.Current.CancellationToken); @@ -30,12 +35,25 @@ public async Task ShouldLoadBlogPost() var tagContent = blogPostFromRepo.Tags; tagContent.ShouldContain("Tag 1"); tagContent.ShouldContain("Tag 2"); + + if (isAuthorEnable) + { + blogPostFromRepo.AuthorName.ShouldBe("Test Author"); + } + else + { + blogPostFromRepo.AuthorName.ShouldBeNull(); + } } - [Fact] - public async Task ShouldSaveBlogPost() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ShouldSaveBlogPost(bool isAuthorEnable) { - var blogPost = BlogPost.Create("Title", "Subtitle", "Content", "url", true, tags: new[] { "Tag 1", "Tag 2" }); + var blogPost = isAuthorEnable + ? BlogPost.Create("Title", "Subtitle", "Content", "url", true, tags: new[] { "Tag 1", "Tag 2" }, authorName: "Test Author") + : BlogPost.Create("Title", "Subtitle", "Content", "url", true, tags: new[] { "Tag 1", "Tag 2" }); await Repository.StoreAsync(blogPost); @@ -53,12 +71,26 @@ public async Task ShouldSaveBlogPost() var tagContent = blogPostFromContext.Tags; tagContent.ShouldContain("Tag 1"); tagContent.ShouldContain("Tag 2"); + + if (isAuthorEnable) + { + blogPostFromContext.AuthorName.ShouldBe("Test Author"); + } + else + { + blogPostFromContext.AuthorName.ShouldBeNull(); + } } - [Fact] - public async Task ShouldGetAllBlogPosts() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ShouldGetAllBlogPosts(bool isAuthorEnable) { - var blogPost = BlogPost.Create("Title", "Subtitle", "Content", "url", true, tags: new[] { "Tag 1", "Tag 2" }); + var blogPost = isAuthorEnable + ? BlogPost.Create("Title", "Subtitle", "Content", "url", true, tags: new[] { "Tag 1", "Tag 2" }, authorName: "Test Author") + : BlogPost.Create("Title", "Subtitle", "Content", "url", true, tags: new[] { "Tag 1", "Tag 2" }); + await DbContext.BlogPosts.AddAsync(blogPost, TestContext.Current.CancellationToken); await DbContext.SaveChangesAsync(TestContext.Current.CancellationToken); @@ -76,6 +108,15 @@ public async Task ShouldGetAllBlogPosts() var tagContent = blogPostFromRepo.Tags; tagContent.ShouldContain("Tag 1"); tagContent.ShouldContain("Tag 2"); + + if (isAuthorEnable) + { + blogPostFromRepo.AuthorName.ShouldBe("Test Author"); + } + else + { + blogPostFromRepo.AuthorName.ShouldBeNull(); + } } [Fact] diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/BlogPostEditor/CreateNewBlogPostPageTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/BlogPostEditor/CreateNewBlogPostPageTests.cs index bb33eaea..d4266e40 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/BlogPostEditor/CreateNewBlogPostPageTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/BlogPostEditor/CreateNewBlogPostPageTests.cs @@ -1,18 +1,21 @@ -using System.Threading; +using System.Threading; using System.Threading.Tasks; using Blazored.Toast.Services; using LinkDotNet.Blog.Domain; using LinkDotNet.Blog.Infrastructure; using LinkDotNet.Blog.Infrastructure.Persistence; using LinkDotNet.Blog.TestUtilities.Fakes; +using LinkDotNet.Blog.Web; using LinkDotNet.Blog.Web.Features; using LinkDotNet.Blog.Web.Features.Admin.BlogPostEditor; using LinkDotNet.Blog.Web.Features.Admin.BlogPostEditor.Components; using LinkDotNet.Blog.Web.Features.Admin.BlogPostEditor.Services; using LinkDotNet.Blog.Web.Features.Components; using LinkDotNet.Blog.Web.Features.Services; +using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using NCronJob; using TestContext = Xunit.TestContext; @@ -20,8 +23,10 @@ namespace LinkDotNet.Blog.IntegrationTests.Web.Features.Admin.BlogPostEditor; public class CreateNewBlogPostPageTests : SqlDatabaseTestBase { - [Fact] - public async Task ShouldSaveBlogPostOnSave() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ShouldSaveBlogPostOnSave(bool isMultiModeEnabled) { await using var ctx = new BunitContext(); ctx.ComponentFactories.Add(); @@ -37,6 +42,23 @@ public async Task ShouldSaveBlogPostOnSave() var shortCodeRepository = Substitute.For>(); shortCodeRepository.GetAllAsync().Returns(PagedList.Empty); ctx.Services.AddScoped(_ => shortCodeRepository); + + var contextAccessor = Substitute.For(); + contextAccessor.HttpContext?.User.Identity?.Name.Returns("Test Author"); + ctx.Services.AddScoped(_ => contextAccessor); + + var options = Substitute.For>(); + + options.Value.Returns(new ApplicationConfiguration() + { + IsMultiModeEnabled = isMultiModeEnabled, + BlogName = "Test", + ConnectionString = "Test", + DatabaseName = "Test" + }); + + ctx.Services.AddScoped(_ => options); + using var cut = ctx.Render(); var newBlogPost = cut.FindComponent(); @@ -45,6 +67,16 @@ public async Task ShouldSaveBlogPostOnSave() var blogPostFromDb = await DbContext.BlogPosts.SingleOrDefaultAsync(t => t.Title == "My Title", TestContext.Current.CancellationToken); blogPostFromDb.ShouldNotBeNull(); blogPostFromDb.ShortDescription.ShouldBe("My short Description"); + + if (isMultiModeEnabled) + { + blogPostFromDb.AuthorName.ShouldBe("Test Author"); + } + else + { + blogPostFromDb.AuthorName.ShouldBeNull(); + } + toastService.Received(1).ShowInfo("Created BlogPost My Title", null); instantRegistry.Received(1).RunInstantJob(Arg.Any(), Arg.Any()); } diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/BlogPostEditor/UpdateBlogPostPageTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/BlogPostEditor/UpdateBlogPostPageTests.cs index c52b9ff5..f274138b 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/BlogPostEditor/UpdateBlogPostPageTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/BlogPostEditor/UpdateBlogPostPageTests.cs @@ -7,14 +7,17 @@ using LinkDotNet.Blog.Infrastructure.Persistence; using LinkDotNet.Blog.TestUtilities; using LinkDotNet.Blog.TestUtilities.Fakes; +using LinkDotNet.Blog.Web; using LinkDotNet.Blog.Web.Features; using LinkDotNet.Blog.Web.Features.Admin.BlogPostEditor; using LinkDotNet.Blog.Web.Features.Admin.BlogPostEditor.Components; using LinkDotNet.Blog.Web.Features.Components; using LinkDotNet.Blog.Web.Features.Services; using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using NCronJob; using TestContext = Xunit.TestContext; @@ -22,8 +25,10 @@ namespace LinkDotNet.Blog.IntegrationTests.Web.Features.Admin.BlogPostEditor; public class UpdateBlogPostPageTests : SqlDatabaseTestBase { - [Fact] - public async Task ShouldSaveBlogPostOnSave() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ShouldSaveBlogPostOnSave(bool isMultiModeEnabled) { await using var ctx = new BunitContext(); ctx.ComponentFactories.Add(); @@ -40,6 +45,23 @@ public async Task ShouldSaveBlogPostOnSave() var shortCodeRepository = Substitute.For>(); shortCodeRepository.GetAllAsync().Returns(PagedList.Empty); ctx.Services.AddScoped(_ => shortCodeRepository); + + var contextAccessor = Substitute.For(); + contextAccessor.HttpContext?.User.Identity?.Name.Returns("Test Author"); + ctx.Services.AddScoped(_ => contextAccessor); + + var options = Substitute.For>(); + + options.Value.Returns(new ApplicationConfiguration() + { + IsMultiModeEnabled = isMultiModeEnabled, + BlogName = "Test", + ConnectionString = "Test", + DatabaseName = "Test" + }); + + ctx.Services.AddScoped(_ => options); + using var cut = ctx.Render( p => p.Add(s => s.BlogPostId, blogPost.Id)); var newBlogPost = cut.FindComponent(); @@ -49,6 +71,16 @@ public async Task ShouldSaveBlogPostOnSave() var blogPostFromDb = await DbContext.BlogPosts.SingleOrDefaultAsync(t => t.Id == blogPost.Id, TestContext.Current.CancellationToken); blogPostFromDb.ShouldNotBeNull(); blogPostFromDb.ShortDescription.ShouldBe("My new Description"); + + if (isMultiModeEnabled) + { + blogPostFromDb.AuthorName.ShouldBe("Test Author"); + } + else + { + blogPostFromDb.AuthorName.ShouldBeNull(); + } + toastService.Received(1).ShowInfo("Updated BlogPost Title", null); instantRegistry.Received(1).RunInstantJob(Arg.Any(), Arg.Any()); } @@ -63,6 +95,22 @@ public void ShouldThrowWhenNoIdProvided() ctx.Services.AddScoped(_ => Substitute.For()); ctx.Services.AddScoped(_ => Substitute.For()); + var contextAccessor = Substitute.For(); + contextAccessor.HttpContext?.User.Identity?.Name.Returns("Test Author"); + ctx.Services.AddScoped(_ => contextAccessor); + + var options = Substitute.For>(); + + options.Value.Returns(new ApplicationConfiguration() + { + IsMultiModeEnabled = true, + BlogName = "Test", + ConnectionString = "Test", + DatabaseName = "Test" + }); + + ctx.Services.AddScoped(_ => options); + Action act = () => ctx.Render( p => p.Add(s => s.BlogPostId, null)); @@ -75,4 +123,4 @@ private static void TriggerUpdate(IRenderedComponent cut) cut.Find("form").Submit(); } -} \ No newline at end of file +} diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Shared/NavMenuTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Shared/NavMenuTests.cs index b91904cc..4facd459 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Shared/NavMenuTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Shared/NavMenuTests.cs @@ -1,8 +1,9 @@ -using System.Linq; +using System.Linq; using AngleSharp.Html.Dom; using LinkDotNet.Blog.TestUtilities; using LinkDotNet.Blog.Web.Features.Home.Components; using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -19,6 +20,7 @@ public NavMenuTests() public void ShouldNavigateToSearchPage() { Services.AddScoped(_ => Options.Create(new ApplicationConfigurationBuilder().Build())); + Services.AddScoped(_ => Substitute.For()); AddAuthorization(); var navigationManager = Services.GetRequiredService(); var cut = Render(); @@ -36,6 +38,7 @@ public void ShouldDisplayAboutMePage() .WithIsAboutMeEnabled(true) .Build()); Services.AddScoped(_ => config); + Services.AddScoped(_ => Substitute.For()); AddAuthorization(); var cut = Render(); @@ -51,6 +54,7 @@ public void ShouldPassCorrectUriToComponent() { var config = Options.Create(new ProfileInformationBuilder().Build()); Services.AddScoped(_ => config); + Services.AddScoped(_ => Substitute.For()); AddAuthorization(); var cut = Render(); @@ -66,7 +70,8 @@ public void ShouldShowBrandImageIfAvailable() .WithBlogBrandUrl("http://localhost/img.png") .Build()); Services.AddScoped(_ => config); - + Services.AddScoped(_ => Substitute.For()); + var profileInfoConfig = Options.Create(new ProfileInformationBuilder().Build()); Services.AddScoped(_ => profileInfoConfig); @@ -90,7 +95,8 @@ public void ShouldShowBlogNameWhenNotBrand(string? brandUrl) .WithBlogName("Steven") .Build()); Services.AddScoped(_ => config); - + Services.AddScoped(_ => Substitute.For()); + var profileInfoConfig = Options.Create(new ProfileInformationBuilder().Build()); Services.AddScoped(_ => profileInfoConfig); diff --git a/tests/LinkDotNet.Blog.TestUtilities/ApplicationConfigurationBuilder.cs b/tests/LinkDotNet.Blog.TestUtilities/ApplicationConfigurationBuilder.cs index 0b3f6128..6d95ebef 100644 --- a/tests/LinkDotNet.Blog.TestUtilities/ApplicationConfigurationBuilder.cs +++ b/tests/LinkDotNet.Blog.TestUtilities/ApplicationConfigurationBuilder.cs @@ -15,6 +15,7 @@ public class ApplicationConfigurationBuilder private bool showReadingIndicator; private bool showSimilarPosts; private string? blogBrandUrl; + private bool isMultiModeEnabled; public ApplicationConfigurationBuilder WithBlogName(string blogName) { @@ -81,7 +82,13 @@ public ApplicationConfigurationBuilder WithBlogBrandUrl(string? blogBrandUrl) this.blogBrandUrl = blogBrandUrl; return this; } - + + public ApplicationConfigurationBuilder WithIsMultiModeEnabled(bool isMultiModeEnabled) + { + this.isMultiModeEnabled = isMultiModeEnabled; + return this; + } + public ApplicationConfiguration Build() { return new ApplicationConfiguration @@ -97,6 +104,7 @@ public ApplicationConfiguration Build() ShowReadingIndicator = showReadingIndicator, ShowSimilarPosts = showSimilarPosts, BlogBrandUrl = blogBrandUrl, + IsMultiModeEnabled = isMultiModeEnabled, }; } } diff --git a/tests/LinkDotNet.Blog.TestUtilities/BlogPostBuilder.cs b/tests/LinkDotNet.Blog.TestUtilities/BlogPostBuilder.cs index 82c4e544..1a413790 100644 --- a/tests/LinkDotNet.Blog.TestUtilities/BlogPostBuilder.cs +++ b/tests/LinkDotNet.Blog.TestUtilities/BlogPostBuilder.cs @@ -1,4 +1,4 @@ -using System; +using System; using LinkDotNet.Blog.Domain; namespace LinkDotNet.Blog.TestUtilities; @@ -15,6 +15,7 @@ public class BlogPostBuilder private int likes; private DateTime? updateDate; private DateTime? scheduledPublishDate; + private string? authorName; public BlogPostBuilder WithTitle(string title) { @@ -76,6 +77,12 @@ public BlogPostBuilder WithScheduledPublishDate(DateTime scheduledPublishDate) return this; } + public BlogPostBuilder WithAuthorName(string authorName) + { + this.authorName = authorName; + return this; + } + public BlogPost Build() { var blogPost = BlogPost.Create( @@ -87,7 +94,8 @@ public BlogPost Build() updateDate, scheduledPublishDate, tags, - previewImageUrlFallback); + previewImageUrlFallback, + authorName); blogPost.Likes = likes; return blogPost; } diff --git a/tests/LinkDotNet.Blog.UnitTests/Domain/BlogPostTests.cs b/tests/LinkDotNet.Blog.UnitTests/Domain/BlogPostTests.cs index 0edc6e73..e8ddf670 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Domain/BlogPostTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Domain/BlogPostTests.cs @@ -7,12 +7,18 @@ namespace LinkDotNet.Blog.UnitTests.Domain; public class BlogPostTests { - [Fact] - public void ShouldUpdateBlogPost() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ShouldUpdateBlogPost(bool hasAuthorName) { var blogPostToUpdate = new BlogPostBuilder().Build(); blogPostToUpdate.Id = "random-id"; - var blogPost = BlogPost.Create("Title", "Desc", "Other Content", "Url", true, previewImageUrlFallback: "Url2"); + + var blogPost = hasAuthorName + ? BlogPost.Create("Title", "Desc", "Other Content", "Url", true, previewImageUrlFallback: "Url2", authorName: "Test Author") + : BlogPost.Create("Title", "Desc", "Other Content", "Url", true, previewImageUrlFallback: "Url2"); + blogPost.Id = "something else"; blogPostToUpdate.Update(blogPost); @@ -26,6 +32,15 @@ public void ShouldUpdateBlogPost() blogPostToUpdate.Tags.ShouldBeEmpty(); blogPostToUpdate.Slug.ShouldNotBeNull(); blogPostToUpdate.ReadingTimeInMinutes.ShouldBe(1); + + if (hasAuthorName) + { + blogPostToUpdate.AuthorName.ShouldBe("Test Author"); + } + else + { + blogPostToUpdate.AuthorName.ShouldBeNull(); + } } [Theory] diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/ApplicationConfigurationTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/ApplicationConfigurationTests.cs index 8005b883..161edeb7 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/ApplicationConfigurationTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/ApplicationConfigurationTests.cs @@ -45,7 +45,8 @@ public void ShouldMapFromAppConfiguration() { "Authentication:Provider","Auth0"}, { "Authentication:ClientId","123"}, { "Authentication:ClientSecret","qwe"}, - { "Authentication:Domain","example.com"} + { "Authentication:Domain","example.com"}, + { "IsMultiModeEnabled","true"} }; var configuration = new ConfigurationBuilder() @@ -64,6 +65,7 @@ public void ShouldMapFromAppConfiguration() appConfiguration.BlogPostsPerPage.ShouldBe(5); appConfiguration.IsAboutMeEnabled.ShouldBeTrue(); appConfiguration.ShowReadingIndicator.ShouldBeTrue(); + appConfiguration.IsMultiModeEnabled.ShouldBeTrue(); var giscusConfiguration = new GiscusConfigurationBuilder().Build(); configuration.GetSection(GiscusConfiguration.GiscusConfigurationSection).Bind(giscusConfiguration); diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPostTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPostTests.cs index 472db955..22b58e98 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPostTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPostTests.cs @@ -7,11 +7,14 @@ using LinkDotNet.Blog.Infrastructure.Persistence; using LinkDotNet.Blog.TestUtilities; using LinkDotNet.Blog.TestUtilities.Fakes; +using LinkDotNet.Blog.Web; using LinkDotNet.Blog.Web.Features.Admin.BlogPostEditor.Components; using LinkDotNet.Blog.Web.Features.Components; using LinkDotNet.Blog.Web.Features.Services; using Microsoft.AspNetCore.Components.Routing; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using NCronJob; namespace LinkDotNet.Blog.UnitTests.Web.Features.Admin.BlogPostEditor.Components; @@ -19,7 +22,8 @@ namespace LinkDotNet.Blog.UnitTests.Web.Features.Admin.BlogPostEditor.Components public class CreateNewBlogPostTests : BunitContext { private readonly CacheService cacheService = new CacheService(); - + private readonly IOptions options; + public CreateNewBlogPostTests() { var shortCodeRepository = Substitute.For>(); @@ -30,10 +34,25 @@ public CreateNewBlogPostTests() Services.AddScoped(_ => Substitute.For()); Services.AddScoped(_ => cacheService); Services.AddScoped(_ => Substitute.For()); + options = Substitute.For>(); + + options.Value.Returns(new ApplicationConfiguration() + { + IsMultiModeEnabled = true, + BlogName = "Test", + ConnectionString = "Test", + DatabaseName = "Test" + }); + + Services.AddScoped(_ => options); + + var contextAccessor = Substitute.For(); + contextAccessor.HttpContext?.User.Identity?.Name.Returns("Test Author"); + Services.AddScoped(_ => contextAccessor); } [Fact] - public void ShouldCreateNewBlogPostWhenValidDataGiven() + public void ShouldCreateNewBlogPostWhenMultiModeIsEnabled() { BlogPost? blogPost = null; var cut = Render( @@ -57,12 +76,42 @@ public void ShouldCreateNewBlogPostWhenValidDataGiven() blogPost.PreviewImageUrlFallback.ShouldBe("My fallback preview url"); blogPost.IsPublished.ShouldBeFalse(); blogPost.UpdatedDate.ShouldNotBe(default); + blogPost.AuthorName.ShouldBe("Test Author"); blogPost.Tags.Count.ShouldBe(3); blogPost.Tags.ShouldContain("Tag1"); blogPost.Tags.ShouldContain("Tag2"); blogPost.Tags.ShouldContain("Tag3"); } + [Fact] + public void ShouldAuthorNameIsNullWhenMultiModeIsDisable() + { + options.Value.Returns(new ApplicationConfiguration() + { + IsMultiModeEnabled = false, + BlogName = "Test", + ConnectionString = "Test", + DatabaseName = "Test" + }); + + BlogPost? blogPost = null; + var cut = Render( + p => p.Add(c => c.OnBlogPostCreated, bp => blogPost = bp)); + cut.Find("#title").Input("My Title"); + cut.Find("#short").Input("My short Description"); + cut.Find("#content").Input("My content"); + cut.Find("#preview").Change("My preview url"); + cut.Find("#fallback-preview").Change("My fallback preview url"); + cut.Find("#published").Change(false); + cut.Find("#tags").Change("Tag1,Tag2,Tag3"); + + cut.Find("form").Submit(); + + cut.WaitForState(() => cut.Find("#title").TextContent == string.Empty); + blogPost.ShouldNotBeNull(); + blogPost.AuthorName.ShouldBeNull(); + } + [Fact] public void ShouldFillGivenBlogPost() { diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Home/Components/AccessControlTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Home/Components/AccessControlTests.cs index 05637ee3..2425d24d 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Home/Components/AccessControlTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Home/Components/AccessControlTests.cs @@ -1,10 +1,35 @@ -using AngleSharp.Html.Dom; +using AngleSharp.Html.Dom; +using LinkDotNet.Blog.Web; using LinkDotNet.Blog.Web.Features.Home.Components; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; namespace LinkDotNet.Blog.UnitTests.Web.Features.Home.Components; public class AccessControlTests : BunitContext { + private readonly IOptions options; + + public AccessControlTests() + { + options = Substitute.For>(); + + options.Value.Returns(new ApplicationConfiguration() + { + IsMultiModeEnabled = true, + BlogName = "Test", + ConnectionString = "Test", + DatabaseName = "Test" + }); + + Services.AddScoped(_ => options); + + var contextAccessor = Substitute.For(); + contextAccessor.HttpContext?.User.Identity?.Name.Returns("Test Author"); + Services.AddScoped(_ => contextAccessor); + } + [Fact] public void ShouldShowLoginAndHideAdminWhenNotLoggedIn() { @@ -50,4 +75,31 @@ public void LogoutShouldHaveCurrentUriAsRedirectUri() ((IHtmlAnchorElement)cut.Find("a:contains('Log out')")).Href.ShouldContain(currentUri); } -} \ No newline at end of file + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ShouldShowOrHideAuthorNameWhenLoggedIn(bool isMultiModeEnabled) + { + options.Value.Returns(new ApplicationConfiguration() + { + IsMultiModeEnabled = isMultiModeEnabled, + BlogName = "Test", + ConnectionString = "Test", + DatabaseName = "Test" + }); + + AddAuthorization().SetAuthorized("steven"); + + var cut = Render(); + + if (isMultiModeEnabled) + { + cut.FindAll("label:contains('Test Author')").ShouldHaveSingleItem(); + } + else + { + cut.FindAll("label:contains('Test Author')").ShouldBeEmpty(); + } + } +} diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Pages/LoginModelTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Pages/LoginModelTests.cs index e955f4f3..cdefeba1 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Pages/LoginModelTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Pages/LoginModelTests.cs @@ -1,20 +1,77 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; +using LinkDotNet.Blog.Web; using LinkDotNet.Blog.Web.Authentication; using LinkDotNet.Blog.Web.Pages; +using Microsoft.Extensions.Options; namespace LinkDotNet.Blog.UnitTests.Web.Pages; public class LoginModelTests { [Fact] - public async Task ShouldLogin() + public async Task ShouldLoginOnGetWhenMultiModeIsDisable() { var loginManager = Substitute.For(); - var sut = new LoginModel(loginManager); - const string redirectUrl = "newUrl"; + var options = Substitute.For>(); + + options.Value.Returns(new ApplicationConfiguration() + { + IsMultiModeEnabled = false, + BlogName = "Test", + ConnectionString = "Test", + DatabaseName = "Test" + }); + + var sut = new LoginModel(loginManager, options); + const string redirectUrl = "newUrl"; await sut.OnGet(redirectUrl); await loginManager.Received(1).SignInAsync(redirectUrl); } -} \ No newline at end of file + + [Fact] + public async Task ShouldNotLoginOnGetWhenMultiModeIsEnable() + { + var loginManager = Substitute.For(); + var options = Substitute.For>(); + + options.Value.Returns(new ApplicationConfiguration() + { + IsMultiModeEnabled = true, + BlogName = "Test", + ConnectionString = "Test", + DatabaseName = "Test" + }); + + var sut = new LoginModel(loginManager, options); + const string redirectUrl = "newUrl"; + + await sut.OnGet(redirectUrl); + + await loginManager.Received(0).SignInAsync(redirectUrl); + } + + [Fact] + public async Task ShouldLoginOnPost() + { + var loginManager = Substitute.For(); + var options = Substitute.For>(); + + options.Value.Returns(new ApplicationConfiguration() + { + IsMultiModeEnabled = true, + BlogName = "Test", + ConnectionString = "Test", + DatabaseName = "Test" + }); + + var sut = new LoginModel(loginManager, options); + const string redirectUrl = "newUrl"; + const string authorName = "Test Author"; + + await sut.OnPost(redirectUrl, authorName); + + await loginManager.Received(1).SignInAsync(redirectUrl, authorName); + } +} From 840f33d3813f59c5877626baa90259f6aa1b8d1c Mon Sep 17 00:00:00 2001 From: Arnab Roy Chowdhury Date: Sat, 30 Aug 2025 16:49:19 +0530 Subject: [PATCH 07/19] Rename config (address review comments) --- src/LinkDotNet.Blog.Web/ApplicationConfiguration.cs | 2 +- .../BlogPostEditor/Components/CreateNewBlogPost.razor | 2 +- .../Features/Home/Components/AccessControl.razor | 2 +- src/LinkDotNet.Blog.Web/Pages/Login.cshtml.cs | 2 +- src/LinkDotNet.Blog.Web/appsettings.json | 2 +- .../ApplicationConfigurationBuilder.cs | 8 ++++---- .../Web/ApplicationConfigurationTests.cs | 4 ++-- .../BlogPostEditor/Components/CreateNewBlogPostTests.cs | 4 ++-- .../Web/Pages/LoginModelTests.cs | 6 +++--- 9 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/LinkDotNet.Blog.Web/ApplicationConfiguration.cs b/src/LinkDotNet.Blog.Web/ApplicationConfiguration.cs index 7b459e3e..5db35a25 100644 --- a/src/LinkDotNet.Blog.Web/ApplicationConfiguration.cs +++ b/src/LinkDotNet.Blog.Web/ApplicationConfiguration.cs @@ -24,5 +24,5 @@ public sealed record ApplicationConfiguration public bool ShowSimilarPosts { get; init; } - public bool IsMultiModeEnabled { get; init; } + public bool UseMultiAuthorMode { get; init; } } diff --git a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor index fdc7fc93..5574b0ab 100644 --- a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor +++ b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor @@ -285,7 +285,7 @@ { shortCodes = await ShortCodeRepository.GetAllAsync(); - if (AppConfiguration.Value.IsMultiModeEnabled) + if (AppConfiguration.Value.UseMultiAuthorMode) { authorName = HttpContextAccessor.HttpContext?.User.Identity?.Name; } diff --git a/src/LinkDotNet.Blog.Web/Features/Home/Components/AccessControl.razor b/src/LinkDotNet.Blog.Web/Features/Home/Components/AccessControl.razor index 92ab5cb3..046b8a26 100644 --- a/src/LinkDotNet.Blog.Web/Features/Home/Components/AccessControl.razor +++ b/src/LinkDotNet.Blog.Web/Features/Home/Components/AccessControl.razor @@ -39,7 +39,7 @@ protected override void OnInitialized() { - if (AppConfiguration.Value.IsMultiModeEnabled) + if (AppConfiguration.Value.UseMultiAuthorMode) { authorName = HttpContextAccessor.HttpContext?.User.Identity?.Name; } diff --git a/src/LinkDotNet.Blog.Web/Pages/Login.cshtml.cs b/src/LinkDotNet.Blog.Web/Pages/Login.cshtml.cs index 694762ba..c133d591 100644 --- a/src/LinkDotNet.Blog.Web/Pages/Login.cshtml.cs +++ b/src/LinkDotNet.Blog.Web/Pages/Login.cshtml.cs @@ -21,7 +21,7 @@ public LoginModel(ILoginManager loginManager, IOptions public async Task OnGet(string redirectUri) { - if (!applicationConfiguration.IsMultiModeEnabled) + if (!applicationConfiguration.UseMultiAuthorMode) { await loginManager.SignInAsync(redirectUri); } diff --git a/src/LinkDotNet.Blog.Web/appsettings.json b/src/LinkDotNet.Blog.Web/appsettings.json index bfac91c2..702c9b61 100644 --- a/src/LinkDotNet.Blog.Web/appsettings.json +++ b/src/LinkDotNet.Blog.Web/appsettings.json @@ -47,5 +47,5 @@ }, "ShowReadingIndicator": true, "ShowSimilarPosts": true, - "IsMultiModeEnabled": true + "UseMultiAuthorMode": false } diff --git a/tests/LinkDotNet.Blog.TestUtilities/ApplicationConfigurationBuilder.cs b/tests/LinkDotNet.Blog.TestUtilities/ApplicationConfigurationBuilder.cs index 6d95ebef..ce2b3f64 100644 --- a/tests/LinkDotNet.Blog.TestUtilities/ApplicationConfigurationBuilder.cs +++ b/tests/LinkDotNet.Blog.TestUtilities/ApplicationConfigurationBuilder.cs @@ -15,7 +15,7 @@ public class ApplicationConfigurationBuilder private bool showReadingIndicator; private bool showSimilarPosts; private string? blogBrandUrl; - private bool isMultiModeEnabled; + private bool useMultiAuthorMode; public ApplicationConfigurationBuilder WithBlogName(string blogName) { @@ -83,9 +83,9 @@ public ApplicationConfigurationBuilder WithBlogBrandUrl(string? blogBrandUrl) return this; } - public ApplicationConfigurationBuilder WithIsMultiModeEnabled(bool isMultiModeEnabled) + public ApplicationConfigurationBuilder WithUseMultiAuthorMode(bool useMultiAuthorMode) { - this.isMultiModeEnabled = isMultiModeEnabled; + this.useMultiAuthorMode = useMultiAuthorMode; return this; } @@ -104,7 +104,7 @@ public ApplicationConfiguration Build() ShowReadingIndicator = showReadingIndicator, ShowSimilarPosts = showSimilarPosts, BlogBrandUrl = blogBrandUrl, - IsMultiModeEnabled = isMultiModeEnabled, + UseMultiAuthorMode = useMultiAuthorMode, }; } } diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/ApplicationConfigurationTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/ApplicationConfigurationTests.cs index 161edeb7..46f61c14 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/ApplicationConfigurationTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/ApplicationConfigurationTests.cs @@ -46,7 +46,7 @@ public void ShouldMapFromAppConfiguration() { "Authentication:ClientId","123"}, { "Authentication:ClientSecret","qwe"}, { "Authentication:Domain","example.com"}, - { "IsMultiModeEnabled","true"} + { "UseMultiAuthorMode","true"} }; var configuration = new ConfigurationBuilder() @@ -65,7 +65,7 @@ public void ShouldMapFromAppConfiguration() appConfiguration.BlogPostsPerPage.ShouldBe(5); appConfiguration.IsAboutMeEnabled.ShouldBeTrue(); appConfiguration.ShowReadingIndicator.ShouldBeTrue(); - appConfiguration.IsMultiModeEnabled.ShouldBeTrue(); + appConfiguration.UseMultiAuthorMode.ShouldBeTrue(); var giscusConfiguration = new GiscusConfigurationBuilder().Build(); configuration.GetSection(GiscusConfiguration.GiscusConfigurationSection).Bind(giscusConfiguration); diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPostTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPostTests.cs index 22b58e98..24757b74 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPostTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPostTests.cs @@ -38,7 +38,7 @@ public CreateNewBlogPostTests() options.Value.Returns(new ApplicationConfiguration() { - IsMultiModeEnabled = true, + UseMultiAuthorMode = true, BlogName = "Test", ConnectionString = "Test", DatabaseName = "Test" @@ -88,7 +88,7 @@ public void ShouldAuthorNameIsNullWhenMultiModeIsDisable() { options.Value.Returns(new ApplicationConfiguration() { - IsMultiModeEnabled = false, + UseMultiAuthorMode = false, BlogName = "Test", ConnectionString = "Test", DatabaseName = "Test" diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Pages/LoginModelTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Pages/LoginModelTests.cs index cdefeba1..7a06f1ac 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Pages/LoginModelTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Pages/LoginModelTests.cs @@ -16,7 +16,7 @@ public async Task ShouldLoginOnGetWhenMultiModeIsDisable() options.Value.Returns(new ApplicationConfiguration() { - IsMultiModeEnabled = false, + UseMultiAuthorMode = false, BlogName = "Test", ConnectionString = "Test", DatabaseName = "Test" @@ -38,7 +38,7 @@ public async Task ShouldNotLoginOnGetWhenMultiModeIsEnable() options.Value.Returns(new ApplicationConfiguration() { - IsMultiModeEnabled = true, + UseMultiAuthorMode = true, BlogName = "Test", ConnectionString = "Test", DatabaseName = "Test" @@ -60,7 +60,7 @@ public async Task ShouldLoginOnPost() options.Value.Returns(new ApplicationConfiguration() { - IsMultiModeEnabled = true, + UseMultiAuthorMode = true, BlogName = "Test", ConnectionString = "Test", DatabaseName = "Test" From be4925eefdf2b9a588fb2cdf18f91149c2169eea Mon Sep 17 00:00:00 2001 From: Arnab Roy Chowdhury Date: Sat, 30 Aug 2025 16:50:41 +0530 Subject: [PATCH 08/19] Remove if else from tests (address review comments) --- .../Sql/BlogPostRepositoryTests.cs | 98 ++++++++++--------- .../CreateNewBlogPostPageTests.cs | 62 +++++++++--- .../BlogPostEditor/UpdateBlogPostPageTests.cs | 66 ++++++++++--- .../Domain/BlogPostTests.cs | 33 +++---- .../Home/Components/AccessControlTests.cs | 35 ++++--- 5 files changed, 188 insertions(+), 106 deletions(-) diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Infrastructure/Persistence/Sql/BlogPostRepositoryTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Infrastructure/Persistence/Sql/BlogPostRepositoryTests.cs index b07b379d..10726466 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Infrastructure/Persistence/Sql/BlogPostRepositoryTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Infrastructure/Persistence/Sql/BlogPostRepositoryTests.cs @@ -11,15 +11,10 @@ namespace LinkDotNet.Blog.IntegrationTests.Infrastructure.Persistence.Sql; public sealed class BlogPostRepositoryTests : SqlDatabaseTestBase { - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task ShouldLoadBlogPost(bool isAuthorEnable) + [Fact] + public async Task ShouldLoadBlogPost() { - var blogPost = isAuthorEnable - ? BlogPost.Create("Title", "Subtitle", "Content", "url", true, tags: new[] { "Tag 1", "Tag 2" }, authorName: "Test Author") - : BlogPost.Create("Title", "Subtitle", "Content", "url", true, tags: new[] { "Tag 1", "Tag 2" }); - + var blogPost = BlogPost.Create("Title", "Subtitle", "Content", "url", true, tags: new[] { "Tag 1", "Tag 2" }, authorName: "Test Author"); await DbContext.BlogPosts.AddAsync(blogPost, TestContext.Current.CancellationToken); await DbContext.SaveChangesAsync(TestContext.Current.CancellationToken); @@ -31,29 +26,30 @@ public async Task ShouldLoadBlogPost(bool isAuthorEnable) blogPostFromRepo.Content.ShouldBe("Content"); blogPostFromRepo.PreviewImageUrl.ShouldBe("url"); blogPostFromRepo.IsPublished.ShouldBeTrue(); + blogPostFromRepo.AuthorName.ShouldBe("Test Author"); blogPostFromRepo.Tags.Count.ShouldBe(2); var tagContent = blogPostFromRepo.Tags; tagContent.ShouldContain("Tag 1"); tagContent.ShouldContain("Tag 2"); + } + + [Fact] + public async Task ShouldAuthorNameNullWhenNotGiven() + { + var blogPost = BlogPost.Create("Title", "Subtitle", "Content", "url", true, tags: new[] { "Tag 1", "Tag 2" }); + await DbContext.BlogPosts.AddAsync(blogPost, TestContext.Current.CancellationToken); + await DbContext.SaveChangesAsync(TestContext.Current.CancellationToken); + + var blogPostFromRepo = await Repository.GetByIdAsync(blogPost.Id); - if (isAuthorEnable) - { - blogPostFromRepo.AuthorName.ShouldBe("Test Author"); - } - else - { - blogPostFromRepo.AuthorName.ShouldBeNull(); - } + blogPostFromRepo.ShouldNotBeNull(); + blogPostFromRepo.AuthorName.ShouldBeNull(); } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task ShouldSaveBlogPost(bool isAuthorEnable) + [Fact] + public async Task ShouldSaveBlogPost() { - var blogPost = isAuthorEnable - ? BlogPost.Create("Title", "Subtitle", "Content", "url", true, tags: new[] { "Tag 1", "Tag 2" }, authorName: "Test Author") - : BlogPost.Create("Title", "Subtitle", "Content", "url", true, tags: new[] { "Tag 1", "Tag 2" }); + var blogPost = BlogPost.Create("Title", "Subtitle", "Content", "url", true, tags: new[] { "Tag 1", "Tag 2" }, authorName: "Test Author"); await Repository.StoreAsync(blogPost); @@ -67,30 +63,32 @@ public async Task ShouldSaveBlogPost(bool isAuthorEnable) blogPostFromContext.Content.ShouldBe("Content"); blogPostFromContext.IsPublished.ShouldBeTrue(); blogPostFromContext.PreviewImageUrl.ShouldBe("url"); + blogPostFromContext.AuthorName.ShouldBe("Test Author"); blogPostFromContext.Tags.Count.ShouldBe(2); var tagContent = blogPostFromContext.Tags; tagContent.ShouldContain("Tag 1"); tagContent.ShouldContain("Tag 2"); - - if (isAuthorEnable) - { - blogPostFromContext.AuthorName.ShouldBe("Test Author"); - } - else - { - blogPostFromContext.AuthorName.ShouldBeNull(); - } } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task ShouldGetAllBlogPosts(bool isAuthorEnable) + [Fact] + public async Task ShouldSaveAuthorNameAsNullWhenNotGiven() { - var blogPost = isAuthorEnable - ? BlogPost.Create("Title", "Subtitle", "Content", "url", true, tags: new[] { "Tag 1", "Tag 2" }, authorName: "Test Author") - : BlogPost.Create("Title", "Subtitle", "Content", "url", true, tags: new[] { "Tag 1", "Tag 2" }); + var blogPost = BlogPost.Create("Title", "Subtitle", "Content", "url", true, tags: new[] { "Tag 1", "Tag 2" }); + await Repository.StoreAsync(blogPost); + var blogPostFromContext = await DbContext + .BlogPosts + .AsNoTracking() + .SingleOrDefaultAsync(s => s.Id == blogPost.Id, TestContext.Current.CancellationToken); + + blogPostFromContext.ShouldNotBeNull(); + blogPostFromContext.AuthorName.ShouldBeNull(); + } + + [Fact] + public async Task ShouldGetAllBlogPosts() + { + var blogPost = BlogPost.Create("Title", "Subtitle", "Content", "url", true, tags: new[] { "Tag 1", "Tag 2" }, authorName: "Test Author"); await DbContext.BlogPosts.AddAsync(blogPost, TestContext.Current.CancellationToken); await DbContext.SaveChangesAsync(TestContext.Current.CancellationToken); @@ -104,19 +102,25 @@ public async Task ShouldGetAllBlogPosts(bool isAuthorEnable) blogPostFromRepo.Content.ShouldBe("Content"); blogPostFromRepo.PreviewImageUrl.ShouldBe("url"); blogPostFromRepo.IsPublished.ShouldBeTrue(); + blogPostFromRepo.AuthorName.ShouldBe("Test Author"); blogPostFromRepo.Tags.Count.ShouldBe(2); var tagContent = blogPostFromRepo.Tags; tagContent.ShouldContain("Tag 1"); tagContent.ShouldContain("Tag 2"); + } - if (isAuthorEnable) - { - blogPostFromRepo.AuthorName.ShouldBe("Test Author"); - } - else - { - blogPostFromRepo.AuthorName.ShouldBeNull(); - } + [Fact] + public async Task ShouldGetAuthorNameAsNullWhenNotGiven() + { + var blogPost = BlogPost.Create("Title", "Subtitle", "Content", "url", true, tags: new[] { "Tag 1", "Tag 2" }); + await DbContext.BlogPosts.AddAsync(blogPost, TestContext.Current.CancellationToken); + await DbContext.SaveChangesAsync(TestContext.Current.CancellationToken); + + var blogPostsFromRepo = await Repository.GetAllAsync(); + + blogPostsFromRepo.ShouldNotBeNull(); + var blogPostFromRepo = blogPostsFromRepo.Single(); + blogPostFromRepo.AuthorName.ShouldBeNull(); } [Fact] diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/BlogPostEditor/CreateNewBlogPostPageTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/BlogPostEditor/CreateNewBlogPostPageTests.cs index d4266e40..303db4e9 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/BlogPostEditor/CreateNewBlogPostPageTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/BlogPostEditor/CreateNewBlogPostPageTests.cs @@ -23,10 +23,8 @@ namespace LinkDotNet.Blog.IntegrationTests.Web.Features.Admin.BlogPostEditor; public class CreateNewBlogPostPageTests : SqlDatabaseTestBase { - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task ShouldSaveBlogPostOnSave(bool isMultiModeEnabled) + [Fact] + public async Task ShouldSaveBlogPostOnSave() { await using var ctx = new BunitContext(); ctx.ComponentFactories.Add(); @@ -51,7 +49,7 @@ public async Task ShouldSaveBlogPostOnSave(bool isMultiModeEnabled) options.Value.Returns(new ApplicationConfiguration() { - IsMultiModeEnabled = isMultiModeEnabled, + UseMultiAuthorMode = true, BlogName = "Test", ConnectionString = "Test", DatabaseName = "Test" @@ -67,20 +65,56 @@ public async Task ShouldSaveBlogPostOnSave(bool isMultiModeEnabled) var blogPostFromDb = await DbContext.BlogPosts.SingleOrDefaultAsync(t => t.Title == "My Title", TestContext.Current.CancellationToken); blogPostFromDb.ShouldNotBeNull(); blogPostFromDb.ShortDescription.ShouldBe("My short Description"); - - if (isMultiModeEnabled) - { - blogPostFromDb.AuthorName.ShouldBe("Test Author"); - } - else - { - blogPostFromDb.AuthorName.ShouldBeNull(); - } + blogPostFromDb.AuthorName.ShouldBe("Test Author"); toastService.Received(1).ShowInfo("Created BlogPost My Title", null); instantRegistry.Received(1).RunInstantJob(Arg.Any(), Arg.Any()); } + [Fact] + public async Task ShouldSaveAuthorNameAsNullWhenMultiAuthorModeIsDisabled() + { + await using var ctx = new BunitContext(); + ctx.ComponentFactories.Add(); + var toastService = Substitute.For(); + var instantRegistry = Substitute.For(); + ctx.JSInterop.SetupVoid("hljs.highlightAll"); + ctx.AddAuthorization().SetAuthorized("some username"); + ctx.Services.AddScoped(_ => Repository); + ctx.Services.AddScoped(_ => toastService); + ctx.Services.AddScoped(_ => Substitute.For()); + ctx.Services.AddScoped(_ => instantRegistry); + ctx.Services.AddScoped(_ => Substitute.For()); + var shortCodeRepository = Substitute.For>(); + shortCodeRepository.GetAllAsync().Returns(PagedList.Empty); + ctx.Services.AddScoped(_ => shortCodeRepository); + + var contextAccessor = Substitute.For(); + contextAccessor.HttpContext?.User.Identity?.Name.Returns("Test Author"); + ctx.Services.AddScoped(_ => contextAccessor); + + var options = Substitute.For>(); + + options.Value.Returns(new ApplicationConfiguration() + { + UseMultiAuthorMode = false, + BlogName = "Test", + ConnectionString = "Test", + DatabaseName = "Test" + }); + + ctx.Services.AddScoped(_ => options); + + using var cut = ctx.Render(); + var newBlogPost = cut.FindComponent(); + + TriggerNewBlogPost(newBlogPost); + + var blogPostFromDb = await DbContext.BlogPosts.SingleOrDefaultAsync(t => t.Title == "My Title", TestContext.Current.CancellationToken); + blogPostFromDb.ShouldNotBeNull(); + blogPostFromDb.AuthorName.ShouldBeNull(); + } + private static void TriggerNewBlogPost(IRenderedComponent cut) { cut.Find("#title").Input("My Title"); diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/BlogPostEditor/UpdateBlogPostPageTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/BlogPostEditor/UpdateBlogPostPageTests.cs index f274138b..4cc02f54 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/BlogPostEditor/UpdateBlogPostPageTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/BlogPostEditor/UpdateBlogPostPageTests.cs @@ -25,10 +25,8 @@ namespace LinkDotNet.Blog.IntegrationTests.Web.Features.Admin.BlogPostEditor; public class UpdateBlogPostPageTests : SqlDatabaseTestBase { - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task ShouldSaveBlogPostOnSave(bool isMultiModeEnabled) + [Fact] + public async Task ShouldSaveBlogPostOnSave() { await using var ctx = new BunitContext(); ctx.ComponentFactories.Add(); @@ -54,7 +52,7 @@ public async Task ShouldSaveBlogPostOnSave(bool isMultiModeEnabled) options.Value.Returns(new ApplicationConfiguration() { - IsMultiModeEnabled = isMultiModeEnabled, + UseMultiAuthorMode = true, BlogName = "Test", ConnectionString = "Test", DatabaseName = "Test" @@ -71,20 +69,58 @@ public async Task ShouldSaveBlogPostOnSave(bool isMultiModeEnabled) var blogPostFromDb = await DbContext.BlogPosts.SingleOrDefaultAsync(t => t.Id == blogPost.Id, TestContext.Current.CancellationToken); blogPostFromDb.ShouldNotBeNull(); blogPostFromDb.ShortDescription.ShouldBe("My new Description"); - - if (isMultiModeEnabled) - { - blogPostFromDb.AuthorName.ShouldBe("Test Author"); - } - else - { - blogPostFromDb.AuthorName.ShouldBeNull(); - } + blogPostFromDb.AuthorName.ShouldBe("Test Author"); toastService.Received(1).ShowInfo("Updated BlogPost Title", null); instantRegistry.Received(1).RunInstantJob(Arg.Any(), Arg.Any()); } + [Fact] + public async Task ShouldSaveAuthorNameAsNullWhenMultiAuthorModeIsDisabled() + { + await using var ctx = new BunitContext(); + ctx.ComponentFactories.Add(); + ctx.JSInterop.SetupVoid("hljs.highlightAll"); + var toastService = Substitute.For(); + ctx.Services.AddScoped(_ => Substitute.For()); + var instantRegistry = Substitute.For(); + var blogPost = new BlogPostBuilder().WithTitle("Title").WithShortDescription("Sub").Build(); + await Repository.StoreAsync(blogPost); + ctx.AddAuthorization().SetAuthorized("some username"); + ctx.Services.AddScoped(_ => Repository); + ctx.Services.AddScoped(_ => toastService); + ctx.Services.AddScoped(_ => instantRegistry); + var shortCodeRepository = Substitute.For>(); + shortCodeRepository.GetAllAsync().Returns(PagedList.Empty); + ctx.Services.AddScoped(_ => shortCodeRepository); + + var contextAccessor = Substitute.For(); + contextAccessor.HttpContext?.User.Identity?.Name.Returns("Test Author"); + ctx.Services.AddScoped(_ => contextAccessor); + + var options = Substitute.For>(); + + options.Value.Returns(new ApplicationConfiguration() + { + UseMultiAuthorMode = false, + BlogName = "Test", + ConnectionString = "Test", + DatabaseName = "Test" + }); + + ctx.Services.AddScoped(_ => options); + + using var cut = ctx.Render( + p => p.Add(s => s.BlogPostId, blogPost.Id)); + var newBlogPost = cut.FindComponent(); + + TriggerUpdate(newBlogPost); + + var blogPostFromDb = await DbContext.BlogPosts.SingleOrDefaultAsync(t => t.Id == blogPost.Id, TestContext.Current.CancellationToken); + blogPostFromDb.ShouldNotBeNull(); + blogPostFromDb.AuthorName.ShouldBeNull(); + } + [Fact] public void ShouldThrowWhenNoIdProvided() { @@ -103,7 +139,7 @@ public void ShouldThrowWhenNoIdProvided() options.Value.Returns(new ApplicationConfiguration() { - IsMultiModeEnabled = true, + UseMultiAuthorMode = true, BlogName = "Test", ConnectionString = "Test", DatabaseName = "Test" diff --git a/tests/LinkDotNet.Blog.UnitTests/Domain/BlogPostTests.cs b/tests/LinkDotNet.Blog.UnitTests/Domain/BlogPostTests.cs index e8ddf670..eb5e6f97 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Domain/BlogPostTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Domain/BlogPostTests.cs @@ -7,18 +7,12 @@ namespace LinkDotNet.Blog.UnitTests.Domain; public class BlogPostTests { - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ShouldUpdateBlogPost(bool hasAuthorName) + [Fact] + public void ShouldUpdateBlogPost() { var blogPostToUpdate = new BlogPostBuilder().Build(); blogPostToUpdate.Id = "random-id"; - - var blogPost = hasAuthorName - ? BlogPost.Create("Title", "Desc", "Other Content", "Url", true, previewImageUrlFallback: "Url2", authorName: "Test Author") - : BlogPost.Create("Title", "Desc", "Other Content", "Url", true, previewImageUrlFallback: "Url2"); - + var blogPost = BlogPost.Create("Title", "Desc", "Other Content", "Url", true, previewImageUrlFallback: "Url2", authorName: "Test Author"); blogPost.Id = "something else"; blogPostToUpdate.Update(blogPost); @@ -32,15 +26,20 @@ public void ShouldUpdateBlogPost(bool hasAuthorName) blogPostToUpdate.Tags.ShouldBeEmpty(); blogPostToUpdate.Slug.ShouldNotBeNull(); blogPostToUpdate.ReadingTimeInMinutes.ShouldBe(1); + blogPostToUpdate.AuthorName.ShouldBe("Test Author"); + } + + [Fact] + public void ShouldUpdateAuthorNameAsNullWhenNotGiven() + { + var blogPostToUpdate = new BlogPostBuilder().Build(); + blogPostToUpdate.Id = "random-id"; + var blogPost = BlogPost.Create("Title", "Desc", "Other Content", "Url", true, previewImageUrlFallback: "Url2"); + blogPost.Id = "something else"; + + blogPostToUpdate.Update(blogPost); - if (hasAuthorName) - { - blogPostToUpdate.AuthorName.ShouldBe("Test Author"); - } - else - { - blogPostToUpdate.AuthorName.ShouldBeNull(); - } + blogPostToUpdate.AuthorName.ShouldBeNull(); } [Theory] diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Home/Components/AccessControlTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Home/Components/AccessControlTests.cs index 2425d24d..66cb6858 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Home/Components/AccessControlTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Home/Components/AccessControlTests.cs @@ -17,7 +17,7 @@ public AccessControlTests() options.Value.Returns(new ApplicationConfiguration() { - IsMultiModeEnabled = true, + UseMultiAuthorMode = true, BlogName = "Test", ConnectionString = "Test", DatabaseName = "Test" @@ -76,14 +76,12 @@ public void LogoutShouldHaveCurrentUriAsRedirectUri() ((IHtmlAnchorElement)cut.Find("a:contains('Log out')")).Href.ShouldContain(currentUri); } - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ShouldShowOrHideAuthorNameWhenLoggedIn(bool isMultiModeEnabled) + [Fact] + public void ShouldShowAuthorNameWhenUseMultiAuthorModeIsEnabled() { options.Value.Returns(new ApplicationConfiguration() { - IsMultiModeEnabled = isMultiModeEnabled, + UseMultiAuthorMode = true, BlogName = "Test", ConnectionString = "Test", DatabaseName = "Test" @@ -93,13 +91,24 @@ public void ShouldShowOrHideAuthorNameWhenLoggedIn(bool isMultiModeEnabled) var cut = Render(); - if (isMultiModeEnabled) - { - cut.FindAll("label:contains('Test Author')").ShouldHaveSingleItem(); - } - else + cut.FindAll("label:contains('Test Author')").ShouldHaveSingleItem(); + } + + [Fact] + public void ShouldHideAuthorNameWhenUseMultiAuthorModeIsDisabled() + { + options.Value.Returns(new ApplicationConfiguration() { - cut.FindAll("label:contains('Test Author')").ShouldBeEmpty(); - } + UseMultiAuthorMode = false, + BlogName = "Test", + ConnectionString = "Test", + DatabaseName = "Test" + }); + + AddAuthorization().SetAuthorized("steven"); + + var cut = Render(); + + cut.FindAll("label:contains('Test Author')").ShouldBeEmpty(); } } From bbc359f14e6180d97981c6b6b6e7169ca0e5ed19 Mon Sep 17 00:00:00 2001 From: Arnab Roy Chowdhury Date: Sat, 30 Aug 2025 16:51:26 +0530 Subject: [PATCH 09/19] Add if in the razor view (address review comments) --- .../Features/Home/Components/AccessControl.razor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LinkDotNet.Blog.Web/Features/Home/Components/AccessControl.razor b/src/LinkDotNet.Blog.Web/Features/Home/Components/AccessControl.razor index 046b8a26..102a63d3 100644 --- a/src/LinkDotNet.Blog.Web/Features/Home/Components/AccessControl.razor +++ b/src/LinkDotNet.Blog.Web/Features/Home/Components/AccessControl.razor @@ -24,7 +24,7 @@
  • Releases
  • - + From dfe9134abffd2460f21ce2154c2acbd1de5cd83a Mon Sep 17 00:00:00 2001 From: Arnab Roy Chowdhury Date: Sat, 30 Aug 2025 16:51:53 +0530 Subject: [PATCH 10/19] Add ef migration for author name (address review comments) --- ...110439_AddAuthorNameInBlogPost.Designer.cs | 253 ++++++++++++++++++ .../20250830110439_AddAuthorNameInBlogPost.cs | 29 ++ .../Migrations/BlogDbContextModelSnapshot.cs | 6 +- 3 files changed, 287 insertions(+), 1 deletion(-) create mode 100644 src/LinkDotNet.Blog.Infrastructure/Migrations/20250830110439_AddAuthorNameInBlogPost.Designer.cs create mode 100644 src/LinkDotNet.Blog.Infrastructure/Migrations/20250830110439_AddAuthorNameInBlogPost.cs diff --git a/src/LinkDotNet.Blog.Infrastructure/Migrations/20250830110439_AddAuthorNameInBlogPost.Designer.cs b/src/LinkDotNet.Blog.Infrastructure/Migrations/20250830110439_AddAuthorNameInBlogPost.Designer.cs new file mode 100644 index 00000000..05beb5df --- /dev/null +++ b/src/LinkDotNet.Blog.Infrastructure/Migrations/20250830110439_AddAuthorNameInBlogPost.Designer.cs @@ -0,0 +1,253 @@ +// +using System; +using LinkDotNet.Blog.Infrastructure.Persistence.Sql; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace LinkDotNet.Blog.Web.Migrations +{ + [DbContext(typeof(BlogDbContext))] + [Migration("20250830110439_AddAuthorNameInBlogPost")] + partial class AddAuthorNameInBlogPost + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("LinkDotNet.Blog.Domain.BlogPost", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .IsUnicode(false) + .HasColumnType("varchar(900)"); + + b.Property("AuthorName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("Content") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsPublished") + .HasColumnType("bit"); + + b.Property("Likes") + .HasColumnType("int"); + + b.Property("PreviewImageUrl") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("PreviewImageUrlFallback") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("ReadingTimeInMinutes") + .HasColumnType("int"); + + b.Property("ScheduledPublishDate") + .HasColumnType("datetime2"); + + b.Property("ShortDescription") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.PrimitiveCollection("Tags") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("UpdatedDate") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("IsPublished", "UpdatedDate") + .IsDescending(false, true) + .HasDatabaseName("IX_BlogPosts_IsPublished_UpdatedDate"); + + b.ToTable("BlogPosts"); + }); + + modelBuilder.Entity("LinkDotNet.Blog.Domain.BlogPostRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .IsUnicode(false) + .HasColumnType("varchar(900)"); + + b.Property("BlogPostId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("Clicks") + .HasColumnType("int"); + + b.Property("DateClicked") + .HasColumnType("date"); + + b.HasKey("Id"); + + b.ToTable("BlogPostRecords"); + }); + + modelBuilder.Entity("LinkDotNet.Blog.Domain.ProfileInformationEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .IsUnicode(false) + .HasColumnType("varchar(900)"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("ProfileInformationEntries"); + }); + + modelBuilder.Entity("LinkDotNet.Blog.Domain.ShortCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .IsUnicode(false) + .HasColumnType("varchar(900)"); + + b.Property("MarkdownContent") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)"); + + b.HasKey("Id"); + + b.ToTable("ShortCodes"); + }); + + modelBuilder.Entity("LinkDotNet.Blog.Domain.SimilarBlogPost", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .IsUnicode(false) + .HasColumnType("varchar(900)"); + + b.PrimitiveCollection("SimilarBlogPostIds") + .IsRequired() + .HasMaxLength(1350) + .HasColumnType("nvarchar(1350)"); + + b.HasKey("Id"); + + b.ToTable("SimilarBlogPosts"); + }); + + modelBuilder.Entity("LinkDotNet.Blog.Domain.Skill", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .IsUnicode(false) + .HasColumnType("varchar(900)"); + + b.Property("Capability") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("IconUrl") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("ProficiencyLevel") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("nvarchar(32)"); + + b.HasKey("Id"); + + b.ToTable("Skills"); + }); + + modelBuilder.Entity("LinkDotNet.Blog.Domain.Talk", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .IsUnicode(false) + .HasColumnType("varchar(900)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Place") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PresentationTitle") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PublishedDate") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("Talks"); + }); + + modelBuilder.Entity("LinkDotNet.Blog.Domain.UserRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .IsUnicode(false) + .HasColumnType("varchar(900)"); + + b.Property("DateClicked") + .HasColumnType("date"); + + b.Property("UrlClicked") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.ToTable("UserRecords"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/LinkDotNet.Blog.Infrastructure/Migrations/20250830110439_AddAuthorNameInBlogPost.cs b/src/LinkDotNet.Blog.Infrastructure/Migrations/20250830110439_AddAuthorNameInBlogPost.cs new file mode 100644 index 00000000..f74b683a --- /dev/null +++ b/src/LinkDotNet.Blog.Infrastructure/Migrations/20250830110439_AddAuthorNameInBlogPost.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LinkDotNet.Blog.Web.Migrations +{ + /// + public partial class AddAuthorNameInBlogPost : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AuthorName", + table: "BlogPosts", + type: "nvarchar(256)", + maxLength: 256, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "AuthorName", + table: "BlogPosts"); + } + } +} diff --git a/src/LinkDotNet.Blog.Infrastructure/Migrations/BlogDbContextModelSnapshot.cs b/src/LinkDotNet.Blog.Infrastructure/Migrations/BlogDbContextModelSnapshot.cs index d5b07c7f..722942de 100644 --- a/src/LinkDotNet.Blog.Infrastructure/Migrations/BlogDbContextModelSnapshot.cs +++ b/src/LinkDotNet.Blog.Infrastructure/Migrations/BlogDbContextModelSnapshot.cs @@ -1,4 +1,4 @@ -// +// using System; using LinkDotNet.Blog.Infrastructure.Persistence.Sql; using Microsoft.EntityFrameworkCore; @@ -24,6 +24,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsUnicode(false) .HasColumnType("TEXT"); + b.Property("AuthorName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + b.Property("Content") .IsRequired() .HasColumnType("TEXT"); From 846c5c4e165b9f7f31491ea049c7cb42d2a63288 Mon Sep 17 00:00:00 2001 From: Arnab Roy Chowdhury Date: Sat, 30 Aug 2025 17:55:29 +0530 Subject: [PATCH 11/19] Use bootstrap 5x (address review comments) --- src/LinkDotNet.Blog.Web/Pages/Login.cshtml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/LinkDotNet.Blog.Web/Pages/Login.cshtml b/src/LinkDotNet.Blog.Web/Pages/Login.cshtml index fb36714f..0aea49e9 100644 --- a/src/LinkDotNet.Blog.Web/Pages/Login.cshtml +++ b/src/LinkDotNet.Blog.Web/Pages/Login.cshtml @@ -8,9 +8,10 @@ Login + href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.7/css/bootstrap.min.css" + integrity="sha512-fw7f+TcMjTb7bpbLJZlP8g2Y4XcCyFZW8uy8HsRZsH/SwbMw0plKHFHr99DN3l04VsYNwvzicUX/6qurvIxbxw==" + crossorigin="anonymous" + referrerpolicy="no-referrer" />
    From 6505604f38dd125c41fb453fdc5ee064aa459cc6 Mon Sep 17 00:00:00 2001 From: Arnab Roy Chowdhury Date: Sat, 30 Aug 2025 19:10:49 +0530 Subject: [PATCH 12/19] add test for RavenDb (i forgot this before) --- .../RavenDb/BlogPostRepositoryTests.cs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Infrastructure/Persistence/RavenDb/BlogPostRepositoryTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Infrastructure/Persistence/RavenDb/BlogPostRepositoryTests.cs index 7e0ddfe4..d7e056ca 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Infrastructure/Persistence/RavenDb/BlogPostRepositoryTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Infrastructure/Persistence/RavenDb/BlogPostRepositoryTests.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using System.Threading.Tasks; using LinkDotNet.Blog.Domain; using LinkDotNet.Blog.Infrastructure.Persistence; @@ -27,7 +27,7 @@ public BlogPostRepositoryTests() [Fact] public async Task ShouldLoadBlogPost() { - var blogPost = BlogPost.Create("Title", "Subtitle", "Content", "url", true, tags: new[] { "Tag 1", "Tag 2" }); + var blogPost = BlogPost.Create("Title", "Subtitle", "Content", "url", true, tags: new[] { "Tag 1", "Tag 2" }, authorName: "Test Author"); await SaveBlogPostAsync(blogPost); var blogPostFromRepo = await sut.GetByIdAsync(blogPost.Id); @@ -38,12 +38,25 @@ public async Task ShouldLoadBlogPost() blogPostFromRepo.Content.ShouldBe("Content"); blogPostFromRepo.PreviewImageUrl.ShouldBe("url"); blogPostFromRepo.IsPublished.ShouldBeTrue(); + blogPostFromRepo.AuthorName.ShouldBe("Test Author"); blogPostFromRepo.Tags.Count.ShouldBe(2); var tagContent = blogPostFromRepo.Tags; tagContent.ShouldContain("Tag 1"); tagContent.ShouldContain("Tag 2"); } + [Fact] + public async Task ShouldAuthorNameNullWhenNotGiven() + { + var blogPost = BlogPost.Create("Title", "Subtitle", "Content", "url", true, tags: new[] { "Tag 1", "Tag 2" }); + await SaveBlogPostAsync(blogPost); + + var blogPostFromRepo = await sut.GetByIdAsync(blogPost.Id); + + blogPostFromRepo.ShouldNotBeNull(); + blogPostFromRepo.AuthorName.ShouldBeNull(); + } + [Fact] public async Task ShouldFilterAndOrder() { From 160cfd12381e73be5bd093f618d814dbd729f2bf Mon Sep 17 00:00:00 2001 From: Arnab Roy Chowdhury Date: Mon, 1 Sep 2025 08:44:45 +0530 Subject: [PATCH 13/19] Address review comments --- docs/Migrations/Readme.md | 6 +- docs/Setup/Configuration.md | 4 +- .../20250830110439_AddAuthorNameInBlogPost.cs | 44 ++++++------ .../Migrations/BlogDbContextModelSnapshot.cs | 3 +- .../Authentication/Dummy/DummyLoginManager.cs | 5 +- .../Authentication/ILoginManager.cs | 4 +- .../OpenIdConnect/AuthExtensions.cs | 1 + .../OpenIdConnect/AuthLoginManager.cs | 24 ++----- .../Components/CreateNewBlogPost.razor | 5 +- .../Home/Components/AccessControl.razor | 8 +-- .../Features/Services/IUserRecordService.cs | 6 +- .../Features/Services/UserRecordService.cs | 17 ++++- src/LinkDotNet.Blog.Web/Pages/Login.cshtml | 29 +------- src/LinkDotNet.Blog.Web/Pages/Login.cshtml.cs | 16 +---- .../Sql/BlogPostRepositoryTests.cs | 2 +- .../CreateNewBlogPostPageTests.cs | 12 ++-- .../BlogPostEditor/UpdateBlogPostPageTests.cs | 18 ++--- .../Web/Shared/NavMenuTests.cs | 11 +-- .../Components/CreateNewBlogPostTests.cs | 10 +-- .../Home/Components/AccessControlTests.cs | 7 +- .../Services/UserRecordServiceTests.cs | 25 ++++++- .../Web/Pages/LoginModelTests.cs | 67 ++----------------- 22 files changed, 133 insertions(+), 191 deletions(-) diff --git a/docs/Migrations/Readme.md b/docs/Migrations/Readme.md index 5eb0637c..8511f014 100644 --- a/docs/Migrations/Readme.md +++ b/docs/Migrations/Readme.md @@ -6,4 +6,8 @@ This is contrasted by Minor changes. These are things where the user does not ne Breaking changes are recorded in the [MIGRATION.md](../../MIGRATION.md). Since version 9 of the blog, “Entity Framework Migrations” has been introduced for all SQL providers. You can read more in the [documentation](../Storage/Readme.md). In a nutshell, this means that database migration can be carried out easily via the “ef migration” CLI tool. More on this in the documentation linked above. -Changes for the appsettings.json must currently still be made manually. The exact changes that need to be made here can be found in MIGRATION.md. \ No newline at end of file +Changes for the appsettings.json must currently still be made manually. The exact changes that need to be made here can be found in MIGRATION.md. + +## UNRELEASED + +A new config has been added `UseMultiAuthorMode` in `appsettings.json`. The default value of this config is `false`. If set to `true` then author name will be associated with blog posts at the time of creation. diff --git a/docs/Setup/Configuration.md b/docs/Setup/Configuration.md index 28704dd5..2c54b1a4 100644 --- a/docs/Setup/Configuration.md +++ b/docs/Setup/Configuration.md @@ -65,7 +65,8 @@ The appsettings.json file has a lot of options to customize the content of the b "ServiceUrl": "", "ContainerName": "", "CdnEndpoint": "" - } + }, + "UseMultiAuthorMode": false } ``` @@ -109,3 +110,4 @@ The appsettings.json file has a lot of options to customize the content of the b | ServiceUrl | string | The host url of the Azure blob storage. Only used if `AuthenticationMode` is set to `Default` | | ContainerName | string | The container name for the image storage provider | | CdnEndpoint | string | Optional CDN endpoint to use for uploaded images. If set, the blog will return this URL instead of the storage account URL for uploaded assets. | +| UseMultiAuthorMode | boolean | The default value is `false`. If set to `true` then author name will be associated with blog posts at the time of creation. diff --git a/src/LinkDotNet.Blog.Infrastructure/Migrations/20250830110439_AddAuthorNameInBlogPost.cs b/src/LinkDotNet.Blog.Infrastructure/Migrations/20250830110439_AddAuthorNameInBlogPost.cs index f74b683a..457cb113 100644 --- a/src/LinkDotNet.Blog.Infrastructure/Migrations/20250830110439_AddAuthorNameInBlogPost.cs +++ b/src/LinkDotNet.Blog.Infrastructure/Migrations/20250830110439_AddAuthorNameInBlogPost.cs @@ -1,29 +1,33 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using System; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace LinkDotNet.Blog.Web.Migrations +namespace LinkDotNet.Blog.Web.Migrations; + +/// +public partial class AddAuthorNameInBlogPost : Migration { /// - public partial class AddAuthorNameInBlogPost : Migration + protected override void Up(MigrationBuilder migrationBuilder) + { + ArgumentNullException.ThrowIfNull(migrationBuilder); + + migrationBuilder.AddColumn( + name: "AuthorName", + table: "BlogPosts", + type: "nvarchar(256)", + maxLength: 256, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "AuthorName", - table: "BlogPosts", - type: "nvarchar(256)", - maxLength: 256, - nullable: true); - } + ArgumentNullException.ThrowIfNull(migrationBuilder); - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "AuthorName", - table: "BlogPosts"); - } + migrationBuilder.DropColumn( + name: "AuthorName", + table: "BlogPosts"); } } diff --git a/src/LinkDotNet.Blog.Infrastructure/Migrations/BlogDbContextModelSnapshot.cs b/src/LinkDotNet.Blog.Infrastructure/Migrations/BlogDbContextModelSnapshot.cs index 722942de..3617bb01 100644 --- a/src/LinkDotNet.Blog.Infrastructure/Migrations/BlogDbContextModelSnapshot.cs +++ b/src/LinkDotNet.Blog.Infrastructure/Migrations/BlogDbContextModelSnapshot.cs @@ -25,8 +25,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT"); b.Property("AuthorName") - .HasMaxLength(256) - .HasColumnType("nvarchar(256)"); + .HasMaxLength(256); b.Property("Content") .IsRequired() diff --git a/src/LinkDotNet.Blog.Web/Authentication/Dummy/DummyLoginManager.cs b/src/LinkDotNet.Blog.Web/Authentication/Dummy/DummyLoginManager.cs index fa6a02db..77d93d60 100644 --- a/src/LinkDotNet.Blog.Web/Authentication/Dummy/DummyLoginManager.cs +++ b/src/LinkDotNet.Blog.Web/Authentication/Dummy/DummyLoginManager.cs @@ -22,11 +22,12 @@ public async Task SignOutAsync(string redirectUri = "/") context.Response.Redirect(redirectUri); } - public async Task SignInAsync(string redirectUri, string? authorName = null) + public async Task SignInAsync(string redirectUri) { var claims = new[] { - new Claim(ClaimTypes.Name, authorName ?? "Dummy user"), + new Claim(ClaimTypes.Name, "Dummy user"), + new Claim("name", "Dummy user"), new Claim(ClaimTypes.NameIdentifier, Guid.NewGuid().ToString()), }; var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); diff --git a/src/LinkDotNet.Blog.Web/Authentication/ILoginManager.cs b/src/LinkDotNet.Blog.Web/Authentication/ILoginManager.cs index f3a31f18..624a5975 100644 --- a/src/LinkDotNet.Blog.Web/Authentication/ILoginManager.cs +++ b/src/LinkDotNet.Blog.Web/Authentication/ILoginManager.cs @@ -1,10 +1,10 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; namespace LinkDotNet.Blog.Web.Authentication; public interface ILoginManager { - Task SignInAsync(string redirectUri, string? authorName = null); + Task SignInAsync(string redirectUri); Task SignOutAsync(string redirectUri = "/"); } diff --git a/src/LinkDotNet.Blog.Web/Authentication/OpenIdConnect/AuthExtensions.cs b/src/LinkDotNet.Blog.Web/Authentication/OpenIdConnect/AuthExtensions.cs index 551e8e6a..dcb47666 100644 --- a/src/LinkDotNet.Blog.Web/Authentication/OpenIdConnect/AuthExtensions.cs +++ b/src/LinkDotNet.Blog.Web/Authentication/OpenIdConnect/AuthExtensions.cs @@ -39,6 +39,7 @@ public static void UseAuthentication(this IServiceCollection services) options.Scope.Clear(); options.Scope.Add("openid"); + options.Scope.Add("profile"); // Set the callback path, so Auth provider will call back to http://localhost:1234/callback // Also ensure that you have added the URL as an Allowed Callback URL in your Auth provider dashboard diff --git a/src/LinkDotNet.Blog.Web/Authentication/OpenIdConnect/AuthLoginManager.cs b/src/LinkDotNet.Blog.Web/Authentication/OpenIdConnect/AuthLoginManager.cs index d34160d6..716adbc9 100644 --- a/src/LinkDotNet.Blog.Web/Authentication/OpenIdConnect/AuthLoginManager.cs +++ b/src/LinkDotNet.Blog.Web/Authentication/OpenIdConnect/AuthLoginManager.cs @@ -1,5 +1,4 @@ using System; -using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; @@ -22,27 +21,12 @@ public AuthLoginManager(IHttpContextAccessor httpContextAccessor, IOptions ShortCodeRepository @inject IOptions AppConfiguration -@inject IHttpContextAccessor HttpContextAccessor +@inject IUserRecordService UserRecordService Creating new Blog Post @@ -287,7 +286,7 @@ if (AppConfiguration.Value.UseMultiAuthorMode) { - authorName = HttpContextAccessor.HttpContext?.User.Identity?.Name; + authorName = await UserRecordService.GetDisplayNameAsync(); } } diff --git a/src/LinkDotNet.Blog.Web/Features/Home/Components/AccessControl.razor b/src/LinkDotNet.Blog.Web/Features/Home/Components/AccessControl.razor index 102a63d3..5ccc545c 100644 --- a/src/LinkDotNet.Blog.Web/Features/Home/Components/AccessControl.razor +++ b/src/LinkDotNet.Blog.Web/Features/Home/Components/AccessControl.razor @@ -1,6 +1,6 @@ -@using Microsoft.AspNetCore.Http +@using LinkDotNet.Blog.Web.Features.Services @inject IOptions AppConfiguration -@inject IHttpContextAccessor HttpContextAccessor +@inject IUserRecordService UserRecordService @@ -37,11 +37,11 @@ private string? authorName; - protected override void OnInitialized() + protected override async Task OnInitializedAsync() { if (AppConfiguration.Value.UseMultiAuthorMode) { - authorName = HttpContextAccessor.HttpContext?.User.Identity?.Name; + authorName = await UserRecordService.GetDisplayNameAsync(); } } } diff --git a/src/LinkDotNet.Blog.Web/Features/Services/IUserRecordService.cs b/src/LinkDotNet.Blog.Web/Features/Services/IUserRecordService.cs index 2c6bc356..c7cda6d2 100644 --- a/src/LinkDotNet.Blog.Web/Features/Services/IUserRecordService.cs +++ b/src/LinkDotNet.Blog.Web/Features/Services/IUserRecordService.cs @@ -1,8 +1,10 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; namespace LinkDotNet.Blog.Web.Features.Services; public interface IUserRecordService { ValueTask StoreUserRecordAsync(); -} \ No newline at end of file + + ValueTask GetDisplayNameAsync(); +} diff --git a/src/LinkDotNet.Blog.Web/Features/Services/UserRecordService.cs b/src/LinkDotNet.Blog.Web/Features/Services/UserRecordService.cs index 77e28450..f15895ef 100644 --- a/src/LinkDotNet.Blog.Web/Features/Services/UserRecordService.cs +++ b/src/LinkDotNet.Blog.Web/Features/Services/UserRecordService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading.Tasks; using LinkDotNet.Blog.Domain; using LinkDotNet.Blog.Infrastructure.Persistence; @@ -39,6 +39,21 @@ public async ValueTask StoreUserRecordAsync() } } + public async ValueTask GetDisplayNameAsync() + { + var user = (await authenticationStateProvider.GetAuthenticationStateAsync()).User; + if (user?.Identity is not { IsAuthenticated: true }) + { + return null; + } + + var name = user.FindFirst("Name")?.Value + ?? user.FindFirst("preferred_username")?.Value + ?? user.FindFirst("nickname")?.Value; + + return string.IsNullOrWhiteSpace(name) ? null : name; + } + private async ValueTask GetAndStoreUserRecordAsync() { var userIdentity = (await authenticationStateProvider.GetAuthenticationStateAsync()).User.Identity; diff --git a/src/LinkDotNet.Blog.Web/Pages/Login.cshtml b/src/LinkDotNet.Blog.Web/Pages/Login.cshtml index 0aea49e9..b60f52a3 100644 --- a/src/LinkDotNet.Blog.Web/Pages/Login.cshtml +++ b/src/LinkDotNet.Blog.Web/Pages/Login.cshtml @@ -1,29 +1,4 @@ -@page +@page @model LinkDotNet.Blog.Web.Pages.LoginModel -@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @{ -} - - - - Login - - - -
    -

    Login

    -
    -
    - - -
    - - Back to home -
    -
    - - +} \ No newline at end of file diff --git a/src/LinkDotNet.Blog.Web/Pages/Login.cshtml.cs b/src/LinkDotNet.Blog.Web/Pages/Login.cshtml.cs index c133d591..5b0f520b 100644 --- a/src/LinkDotNet.Blog.Web/Pages/Login.cshtml.cs +++ b/src/LinkDotNet.Blog.Web/Pages/Login.cshtml.cs @@ -1,32 +1,20 @@ -using System; using System.Threading.Tasks; using LinkDotNet.Blog.Web.Authentication; using Microsoft.AspNetCore.Mvc.RazorPages; -using Microsoft.Extensions.Options; namespace LinkDotNet.Blog.Web.Pages; public sealed partial class LoginModel : PageModel { private readonly ILoginManager loginManager; - private readonly ApplicationConfiguration applicationConfiguration; - public LoginModel(ILoginManager loginManager, IOptions options) + public LoginModel(ILoginManager loginManager) { - ArgumentNullException.ThrowIfNull(options); - this.loginManager = loginManager; - applicationConfiguration = options.Value; } public async Task OnGet(string redirectUri) { - if (!applicationConfiguration.UseMultiAuthorMode) - { - await loginManager.SignInAsync(redirectUri); - } + await loginManager.SignInAsync(redirectUri); } - - public async Task OnPost(string redirectUri, string authorName) - => await loginManager.SignInAsync(redirectUri, authorName); } diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Infrastructure/Persistence/Sql/BlogPostRepositoryTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Infrastructure/Persistence/Sql/BlogPostRepositoryTests.cs index 10726466..d2ed4a13 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Infrastructure/Persistence/Sql/BlogPostRepositoryTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Infrastructure/Persistence/Sql/BlogPostRepositoryTests.cs @@ -34,7 +34,7 @@ public async Task ShouldLoadBlogPost() } [Fact] - public async Task ShouldAuthorNameNullWhenNotGiven() + public async Task ShouldLoadAuthorNameAsNullWhenNotGiven() { var blogPost = BlogPost.Create("Title", "Subtitle", "Content", "url", true, tags: new[] { "Tag 1", "Tag 2" }); await DbContext.BlogPosts.AddAsync(blogPost, TestContext.Current.CancellationToken); diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/BlogPostEditor/CreateNewBlogPostPageTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/BlogPostEditor/CreateNewBlogPostPageTests.cs index 303db4e9..f8bb4a50 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/BlogPostEditor/CreateNewBlogPostPageTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/BlogPostEditor/CreateNewBlogPostPageTests.cs @@ -41,9 +41,9 @@ public async Task ShouldSaveBlogPostOnSave() shortCodeRepository.GetAllAsync().Returns(PagedList.Empty); ctx.Services.AddScoped(_ => shortCodeRepository); - var contextAccessor = Substitute.For(); - contextAccessor.HttpContext?.User.Identity?.Name.Returns("Test Author"); - ctx.Services.AddScoped(_ => contextAccessor); + var userRecordService = Substitute.For(); + userRecordService.GetDisplayNameAsync().Returns("Test Author"); + ctx.Services.AddScoped(_ => userRecordService); var options = Substitute.For>(); @@ -89,9 +89,9 @@ public async Task ShouldSaveAuthorNameAsNullWhenMultiAuthorModeIsDisabled() shortCodeRepository.GetAllAsync().Returns(PagedList.Empty); ctx.Services.AddScoped(_ => shortCodeRepository); - var contextAccessor = Substitute.For(); - contextAccessor.HttpContext?.User.Identity?.Name.Returns("Test Author"); - ctx.Services.AddScoped(_ => contextAccessor); + var userRecordService = Substitute.For(); + userRecordService.GetDisplayNameAsync().Returns("Test Author"); + ctx.Services.AddScoped(_ => userRecordService); var options = Substitute.For>(); diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/BlogPostEditor/UpdateBlogPostPageTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/BlogPostEditor/UpdateBlogPostPageTests.cs index 4cc02f54..608e57a2 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/BlogPostEditor/UpdateBlogPostPageTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/BlogPostEditor/UpdateBlogPostPageTests.cs @@ -44,9 +44,9 @@ public async Task ShouldSaveBlogPostOnSave() shortCodeRepository.GetAllAsync().Returns(PagedList.Empty); ctx.Services.AddScoped(_ => shortCodeRepository); - var contextAccessor = Substitute.For(); - contextAccessor.HttpContext?.User.Identity?.Name.Returns("Test Author"); - ctx.Services.AddScoped(_ => contextAccessor); + var userRecordService = Substitute.For(); + userRecordService.GetDisplayNameAsync().Returns("Test Author"); + ctx.Services.AddScoped(_ => userRecordService); var options = Substitute.For>(); @@ -94,9 +94,9 @@ public async Task ShouldSaveAuthorNameAsNullWhenMultiAuthorModeIsDisabled() shortCodeRepository.GetAllAsync().Returns(PagedList.Empty); ctx.Services.AddScoped(_ => shortCodeRepository); - var contextAccessor = Substitute.For(); - contextAccessor.HttpContext?.User.Identity?.Name.Returns("Test Author"); - ctx.Services.AddScoped(_ => contextAccessor); + var userRecordService = Substitute.For(); + userRecordService.GetDisplayNameAsync().Returns("Test Author"); + ctx.Services.AddScoped(_ => userRecordService); var options = Substitute.For>(); @@ -131,9 +131,9 @@ public void ShouldThrowWhenNoIdProvided() ctx.Services.AddScoped(_ => Substitute.For()); ctx.Services.AddScoped(_ => Substitute.For()); - var contextAccessor = Substitute.For(); - contextAccessor.HttpContext?.User.Identity?.Name.Returns("Test Author"); - ctx.Services.AddScoped(_ => contextAccessor); + var userRecordService = Substitute.For(); + userRecordService.GetDisplayNameAsync().Returns("Test Author"); + ctx.Services.AddScoped(_ => userRecordService); var options = Substitute.For>(); diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Shared/NavMenuTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Shared/NavMenuTests.cs index 4facd459..b2f0de73 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Shared/NavMenuTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Shared/NavMenuTests.cs @@ -2,6 +2,7 @@ using AngleSharp.Html.Dom; using LinkDotNet.Blog.TestUtilities; using LinkDotNet.Blog.Web.Features.Home.Components; +using LinkDotNet.Blog.Web.Features.Services; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; @@ -20,7 +21,7 @@ public NavMenuTests() public void ShouldNavigateToSearchPage() { Services.AddScoped(_ => Options.Create(new ApplicationConfigurationBuilder().Build())); - Services.AddScoped(_ => Substitute.For()); + Services.AddScoped(_ => Substitute.For()); AddAuthorization(); var navigationManager = Services.GetRequiredService(); var cut = Render(); @@ -38,7 +39,7 @@ public void ShouldDisplayAboutMePage() .WithIsAboutMeEnabled(true) .Build()); Services.AddScoped(_ => config); - Services.AddScoped(_ => Substitute.For()); + Services.AddScoped(_ => Substitute.For()); AddAuthorization(); var cut = Render(); @@ -54,7 +55,7 @@ public void ShouldPassCorrectUriToComponent() { var config = Options.Create(new ProfileInformationBuilder().Build()); Services.AddScoped(_ => config); - Services.AddScoped(_ => Substitute.For()); + Services.AddScoped(_ => Substitute.For()); AddAuthorization(); var cut = Render(); @@ -70,7 +71,7 @@ public void ShouldShowBrandImageIfAvailable() .WithBlogBrandUrl("http://localhost/img.png") .Build()); Services.AddScoped(_ => config); - Services.AddScoped(_ => Substitute.For()); + Services.AddScoped(_ => Substitute.For()); var profileInfoConfig = Options.Create(new ProfileInformationBuilder().Build()); Services.AddScoped(_ => profileInfoConfig); @@ -95,7 +96,7 @@ public void ShouldShowBlogNameWhenNotBrand(string? brandUrl) .WithBlogName("Steven") .Build()); Services.AddScoped(_ => config); - Services.AddScoped(_ => Substitute.For()); + Services.AddScoped(_ => Substitute.For()); var profileInfoConfig = Options.Create(new ProfileInformationBuilder().Build()); Services.AddScoped(_ => profileInfoConfig); diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPostTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPostTests.cs index 24757b74..8f2369e7 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPostTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPostTests.cs @@ -46,13 +46,13 @@ public CreateNewBlogPostTests() Services.AddScoped(_ => options); - var contextAccessor = Substitute.For(); - contextAccessor.HttpContext?.User.Identity?.Name.Returns("Test Author"); - Services.AddScoped(_ => contextAccessor); + var userRecordService = Substitute.For(); + userRecordService.GetDisplayNameAsync().Returns("Test Author"); + Services.AddScoped(_ => userRecordService); } [Fact] - public void ShouldCreateNewBlogPostWhenMultiModeIsEnabled() + public void ShouldCreateNewBlogPostWhenMultiAuthorModeIsEnabled() { BlogPost? blogPost = null; var cut = Render( @@ -84,7 +84,7 @@ public void ShouldCreateNewBlogPostWhenMultiModeIsEnabled() } [Fact] - public void ShouldAuthorNameIsNullWhenMultiModeIsDisable() + public void ShouldAuthorNameIsNullWhenMultiAuthorModeIsDisable() { options.Value.Returns(new ApplicationConfiguration() { diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Home/Components/AccessControlTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Home/Components/AccessControlTests.cs index 66cb6858..76040616 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Home/Components/AccessControlTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Home/Components/AccessControlTests.cs @@ -1,6 +1,7 @@ using AngleSharp.Html.Dom; using LinkDotNet.Blog.Web; using LinkDotNet.Blog.Web.Features.Home.Components; +using LinkDotNet.Blog.Web.Features.Services; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -25,9 +26,9 @@ public AccessControlTests() Services.AddScoped(_ => options); - var contextAccessor = Substitute.For(); - contextAccessor.HttpContext?.User.Identity?.Name.Returns("Test Author"); - Services.AddScoped(_ => contextAccessor); + var userRecordService = Substitute.For(); + userRecordService.GetDisplayNameAsync().Returns("Test Author"); + Services.AddScoped(_ => userRecordService); } [Fact] diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Services/UserRecordServiceTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Services/UserRecordServiceTests.cs index 6ec6b0ec..a9dc5f28 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Services/UserRecordServiceTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Services/UserRecordServiceTests.cs @@ -1,9 +1,12 @@ -using System; +using System; +using System.Collections.Generic; +using System.Security.Claims; using System.Threading.Tasks; using LinkDotNet.Blog.Domain; using LinkDotNet.Blog.Infrastructure.Persistence; using LinkDotNet.Blog.Web.Features.Services; using Microsoft.Extensions.Logging; +using NSubstitute; namespace LinkDotNet.Blog.UnitTests.Web.Features.Services; @@ -69,4 +72,24 @@ public async Task ShouldRemoveQueryStringIfPresent(string url, string expectedRe recordToDb.ShouldNotBeNull(); recordToDb.UrlClicked.ShouldBe(expectedRecord); } + + [Fact] + public async Task ShouldGetDisplayNameWhenAuthenticated() + { + var claims = new List() + { + new Claim("name", "Test Author") + }; + + fakeAuthenticationStateProvider.TriggerAuthenticationStateChanged("Steven", claims: claims); + var authorName = await sut.GetDisplayNameAsync(); + authorName.ShouldBe("Test Author"); + } + + [Fact] + public async Task ShouldGetNullAsDisplayNameWhenUnauthenticated() + { + var authorName = await sut.GetDisplayNameAsync(); + authorName.ShouldBeNull(); + } } diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Pages/LoginModelTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Pages/LoginModelTests.cs index 7a06f1ac..e955f4f3 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Pages/LoginModelTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Pages/LoginModelTests.cs @@ -1,77 +1,20 @@ -using System.Threading.Tasks; -using LinkDotNet.Blog.Web; +using System.Threading.Tasks; using LinkDotNet.Blog.Web.Authentication; using LinkDotNet.Blog.Web.Pages; -using Microsoft.Extensions.Options; namespace LinkDotNet.Blog.UnitTests.Web.Pages; public class LoginModelTests { [Fact] - public async Task ShouldLoginOnGetWhenMultiModeIsDisable() + public async Task ShouldLogin() { var loginManager = Substitute.For(); - var options = Substitute.For>(); - - options.Value.Returns(new ApplicationConfiguration() - { - UseMultiAuthorMode = false, - BlogName = "Test", - ConnectionString = "Test", - DatabaseName = "Test" - }); - - var sut = new LoginModel(loginManager, options); - const string redirectUrl = "newUrl"; - - await sut.OnGet(redirectUrl); - - await loginManager.Received(1).SignInAsync(redirectUrl); - } - - [Fact] - public async Task ShouldNotLoginOnGetWhenMultiModeIsEnable() - { - var loginManager = Substitute.For(); - var options = Substitute.For>(); - - options.Value.Returns(new ApplicationConfiguration() - { - UseMultiAuthorMode = true, - BlogName = "Test", - ConnectionString = "Test", - DatabaseName = "Test" - }); - - var sut = new LoginModel(loginManager, options); + var sut = new LoginModel(loginManager); const string redirectUrl = "newUrl"; await sut.OnGet(redirectUrl); - await loginManager.Received(0).SignInAsync(redirectUrl); - } - - [Fact] - public async Task ShouldLoginOnPost() - { - var loginManager = Substitute.For(); - var options = Substitute.For>(); - - options.Value.Returns(new ApplicationConfiguration() - { - UseMultiAuthorMode = true, - BlogName = "Test", - ConnectionString = "Test", - DatabaseName = "Test" - }); - - var sut = new LoginModel(loginManager, options); - const string redirectUrl = "newUrl"; - const string authorName = "Test Author"; - - await sut.OnPost(redirectUrl, authorName); - - await loginManager.Received(1).SignInAsync(redirectUrl, authorName); + await loginManager.Received(1).SignInAsync(redirectUrl); } -} +} \ No newline at end of file From a115a1cf5722f3e1b36792e15ce9e48bf3ac3002 Mon Sep 17 00:00:00 2001 From: Arnab Roy Chowdhury Date: Tue, 2 Sep 2025 08:41:42 +0530 Subject: [PATCH 14/19] Address review comments --- .../Migrations/BlogDbContextModelSnapshot.cs | 3 +- .../Components/CreateNewBlogPost.razor | 4 +- .../Home/Components/AccessControl.razor | 6 +-- .../Features/Services/CurrentUserService.cs | 27 ++++++++++ .../Features/Services/ICurrentUserService.cs | 8 +++ .../Features/Services/IUserRecordService.cs | 6 +-- .../Features/Services/UserRecordService.cs | 17 +------ src/LinkDotNet.Blog.Web/ServiceExtensions.cs | 3 +- .../RavenDb/BlogPostRepositoryTests.cs | 2 +- .../CreateNewBlogPostPageTests.cs | 13 +++-- .../BlogPostEditor/UpdateBlogPostPageTests.cs | 19 ++++--- .../Web/Shared/NavMenuTests.cs | 10 ++-- .../Components/CreateNewBlogPostTests.cs | 9 ++-- .../Home/Components/AccessControlTests.cs | 11 ++--- .../Services/CurrentUserServiceTests.cs | 49 +++++++++++++++++++ .../Services/UserRecordServiceTests.cs | 25 +--------- 16 files changed, 127 insertions(+), 85 deletions(-) create mode 100644 src/LinkDotNet.Blog.Web/Features/Services/CurrentUserService.cs create mode 100644 src/LinkDotNet.Blog.Web/Features/Services/ICurrentUserService.cs create mode 100644 tests/LinkDotNet.Blog.UnitTests/Web/Features/Services/CurrentUserServiceTests.cs diff --git a/src/LinkDotNet.Blog.Infrastructure/Migrations/BlogDbContextModelSnapshot.cs b/src/LinkDotNet.Blog.Infrastructure/Migrations/BlogDbContextModelSnapshot.cs index 3617bb01..722942de 100644 --- a/src/LinkDotNet.Blog.Infrastructure/Migrations/BlogDbContextModelSnapshot.cs +++ b/src/LinkDotNet.Blog.Infrastructure/Migrations/BlogDbContextModelSnapshot.cs @@ -25,7 +25,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT"); b.Property("AuthorName") - .HasMaxLength(256); + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); b.Property("Content") .IsRequired() diff --git a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor index 1d4d1bd0..ecea7d1c 100644 --- a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor +++ b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor @@ -9,7 +9,7 @@ @inject IInstantJobRegistry InstantJobRegistry @inject IRepository ShortCodeRepository @inject IOptions AppConfiguration -@inject IUserRecordService UserRecordService +@inject ICurrentUserService CurrentUserService Creating new Blog Post @@ -286,7 +286,7 @@ if (AppConfiguration.Value.UseMultiAuthorMode) { - authorName = await UserRecordService.GetDisplayNameAsync(); + authorName = await CurrentUserService.GetDisplayNameAsync(); } } diff --git a/src/LinkDotNet.Blog.Web/Features/Home/Components/AccessControl.razor b/src/LinkDotNet.Blog.Web/Features/Home/Components/AccessControl.razor index 5ccc545c..a7d04b79 100644 --- a/src/LinkDotNet.Blog.Web/Features/Home/Components/AccessControl.razor +++ b/src/LinkDotNet.Blog.Web/Features/Home/Components/AccessControl.razor @@ -1,6 +1,6 @@ @using LinkDotNet.Blog.Web.Features.Services @inject IOptions AppConfiguration -@inject IUserRecordService UserRecordService +@inject ICurrentUserService CurrentUserService @@ -24,7 +24,7 @@
  • Releases
  • - +
    @@ -41,7 +41,7 @@ { if (AppConfiguration.Value.UseMultiAuthorMode) { - authorName = await UserRecordService.GetDisplayNameAsync(); + authorName = await CurrentUserService.GetDisplayNameAsync(); } } } diff --git a/src/LinkDotNet.Blog.Web/Features/Services/CurrentUserService.cs b/src/LinkDotNet.Blog.Web/Features/Services/CurrentUserService.cs new file mode 100644 index 00000000..86271875 --- /dev/null +++ b/src/LinkDotNet.Blog.Web/Features/Services/CurrentUserService.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Components.Authorization; +using System.Threading.Tasks; + +namespace LinkDotNet.Blog.Web.Features.Services; + +public class CurrentUserService : ICurrentUserService +{ + private readonly AuthenticationStateProvider authenticationStateProvider; + + public CurrentUserService(AuthenticationStateProvider authenticationStateProvider) + => this.authenticationStateProvider = authenticationStateProvider; + + public async ValueTask GetDisplayNameAsync() + { + var user = (await authenticationStateProvider.GetAuthenticationStateAsync()).User; + if (user?.Identity is not { IsAuthenticated: true }) + { + return null; + } + + var name = user.FindFirst("Name")?.Value + ?? user.FindFirst("preferred_username")?.Value + ?? user.FindFirst("nickname")?.Value; + + return string.IsNullOrWhiteSpace(name) ? null : name; + } +} diff --git a/src/LinkDotNet.Blog.Web/Features/Services/ICurrentUserService.cs b/src/LinkDotNet.Blog.Web/Features/Services/ICurrentUserService.cs new file mode 100644 index 00000000..39e3121d --- /dev/null +++ b/src/LinkDotNet.Blog.Web/Features/Services/ICurrentUserService.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace LinkDotNet.Blog.Web.Features.Services; + +public interface ICurrentUserService +{ + ValueTask GetDisplayNameAsync(); +} diff --git a/src/LinkDotNet.Blog.Web/Features/Services/IUserRecordService.cs b/src/LinkDotNet.Blog.Web/Features/Services/IUserRecordService.cs index c7cda6d2..2c6bc356 100644 --- a/src/LinkDotNet.Blog.Web/Features/Services/IUserRecordService.cs +++ b/src/LinkDotNet.Blog.Web/Features/Services/IUserRecordService.cs @@ -1,10 +1,8 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; namespace LinkDotNet.Blog.Web.Features.Services; public interface IUserRecordService { ValueTask StoreUserRecordAsync(); - - ValueTask GetDisplayNameAsync(); -} +} \ No newline at end of file diff --git a/src/LinkDotNet.Blog.Web/Features/Services/UserRecordService.cs b/src/LinkDotNet.Blog.Web/Features/Services/UserRecordService.cs index f15895ef..77e28450 100644 --- a/src/LinkDotNet.Blog.Web/Features/Services/UserRecordService.cs +++ b/src/LinkDotNet.Blog.Web/Features/Services/UserRecordService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading.Tasks; using LinkDotNet.Blog.Domain; using LinkDotNet.Blog.Infrastructure.Persistence; @@ -39,21 +39,6 @@ public async ValueTask StoreUserRecordAsync() } } - public async ValueTask GetDisplayNameAsync() - { - var user = (await authenticationStateProvider.GetAuthenticationStateAsync()).User; - if (user?.Identity is not { IsAuthenticated: true }) - { - return null; - } - - var name = user.FindFirst("Name")?.Value - ?? user.FindFirst("preferred_username")?.Value - ?? user.FindFirst("nickname")?.Value; - - return string.IsNullOrWhiteSpace(name) ? null : name; - } - private async ValueTask GetAndStoreUserRecordAsync() { var userIdentity = (await authenticationStateProvider.GetAuthenticationStateAsync()).User.Identity; diff --git a/src/LinkDotNet.Blog.Web/ServiceExtensions.cs b/src/LinkDotNet.Blog.Web/ServiceExtensions.cs index ef4a1c01..7d569325 100644 --- a/src/LinkDotNet.Blog.Web/ServiceExtensions.cs +++ b/src/LinkDotNet.Blog.Web/ServiceExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading.RateLimiting; using Blazorise; using Blazorise.Bootstrap5; @@ -24,6 +24,7 @@ public static IServiceCollection AddApplicationServices(this IServiceCollection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddSingleton(); services.AddSingleton(s => s.GetRequiredService()); diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Infrastructure/Persistence/RavenDb/BlogPostRepositoryTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Infrastructure/Persistence/RavenDb/BlogPostRepositoryTests.cs index d7e056ca..f25391eb 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Infrastructure/Persistence/RavenDb/BlogPostRepositoryTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Infrastructure/Persistence/RavenDb/BlogPostRepositoryTests.cs @@ -46,7 +46,7 @@ public async Task ShouldLoadBlogPost() } [Fact] - public async Task ShouldAuthorNameNullWhenNotGiven() + public async Task ShouldSetAuthorNameAsNullWhenNotGiven() { var blogPost = BlogPost.Create("Title", "Subtitle", "Content", "url", true, tags: new[] { "Tag 1", "Tag 2" }); await SaveBlogPostAsync(blogPost); diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/BlogPostEditor/CreateNewBlogPostPageTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/BlogPostEditor/CreateNewBlogPostPageTests.cs index f8bb4a50..daf8d204 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/BlogPostEditor/CreateNewBlogPostPageTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/BlogPostEditor/CreateNewBlogPostPageTests.cs @@ -12,7 +12,6 @@ using LinkDotNet.Blog.Web.Features.Admin.BlogPostEditor.Services; using LinkDotNet.Blog.Web.Features.Components; using LinkDotNet.Blog.Web.Features.Services; -using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -41,9 +40,9 @@ public async Task ShouldSaveBlogPostOnSave() shortCodeRepository.GetAllAsync().Returns(PagedList.Empty); ctx.Services.AddScoped(_ => shortCodeRepository); - var userRecordService = Substitute.For(); - userRecordService.GetDisplayNameAsync().Returns("Test Author"); - ctx.Services.AddScoped(_ => userRecordService); + var currentUserService = Substitute.For(); + currentUserService.GetDisplayNameAsync().Returns("Test Author"); + ctx.Services.AddScoped(_ => currentUserService); var options = Substitute.For>(); @@ -89,9 +88,9 @@ public async Task ShouldSaveAuthorNameAsNullWhenMultiAuthorModeIsDisabled() shortCodeRepository.GetAllAsync().Returns(PagedList.Empty); ctx.Services.AddScoped(_ => shortCodeRepository); - var userRecordService = Substitute.For(); - userRecordService.GetDisplayNameAsync().Returns("Test Author"); - ctx.Services.AddScoped(_ => userRecordService); + var currentUserService = Substitute.For(); + currentUserService.GetDisplayNameAsync().Returns("Test Author"); + ctx.Services.AddScoped(_ => currentUserService); var options = Substitute.For>(); diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/BlogPostEditor/UpdateBlogPostPageTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/BlogPostEditor/UpdateBlogPostPageTests.cs index 608e57a2..c24809ff 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/BlogPostEditor/UpdateBlogPostPageTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/BlogPostEditor/UpdateBlogPostPageTests.cs @@ -14,7 +14,6 @@ using LinkDotNet.Blog.Web.Features.Components; using LinkDotNet.Blog.Web.Features.Services; using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -44,9 +43,9 @@ public async Task ShouldSaveBlogPostOnSave() shortCodeRepository.GetAllAsync().Returns(PagedList.Empty); ctx.Services.AddScoped(_ => shortCodeRepository); - var userRecordService = Substitute.For(); - userRecordService.GetDisplayNameAsync().Returns("Test Author"); - ctx.Services.AddScoped(_ => userRecordService); + var currentUserService = Substitute.For(); + currentUserService.GetDisplayNameAsync().Returns("Test Author"); + ctx.Services.AddScoped(_ => currentUserService); var options = Substitute.For>(); @@ -94,9 +93,9 @@ public async Task ShouldSaveAuthorNameAsNullWhenMultiAuthorModeIsDisabled() shortCodeRepository.GetAllAsync().Returns(PagedList.Empty); ctx.Services.AddScoped(_ => shortCodeRepository); - var userRecordService = Substitute.For(); - userRecordService.GetDisplayNameAsync().Returns("Test Author"); - ctx.Services.AddScoped(_ => userRecordService); + var currentUserService = Substitute.For(); + currentUserService.GetDisplayNameAsync().Returns("Test Author"); + ctx.Services.AddScoped(_ => currentUserService); var options = Substitute.For>(); @@ -131,9 +130,9 @@ public void ShouldThrowWhenNoIdProvided() ctx.Services.AddScoped(_ => Substitute.For()); ctx.Services.AddScoped(_ => Substitute.For()); - var userRecordService = Substitute.For(); - userRecordService.GetDisplayNameAsync().Returns("Test Author"); - ctx.Services.AddScoped(_ => userRecordService); + var currentUserService = Substitute.For(); + currentUserService.GetDisplayNameAsync().Returns("Test Author"); + ctx.Services.AddScoped(_ => currentUserService); var options = Substitute.For>(); diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Shared/NavMenuTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Shared/NavMenuTests.cs index b2f0de73..0e4e91b7 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Shared/NavMenuTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Shared/NavMenuTests.cs @@ -21,7 +21,7 @@ public NavMenuTests() public void ShouldNavigateToSearchPage() { Services.AddScoped(_ => Options.Create(new ApplicationConfigurationBuilder().Build())); - Services.AddScoped(_ => Substitute.For()); + Services.AddScoped(_ => Substitute.For()); AddAuthorization(); var navigationManager = Services.GetRequiredService(); var cut = Render(); @@ -39,7 +39,7 @@ public void ShouldDisplayAboutMePage() .WithIsAboutMeEnabled(true) .Build()); Services.AddScoped(_ => config); - Services.AddScoped(_ => Substitute.For()); + Services.AddScoped(_ => Substitute.For()); AddAuthorization(); var cut = Render(); @@ -55,7 +55,7 @@ public void ShouldPassCorrectUriToComponent() { var config = Options.Create(new ProfileInformationBuilder().Build()); Services.AddScoped(_ => config); - Services.AddScoped(_ => Substitute.For()); + Services.AddScoped(_ => Substitute.For()); AddAuthorization(); var cut = Render(); @@ -71,7 +71,7 @@ public void ShouldShowBrandImageIfAvailable() .WithBlogBrandUrl("http://localhost/img.png") .Build()); Services.AddScoped(_ => config); - Services.AddScoped(_ => Substitute.For()); + Services.AddScoped(_ => Substitute.For()); var profileInfoConfig = Options.Create(new ProfileInformationBuilder().Build()); Services.AddScoped(_ => profileInfoConfig); @@ -96,7 +96,7 @@ public void ShouldShowBlogNameWhenNotBrand(string? brandUrl) .WithBlogName("Steven") .Build()); Services.AddScoped(_ => config); - Services.AddScoped(_ => Substitute.For()); + Services.AddScoped(_ => Substitute.For()); var profileInfoConfig = Options.Create(new ProfileInformationBuilder().Build()); Services.AddScoped(_ => profileInfoConfig); diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPostTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPostTests.cs index 8f2369e7..58e19dcf 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPostTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPostTests.cs @@ -12,7 +12,6 @@ using LinkDotNet.Blog.Web.Features.Components; using LinkDotNet.Blog.Web.Features.Services; using Microsoft.AspNetCore.Components.Routing; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using NCronJob; @@ -46,9 +45,9 @@ public CreateNewBlogPostTests() Services.AddScoped(_ => options); - var userRecordService = Substitute.For(); - userRecordService.GetDisplayNameAsync().Returns("Test Author"); - Services.AddScoped(_ => userRecordService); + var currentUserService = Substitute.For(); + currentUserService.GetDisplayNameAsync().Returns("Test Author"); + Services.AddScoped(_ => currentUserService); } [Fact] @@ -84,7 +83,7 @@ public void ShouldCreateNewBlogPostWhenMultiAuthorModeIsEnabled() } [Fact] - public void ShouldAuthorNameIsNullWhenMultiAuthorModeIsDisable() + public void ShouldSetAuthorNameAsNullWhenMultiAuthorModeIsDisable() { options.Value.Returns(new ApplicationConfiguration() { diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Home/Components/AccessControlTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Home/Components/AccessControlTests.cs index 76040616..8b788059 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Home/Components/AccessControlTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Home/Components/AccessControlTests.cs @@ -2,7 +2,6 @@ using LinkDotNet.Blog.Web; using LinkDotNet.Blog.Web.Features.Home.Components; using LinkDotNet.Blog.Web.Features.Services; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -26,9 +25,9 @@ public AccessControlTests() Services.AddScoped(_ => options); - var userRecordService = Substitute.For(); - userRecordService.GetDisplayNameAsync().Returns("Test Author"); - Services.AddScoped(_ => userRecordService); + var currentUserService = Substitute.For(); + currentUserService.GetDisplayNameAsync().Returns("Test Author"); + Services.AddScoped(_ => currentUserService); } [Fact] @@ -92,7 +91,7 @@ public void ShouldShowAuthorNameWhenUseMultiAuthorModeIsEnabled() var cut = Render(); - cut.FindAll("label:contains('Test Author')").ShouldHaveSingleItem(); + cut.FindAll("a:contains('Log out Test Author')").ShouldHaveSingleItem(); } [Fact] @@ -110,6 +109,6 @@ public void ShouldHideAuthorNameWhenUseMultiAuthorModeIsDisabled() var cut = Render(); - cut.FindAll("label:contains('Test Author')").ShouldBeEmpty(); + cut.FindAll("a:contains('Log out Test Author')").ShouldBeEmpty(); } } diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Services/CurrentUserServiceTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Services/CurrentUserServiceTests.cs new file mode 100644 index 00000000..c83ae472 --- /dev/null +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Services/CurrentUserServiceTests.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using LinkDotNet.Blog.Web.Features.Services; + +namespace LinkDotNet.Blog.UnitTests.Web.Features.Services; + +public class CurrentUserServiceTests : BunitContext +{ + private readonly BunitAuthenticationStateProvider fakeAuthenticationStateProvider; + private readonly CurrentUserService sut; + + public CurrentUserServiceTests() + { + fakeAuthenticationStateProvider = new BunitAuthenticationStateProvider(); + sut = new CurrentUserService(fakeAuthenticationStateProvider); + } + + [Theory] + [InlineData("name")] + [InlineData("preferred_username")] + [InlineData("nickname")] + public async Task ShouldGetDisplayNameWhenAuthenticated(string claimType) + { + var claims = new List() + { + new Claim(claimType, "Test Author") + }; + + fakeAuthenticationStateProvider.TriggerAuthenticationStateChanged("Steven", claims: claims); + var authorName = await sut.GetDisplayNameAsync(); + authorName.ShouldBe("Test Author"); + } + + [Fact] + public async Task ShouldGetNullAsDisplayNameWhenNoClaimGiven() + { + fakeAuthenticationStateProvider.TriggerAuthenticationStateChanged("Steven"); + var authorName = await sut.GetDisplayNameAsync(); + authorName.ShouldBeNull(); + } + + [Fact] + public async Task ShouldGetNullAsDisplayNameWhenUnauthenticated() + { + var authorName = await sut.GetDisplayNameAsync(); + authorName.ShouldBeNull(); + } +} diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Services/UserRecordServiceTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Services/UserRecordServiceTests.cs index a9dc5f28..6ec6b0ec 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Services/UserRecordServiceTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Services/UserRecordServiceTests.cs @@ -1,12 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Security.Claims; +using System; using System.Threading.Tasks; using LinkDotNet.Blog.Domain; using LinkDotNet.Blog.Infrastructure.Persistence; using LinkDotNet.Blog.Web.Features.Services; using Microsoft.Extensions.Logging; -using NSubstitute; namespace LinkDotNet.Blog.UnitTests.Web.Features.Services; @@ -72,24 +69,4 @@ public async Task ShouldRemoveQueryStringIfPresent(string url, string expectedRe recordToDb.ShouldNotBeNull(); recordToDb.UrlClicked.ShouldBe(expectedRecord); } - - [Fact] - public async Task ShouldGetDisplayNameWhenAuthenticated() - { - var claims = new List() - { - new Claim("name", "Test Author") - }; - - fakeAuthenticationStateProvider.TriggerAuthenticationStateChanged("Steven", claims: claims); - var authorName = await sut.GetDisplayNameAsync(); - authorName.ShouldBe("Test Author"); - } - - [Fact] - public async Task ShouldGetNullAsDisplayNameWhenUnauthenticated() - { - var authorName = await sut.GetDisplayNameAsync(); - authorName.ShouldBeNull(); - } } From 79006f1b92b51b049e0bb331280c915477da24dc Mon Sep 17 00:00:00 2001 From: Arnab Roy Chowdhury Date: Tue, 2 Sep 2025 13:54:27 +0530 Subject: [PATCH 15/19] Update config doc --- docs/Setup/Configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Setup/Configuration.md b/docs/Setup/Configuration.md index 2c54b1a4..aa8ace18 100644 --- a/docs/Setup/Configuration.md +++ b/docs/Setup/Configuration.md @@ -110,4 +110,4 @@ The appsettings.json file has a lot of options to customize the content of the b | ServiceUrl | string | The host url of the Azure blob storage. Only used if `AuthenticationMode` is set to `Default` | | ContainerName | string | The container name for the image storage provider | | CdnEndpoint | string | Optional CDN endpoint to use for uploaded images. If set, the blog will return this URL instead of the storage account URL for uploaded assets. | -| UseMultiAuthorMode | boolean | The default value is `false`. If set to `true` then author name will be associated with blog posts at the time of creation. +| UseMultiAuthorMode | boolean | The default value is `false`. If set to `true` then author name will be associated with blog posts at the time of creation. This author name will be fetched from the identity provider's `name` or `nickname` or `preferred_username` claim property. | From c56f694e7f0884438b00579fd8d92524cef040a7 Mon Sep 17 00:00:00 2001 From: Arnab Roy Chowdhury Date: Fri, 5 Sep 2025 21:02:16 +0530 Subject: [PATCH 16/19] Show author name in blog preview --- .../Features/Components/ShortBlogPost.razor | 7 +- .../Web/Features/Home/IndexTests.cs | 47 ++++++- .../Features/Components/ShortBlogPostTests.cs | 121 ++++++++++++++++++ 3 files changed, 169 insertions(+), 6 deletions(-) diff --git a/src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor b/src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor index 0be7b840..fd7cac8f 100644 --- a/src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor +++ b/src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor @@ -2,6 +2,7 @@ @using LinkDotNet.Blog.Web.Features.Bookmarks @using LinkDotNet.Blog.Web.Features.Bookmarks.Components @inject IBookmarkService BookmarkService +@inject IOptions AppConfiguration
    @@ -33,7 +34,11 @@ }
  • @BlogPost.ReadingTimeInMinutes minute read
  • - + @if (AppConfiguration.Value.UseMultiAuthorMode) + { +
  • @BlogPost.AuthorName
  • + } +
    diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Home/IndexTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Home/IndexTests.cs index 3dbb7f5d..6632a795 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Home/IndexTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Home/IndexTests.cs @@ -1,5 +1,6 @@ -using System.Linq; +using System.Linq; using System.Threading.Tasks; +using Bunit.Extensions.WaitForHelpers; using LinkDotNet.Blog.Domain; using LinkDotNet.Blog.TestUtilities; using LinkDotNet.Blog.Web; @@ -139,12 +140,48 @@ public async Task ShouldSetPageToFirstIfOutOfRange(int? page) cut.FindAll(".blog-card").Count.ShouldBe(10); } + [Fact] + public async Task ShouldShowAuthorNameWhenUseMultiAuthorModeIsTrue() + { + var publishedPost = new BlogPostBuilder() + .WithAuthorName("Test Author") + .Build(); + + await Repository.StoreAsync(publishedPost); + using var ctx = new BunitContext(); + ctx.JSInterop.Mode = JSRuntimeMode.Loose; + RegisterComponents(ctx, useMultiAuthorMode: true); + var cut = ctx.Render(); + + cut.WaitForElement("li:contains('Test Author')"); + cut.FindAll("li:contains('Test Author')").ShouldHaveSingleItem(); + } + + [Fact] + public async Task ShouldNotShowAuthorNameWhenUseMultiAuthorModeIsFalse() + { + var publishedPost = new BlogPostBuilder() + .WithAuthorName("Test Author") + .Build(); + + await Repository.StoreAsync(publishedPost); + using var ctx = new BunitContext(); + ctx.JSInterop.Mode = JSRuntimeMode.Loose; + RegisterComponents(ctx, useMultiAuthorMode: false); + var cut = ctx.Render(); + + var func = () => cut.WaitForElement("li:contains('Test Author')"); + func.ShouldThrow(); + cut.FindAll("li:contains('Test Author')").ShouldBeEmpty(); + } + private static (ApplicationConfiguration ApplicationConfiguration, Introduction Introduction) - CreateSampleAppConfiguration(string? profilePictureUri = null) + CreateSampleAppConfiguration(string? profilePictureUri = null, bool useMultiAuthorMode = false) { return (new ApplicationConfigurationBuilder() .WithBlogName(string.Empty) .WithBlogPostsPerPage(10) + .WithUseMultiAuthorMode(useMultiAuthorMode) .Build(), new Introduction { @@ -163,11 +200,11 @@ private async Task CreatePublishedBlogPosts(int amount) } } - private void RegisterComponents(BunitContext ctx, string? profilePictureUri = null) + private void RegisterComponents(BunitContext ctx, string? profilePictureUri = null, bool useMultiAuthorMode = false) { ctx.Services.AddScoped(_ => Repository); - ctx.Services.AddScoped(_ => Options.Create(CreateSampleAppConfiguration(profilePictureUri).ApplicationConfiguration)); - ctx.Services.AddScoped(_ => Options.Create(CreateSampleAppConfiguration(profilePictureUri).Introduction)); + ctx.Services.AddScoped(_ => Options.Create(CreateSampleAppConfiguration(profilePictureUri, useMultiAuthorMode).ApplicationConfiguration)); + ctx.Services.AddScoped(_ => Options.Create(CreateSampleAppConfiguration(profilePictureUri, useMultiAuthorMode).Introduction)); ctx.Services.AddScoped(_ => Substitute.For()); ctx.Services.AddScoped(_ => Substitute.For()); } diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Components/ShortBlogPostTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Components/ShortBlogPostTests.cs index b3d4511d..daa490fd 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Components/ShortBlogPostTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Components/ShortBlogPostTests.cs @@ -1,9 +1,12 @@ using System; using System.Linq; using LinkDotNet.Blog.TestUtilities; +using LinkDotNet.Blog.Web; using LinkDotNet.Blog.Web.Features.Bookmarks; using LinkDotNet.Blog.Web.Features.Components; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using static Microsoft.IO.RecyclableMemoryStreamManager; namespace LinkDotNet.Blog.UnitTests.Web.Features.Components; @@ -13,6 +16,17 @@ public class ShortBlogPostTests : BunitContext public void ShouldOpenBlogPost() { Services.AddScoped(_ => Substitute.For()); + var options = Substitute.For>(); + + options.Value.Returns(new ApplicationConfiguration() + { + UseMultiAuthorMode = true, + BlogName = "Test", + ConnectionString = "Test", + DatabaseName = "Test" + }); + + Services.AddScoped(_ => options); var blogPost = new BlogPostBuilder().Build(); blogPost.Id = "SomeId"; var cut = Render( @@ -27,6 +41,17 @@ public void ShouldOpenBlogPost() public void ShouldNavigateToEscapedTagSiteWhenClickingOnTag() { Services.AddScoped(_ => Substitute.For()); + var options = Substitute.For>(); + + options.Value.Returns(new ApplicationConfiguration() + { + UseMultiAuthorMode = true, + BlogName = "Test", + ConnectionString = "Test", + DatabaseName = "Test" + }); + + Services.AddScoped(_ => options); var blogPost = new BlogPostBuilder().WithTags("Tag 1").Build(); var cut = Render( p => p.Add(c => c.BlogPost, blogPost)); @@ -40,6 +65,17 @@ public void ShouldNavigateToEscapedTagSiteWhenClickingOnTag() public void WhenNoTagsAreGivenThenTagsAreNotShown() { Services.AddScoped(_ => Substitute.For()); + var options = Substitute.For>(); + + options.Value.Returns(new ApplicationConfiguration() + { + UseMultiAuthorMode = true, + BlogName = "Test", + ConnectionString = "Test", + DatabaseName = "Test" + }); + + Services.AddScoped(_ => options); var blogPost = new BlogPostBuilder().Build(); var cut = Render( @@ -52,6 +88,17 @@ public void WhenNoTagsAreGivenThenTagsAreNotShown() public void GivenBlogPostThatIsScheduled_ThenIndicating() { Services.AddScoped(_ => Substitute.For()); + var options = Substitute.For>(); + + options.Value.Returns(new ApplicationConfiguration() + { + UseMultiAuthorMode = true, + BlogName = "Test", + ConnectionString = "Test", + DatabaseName = "Test" + }); + + Services.AddScoped(_ => options); var blogPost = new BlogPostBuilder().IsPublished(false).WithScheduledPublishDate(new DateTime(2099, 1, 1)) .Build(); @@ -65,6 +112,17 @@ public void GivenBlogPostThatIsScheduled_ThenIndicating() public void GivenBlogPostThatIsNotPublishedAndNotScheduled_ThenIndicating() { Services.AddScoped(_ => Substitute.For()); + var options = Substitute.For>(); + + options.Value.Returns(new ApplicationConfiguration() + { + UseMultiAuthorMode = true, + BlogName = "Test", + ConnectionString = "Test", + DatabaseName = "Test" + }); + + Services.AddScoped(_ => options); var blogPost = new BlogPostBuilder().IsPublished(false).Build(); var cut = Render( @@ -77,6 +135,17 @@ public void GivenBlogPostThatIsNotPublishedAndNotScheduled_ThenIndicating() public void GivenBlogPostThatIsPublished_ThenNoDraft() { Services.AddScoped(_ => Substitute.For()); + var options = Substitute.For>(); + + options.Value.Returns(new ApplicationConfiguration() + { + UseMultiAuthorMode = true, + BlogName = "Test", + ConnectionString = "Test", + DatabaseName = "Test" + }); + + Services.AddScoped(_ => options); var blogPost = new BlogPostBuilder().IsPublished(true).Build(); var cut = Render( @@ -85,4 +154,56 @@ public void GivenBlogPostThatIsPublished_ThenNoDraft() cut.FindAll(".draft").ShouldBeEmpty(); cut.FindAll(".scheduled").ShouldBeEmpty(); } + + [Fact] + public void ShouldShowAuthorNameWhenUseMultiAuthorModeIsTrue() + { + Services.AddScoped(_ => Substitute.For()); + var options = Substitute.For>(); + + options.Value.Returns(new ApplicationConfiguration() + { + UseMultiAuthorMode = true, + BlogName = "Test", + ConnectionString = "Test", + DatabaseName = "Test" + }); + + Services.AddScoped(_ => options); + + var blogPost = new BlogPostBuilder() + .WithAuthorName("Test Author") + .IsPublished(true) + .Build(); + + var cut = Render(p => p.Add(c => c.BlogPost, blogPost)); + + cut.FindAll("li:contains('Test Author')").ShouldHaveSingleItem(); + } + + [Fact] + public void ShouldNotShowAuthorNameWhenUseMultiAuthorModeIsFalse() + { + Services.AddScoped(_ => Substitute.For()); + var options = Substitute.For>(); + + options.Value.Returns(new ApplicationConfiguration() + { + UseMultiAuthorMode = false, + BlogName = "Test", + ConnectionString = "Test", + DatabaseName = "Test" + }); + + Services.AddScoped(_ => options); + + var blogPost = new BlogPostBuilder() + .WithAuthorName("Test Author") + .IsPublished(true) + .Build(); + + var cut = Render(p => p.Add(c => c.BlogPost, blogPost)); + + cut.FindAll("li:contains('Test Author')").ShouldBeEmpty(); + } } From 9a1df8f51349cc67a8bbea9f134b838ad6d4ad2d Mon Sep 17 00:00:00 2001 From: Arnab Roy Chowdhury Date: Fri, 5 Sep 2025 22:34:07 +0530 Subject: [PATCH 17/19] Show author name in read blog post page --- .../ShowBlogPost/ShowBlogPostPage.razor | 6 ++- .../ShowBlogPost/ShowBlogPostPageTests.cs | 40 +++++++++++++++++-- .../ShowBlogPost/ShowBlogPostPageTests.cs | 40 ++++++++++++++++++- 3 files changed, 79 insertions(+), 7 deletions(-) diff --git a/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/ShowBlogPostPage.razor b/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/ShowBlogPostPage.razor index 5a2636e0..1b4b27bc 100644 --- a/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/ShowBlogPostPage.razor +++ b/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/ShowBlogPostPage.razor @@ -53,7 +53,11 @@ else if (BlogPost is not null)
    @BlogPost.ReadingTimeInMinutes minute read -
    + @if (AppConfiguration.Value.UseMultiAuthorMode) + { + @BlogPost.AuthorName + } +
    @if (BlogPost.Tags is not null && BlogPost.Tags.Any()) diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs index 74dd604d..13f0c3a3 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs @@ -1,4 +1,4 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using Blazored.Toast.Services; using LinkDotNet.Blog.Domain; using LinkDotNet.Blog.Infrastructure; @@ -138,17 +138,49 @@ public async Task ShortCodesShouldBeReplacedByTheirContent() cut.Find(".blogpost-content > p").TextContent.ShouldBe("This is a Content shortcode"); } - private void RegisterComponents(BunitContext ctx, ILocalStorageService? localStorageService = null) + [Fact] + public async Task ShouldShowAuthorNameWhenUseMultiAuthorModeIsTrue() + { + using var ctx = new BunitContext(); + ctx.JSInterop.Mode = JSRuntimeMode.Loose; + ctx.AddAuthorization(); + RegisterComponents(ctx, useMultiAuthorMode: true); + var blogPost = new BlogPostBuilder().WithAuthorName("Test Author").IsPublished().Build(); + await Repository.StoreAsync(blogPost); + + var cut = ctx.Render( + p => p.Add(b => b.BlogPostId, blogPost.Id)); + + cut.FindAll("span:contains('Test Author')").ShouldHaveSingleItem(); + } + + [Fact] + public async Task ShouldNotShowAuthorNameWhenUseMultiAuthorModeIsFalse() + { + using var ctx = new BunitContext(); + ctx.JSInterop.Mode = JSRuntimeMode.Loose; + ctx.AddAuthorization(); + RegisterComponents(ctx, useMultiAuthorMode: false); + var blogPost = new BlogPostBuilder().WithAuthorName("Test Author").IsPublished().Build(); + await Repository.StoreAsync(blogPost); + + var cut = ctx.Render( + p => p.Add(b => b.BlogPostId, blogPost.Id)); + + cut.FindAll("span:contains('Test Author')").ShouldBeEmpty(); + } + + private void RegisterComponents(BunitContext ctx, ILocalStorageService? localStorageService = null, bool useMultiAuthorMode = false) { ctx.Services.AddScoped(_ => Repository); ctx.Services.AddScoped(_ => localStorageService ?? Substitute.For()); ctx.Services.AddScoped(_ => Substitute.For()); ctx.Services.AddScoped(_ => Substitute.For()); - ctx.Services.AddScoped(_ => Options.Create(new ApplicationConfigurationBuilder().Build())); + ctx.Services.AddScoped(_ => Options.Create(new ApplicationConfigurationBuilder().WithUseMultiAuthorMode(useMultiAuthorMode).Build())); ctx.Services.AddScoped(_ => Substitute.For()); 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/ShowBlogPost/ShowBlogPostPageTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs index 98baee9d..6792d574 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs @@ -164,7 +164,43 @@ public void ShouldSetCanoncialUrlOfOgDataWithoutSlug() cut.FindComponent().Instance.CanonicalRelativeUrl.ShouldBe("blogPost/1"); } - + + [Fact] + public void ShouldShowAuthorNameWhenUseMultiAuthorModeIsTrue() + { + var repositoryMock = Substitute.For>(); + var blogPost = new BlogPostBuilder() + .WithAuthorName("Test Author") + .Build(); + blogPost.Id = "1"; + repositoryMock.GetByIdAsync("1").Returns(blogPost); + Services.AddScoped(_ => repositoryMock); + Services.AddScoped(_ => Options.Create(new ApplicationConfigurationBuilder().WithUseMultiAuthorMode(true).Build())); + + var cut = Render( + p => p.Add(s => s.BlogPostId, "1")); + + cut.FindAll("span:contains('Test Author')").ShouldHaveSingleItem(); + } + + [Fact] + public void ShouldNotShowAuthorNameWhenUseMultiAuthorModeIsFalse() + { + var repositoryMock = Substitute.For>(); + var blogPost = new BlogPostBuilder() + .WithAuthorName("Test Author") + .Build(); + blogPost.Id = "1"; + repositoryMock.GetByIdAsync("1").Returns(blogPost); + Services.AddScoped(_ => repositoryMock); + Services.AddScoped(_ => Options.Create(new ApplicationConfigurationBuilder().WithUseMultiAuthorMode(false).Build())); + + var cut = Render( + p => p.Add(s => s.BlogPostId, "1")); + + cut.FindAll("span:contains('Test Author')").ShouldBeEmpty(); + } + private class PageTitleStub : ComponentBase { [Parameter] @@ -185,4 +221,4 @@ private class SimilarBlogPostSectionStub : ComponentBase [Parameter] public BlogPost BlogPost { get; set; } = default!; } -} \ No newline at end of file +} From cdba0ae0bd151f1d148c9a54fd7a16092071723e Mon Sep 17 00:00:00 2001 From: Arnab Roy Chowdhury Date: Fri, 5 Sep 2025 22:55:19 +0530 Subject: [PATCH 18/19] Show author name in top menu --- .../Features/Home/Components/AccessControl.razor | 16 +++++++++++++++- .../Home/Components/AccessControlTests.cs | 4 ++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/LinkDotNet.Blog.Web/Features/Home/Components/AccessControl.razor b/src/LinkDotNet.Blog.Web/Features/Home/Components/AccessControl.razor index a7d04b79..32387832 100644 --- a/src/LinkDotNet.Blog.Web/Features/Home/Components/AccessControl.razor +++ b/src/LinkDotNet.Blog.Web/Features/Home/Components/AccessControl.razor @@ -24,7 +24,21 @@
  • Releases
  • - + @if (authorName is not null) + { + + } + else + { + + } diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Home/Components/AccessControlTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Home/Components/AccessControlTests.cs index 8b788059..deff2805 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Home/Components/AccessControlTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Home/Components/AccessControlTests.cs @@ -91,7 +91,7 @@ public void ShouldShowAuthorNameWhenUseMultiAuthorModeIsEnabled() var cut = Render(); - cut.FindAll("a:contains('Log out Test Author')").ShouldHaveSingleItem(); + cut.FindAll("a:contains('Test Author')").ShouldHaveSingleItem(); } [Fact] @@ -109,6 +109,6 @@ public void ShouldHideAuthorNameWhenUseMultiAuthorModeIsDisabled() var cut = Render(); - cut.FindAll("a:contains('Log out Test Author')").ShouldBeEmpty(); + cut.FindAll("a:contains('Test Author')").ShouldBeEmpty(); } } From dfa563be2bb233c10caea26108194bca440b43bf Mon Sep 17 00:00:00 2001 From: Arnab Roy Chowdhury Date: Tue, 9 Sep 2025 08:39:48 +0530 Subject: [PATCH 19/19] Add icon --- .../Features/Components/ShortBlogPost.razor | 4 +-- .../ShowBlogPost/ShowBlogPostPage.razor | 3 +- .../Web/Features/Home/IndexTests.cs | 18 ++++++++++++ .../ShowBlogPost/ShowBlogPostPageTests.cs | 19 +++++++++++++ .../Features/Components/ShortBlogPostTests.cs | 28 +++++++++++++++++++ .../ShowBlogPost/ShowBlogPostPageTests.cs | 19 +++++++++++++ 6 files changed, 88 insertions(+), 3 deletions(-) diff --git a/src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor b/src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor index fd7cac8f..61c1df42 100644 --- a/src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor +++ b/src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor @@ -34,9 +34,9 @@ }
  • @BlogPost.ReadingTimeInMinutes minute read
  • - @if (AppConfiguration.Value.UseMultiAuthorMode) + @if (AppConfiguration.Value.UseMultiAuthorMode && BlogPost.AuthorName is not null) { -
  • @BlogPost.AuthorName
  • +
  • @BlogPost.AuthorName
  • }
    diff --git a/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/ShowBlogPostPage.razor b/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/ShowBlogPostPage.razor index 1b4b27bc..d55f1f76 100644 --- a/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/ShowBlogPostPage.razor +++ b/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/ShowBlogPostPage.razor @@ -53,8 +53,9 @@ else if (BlogPost is not null)
    @BlogPost.ReadingTimeInMinutes minute read - @if (AppConfiguration.Value.UseMultiAuthorMode) + @if (AppConfiguration.Value.UseMultiAuthorMode && BlogPost.AuthorName is not null) { + @BlogPost.AuthorName }
    diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Home/IndexTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Home/IndexTests.cs index 6632a795..c88a72b3 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Home/IndexTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Home/IndexTests.cs @@ -155,6 +155,7 @@ public async Task ShouldShowAuthorNameWhenUseMultiAuthorModeIsTrue() cut.WaitForElement("li:contains('Test Author')"); cut.FindAll("li:contains('Test Author')").ShouldHaveSingleItem(); + cut.FindAll("i.user-tie").ShouldHaveSingleItem(); } [Fact] @@ -173,6 +174,23 @@ public async Task ShouldNotShowAuthorNameWhenUseMultiAuthorModeIsFalse() var func = () => cut.WaitForElement("li:contains('Test Author')"); func.ShouldThrow(); cut.FindAll("li:contains('Test Author')").ShouldBeEmpty(); + cut.FindAll("i.user-tie").ShouldBeEmpty(); + } + + [Fact] + public async Task ShouldNotShowAuthorNameWhenAuthorNameIsNull() + { + var publishedPost = new BlogPostBuilder().Build(); // Author name is null here. + await Repository.StoreAsync(publishedPost); + using var ctx = new BunitContext(); + ctx.JSInterop.Mode = JSRuntimeMode.Loose; + RegisterComponents(ctx, useMultiAuthorMode: true); + var cut = ctx.Render(); + + var func = () => cut.WaitForElement("li:contains('Test Author')"); + func.ShouldThrow(); + cut.FindAll("li:contains('Test Author')").ShouldBeEmpty(); + cut.FindAll("i.user-tie").ShouldBeEmpty(); } private static (ApplicationConfiguration ApplicationConfiguration, Introduction Introduction) diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs index 13f0c3a3..eb364b4b 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs @@ -152,6 +152,7 @@ public async Task ShouldShowAuthorNameWhenUseMultiAuthorModeIsTrue() p => p.Add(b => b.BlogPostId, blogPost.Id)); cut.FindAll("span:contains('Test Author')").ShouldHaveSingleItem(); + cut.FindAll("i.user-tie").ShouldHaveSingleItem(); } [Fact] @@ -168,6 +169,24 @@ public async Task ShouldNotShowAuthorNameWhenUseMultiAuthorModeIsFalse() p => p.Add(b => b.BlogPostId, blogPost.Id)); cut.FindAll("span:contains('Test Author')").ShouldBeEmpty(); + cut.FindAll("i.user-tie").ShouldBeEmpty(); + } + + [Fact] + public async Task ShouldNotShowAuthorNameWhenAuthorNameIsNull() + { + using var ctx = new BunitContext(); + ctx.JSInterop.Mode = JSRuntimeMode.Loose; + ctx.AddAuthorization(); + RegisterComponents(ctx, useMultiAuthorMode: true); + var blogPost = new BlogPostBuilder().IsPublished().Build(); // Author name is null here. + await Repository.StoreAsync(blogPost); + + var cut = ctx.Render( + p => p.Add(b => b.BlogPostId, blogPost.Id)); + + cut.FindAll("span:contains('Test Author')").ShouldBeEmpty(); + cut.FindAll("i.user-tie").ShouldBeEmpty(); } private void RegisterComponents(BunitContext ctx, ILocalStorageService? localStorageService = null, bool useMultiAuthorMode = false) diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Components/ShortBlogPostTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Components/ShortBlogPostTests.cs index daa490fd..feda25f7 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Components/ShortBlogPostTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Components/ShortBlogPostTests.cs @@ -179,6 +179,7 @@ public void ShouldShowAuthorNameWhenUseMultiAuthorModeIsTrue() var cut = Render(p => p.Add(c => c.BlogPost, blogPost)); cut.FindAll("li:contains('Test Author')").ShouldHaveSingleItem(); + cut.FindAll("i.user-tie").ShouldHaveSingleItem(); } [Fact] @@ -205,5 +206,32 @@ public void ShouldNotShowAuthorNameWhenUseMultiAuthorModeIsFalse() var cut = Render(p => p.Add(c => c.BlogPost, blogPost)); cut.FindAll("li:contains('Test Author')").ShouldBeEmpty(); + cut.FindAll("i.user-tie").ShouldBeEmpty(); + } + + [Fact] + public void ShouldNotShowAuthorNameWhenAuthorNameIsNull() + { + Services.AddScoped(_ => Substitute.For()); + var options = Substitute.For>(); + + options.Value.Returns(new ApplicationConfiguration() + { + UseMultiAuthorMode = true, + BlogName = "Test", + ConnectionString = "Test", + DatabaseName = "Test" + }); + + Services.AddScoped(_ => options); + + var blogPost = new BlogPostBuilder() + .IsPublished(true) + .Build(); // Author name is null here. + + var cut = Render(p => p.Add(c => c.BlogPost, blogPost)); + + cut.FindAll("li:contains('Test Author')").ShouldBeEmpty(); + cut.FindAll("i.user-tie").ShouldBeEmpty(); } } diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs index 6792d574..250c22a7 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs @@ -181,6 +181,7 @@ public void ShouldShowAuthorNameWhenUseMultiAuthorModeIsTrue() p => p.Add(s => s.BlogPostId, "1")); cut.FindAll("span:contains('Test Author')").ShouldHaveSingleItem(); + cut.FindAll("i.user-tie").ShouldHaveSingleItem(); } [Fact] @@ -199,6 +200,24 @@ public void ShouldNotShowAuthorNameWhenUseMultiAuthorModeIsFalse() p => p.Add(s => s.BlogPostId, "1")); cut.FindAll("span:contains('Test Author')").ShouldBeEmpty(); + cut.FindAll("i.user-tie").ShouldBeEmpty(); + } + + [Fact] + public void ShouldNotShowAuthorNameWhenAuthorNameIsNull() + { + var repositoryMock = Substitute.For>(); + var blogPost = new BlogPostBuilder().Build(); // Author name is null here. + blogPost.Id = "1"; + repositoryMock.GetByIdAsync("1").Returns(blogPost); + Services.AddScoped(_ => repositoryMock); + Services.AddScoped(_ => Options.Create(new ApplicationConfigurationBuilder().WithUseMultiAuthorMode(true).Build())); + + var cut = Render( + p => p.Add(s => s.BlogPostId, "1")); + + cut.FindAll("span:contains('Test Author')").ShouldBeEmpty(); + cut.FindAll("i.user-tie").ShouldBeEmpty(); } private class PageTitleStub : ComponentBase