feat: add individual post page (#4)
* chore: add markdown lint to ignore no h1 heading in markdown files * chore: update example settings * chore: update example development env file * fix: remove fixed 100% height on container * feat: add individual post page * chore: update README.md * feat: add links to headings in post and add copy button to code blocks * tests: add tests for get post async method * tests: remove heading for test blogs * fix: only add links to headings in markdown body * tests: add tests for post page * tests: add test to check for blog content * docs: add jsdoc comments * chore: fix incorrect env variable format in example * fix: don't render anything if blog post is null * fix: don't render anything until posts are fetched * chore: add docker compose file for prod
This commit is contained in:
Vendored
+5
-1
@@ -6,5 +6,9 @@
|
|||||||
"Markdig",
|
"Markdig",
|
||||||
"msedge"
|
"msedge"
|
||||||
],
|
],
|
||||||
"dotnet.unitTests.runSettingsPath": "./test/Blog.Tests/.runsettings"
|
"dotnet.unitTests.runSettingsPath": "./test/Blog.Tests/.runsettings",
|
||||||
|
"markdownlint.config": {
|
||||||
|
"default": true,
|
||||||
|
"MD041": false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
version: '3.8'
|
||||||
|
name: blog-production
|
||||||
|
services:
|
||||||
|
blog:
|
||||||
|
build:
|
||||||
|
target: production-stage
|
||||||
|
context: ./src/Blog/
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
ports:
|
||||||
|
- 8080
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
- FilePostServiceOptions__PostsDirectory=wwwroot/posts
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
.container {
|
.container {
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
@page "/{slug:nonfile}"
|
||||||
|
@implements IAsyncDisposable
|
||||||
|
@inject IPostService PostService
|
||||||
|
@inject NavigationManager NavigationManager
|
||||||
|
@inject IJSRuntime JSRuntime
|
||||||
|
@inject ILogger<Post> Logger
|
||||||
|
|
||||||
|
@if (BlogPost is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<PageTitle>@BlogPost.Title - journal</PageTitle>
|
||||||
|
|
||||||
|
<HeadContent>
|
||||||
|
<meta name="description" content="@BlogPost.Lead">
|
||||||
|
<link rel="stylesheet" href="markdown.css">
|
||||||
|
<link rel="stylesheet" href="prism.css">
|
||||||
|
<script src="prism.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/anchor-js/anchor.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/clipboard@2.0.11/dist/clipboard.min.js"></script>
|
||||||
|
</HeadContent>
|
||||||
|
|
||||||
|
<article>
|
||||||
|
<div class="heading-container">
|
||||||
|
<h2>@BlogPost.Title</h2>
|
||||||
|
<p>@BlogPost.PublishedAt.ToString("MMMM d, yyyy")</p>
|
||||||
|
</div>
|
||||||
|
<div class="markdown-body">
|
||||||
|
@((MarkupString)BlogPost.Content)
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter]
|
||||||
|
public string Slug { get; set; } = string.Empty;
|
||||||
|
private PostWithContent? BlogPost;
|
||||||
|
private IJSObjectReference? module;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
var post = await PostService.GetPostAsync(Slug);
|
||||||
|
|
||||||
|
if (post is null)
|
||||||
|
{
|
||||||
|
NavigationManager.NavigateTo("/Error/404");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
BlogPost = post;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (firstRender)
|
||||||
|
{
|
||||||
|
module = await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./Components/Pages/Post.razor.js");
|
||||||
|
await module.InvokeVoidAsync("addAnchors");
|
||||||
|
await module.InvokeVoidAsync("addClipboard");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async ValueTask IAsyncDisposable.DisposeAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (module is not null)
|
||||||
|
{
|
||||||
|
await module.DisposeAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is JSDisconnectedException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "Error disposing of the module");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
article {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2rem;
|
||||||
|
|
||||||
|
& .heading-container {
|
||||||
|
& h2 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
& p {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body {
|
||||||
|
& pre {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
& .copy-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--primary-white);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0.5rem;
|
||||||
|
transition-property: color;
|
||||||
|
transition-duration: 0.3s;
|
||||||
|
transition-timing-function: ease-in-out;
|
||||||
|
|
||||||
|
& svg {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Adds anchors to each heading in the post
|
||||||
|
* so that users can link directly to them.
|
||||||
|
*/
|
||||||
|
export function addAnchors() {
|
||||||
|
const selectors = [
|
||||||
|
'.markdown-body h2',
|
||||||
|
'.markdown-body h3',
|
||||||
|
'.markdown-body h4',
|
||||||
|
'.markdown-body h5',
|
||||||
|
'.markdown-body h6'
|
||||||
|
];
|
||||||
|
|
||||||
|
// relies on AnchorJS library
|
||||||
|
// being loaded in Post.razor
|
||||||
|
anchors.add(selectors.join(','));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Adds a button to each code block that
|
||||||
|
* allows users to copy the code to their clipboard.
|
||||||
|
*/
|
||||||
|
export function addClipboard() {
|
||||||
|
const codeBlocks = document.querySelectorAll('pre');
|
||||||
|
|
||||||
|
codeBlocks.forEach((block) => {
|
||||||
|
const button = document.createElement('button');
|
||||||
|
button.className = 'copy-button';
|
||||||
|
button.type = 'button';
|
||||||
|
button.innerHTML = `
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" fill="currentColor">
|
||||||
|
<path d="M384 336H192c-8.8 0-16-7.2-16-16V64c0-8.8 7.2-16 16-16l140.1 0L400 115.9V320c0 8.8-7.2 16-16 16zM192 384H384c35.3 0 64-28.7 64-64V115.9c0-12.7-5.1-24.9-14.1-33.9L366.1 14.1c-9-9-21.2-14.1-33.9-14.1H192c-35.3 0-64 28.7-64 64V320c0 35.3 28.7 64 64 64zM64 128c-35.3 0-64 28.7-64 64V448c0 35.3 28.7 64 64 64H256c35.3 0 64-28.7 64-64V416H272v32c0 8.8-7.2 16-16 16H64c-8.8 0-16-7.2-16-16V192c0-8.8 7.2-16 16-16H96V128H64z"/>
|
||||||
|
</svg>
|
||||||
|
`
|
||||||
|
button.onclick = () => {
|
||||||
|
button.style.color = 'green';
|
||||||
|
setTimeout(() => {
|
||||||
|
button.style.color = '';
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const buttonText = document.createElement('span');
|
||||||
|
buttonText.className = 'sr-only';
|
||||||
|
buttonText.textContent = 'Copy';
|
||||||
|
|
||||||
|
button.appendChild(buttonText);
|
||||||
|
block.appendChild(button);
|
||||||
|
});
|
||||||
|
|
||||||
|
// relies on ClipboardJS library
|
||||||
|
// being loaded in Post.razor
|
||||||
|
var clipboard = new ClipboardJS('.copy-button', {
|
||||||
|
target: (trigger) => {
|
||||||
|
return trigger.previousElementSibling;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
clipboard.on('success', function(e) {
|
||||||
|
e.clearSelection();
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -2,7 +2,12 @@
|
|||||||
@inject IPostService postService
|
@inject IPostService postService
|
||||||
@inject IJSRuntime JS
|
@inject IJSRuntime JS
|
||||||
|
|
||||||
@if (posts.Count == 0)
|
|
||||||
|
@if (Posts is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (Posts.Count == 0)
|
||||||
{
|
{
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<p>Looks like writers block. Check back later.</p>
|
<p>Looks like writers block. Check back later.</p>
|
||||||
@@ -11,7 +16,7 @@
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
<section role="feed" class="container">
|
<section role="feed" class="container">
|
||||||
@foreach (var post in posts)
|
@foreach (var post in Posts)
|
||||||
{
|
{
|
||||||
<a href="/@post.Slug">
|
<a href="/@post.Slug">
|
||||||
<article>
|
<article>
|
||||||
@@ -25,10 +30,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
private List<Post> posts = [];
|
private List<Post>? Posts;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
posts = await postService.GetPostsAsync();
|
Posts = await postService.GetPostsAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -105,4 +105,46 @@ class FilePostService(
|
|||||||
|
|
||||||
return [.. posts.OrderByDescending(x => x.PublishedAt)];
|
return [.. posts.OrderByDescending(x => x.PublishedAt)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<PostWithContent?> GetPostAsync(string slug)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var postPath = _fileSystem.Path.Combine(_options.PostsDirectory, slug, IndexFile);
|
||||||
|
|
||||||
|
if (_fileSystem.File.Exists(postPath) is false)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var postText = await _fileSystem.File.ReadAllTextAsync(postPath);
|
||||||
|
|
||||||
|
var (metaContent, document) = ParsePost(postText);
|
||||||
|
|
||||||
|
if (metaContent is null || document.Count is 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var post = JsonSerializer.Deserialize<PostWithContent>(metaContent, JsonSerializerOptions);
|
||||||
|
|
||||||
|
if (post is null || post.IsPublished is false)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var postWithContent = post with
|
||||||
|
{
|
||||||
|
Slug = slug,
|
||||||
|
Content = document.ToHtml(MarkdownPipeline)
|
||||||
|
};
|
||||||
|
|
||||||
|
return postWithContent;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error while getting post {Slug}", slug);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,4 +3,5 @@ namespace Blog.Posts;
|
|||||||
interface IPostService
|
interface IPostService
|
||||||
{
|
{
|
||||||
Task<List<Post>> GetPostsAsync();
|
Task<List<Post>> GetPostsAsync();
|
||||||
|
Task<PostWithContent?> GetPostAsync(string slug);
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,12 @@
|
|||||||
{
|
{
|
||||||
"Urls": "https://localhost:4500;http://localhost:4501",
|
"Urls": "https://localhost:4500;http://localhost:4501",
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*",
|
||||||
"FilePostServiceOptions": {
|
"FilePostServiceOptions": {
|
||||||
"PostsDirectory": "wwwroot/posts"
|
"PostsDirectory": "wwwroot/posts"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
URLS=https://0.0.0.0:4500;http://0.0.0.0:4501
|
URLS=https://0.0.0.0:4500;http://0.0.0.0:4501
|
||||||
|
FilePostServiceOptions__PostsDirectory=wwwroot/posts
|
||||||
|
|
||||||
# Path needs to match the path where the cert file
|
# Path needs to match the path where the cert file
|
||||||
# is mounted in the container
|
# is mounted in the container
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,76 @@
|
|||||||
|
```json meta
|
||||||
|
{
|
||||||
|
"title": "Test Blog",
|
||||||
|
"lead": "This is a test blog post.",
|
||||||
|
"isPublished": true,
|
||||||
|
"publishedAt": "2024-04-10"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Welcome to my Markdown blog post! In this post, I'll cover various Markdown elements to help you test styling.
|
||||||
|
|
||||||
|
## Text Formatting
|
||||||
|
|
||||||
|
This is a ""citation of someone""
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -29,7 +29,7 @@ pre[class*=language-] {
|
|||||||
|
|
||||||
:not(pre)>code[class*=language-],
|
:not(pre)>code[class*=language-],
|
||||||
pre[class*=language-] {
|
pre[class*=language-] {
|
||||||
background: #2d2d2d
|
background: var(--secondary-black);
|
||||||
}
|
}
|
||||||
|
|
||||||
:not(pre)>code[class*=language-] {
|
:not(pre)>code[class*=language-] {
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
namespace Blog.Tests.EndToEnd;
|
||||||
|
|
||||||
|
[TestFixture]
|
||||||
|
public class PostTests : BlogTest
|
||||||
|
{
|
||||||
|
private const string TestPostSlug = "test-blog";
|
||||||
|
private const string TestPostTitle = "Test Blog";
|
||||||
|
private const string TestPostDate = "January 1, 2021";
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Post_WhenNavigatedTo_ItShouldDisplayCorrectPageTitle()
|
||||||
|
{
|
||||||
|
await Page.GotoAsync($"/{TestPostSlug}");
|
||||||
|
var title = await Page.TitleAsync();
|
||||||
|
title.Should().Be($"{TestPostTitle} - journal");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Post_WhenNavigatedTo_ItShouldPostTitle()
|
||||||
|
{
|
||||||
|
await Page.GotoAsync($"/{TestPostSlug}");
|
||||||
|
var heading = Page.GetByRole(AriaRole.Heading, new() { Name = TestPostTitle });
|
||||||
|
await Expect(heading).ToBeVisibleAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Post_WhenNavigatedTo_ItShouldDisplayPostDate()
|
||||||
|
{
|
||||||
|
await Page.GotoAsync($"/{TestPostSlug}");
|
||||||
|
var date = Page.GetByText(TestPostDate);
|
||||||
|
await Expect(date).ToBeVisibleAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Post_WhenNavigatedTo_ItShouldDisplayContent()
|
||||||
|
{
|
||||||
|
await Page.GotoAsync($"/{TestPostSlug}");
|
||||||
|
var content = Page.GetByRole(AriaRole.Article);
|
||||||
|
await Expect(content).ToBeVisibleAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Post_WhenNavigatedToAndPostDoesNotExist_ItShouldDisplay404()
|
||||||
|
{
|
||||||
|
await Page.GotoAsync("/non-existent-post");
|
||||||
|
Page.Url.Should().Contain("/Error/404");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
# Test Blog
|
|
||||||
|
|
||||||
```json meta
|
```json meta
|
||||||
{
|
{
|
||||||
"title": "Another Test Blog",
|
"title": "Another Test Blog",
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
# Test Blog
|
|
||||||
|
|
||||||
```json meta
|
```json meta
|
||||||
{
|
{
|
||||||
"title": "Test Blog",
|
"title": "Test Blog",
|
||||||
|
|||||||
@@ -420,4 +420,197 @@ class FilePostServiceTests
|
|||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task GetPostAsync_WhenCalledAndSlugDoesNotExist_ItShouldReturnNull()
|
||||||
|
{
|
||||||
|
_mockFileSystem
|
||||||
|
.Setup(x => x.Directory.Exists(It.IsAny<string>()))
|
||||||
|
.Returns(false);
|
||||||
|
|
||||||
|
var result = await _sut.GetPostAsync("slug");
|
||||||
|
|
||||||
|
result.Should().BeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task GetPostAsync_WhenCalledAndPostExistsButContainsNoMetaData_ItShouldReturnNull()
|
||||||
|
{
|
||||||
|
_mockFileSystem
|
||||||
|
.Setup(x => x.Directory.Exists(It.IsAny<string>()))
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
_mockFileSystem
|
||||||
|
.Setup(x => x.Path.Combine(It.IsAny<string>(), It.IsAny<string>()))
|
||||||
|
.Returns("slug/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");
|
||||||
|
|
||||||
|
var result = await _sut.GetPostAsync("slug");
|
||||||
|
|
||||||
|
result.Should().BeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task GetPostAsync_WhenCalledAndPostContainsNoContent_ItShouldReturnNull()
|
||||||
|
{
|
||||||
|
_mockFileSystem
|
||||||
|
.Setup(x => x.Directory.Exists(It.IsAny<string>()))
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
_mockFileSystem
|
||||||
|
.Setup(x => x.Path.Combine(It.IsAny<string>(), It.IsAny<string>()))
|
||||||
|
.Returns("slug/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.GetPostAsync("slug");
|
||||||
|
|
||||||
|
result.Should().BeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task GetPostAsync_WhenCalledAndPostExistsButContainsInvalidMetaData_ItShouldReturnNull()
|
||||||
|
{
|
||||||
|
_mockFileSystem
|
||||||
|
.Setup(x => x.Directory.Exists(It.IsAny<string>()))
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
_mockFileSystem
|
||||||
|
.Setup(x => x.Path.Combine(It.IsAny<string>(), It.IsAny<string>()))
|
||||||
|
.Returns("slug/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"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
);
|
||||||
|
|
||||||
|
var result = await _sut.GetPostAsync("slug");
|
||||||
|
|
||||||
|
result.Should().BeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task GetPostAsync_WhenCalledAndPostExistsButIsNotPublished_ItShouldReturnNull()
|
||||||
|
{
|
||||||
|
_mockFileSystem
|
||||||
|
.Setup(x => x.Directory.Exists(It.IsAny<string>()))
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
_mockFileSystem
|
||||||
|
.Setup(x => x.Path.Combine(It.IsAny<string>(), It.IsAny<string>()))
|
||||||
|
.Returns("slug/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.GetPostAsync("slug");
|
||||||
|
|
||||||
|
result.Should().BeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task GetPostAsync_WhenCalledAndPostExists_ItShouldReturnPostWithContent()
|
||||||
|
{
|
||||||
|
_mockFileSystem
|
||||||
|
.Setup(x => x.Directory.Exists(It.IsAny<string>()))
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
_mockFileSystem
|
||||||
|
.Setup(x => x.Path.Combine(It.IsAny<string>(), It.IsAny<string>()))
|
||||||
|
.Returns("slug/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": "slug"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Post content
|
||||||
|
"""
|
||||||
|
);
|
||||||
|
|
||||||
|
var result = await _sut.GetPostAsync("slug");
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Should().BeEquivalentTo(new PostWithContent
|
||||||
|
{
|
||||||
|
Title = "Post Title",
|
||||||
|
Lead = "Post lead",
|
||||||
|
IsPublished = true,
|
||||||
|
PublishedAt = new DateTime(2021, 1, 1),
|
||||||
|
Slug = "slug",
|
||||||
|
Content = "<h1 id=\"post-title\">Post Title</h1>\n<p>Post content</p>\n",
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user