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..aa8ace18 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. This author name will be fetched from the identity provider's `name` or `nickname` or `preferred_username` claim property. | 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/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..457cb113 --- /dev/null +++ b/src/LinkDotNet.Blog.Infrastructure/Migrations/20250830110439_AddAuthorNameInBlogPost.cs @@ -0,0 +1,33 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LinkDotNet.Blog.Web.Migrations; + +/// +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) + { + ArgumentNullException.ThrowIfNull(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"); 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") diff --git a/src/LinkDotNet.Blog.Web/ApplicationConfiguration.cs b/src/LinkDotNet.Blog.Web/ApplicationConfiguration.cs index 33274f8c..5db35a25 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 UseMultiAuthorMode { get; init; } } diff --git a/src/LinkDotNet.Blog.Web/Authentication/Dummy/DummyLoginManager.cs b/src/LinkDotNet.Blog.Web/Authentication/Dummy/DummyLoginManager.cs index e499c888..77d93d60 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; @@ -27,6 +27,7 @@ public async Task SignInAsync(string redirectUri) var claims = new[] { 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/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/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor index 9a2f97e7..ecea7d1c 100644 --- a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor +++ b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor @@ -8,6 +8,8 @@ @inject ICacheInvalidator CacheInvalidator @inject IInstantJobRegistry InstantJobRegistry @inject IRepository ShortCodeRepository +@inject IOptions AppConfiguration +@inject ICurrentUserService CurrentUserService Creating new Blog Post @@ -276,9 +278,16 @@ private bool IsScheduled => model.ScheduledPublishDate.HasValue; + private string? authorName; + protected override async Task OnInitializedAsync() { shortCodes = await ShortCodeRepository.GetAllAsync(); + + if (AppConfiguration.Value.UseMultiAuthorMode) + { + authorName = await CurrentUserService.GetDisplayNameAsync(); + } } protected override void OnParametersSet() @@ -322,6 +331,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; } diff --git a/src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor b/src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor index 0be7b840..61c1df42 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 is not null) + { +
  • @BlogPost.AuthorName
  • + } +
    diff --git a/src/LinkDotNet.Blog.Web/Features/Home/Components/AccessControl.razor b/src/LinkDotNet.Blog.Web/Features/Home/Components/AccessControl.razor index 83e4e2e2..32387832 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 LinkDotNet.Blog.Web.Features.Services +@inject IOptions AppConfiguration +@inject ICurrentUserService CurrentUserService + + - + @if (authorName is not null) + { + + } + else + { + + } @@ -30,4 +48,14 @@ @code { [Parameter] public string CurrentUri { get; set; } = string.Empty; + + private string? authorName; + + protected override async Task OnInitializedAsync() + { + if (AppConfiguration.Value.UseMultiAuthorMode) + { + 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/ShowBlogPost/ShowBlogPostPage.razor b/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/ShowBlogPostPage.razor index 5a2636e0..d55f1f76 100644 --- a/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/ShowBlogPostPage.razor +++ b/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/ShowBlogPostPage.razor @@ -53,7 +53,12 @@ else if (BlogPost is not null)
    @BlogPost.ReadingTimeInMinutes minute read -
    + @if (AppConfiguration.Value.UseMultiAuthorMode && BlogPost.AuthorName is not null) + { + + @BlogPost.AuthorName + } +
    @if (BlogPost.Tags is not null && BlogPost.Tags.Any()) 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/src/LinkDotNet.Blog.Web/appsettings.json b/src/LinkDotNet.Blog.Web/appsettings.json index d61a5cf0..702c9b61 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, + "UseMultiAuthorMode": false } diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Infrastructure/Persistence/RavenDb/BlogPostRepositoryTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Infrastructure/Persistence/RavenDb/BlogPostRepositoryTests.cs index 7e0ddfe4..f25391eb 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 ShouldSetAuthorNameAsNullWhenNotGiven() + { + 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() { diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Infrastructure/Persistence/Sql/BlogPostRepositoryTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Infrastructure/Persistence/Sql/BlogPostRepositoryTests.cs index 9fe8fd06..d2ed4a13 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; @@ -14,7 +14,7 @@ public sealed class BlogPostRepositoryTests : SqlDatabaseTestBase [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 DbContext.BlogPosts.AddAsync(blogPost, TestContext.Current.CancellationToken); await DbContext.SaveChangesAsync(TestContext.Current.CancellationToken); @@ -26,6 +26,7 @@ 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"); @@ -33,9 +34,22 @@ public async Task ShouldLoadBlogPost() } [Fact] - public async Task ShouldSaveBlogPost() + 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); + await DbContext.SaveChangesAsync(TestContext.Current.CancellationToken); + + var blogPostFromRepo = await Repository.GetByIdAsync(blogPost.Id); + + blogPostFromRepo.ShouldNotBeNull(); + blogPostFromRepo.AuthorName.ShouldBeNull(); + } + + [Fact] + public async Task ShouldSaveBlogPost() + { + var blogPost = BlogPost.Create("Title", "Subtitle", "Content", "url", true, tags: new[] { "Tag 1", "Tag 2" }, authorName: "Test Author"); await Repository.StoreAsync(blogPost); @@ -49,6 +63,7 @@ public async Task ShouldSaveBlogPost() 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"); @@ -56,9 +71,24 @@ public async Task ShouldSaveBlogPost() } [Fact] - public async Task ShouldGetAllBlogPosts() + public async Task ShouldSaveAuthorNameAsNullWhenNotGiven() { 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); @@ -72,12 +102,27 @@ public async Task ShouldGetAllBlogPosts() 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 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] public async Task ShouldBeUpdateable() { 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..daf8d204 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/BlogPostEditor/CreateNewBlogPostPageTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/BlogPostEditor/CreateNewBlogPostPageTests.cs @@ -1,10 +1,11 @@ -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; @@ -13,6 +14,7 @@ using LinkDotNet.Blog.Web.Features.Services; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using NCronJob; using TestContext = Xunit.TestContext; @@ -37,6 +39,23 @@ public async Task ShouldSaveBlogPostOnSave() var shortCodeRepository = Substitute.For>(); shortCodeRepository.GetAllAsync().Returns(PagedList.Empty); ctx.Services.AddScoped(_ => shortCodeRepository); + + var currentUserService = Substitute.For(); + currentUserService.GetDisplayNameAsync().Returns("Test Author"); + ctx.Services.AddScoped(_ => currentUserService); + + var options = Substitute.For>(); + + options.Value.Returns(new ApplicationConfiguration() + { + UseMultiAuthorMode = true, + BlogName = "Test", + ConnectionString = "Test", + DatabaseName = "Test" + }); + + ctx.Services.AddScoped(_ => options); + using var cut = ctx.Render(); var newBlogPost = cut.FindComponent(); @@ -45,10 +64,56 @@ 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"); + 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 currentUserService = Substitute.For(); + currentUserService.GetDisplayNameAsync().Returns("Test Author"); + ctx.Services.AddScoped(_ => currentUserService); + + 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 c52b9ff5..c24809ff 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/BlogPostEditor/UpdateBlogPostPageTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/BlogPostEditor/UpdateBlogPostPageTests.cs @@ -7,6 +7,7 @@ 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; @@ -15,6 +16,7 @@ using Microsoft.AspNetCore.Components; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using NCronJob; using TestContext = Xunit.TestContext; @@ -40,6 +42,23 @@ public async Task ShouldSaveBlogPostOnSave() var shortCodeRepository = Substitute.For>(); shortCodeRepository.GetAllAsync().Returns(PagedList.Empty); ctx.Services.AddScoped(_ => shortCodeRepository); + + var currentUserService = Substitute.For(); + currentUserService.GetDisplayNameAsync().Returns("Test Author"); + ctx.Services.AddScoped(_ => currentUserService); + + var options = Substitute.For>(); + + options.Value.Returns(new ApplicationConfiguration() + { + UseMultiAuthorMode = true, + 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,10 +68,58 @@ 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"); + 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 currentUserService = Substitute.For(); + currentUserService.GetDisplayNameAsync().Returns("Test Author"); + ctx.Services.AddScoped(_ => currentUserService); + + 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() { @@ -63,6 +130,22 @@ public void ShouldThrowWhenNoIdProvided() ctx.Services.AddScoped(_ => Substitute.For()); ctx.Services.AddScoped(_ => Substitute.For()); + var currentUserService = Substitute.For(); + currentUserService.GetDisplayNameAsync().Returns("Test Author"); + ctx.Services.AddScoped(_ => currentUserService); + + var options = Substitute.For>(); + + options.Value.Returns(new ApplicationConfiguration() + { + UseMultiAuthorMode = true, + BlogName = "Test", + ConnectionString = "Test", + DatabaseName = "Test" + }); + + ctx.Services.AddScoped(_ => options); + Action act = () => ctx.Render( p => p.Add(s => s.BlogPostId, null)); @@ -75,4 +158,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/Features/Home/IndexTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Home/IndexTests.cs index 3dbb7f5d..c88a72b3 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,66 @@ 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(); + cut.FindAll("i.user-tie").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(); + 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) - 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 +218,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.IntegrationTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs index 74dd604d..eb364b4b 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,68 @@ 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(); + cut.FindAll("i.user-tie").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(); + 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) { 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.IntegrationTests/Web/Shared/NavMenuTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Shared/NavMenuTests.cs index b91904cc..0e4e91b7 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Shared/NavMenuTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Shared/NavMenuTests.cs @@ -1,8 +1,10 @@ -using System.Linq; +using System.Linq; 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; using Microsoft.Extensions.Options; @@ -19,6 +21,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 +39,7 @@ public void ShouldDisplayAboutMePage() .WithIsAboutMeEnabled(true) .Build()); Services.AddScoped(_ => config); + Services.AddScoped(_ => Substitute.For()); AddAuthorization(); var cut = Render(); @@ -51,6 +55,7 @@ public void ShouldPassCorrectUriToComponent() { var config = Options.Create(new ProfileInformationBuilder().Build()); Services.AddScoped(_ => config); + Services.AddScoped(_ => Substitute.For()); AddAuthorization(); var cut = Render(); @@ -66,7 +71,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 +96,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..ce2b3f64 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 useMultiAuthorMode; public ApplicationConfigurationBuilder WithBlogName(string blogName) { @@ -81,7 +82,13 @@ public ApplicationConfigurationBuilder WithBlogBrandUrl(string? blogBrandUrl) this.blogBrandUrl = blogBrandUrl; return this; } - + + public ApplicationConfigurationBuilder WithUseMultiAuthorMode(bool useMultiAuthorMode) + { + this.useMultiAuthorMode = useMultiAuthorMode; + return this; + } + public ApplicationConfiguration Build() { return new ApplicationConfiguration @@ -97,6 +104,7 @@ public ApplicationConfiguration Build() ShowReadingIndicator = showReadingIndicator, ShowSimilarPosts = showSimilarPosts, BlogBrandUrl = blogBrandUrl, + UseMultiAuthorMode = useMultiAuthorMode, }; } } 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..eb5e6f97 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Domain/BlogPostTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Domain/BlogPostTests.cs @@ -12,7 +12,7 @@ public void ShouldUpdateBlogPost() { var blogPostToUpdate = new BlogPostBuilder().Build(); blogPostToUpdate.Id = "random-id"; - var blogPost = 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); @@ -26,6 +26,20 @@ public void ShouldUpdateBlogPost() 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); + + blogPostToUpdate.AuthorName.ShouldBeNull(); } [Theory] diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/ApplicationConfigurationTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/ApplicationConfigurationTests.cs index 8005b883..46f61c14 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"}, + { "UseMultiAuthorMode","true"} }; var configuration = new ConfigurationBuilder() @@ -64,6 +65,7 @@ public void ShouldMapFromAppConfiguration() appConfiguration.BlogPostsPerPage.ShouldBe(5); appConfiguration.IsAboutMeEnabled.ShouldBeTrue(); appConfiguration.ShowReadingIndicator.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 472db955..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 @@ -7,11 +7,13 @@ 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.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using NCronJob; namespace LinkDotNet.Blog.UnitTests.Web.Features.Admin.BlogPostEditor.Components; @@ -19,7 +21,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 +33,25 @@ public CreateNewBlogPostTests() Services.AddScoped(_ => Substitute.For()); Services.AddScoped(_ => cacheService); Services.AddScoped(_ => Substitute.For()); + options = Substitute.For>(); + + options.Value.Returns(new ApplicationConfiguration() + { + UseMultiAuthorMode = true, + BlogName = "Test", + ConnectionString = "Test", + DatabaseName = "Test" + }); + + Services.AddScoped(_ => options); + + var currentUserService = Substitute.For(); + currentUserService.GetDisplayNameAsync().Returns("Test Author"); + Services.AddScoped(_ => currentUserService); } [Fact] - public void ShouldCreateNewBlogPostWhenValidDataGiven() + public void ShouldCreateNewBlogPostWhenMultiAuthorModeIsEnabled() { BlogPost? blogPost = null; var cut = Render( @@ -57,12 +75,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 ShouldSetAuthorNameAsNullWhenMultiAuthorModeIsDisable() + { + options.Value.Returns(new ApplicationConfiguration() + { + UseMultiAuthorMode = 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/Components/ShortBlogPostTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Components/ShortBlogPostTests.cs index b3d4511d..feda25f7 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,84 @@ 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(); + cut.FindAll("i.user-tie").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(); + 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/Home/Components/AccessControlTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Home/Components/AccessControlTests.cs index 05637ee3..deff2805 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 LinkDotNet.Blog.Web.Features.Services; +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() + { + UseMultiAuthorMode = true, + BlogName = "Test", + ConnectionString = "Test", + DatabaseName = "Test" + }); + + Services.AddScoped(_ => options); + + var currentUserService = Substitute.For(); + currentUserService.GetDisplayNameAsync().Returns("Test Author"); + Services.AddScoped(_ => currentUserService); + } + [Fact] public void ShouldShowLoginAndHideAdminWhenNotLoggedIn() { @@ -50,4 +75,40 @@ public void LogoutShouldHaveCurrentUriAsRedirectUri() ((IHtmlAnchorElement)cut.Find("a:contains('Log out')")).Href.ShouldContain(currentUri); } -} \ No newline at end of file + + [Fact] + public void ShouldShowAuthorNameWhenUseMultiAuthorModeIsEnabled() + { + options.Value.Returns(new ApplicationConfiguration() + { + UseMultiAuthorMode = true, + BlogName = "Test", + ConnectionString = "Test", + DatabaseName = "Test" + }); + + AddAuthorization().SetAuthorized("steven"); + + var cut = Render(); + + cut.FindAll("a:contains('Test Author')").ShouldHaveSingleItem(); + } + + [Fact] + public void ShouldHideAuthorNameWhenUseMultiAuthorModeIsDisabled() + { + options.Value.Returns(new ApplicationConfiguration() + { + UseMultiAuthorMode = false, + BlogName = "Test", + ConnectionString = "Test", + DatabaseName = "Test" + }); + + AddAuthorization().SetAuthorized("steven"); + + var cut = Render(); + + cut.FindAll("a:contains('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/ShowBlogPost/ShowBlogPostPageTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs index 98baee9d..250c22a7 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs @@ -164,7 +164,62 @@ 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(); + cut.FindAll("i.user-tie").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(); + 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 { [Parameter] @@ -185,4 +240,4 @@ private class SimilarBlogPostSectionStub : ComponentBase [Parameter] public BlogPost BlogPost { get; set; } = default!; } -} \ No newline at end of file +}