Merge pull request 'fix: correct metadata' (#4) from stevanfreeborn/fix/correct-metadata into main
Deploy / Build and push Docker image (push) Successful in 15m52s
Deploy / Deploy to server (push) Successful in 30s

This commit was merged in pull request #4.
This commit is contained in:
Stevan committed 2026-09-03 19:17:02 +00:00
commit 0300402667
5 files changed
+104 -71

No files matched your search

@@ -3,7 +3,7 @@
"title": "Building a Chunked, Resumable File Upload System in .NET and Vanilla JS",
"lead": "How to bypass reverse proxy payload limits, eliminate double-storage disk overhead, and build a resumable file uploader.",
"isPublished": true,
n"publishedAt": "2026-09-03",
"publishedAt": "2026-09-03",
"openGraphImage": "posts/chunked-resumable-file-uploads-dotnet/og-image.png"
}
```
+6 -40
View File
@@ -2,62 +2,28 @@ namespace Blog.Tests.EndToEnd;
public class BlogHostFactory<TProgram> : WebApplicationFactory<TProgram> where TProgram : class
{
private IHost? _host;
private void EnsureServer()
public BlogHostFactory()
{
if (_host is null)
{
using var _ = CreateDefaultClient();
}
}
protected override void Dispose(bool disposing)
{
_host?.Dispose();
UseKestrel(server => server.Listen(IPAddress.Loopback, 0));
}
public string ServerAddress
{
get
{
EnsureServer();
StartServer();
return ClientOptions.BaseAddress.ToString();
}
}
protected override IHost CreateHost(IHostBuilder builder)
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
var testHost = builder
.ConfigureWebHost(webHostBuilder =>
{
webHostBuilder.ConfigureLogging(config => config.ClearProviders());
webHostBuilder.ConfigureTestServices(services =>
builder.ConfigureLogging(config => config.ClearProviders());
builder.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;
}
}
+2 -2
View File
@@ -3,12 +3,12 @@ namespace Blog.Tests.EndToEnd;
[TestFixture]
public abstract class BlogTest : PageTest
{
private readonly BlogHostFactory<Program> _factory = new();
private static readonly BlogHostFactory<Program> Factory = new();
public override BrowserNewContextOptions ContextOptions()
{
var options = base.ContextOptions();
options.BaseURL = _factory.ServerAddress;
options.BaseURL = Factory.ServerAddress;
return options;
}
-28
View File
@@ -1,28 +0,0 @@
namespace Blog.Tests.EndToEnd;
public class CompositeHost(IHost testHost, IHost kestrelHost) : IHost
{
private readonly IHost _testHost = testHost;
private readonly IHost _kestrelHost = kestrelHost;
public IServiceProvider Services => _testHost.Services;
public void Dispose()
{
_testHost.Dispose();
_kestrelHost.Dispose();
GC.SuppressFinalize(this);
}
public async Task StartAsync(CancellationToken cancellationToken = default)
{
await _testHost.StartAsync(cancellationToken);
await _kestrelHost.StartAsync(cancellationToken);
}
public async Task StopAsync(CancellationToken cancellationToken = default)
{
await _testHost.StopAsync(cancellationToken);
await _kestrelHost.StopAsync(cancellationToken);
}
}
+95
View File
@@ -0,0 +1,95 @@
using System.Text.Json;
using Markdig;
using Markdig.Syntax;
namespace Blog.Tests.Unit;
[TestFixture]
public class PostMetadataTests
{
private static readonly MarkdownPipeline MarkdownPipeline = new MarkdownPipelineBuilder()
.UseAdvancedExtensions()
.Build();
private static readonly JsonSerializerOptions JsonSerializerOptions = new()
{
PropertyNameCaseInsensitive = true,
AllowTrailingCommas = true,
};
private const string MetaFence = "meta";
[Test]
public void PostsDirectory_ShouldContainIndexFiles()
{
var postsDir = GetPostsDirectory();
var files = Directory.GetFiles(postsDir, "index.md", SearchOption.AllDirectories);
files.Should().NotBeEmpty("The wwwroot/posts directory should contain blog posts with index.md files.");
}
[TestCaseSource(nameof(GetPostIndexFiles))]
public async Task IndexMd_Metadata_CanBeProperlyParsedAndRead(string filePath)
{
var markdown = await File.ReadAllTextAsync(filePath);
var document = Markdown.Parse(markdown, MarkdownPipeline);
var metadataBlock = document
.OfType<FencedCodeBlock>()
.FirstOrDefault(block =>
block.Arguments is not null &&
block.Arguments.Contains(MetaFence)
);
metadataBlock.Should().NotBeNull($"file '{filePath}' should contain a fenced code block with '{MetaFence}' argument.");
var jsonContent = metadataBlock.Lines.ToString();
jsonContent.Should().NotBeNullOrWhiteSpace($"metadata code block in '{filePath}' should not be empty.");
Post? post = null;
Action deserializeAction = () => post = JsonSerializer.Deserialize<Post>(jsonContent, JsonSerializerOptions);
deserializeAction.Should().NotThrow($"JSON metadata in '{filePath}' should be valid JSON matching the Post record schema.");
post.Should().NotBeNull($"deserialized Post from '{filePath}' should not be null.");
post.Title.Should().NotBeNullOrWhiteSpace($"post in '{filePath}' must have a title.");
post.Lead.Should().NotBeNullOrWhiteSpace($"post in '{filePath}' must have a lead summary.");
post.PublishedAt.Should().NotBe(default, $"post in '{filePath}' must have a valid published date.");
post.OpenGraphImage.Should().NotBeNullOrWhiteSpace($"post in '{filePath}' must have an openGraphImage path.");
}
private static string GetPostsDirectory()
{
var current = new DirectoryInfo(AppContext.BaseDirectory);
while (current is not null)
{
var candidate = Path.Combine(current.FullName, "src", "Blog", "wwwroot", "posts");
if (Directory.Exists(candidate))
{
return candidate;
}
current = current.Parent;
}
throw new DirectoryNotFoundException($"Could not locate 'src/Blog/wwwroot/posts' starting from '{AppContext.BaseDirectory}'.");
}
private static IEnumerable<TestCaseData> GetPostIndexFiles()
{
var postsDir = GetPostsDirectory();
var files = Directory.GetFiles(postsDir, "index.md", SearchOption.AllDirectories);
foreach (var file in files)
{
var relativePath = Path.GetRelativePath(postsDir, file);
yield return new TestCaseData(file).SetName($"PostMetadata_{relativePath.Replace('\\', '/')}");
}
}
}