diff --git a/CHANGELOG.md b/CHANGELOG.md index edba3e6..7daa766 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - In-memory rate limiting on the login endpoint: 10 attempts per 60 seconds per IP. - `` element in the RSS feed channel. - Periodic cleanup of stale `login_attempts` entries to prevent unbounded memory growth. +- Slug format validation (`[a-z0-9._-]`) preventing path traversal and XSS in templates. +- Username validation in `change_username` matching the create-user regex. +- Escape-on-output for slug and username in admin template attributes. +- Memoization of `Store#all` with mtime-based invalidation, eliminating N×read+parse + per request. +- Atomic file writes (tmp + rename) for posts and `users.toml`. +- Post language directory routing: non-default-language posts now land in + `content_dir//` instead of overwriting root-level files. +- CLI test coverage (version, help, unknown command, option parsing, config bootstrap). +- Test coverage for RSS feed and sitemap endpoints. ### Changed @@ -26,6 +36,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `Rack::Session::Cookie` expiration via `expire_after`. - Derived post excerpts now strip HTML tags before stripping markdown markup, avoiding raw HTML leaks in auto-generated excerpts. +- Draft posts no longer leak through the public detail endpoint (`/api/volumen/posts/:slug`). +- `Config::DEFAULTS` inner hashes are now frozen and `deep_merge` builds fresh + copies, preventing mutation from silently poisoning later `Config.load` calls. ## [0.1.0] — 2026-06-20 diff --git a/test/server_test.rb b/test/server_test.rb index 5a07f01..ccfe971 100644 --- a/test/server_test.rb +++ b/test/server_test.rb @@ -109,4 +109,14 @@ class ServerTest < Minitest::Test assert_equal 404, last_response.status assert_equal "not_found", JSON.parse(last_response.body)["error"] end + + def test_sitemap_endpoint + get "/api/volumen/sitemap.xml" + assert_predicate last_response, :ok? + assert_equal "application/xml", last_response["Content-Type"]&.split(";")&.first + body = last_response.body + assert_includes body, "" + assert_includes body, "https://example.com/hello" + refute_includes body, "draft" + end end