Compare commits

...
9 Commits
Author SHA1 Message Date
Stevan 46ff22b250 Merge pull request 'posts: make it secure' (#6) from stevanfreeborn/posts/make-it-secure into main
Deploy / Build and push Docker image (push) Successful in 16m4s
Deploy / Deploy to server (push) Successful in 41s
2026-09-11 14:23:09 +00:00
Stevan Freeborn b6843919d8 posts: add make it secure post
Pull Request / Run tests (pull_request) Successful in 17m0s
2026-09-11 08:54:57 -05:00
Stevan Freeborn eaa5f4a965 posts: fix og image path
Deploy / Build and push Docker image (push) Successful in 13m58s
Deploy / Deploy to server (push) Successful in 28s
2026-09-06 17:10:37 -05:00
Stevan 2990c82181 Merge pull request 'posts: thoughts about typing code being over' (#5) from stevanfreeborn/posts/thoughts-about-typing-code-being-over into main
Deploy / Build and push Docker image (push) Successful in 18m43s
Deploy / Deploy to server (push) Successful in 56s
2026-09-06 21:44:25 +00:00
Stevan Freeborn e5d7a2b73a posts: add thoughts about typing code being over
Pull Request / Run tests (pull_request) Successful in 4m6s
2026-09-06 16:38:59 -05:00
Stevan 0300402667 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
2026-09-03 19:17:02 +00:00
Stevan Freeborn 81cc1e4f64 tests: fix blog host factory to use built in use kestrel call and dump composite host.
Pull Request / Run tests (pull_request) Successful in 2m3s
2026-09-03 14:13:02 -05:00
Stevan Freeborn 4a7c8b31c5 tests: add test that validates the metadata of all posts 2026-09-03 13:47:34 -05:00
Stevan Freeborn 49fe692ec7 fix: remove metadata issue 2026-09-03 13:34:28 -05:00
9 changed files with 151 additions and 80 deletions

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"
} }
``` ```
@@ -0,0 +1,19 @@
```json meta
{
"title": "\"Make it secure\"",
"lead": "At KCDC, a speaker shared their team's security strategy for AI-generated hardware and access controls: simply prompting the model to 'make it secure.' A reflection on where we are with AI tooling and the vanishing bar for verifiable verification.",
"isPublished": true,
"publishedAt": "2026-09-11",
"openGraphImage": "posts/make-it-secure/og-image.png"
}
```
I sat in on a talk yesterday at the Kansas City Developer Conference that was mostly about how a local small business owner was able to leverage AI tooling to advance his business outcomes.
One of the things he did was incorporate hardware into his business model, including facility access and monitoring-related devices. They developed both the hardware and software using agents and off-the-shelf components.
An audience member asked if he had any concerns about the security of what they were developing and deploying. The speaker's answer left me a bit stunned. I expected at least a qualified answer, or perhaps that they had red-teamed their work as part of the development process.
Instead, the real answer was that they just prompt the LLM to “make it secure” and run several reviews of the implementation across a couple of different models. Surprisingly, there wasn’t much reaction from the audience. I guess this is where we are now.
Honestly, I don’t really have any shade to throw at the presenter. It’s his company, and if he’s comfortable with the risk, more power to him. But I suppose I imagined more people would feel the way I do: wanting something verifiable and provable when it comes to assuring the quality, performance, and security of the platforms and products they build, with or without AI.
Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

@@ -0,0 +1,19 @@
```json meta
{
"title": "Thoughts On Typing Code Being Over",
"lead": "Reflecting on the impact of AI code generation on junior developers, the value of grinding through problems, and the future of software engineering careers.",
"isPublished": true,
"publishedAt": "2026-09-06",
"openGraphImage": "posts/thoughts-on-typing-code-being-over/og-image.png"
}
```
I have been ruminating on [this post](https://x.com/davidfowl/status/2095602923502010398) since first reading it. I understand it, and I can see the truth in it given how quickly you can generate code with an LLM. I won't lie: I still genuinely enjoy the experience of writing code by hand.
However, what gives me greater pause is the feeling that, to some extent, the tradeoff for an increasing reliance on LLMs is a decrease in my own knowledge and competence. So many of the people who inspire me, including David, didn't acquire their abilities by using AI. They spent two decades or more grinding away at problems. I am worried that in this new world, I will end up being robbed of those same development opportunities.
Maybe the truth is that these skills and knowledge won't matter moving forward. I don't quite see evidence of that yet; it seems like the people getting the most out of the LLM era are exactly these veteran folks. But if those skills eventually cease to matter, it raises the question: why waste any more time on this? Why not reinvest in some other field or career? That feels fatalistic, but it is also a reasonable path forward for someone like me who has at least another 19 years of mandatory work ahead.
I honestly don't know what the right answer is. I do think the conversation around this topic always needs to be bifurcated between what you do for fun and enjoyment, and what you do to put food on the table. The former is completely up to you, and all choices can be valid. The latter requires more thought and consideration, especially if you have other human beings relying on you for their economic well-being. You have to take into account the most important questions for a successful career: What am I good at? What do I want to spend my workdays doing? Does the world need that work? Will people pay me for that work?
I know it might seem silly to leap from "typing code is over" to "maybe I should be looking for a different career field." However, it feels like a valid thing to ponder, especially for someone who is only six years into this profession. We are facing a rapidly advancing technology that the most experienced people in our field are enamored with, and it appears to be reducing the need for developers at my level. At the very least, it is reducing the leverage that this knowledge and skill provide in earning a living.
Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

+11 -45
View File
@@ -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()); var postsDirectory = Path.Combine(Directory.GetCurrentDirectory(), "TestPosts");
webHostBuilder.ConfigureTestServices(services => var postServiceOptions = new FilePostServiceOptions() { PostsDirectory = postsDirectory };
{ services.AddSingleton(Options.Create(postServiceOptions));
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;
} }
} }
+6 -6
View File
@@ -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;
} }
@@ -30,10 +30,10 @@ public abstract class BlogTest : PageTest
await Context.Tracing.StopAsync(new() await Context.Tracing.StopAsync(new()
{ {
Path = Path.Combine( Path = Path.Combine(
TestContext.CurrentContext.WorkDirectory, TestContext.CurrentContext.WorkDirectory,
"playwright-traces", "playwright-traces",
$"{TestContext.CurrentContext.Test.ClassName}.{TestContext.CurrentContext.Test.Name}.zip" $"{TestContext.CurrentContext.Test.ClassName}.{TestContext.CurrentContext.Test.Name}.zip"
) )
}); });
} }
} }
-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('\\', '/')}");
}
}
}