fix: correct metadata #4
No files matched your search
@@ -3,7 +3,7 @@
|
|||||||
"title": "Building a Chunked, Resumable File Upload System in .NET and Vanilla JS",
|
"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.",
|
"lead": "How to bypass reverse proxy payload limits, eliminate double-storage disk overhead, and build a resumable file uploader.",
|
||||||
"isPublished": true,
|
"isPublished": true,
|
||||||
n"publishedAt": "2026-09-03",
|
"publishedAt": "2026-09-03",
|
||||||
"openGraphImage": "posts/chunked-resumable-file-uploads-dotnet/og-image.png"
|
"openGraphImage": "posts/chunked-resumable-file-uploads-dotnet/og-image.png"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -2,62 +2,28 @@ namespace Blog.Tests.EndToEnd;
|
|||||||
|
|
||||||
public class BlogHostFactory<TProgram> : WebApplicationFactory<TProgram> where TProgram : class
|
public class BlogHostFactory<TProgram> : WebApplicationFactory<TProgram> where TProgram : class
|
||||||
{
|
{
|
||||||
private IHost? _host;
|
public BlogHostFactory()
|
||||||
|
|
||||||
private void EnsureServer()
|
|
||||||
{
|
{
|
||||||
if (_host is null)
|
UseKestrel(server => server.Listen(IPAddress.Loopback, 0));
|
||||||
{
|
|
||||||
using var _ = CreateDefaultClient();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
|
||||||
{
|
|
||||||
_host?.Dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public string ServerAddress
|
public string ServerAddress
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
EnsureServer();
|
StartServer();
|
||||||
return ClientOptions.BaseAddress.ToString();
|
return ClientOptions.BaseAddress.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override IHost CreateHost(IHostBuilder builder)
|
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||||
{
|
{
|
||||||
var testHost = builder
|
builder.ConfigureLogging(config => config.ClearProviders());
|
||||||
.ConfigureWebHost(webHostBuilder =>
|
builder.ConfigureTestServices(services =>
|
||||||
{
|
|
||||||
webHostBuilder.ConfigureLogging(config => config.ClearProviders());
|
|
||||||
webHostBuilder.ConfigureTestServices(services =>
|
|
||||||
{
|
{
|
||||||
var postsDirectory = Path.Combine(Directory.GetCurrentDirectory(), "TestPosts");
|
var postsDirectory = Path.Combine(Directory.GetCurrentDirectory(), "TestPosts");
|
||||||
var postServiceOptions = new FilePostServiceOptions() { PostsDirectory = postsDirectory };
|
var postServiceOptions = new FilePostServiceOptions() { PostsDirectory = postsDirectory };
|
||||||
services.AddSingleton(Options.Create(postServiceOptions));
|
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,12 +3,12 @@ namespace Blog.Tests.EndToEnd;
|
|||||||
[TestFixture]
|
[TestFixture]
|
||||||
public abstract class BlogTest : PageTest
|
public abstract class BlogTest : PageTest
|
||||||
{
|
{
|
||||||
private readonly BlogHostFactory<Program> _factory = new();
|
private static readonly BlogHostFactory<Program> Factory = new();
|
||||||
|
|
||||||
public override BrowserNewContextOptions ContextOptions()
|
public override BrowserNewContextOptions ContextOptions()
|
||||||
{
|
{
|
||||||
var options = base.ContextOptions();
|
var options = base.ContextOptions();
|
||||||
options.BaseURL = _factory.ServerAddress;
|
options.BaseURL = Factory.ServerAddress;
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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('\\', '/')}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user