Merge branch 'main' of https://github.com/StevanFreeborn/blog.stevanfreeborn.com
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
<RunSettings>
|
||||
<Playwright>
|
||||
<BrowserName>chromium</BrowserName>
|
||||
<ExpectTimeout>30000</ExpectTimeout>
|
||||
<LaunchOptions>
|
||||
<Headless>false</Headless>
|
||||
<Channel>msedge</Channel>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
@@ -11,17 +11,46 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="bunit" Version="1.27.17" />
|
||||
<PackageReference Include="coverlet.msbuild" Version="6.0.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.3" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
|
||||
<PackageReference Include="Microsoft.Playwright.NUnit" Version="1.42.0" />
|
||||
<PackageReference Include="moq" Version="4.20.70" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
|
||||
<PackageReference Include="NUnit.Analyzers" Version="3.6.1" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<CollectCoverage>true</CollectCoverage>
|
||||
<CoverletOutput>./TestResults/coverage/</CoverletOutput>
|
||||
<CoverletOutputFormat>cobertura</CoverletOutputFormat>
|
||||
<Include>[Blog]*</Include>
|
||||
<ExcludeByFile>**/Blog/Program.cs</ExcludeByFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="GenerateHtmlCoverageReport" AfterTargets="GenerateCoverageResultAfterTest">
|
||||
<Exec Command="reportgenerator -reports:./TestResults/coverage/*.xml -targetdir:./TestResults/coverage/report/ -reporttypes:Html_Dark" />
|
||||
</Target>
|
||||
|
||||
<Target Name="OpenTestReport" AfterTargets="GenerateHtmlCoverageReport" Condition="'$(DOTNET_ENVIRONMENT)' != 'CI'">
|
||||
<Exec Command="start ./TestResults/coverage/report/index.html" />
|
||||
</Target>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Blog\Blog.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include=".\EndToEnd\TestPosts\**">
|
||||
<Link>TestPosts\%(RecursiveDir)%(Filename)%(Extension)</Link>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,15 +1,71 @@
|
||||
using Blog.Posts;
|
||||
|
||||
using Microsoft.AspNetCore.Hosting.Server.Features;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Blog.Tests.EndToEnd;
|
||||
|
||||
public class BlogHostFactory<TProgram> : WebApplicationFactory<TProgram> where TProgram : class
|
||||
{
|
||||
private IHost? _host;
|
||||
|
||||
private void EnsureServer()
|
||||
{
|
||||
if (_host is null)
|
||||
{
|
||||
using var _ = CreateDefaultClient();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
_host?.Dispose();
|
||||
}
|
||||
|
||||
public string ServerAddress
|
||||
{
|
||||
get
|
||||
{
|
||||
EnsureServer();
|
||||
return ClientOptions.BaseAddress.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
protected override IHost CreateHost(IHostBuilder builder)
|
||||
{
|
||||
var testHost = base.CreateHost(builder);
|
||||
builder.ConfigureWebHost(webHostBuilder => webHostBuilder.UseKestrel());
|
||||
|
||||
var kestrelHost = builder.Build();
|
||||
kestrelHost.Start();
|
||||
|
||||
return new CompositeHost(testHost, kestrelHost);
|
||||
var testHost = builder
|
||||
.ConfigureWebHost(webHostBuilder =>
|
||||
{
|
||||
webHostBuilder.ConfigureLogging(config => config.ClearProviders());
|
||||
webHostBuilder.ConfigureTestServices(services =>
|
||||
{
|
||||
var postsDirectory = Path.Combine(Directory.GetCurrentDirectory(), "TestPosts");
|
||||
var postServiceOptions = new FilePostServiceOptions() { PostsDirectory = postsDirectory };
|
||||
services.AddSingleton(Options.Create(postServiceOptions));
|
||||
});
|
||||
})
|
||||
.Build();
|
||||
|
||||
builder.ConfigureWebHost(
|
||||
webHostBuilder => webHostBuilder.UseKestrel(
|
||||
o => o.Listen(IPAddress.Loopback, 0)
|
||||
)
|
||||
);
|
||||
|
||||
_host = builder.Build();
|
||||
_host.Start();
|
||||
|
||||
var server = _host.Services.GetRequiredService<IServer>();
|
||||
var addresses = server.Features.GetRequiredFeature<IServerAddressesFeature>();
|
||||
|
||||
ClientOptions.BaseAddress = addresses.Addresses
|
||||
.Select(x => new Uri(x))
|
||||
.Last();
|
||||
|
||||
testHost.Start();
|
||||
return testHost;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using Microsoft.AspNetCore.Hosting.Server.Features;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
|
||||
namespace Blog.Tests.EndToEnd;
|
||||
|
||||
[TestFixture]
|
||||
public class BlogTest : PageTest
|
||||
{
|
||||
private readonly BlogHostFactory<Program> _factory = new();
|
||||
|
||||
public override BrowserNewContextOptions ContextOptions()
|
||||
{
|
||||
var options = base.ContextOptions();
|
||||
options.BaseURL = _factory.ServerAddress;
|
||||
return options;
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public async Task Setup()
|
||||
{
|
||||
await Context.Tracing.StartAsync(new()
|
||||
{
|
||||
Title = TestContext.CurrentContext.Test.ClassName + "." + TestContext.CurrentContext.Test.Name,
|
||||
Screenshots = true,
|
||||
Snapshots = true,
|
||||
Sources = true
|
||||
});
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public async Task TearDown()
|
||||
{
|
||||
await Context.Tracing.StopAsync(new()
|
||||
{
|
||||
Path = Path.Combine(
|
||||
TestContext.CurrentContext.WorkDirectory,
|
||||
"playwright-traces",
|
||||
$"{TestContext.CurrentContext.Test.ClassName}.{TestContext.CurrentContext.Test.Name}.zip"
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
namespace Blog.Tests.EndToEnd;
|
||||
|
||||
[TestFixture]
|
||||
public class HomeTests : BlogTest
|
||||
{
|
||||
[Test]
|
||||
public async Task Home_WhenNavigatedTo_ItShouldDisplayCorrectPageTitle()
|
||||
{
|
||||
await Page.GotoAsync("/");
|
||||
var title = await Page.TitleAsync();
|
||||
title.Should().Be("journal - A blog by Stevan Freeborn");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Home_WhenNavigatedTo_ItShouldCorrectSiteTitle()
|
||||
{
|
||||
await Page.GotoAsync("/");
|
||||
var message = Page.GetByRole(AriaRole.Heading, new() { Name = "journal" });
|
||||
await Expect(message).ToBeVisibleAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Home_WhenNavigatedTo_ItShouldDisplayWrittenByAttribution()
|
||||
{
|
||||
await Page.GotoAsync("/");
|
||||
var message = Page.GetByText("by");
|
||||
var image = Page.GetByAltText("Stevan Freeborn");
|
||||
|
||||
await Expect(message).ToBeVisibleAsync();
|
||||
await Expect(image).ToBeVisibleAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Home_WhenNavigatedTo_ItShouldDisplayPostFeed()
|
||||
{
|
||||
await Page.GotoAsync("/");
|
||||
var feed = Page.GetByRole(AriaRole.Feed);
|
||||
await Expect(feed).ToBeVisibleAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Home_WhenNavigatedTo_ItShouldDisplayPosts()
|
||||
{
|
||||
await Page.GotoAsync("/");
|
||||
var posts = Page.GetByRole(AriaRole.Article);
|
||||
await Expect(posts).ToHaveCountAsync(2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
# Test Blog
|
||||
|
||||
```json meta
|
||||
{
|
||||
"title": "Another Test Blog",
|
||||
"lead": "This is a test blog post",
|
||||
"isPublished": true,
|
||||
"publishedAt": "2020-01-01"
|
||||
}
|
||||
```
|
||||
|
||||
Welcome to my Markdown blog post! In this post, I'll cover various Markdown elements to help you test styling.
|
||||
|
||||
## Text Formatting
|
||||
|
||||
Here are some examples of text formatting:
|
||||
|
||||
- *Italic Text*
|
||||
- **Bold Text**
|
||||
- ***Bold Italic Text***
|
||||
- ~~Strikethrough Text~~
|
||||
|
||||
## Lists
|
||||
|
||||
### Unordered List
|
||||
|
||||
- Item 1
|
||||
- Item 2
|
||||
- Subitem A
|
||||
- Subitem B
|
||||
- Item 3
|
||||
|
||||
### Ordered List
|
||||
|
||||
1. First item
|
||||
2. Second item
|
||||
3. Third item
|
||||
|
||||
## Links and Images
|
||||
|
||||
### Link
|
||||
|
||||
[OpenAI](https://openai.com) - An amazing AI research organization.
|
||||
|
||||
### Image
|
||||
|
||||

|
||||
|
||||
## Blockquotes
|
||||
|
||||
> Markdown is a lightweight markup language with plain-text formatting syntax.
|
||||
|
||||
## Code Blocks
|
||||
|
||||
```python
|
||||
def greet(name):
|
||||
print(f"Hello, {name}!")
|
||||
|
||||
greet("World")
|
||||
```
|
||||
|
||||
## Tables
|
||||
|
||||
| Name | Age |
|
||||
|-------|-----|
|
||||
| Alice | 30 |
|
||||
| Bob | 25 |
|
||||
| Carol | 35 |
|
||||
|
||||
## Horizontal Rule
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
That's it for this Markdown blog post! Feel free to experiment with styling these elements further.
|
||||
@@ -0,0 +1,76 @@
|
||||
# Test Blog
|
||||
|
||||
```json meta
|
||||
{
|
||||
"title": "Test Blog",
|
||||
"lead": "This is a test blog post.",
|
||||
"isPublished": true,
|
||||
"publishedAt": "2021-01-01"
|
||||
}
|
||||
```
|
||||
|
||||
Welcome to my Markdown blog post! In this post, I'll cover various Markdown elements to help you test styling.
|
||||
|
||||
## Text Formatting
|
||||
|
||||
Here are some examples of text formatting:
|
||||
|
||||
- *Italic Text*
|
||||
- **Bold Text**
|
||||
- ***Bold Italic Text***
|
||||
- ~~Strikethrough Text~~
|
||||
|
||||
## Lists
|
||||
|
||||
### Unordered List
|
||||
|
||||
- Item 1
|
||||
- Item 2
|
||||
- Subitem A
|
||||
- Subitem B
|
||||
- Item 3
|
||||
|
||||
### Ordered List
|
||||
|
||||
1. First item
|
||||
2. Second item
|
||||
3. Third item
|
||||
|
||||
## Links and Images
|
||||
|
||||
### Link
|
||||
|
||||
[OpenAI](https://openai.com) - An amazing AI research organization.
|
||||
|
||||
### Image
|
||||
|
||||

|
||||
|
||||
## Blockquotes
|
||||
|
||||
> Markdown is a lightweight markup language with plain-text formatting syntax.
|
||||
|
||||
## Code Blocks
|
||||
|
||||
```python
|
||||
def greet(name):
|
||||
print(f"Hello, {name}!")
|
||||
|
||||
greet("World")
|
||||
```
|
||||
|
||||
## Tables
|
||||
|
||||
| Name | Age |
|
||||
|-------|-----|
|
||||
| Alice | 30 |
|
||||
| Bob | 25 |
|
||||
| Carol | 35 |
|
||||
|
||||
## Horizontal Rule
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
That's it for this Markdown blog post! Feel free to experiment with styling these elements further.
|
||||
@@ -1,6 +1,20 @@
|
||||
global using NUnit.Framework;
|
||||
global using Microsoft.AspNetCore.Mvc.Testing;
|
||||
global using Microsoft.Extensions.Hosting;
|
||||
global using System.IO.Abstractions;
|
||||
global using System.Net;
|
||||
|
||||
global using Blog.Posts;
|
||||
|
||||
global using FluentAssertions;
|
||||
|
||||
global using Microsoft.AspNetCore.Hosting;
|
||||
global using Microsoft.AspNetCore.Hosting.Server;
|
||||
global using Microsoft.AspNetCore.Mvc.Testing;
|
||||
global using Microsoft.Extensions.DependencyInjection;
|
||||
global using Microsoft.Extensions.Hosting;
|
||||
global using Microsoft.Extensions.Options;
|
||||
global using Microsoft.Playwright;
|
||||
global using Microsoft.Playwright.NUnit;
|
||||
global using Microsoft.Playwright;
|
||||
global using Microsoft.Extensions.Logging;
|
||||
|
||||
global using Moq;
|
||||
|
||||
global using NUnit.Framework;
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
@inherits Bunit.TestContext
|
||||
|
||||
@code {
|
||||
private Mock<IPostService> _postServiceMock = new();
|
||||
|
||||
[Test]
|
||||
public void Feed_WhenRendedAndNoPosts_ItShouldRenderNoPostsMessage()
|
||||
{
|
||||
_postServiceMock
|
||||
.Setup(x => x.GetPostsAsync())
|
||||
.ReturnsAsync(new List<Post>());
|
||||
|
||||
var ctx = new TestContext();
|
||||
|
||||
ctx.Services.AddSingleton(_postServiceMock.Object);
|
||||
|
||||
var cut = ctx.Render(@<Feed />);
|
||||
|
||||
cut.Find("p").MarkupMatches("<p>Looks like writers block. Check back later.</p>");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Feed_WhenRendedAndPosts_ItShouldRenderPosts()
|
||||
{
|
||||
List<Post> posts = [
|
||||
new Post() { Title = "Post 1", },
|
||||
new Post() { Title = "Post 2", },
|
||||
];
|
||||
|
||||
_postServiceMock
|
||||
.Setup(x => x.GetPostsAsync())
|
||||
.ReturnsAsync(posts);
|
||||
|
||||
var ctx = new TestContext();
|
||||
|
||||
ctx.Services.AddScoped(_ => _postServiceMock.Object);
|
||||
|
||||
var cut = ctx.Render(@<Feed />);
|
||||
|
||||
var articles = cut.FindAll("article");
|
||||
|
||||
articles.Count.Should().Be(2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,423 @@
|
||||
namespace Blog.Tests.Unit;
|
||||
|
||||
[TestFixture]
|
||||
class FilePostServiceTests
|
||||
{
|
||||
private readonly Mock<IFileSystem> _mockFileSystem = new();
|
||||
private readonly Mock<IOptions<FilePostServiceOptions>> _mockOptions = new();
|
||||
private readonly Mock<ILogger<FilePostService>> _mockLogger = new();
|
||||
private readonly FilePostService _sut;
|
||||
|
||||
public FilePostServiceTests()
|
||||
{
|
||||
_mockOptions
|
||||
.Setup(x => x.Value)
|
||||
.Returns(new FilePostServiceOptions { PostsDirectory = "posts" });
|
||||
|
||||
_sut = new FilePostService(
|
||||
_mockOptions.Object,
|
||||
_mockFileSystem.Object,
|
||||
_mockLogger.Object
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetPostsAsync_WhenCalledAndPostsDirectoryDoesNotExist_ItShouldReturnEmptyList()
|
||||
{
|
||||
_mockFileSystem
|
||||
.Setup(x => x.Directory.Exists(It.IsAny<string>()))
|
||||
.Returns(false);
|
||||
|
||||
var result = await _sut.GetPostsAsync();
|
||||
|
||||
result.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetPostsAsync_WhenCalledAndPostsDirectoryContainsNoSubDirectories_ItShouldReturnEmptyList()
|
||||
{
|
||||
_mockFileSystem
|
||||
.Setup(x => x.Directory.Exists(It.IsAny<string>()))
|
||||
.Returns(true);
|
||||
|
||||
_mockFileSystem
|
||||
.Setup(x => x.Directory.GetDirectories(It.IsAny<string>()))
|
||||
.Returns([]);
|
||||
|
||||
var result = await _sut.GetPostsAsync();
|
||||
|
||||
result.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetPostsAsync_WhenCalledAndPostsDirectoryContainsSubDirectoryWithNoIndexFile_ItShouldReturnEmptyList()
|
||||
{
|
||||
_mockFileSystem
|
||||
.Setup(x => x.Directory.Exists(It.IsAny<string>()))
|
||||
.Returns(true);
|
||||
|
||||
_mockFileSystem
|
||||
.Setup(x => x.Directory.GetDirectories(It.IsAny<string>()))
|
||||
.Returns(["subdirectory"]);
|
||||
|
||||
_mockFileSystem
|
||||
.Setup(x => x.Path.Combine(It.IsAny<string>(), It.IsAny<string>()))
|
||||
.Returns("subdirectory/INDEX.md");
|
||||
|
||||
_mockFileSystem
|
||||
.Setup(x => x.File.Exists(It.IsAny<string>()))
|
||||
.Returns(false);
|
||||
|
||||
var result = await _sut.GetPostsAsync();
|
||||
|
||||
result.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetPostsAsync_WhenCalledAndPostIndexFileIsEmpty_ItShouldReturnEmptyList()
|
||||
{
|
||||
_mockFileSystem
|
||||
.Setup(x => x.Directory.Exists(It.IsAny<string>()))
|
||||
.Returns(true);
|
||||
|
||||
_mockFileSystem
|
||||
.Setup(x => x.Directory.GetDirectories(It.IsAny<string>()))
|
||||
.Returns(["subdirectory"]);
|
||||
|
||||
_mockFileSystem
|
||||
.Setup(x => x.Path.Combine(It.IsAny<string>(), It.IsAny<string>()))
|
||||
.Returns("subdirectory/INDEX.md");
|
||||
|
||||
_mockFileSystem
|
||||
.Setup(x => x.File.Exists(It.IsAny<string>()))
|
||||
.Returns(true);
|
||||
|
||||
_mockFileSystem
|
||||
.Setup(x => x.File.ReadAllTextAsync(It.IsAny<string>(), default))
|
||||
.ReturnsAsync(string.Empty);
|
||||
|
||||
var result = await _sut.GetPostsAsync();
|
||||
|
||||
result.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetPostsAsync_WhenCalledAndPostIndexFileContainsNoMetadata_ItShouldReturnEmptyList()
|
||||
{
|
||||
_mockFileSystem
|
||||
.Setup(x => x.Directory.Exists(It.IsAny<string>()))
|
||||
.Returns(true);
|
||||
|
||||
_mockFileSystem
|
||||
.Setup(x => x.Directory.GetDirectories(It.IsAny<string>()))
|
||||
.Returns(["subdirectory"]);
|
||||
|
||||
_mockFileSystem
|
||||
.Setup(x => x.Path.Combine(It.IsAny<string>(), It.IsAny<string>()))
|
||||
.Returns("subdirectory/INDEX.md");
|
||||
|
||||
_mockFileSystem
|
||||
.Setup(x => x.File.Exists(It.IsAny<string>()))
|
||||
.Returns(true);
|
||||
|
||||
_mockFileSystem
|
||||
.Setup(x => x.File.ReadAllTextAsync(It.IsAny<string>(), default))
|
||||
.ReturnsAsync(
|
||||
"""
|
||||
# Post Title
|
||||
|
||||
Post content
|
||||
"""
|
||||
);
|
||||
|
||||
var result = await _sut.GetPostsAsync();
|
||||
|
||||
result.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetPostsAsync_WhenCalledAndPostIndexFileOnlyContainsMetadata_ItShouldReturnEmptyList()
|
||||
{
|
||||
_mockFileSystem
|
||||
.Setup(x => x.Directory.Exists(It.IsAny<string>()))
|
||||
.Returns(true);
|
||||
|
||||
_mockFileSystem
|
||||
.Setup(x => x.Directory.GetDirectories(It.IsAny<string>()))
|
||||
.Returns(["subdirectory"]);
|
||||
|
||||
_mockFileSystem
|
||||
.Setup(x => x.Path.Combine(It.IsAny<string>(), It.IsAny<string>()))
|
||||
.Returns("subdirectory/INDEX.md");
|
||||
|
||||
_mockFileSystem
|
||||
.Setup(x => x.File.Exists(It.IsAny<string>()))
|
||||
.Returns(true);
|
||||
|
||||
_mockFileSystem
|
||||
.Setup(x => x.File.ReadAllTextAsync(It.IsAny<string>(), default))
|
||||
.ReturnsAsync(
|
||||
"""
|
||||
```json meta
|
||||
{
|
||||
"title": "Post Title",
|
||||
"date": "2021-01-01"
|
||||
}
|
||||
```
|
||||
"""
|
||||
);
|
||||
|
||||
var result = await _sut.GetPostsAsync();
|
||||
|
||||
result.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetPostsAsync_WhenCalledAndPostIndexFileHasInvalidMetadata_ItShouldReturnEmptyList()
|
||||
{
|
||||
_mockFileSystem
|
||||
.Setup(x => x.Directory.Exists(It.IsAny<string>()))
|
||||
.Returns(true);
|
||||
|
||||
_mockFileSystem
|
||||
.Setup(x => x.Directory.GetDirectories(It.IsAny<string>()))
|
||||
.Returns(["subdirectory"]);
|
||||
|
||||
_mockFileSystem
|
||||
.Setup(x => x.Path.Combine(It.IsAny<string>(), It.IsAny<string>()))
|
||||
.Returns("subdirectory/INDEX.md");
|
||||
|
||||
_mockFileSystem
|
||||
.Setup(x => x.File.Exists(It.IsAny<string>()))
|
||||
.Returns(true);
|
||||
|
||||
_mockFileSystem
|
||||
.Setup(x => x.File.ReadAllTextAsync(It.IsAny<string>(), default))
|
||||
.ReturnsAsync(
|
||||
"""
|
||||
# Post Title
|
||||
|
||||
```json meta
|
||||
{
|
||||
"title": "Post Title",
|
||||
"isPublished": "true",
|
||||
"date": "2021-01-01"
|
||||
}
|
||||
```
|
||||
|
||||
Post content
|
||||
"""
|
||||
);
|
||||
|
||||
var result = await _sut.GetPostsAsync();
|
||||
|
||||
result.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetPostsAsync_WhenCalledAndPostIndexFileIsNotPublished_ItShouldReturnEmptyList()
|
||||
{
|
||||
_mockFileSystem
|
||||
.Setup(x => x.Directory.Exists(It.IsAny<string>()))
|
||||
.Returns(true);
|
||||
|
||||
_mockFileSystem
|
||||
.Setup(x => x.Directory.GetDirectories(It.IsAny<string>()))
|
||||
.Returns(["subdirectory"]);
|
||||
|
||||
_mockFileSystem
|
||||
.Setup(x => x.Path.Combine(It.IsAny<string>(), It.IsAny<string>()))
|
||||
.Returns("subdirectory/INDEX.md");
|
||||
|
||||
_mockFileSystem
|
||||
.Setup(x => x.File.Exists(It.IsAny<string>()))
|
||||
.Returns(true);
|
||||
|
||||
_mockFileSystem
|
||||
.Setup(x => x.File.ReadAllTextAsync(It.IsAny<string>(), default))
|
||||
.ReturnsAsync(
|
||||
"""
|
||||
# Post Title
|
||||
|
||||
```json meta
|
||||
{
|
||||
"title": "Post Title",
|
||||
"lead": "Post lead",
|
||||
"isPublished": false,
|
||||
"publishedAt": "2021-01-01"
|
||||
}
|
||||
```
|
||||
|
||||
Post content
|
||||
"""
|
||||
);
|
||||
|
||||
var result = await _sut.GetPostsAsync();
|
||||
|
||||
result.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetPostsAsync_WhenCalledAndPostIndexFileContainsMetadataAndContent_ItShouldReturnPosts()
|
||||
{
|
||||
_mockFileSystem
|
||||
.Setup(x => x.Directory.Exists(It.IsAny<string>()))
|
||||
.Returns(true);
|
||||
|
||||
_mockFileSystem
|
||||
.Setup(x => x.Directory.GetDirectories(It.IsAny<string>()))
|
||||
.Returns(["subdirectory"]);
|
||||
|
||||
_mockFileSystem
|
||||
.Setup(x => x.Path.Combine(It.IsAny<string>(), It.IsAny<string>()))
|
||||
.Returns("subdirectory/INDEX.md");
|
||||
|
||||
_mockFileSystem
|
||||
.Setup(x => x.File.Exists(It.IsAny<string>()))
|
||||
.Returns(true);
|
||||
|
||||
_mockFileSystem
|
||||
.Setup(x => x.File.ReadAllTextAsync(It.IsAny<string>(), default))
|
||||
.ReturnsAsync(
|
||||
"""
|
||||
# Post Title
|
||||
|
||||
```json meta
|
||||
{
|
||||
"title": "Post Title",
|
||||
"lead": "Post lead",
|
||||
"isPublished": true,
|
||||
"publishedAt": "2021-01-01",
|
||||
"slug": "subdirectory"
|
||||
}
|
||||
```
|
||||
|
||||
Post content
|
||||
"""
|
||||
);
|
||||
|
||||
_mockFileSystem
|
||||
.Setup(x => x.Path.GetFileName(It.IsAny<string>()))
|
||||
.Returns("subdirectory");
|
||||
|
||||
var result = await _sut.GetPostsAsync();
|
||||
|
||||
result.Should().NotBeEmpty();
|
||||
result.Should().HaveCount(1);
|
||||
result.Should().BeEquivalentTo([
|
||||
new Post
|
||||
{
|
||||
Title = "Post Title",
|
||||
Lead = "Post lead",
|
||||
IsPublished = true,
|
||||
PublishedAt = new DateTime(2021, 1, 1),
|
||||
Slug = "subdirectory",
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetPostsAsync_WhenCalled_ItShouldValidPostsInDescendingOrder()
|
||||
{
|
||||
_mockFileSystem
|
||||
.Setup(x => x.Directory.Exists(It.IsAny<string>()))
|
||||
.Returns(true);
|
||||
|
||||
_mockFileSystem
|
||||
.Setup(x => x.Directory.GetDirectories(It.IsAny<string>()))
|
||||
.Returns([
|
||||
"valid-blog-post",
|
||||
"another-valid-blog-post",
|
||||
"invalid-blog-post"
|
||||
]);
|
||||
|
||||
_mockFileSystem
|
||||
.SetupSequence(x => x.Path.Combine(It.IsAny<string>(), It.IsAny<string>()))
|
||||
.Returns("valid-blog-post/INDEX.md")
|
||||
.Returns("another-valid-blog-post/INDEX.md")
|
||||
.Returns("invalid-blog-post/INDEX.md");
|
||||
|
||||
_mockFileSystem
|
||||
.Setup(x => x.File.Exists(It.IsAny<string>()))
|
||||
.Returns(true);
|
||||
|
||||
_mockFileSystem
|
||||
.SetupSequence(x => x.File.ReadAllTextAsync(It.IsAny<string>(), default))
|
||||
.ReturnsAsync(
|
||||
"""
|
||||
# Valid Blog Post
|
||||
|
||||
```json meta
|
||||
{
|
||||
"title": "Valid Blog Post",
|
||||
"lead": "Valid post lead",
|
||||
"isPublished": true,
|
||||
"publishedAt": "2021-01-01",
|
||||
"slug": "valid-blog-post"
|
||||
}
|
||||
```
|
||||
|
||||
Post content
|
||||
"""
|
||||
)
|
||||
.ReturnsAsync(
|
||||
"""
|
||||
# Another Valid Blog Post
|
||||
|
||||
```json meta
|
||||
{
|
||||
"title": "Another Valid Blog Post",
|
||||
"lead": "Another valid post lead",
|
||||
"isPublished": true,
|
||||
"publishedAt": "2022-01-02",
|
||||
"slug": "another-valid-blog-post"
|
||||
}
|
||||
```
|
||||
|
||||
Post content
|
||||
"""
|
||||
)
|
||||
.ReturnsAsync(
|
||||
"""
|
||||
```json meta
|
||||
{
|
||||
"title": "Invalid Blog Post",
|
||||
"lead": "Invalid post lead",
|
||||
"isPublished": false,
|
||||
"publishedAt": "2023-01-03",
|
||||
"slug": "invalid-blog-post"
|
||||
}
|
||||
```
|
||||
"""
|
||||
);
|
||||
|
||||
_mockFileSystem
|
||||
.SetupSequence(x => x.Path.GetFileName(It.IsAny<string>()))
|
||||
.Returns("valid-blog-post")
|
||||
.Returns("another-valid-blog-post")
|
||||
.Returns("invalid-blog-post");
|
||||
|
||||
var result = await _sut.GetPostsAsync();
|
||||
|
||||
result.Should().NotBeEmpty();
|
||||
result.Should().HaveCount(2);
|
||||
result.Should().BeEquivalentTo([
|
||||
new Post
|
||||
{
|
||||
Title = "Another Valid Blog Post",
|
||||
Lead = "Another valid post lead",
|
||||
IsPublished = true,
|
||||
PublishedAt = new DateTime(2022, 1, 2),
|
||||
Slug = "another-valid-blog-post",
|
||||
},
|
||||
new Post
|
||||
{
|
||||
Title = "Valid Blog Post",
|
||||
Lead = "Valid post lead",
|
||||
IsPublished = true,
|
||||
PublishedAt = new DateTime(2021, 1, 1),
|
||||
Slug = "valid-blog-post",
|
||||
}
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
@inherits Bunit.TestContext
|
||||
|
||||
@code
|
||||
{
|
||||
[Test]
|
||||
public void Header_WhenRendered_ItShouldContainSiteTitle()
|
||||
{
|
||||
var cut = Render(@<Header />);
|
||||
|
||||
var heading = cut.Find("h1");
|
||||
|
||||
heading.MarkupMatches(@<h1>journal</h1>);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Header_WhenRendered_ItShouldContainAuthorInfo()
|
||||
{
|
||||
var cut = Render(@<Header />);
|
||||
|
||||
var author = cut.Find(".author-container > span");
|
||||
|
||||
author.MarkupMatches(@<span>by</span>);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Header_WhenRendered_ItShouldContainAuthorImage()
|
||||
{
|
||||
var cut = Render(@<Header />);
|
||||
var image = cut.Find("img[alt='Stevan Freeborn']");
|
||||
image.Should().NotBeNull();
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
namespace Blog.Tests;
|
||||
|
||||
public class Tests
|
||||
{
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test1()
|
||||
{
|
||||
Assert.Pass();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@using Bunit
|
||||
@using Blog.Components
|
||||
@using Blog.Components.Layout
|
||||
@using FluentAssertions
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RunSettings>
|
||||
<Playwright>
|
||||
<BrowserName>chromium</BrowserName>
|
||||
<ExpectTimeout>10000</ExpectTimeout>
|
||||
<LaunchOptions>
|
||||
<Headless>true</Headless>
|
||||
<Channel>msedge</Channel>
|
||||
</LaunchOptions>
|
||||
</Playwright>
|
||||
</RunSettings>
|
||||
Reference in New Issue
Block a user