Expand test coverage across generator and plugins

This commit is contained in:
Sami Samhuri 2026-02-07 19:31:19 -08:00
parent 674fe01e04
commit 08bc3e4d99
No known key found for this signature in database
17 changed files with 1275 additions and 0 deletions

View file

@ -40,6 +40,215 @@ class Pressa::Config::LoaderTest < Minitest::Test
end
end
def test_build_site_raises_for_missing_projects_array
Dir.mktmpdir do |dir|
File.write(File.join(dir, "site.toml"), <<~TOML)
author = "Sami Samhuri"
email = "sami@samhuri.net"
title = "samhuri.net"
description = "blog"
url = "https://samhuri.net"
TOML
File.write(File.join(dir, "projects.toml"), "title = \"no projects\"\n")
loader = Pressa::Config::Loader.new(source_path: dir)
error = assert_raises(Pressa::Config::ValidationError) { loader.build_site }
assert_match(/Missing required top-level array 'projects'/, error.message)
end
end
def test_build_site_raises_for_invalid_project_entries
Dir.mktmpdir do |dir|
File.write(File.join(dir, "site.toml"), <<~TOML)
author = "Sami Samhuri"
email = "sami@samhuri.net"
title = "samhuri.net"
description = "blog"
url = "https://samhuri.net"
TOML
File.write(File.join(dir, "projects.toml"), <<~TOML)
projects = [1]
TOML
loader = Pressa::Config::Loader.new(source_path: dir)
error = assert_raises(Pressa::Config::ValidationError) { loader.build_site }
assert_match(/Project entry 1 must be a table/, error.message)
end
end
def test_build_site_raises_for_invalid_projects_plugin_type
Dir.mktmpdir do |dir|
File.write(File.join(dir, "site.toml"), <<~TOML)
author = "Sami Samhuri"
email = "sami@samhuri.net"
title = "samhuri.net"
description = "blog"
url = "https://samhuri.net"
projects_plugin = []
TOML
File.write(File.join(dir, "projects.toml"), <<~TOML)
[[projects]]
name = "demo"
title = "demo"
description = "demo project"
url = "https://github.com/samsonjs/demo"
TOML
loader = Pressa::Config::Loader.new(source_path: dir)
error = assert_raises(Pressa::Config::ValidationError) { loader.build_site }
assert_match(/Expected site\.toml projects_plugin to be a table/, error.message)
end
end
def test_build_site_raises_for_invalid_script_and_style_entries
Dir.mktmpdir do |dir|
File.write(File.join(dir, "site.toml"), <<~TOML)
author = "Sami Samhuri"
email = "sami@samhuri.net"
title = "samhuri.net"
description = "blog"
url = "https://samhuri.net"
scripts = [{}]
styles = [123]
TOML
File.write(File.join(dir, "projects.toml"), <<~TOML)
[[projects]]
name = "demo"
title = "demo"
description = "demo project"
url = "https://github.com/samsonjs/demo"
TOML
loader = Pressa::Config::Loader.new(source_path: dir)
script_error = assert_raises(Pressa::Config::ValidationError) { loader.build_site }
assert_match(/Expected site\.toml scripts\[0\]\.src to be a String/, script_error.message)
File.write(File.join(dir, "site.toml"), <<~TOML)
author = "Sami Samhuri"
email = "sami@samhuri.net"
title = "samhuri.net"
description = "blog"
url = "https://samhuri.net"
scripts = []
styles = [123]
TOML
style_error = assert_raises(Pressa::Config::ValidationError) { loader.build_site }
assert_match(/Expected site\.toml styles\[0\] to be a String or table/, style_error.message)
end
end
def test_build_site_accepts_script_hashes_and_absolute_image_url
Dir.mktmpdir do |dir|
File.write(File.join(dir, "site.toml"), <<~TOML)
author = "Sami Samhuri"
email = "sami@samhuri.net"
title = "samhuri.net"
description = "blog"
url = "https://samhuri.net"
image_url = "https://images.example.net/me.jpg"
scripts = [{"src": "/js/site.js", "defer": false}]
styles = [{"href": "/css/site.css"}]
[projects_plugin]
scripts = [{"src": "/js/projects.js", "defer": true}]
styles = [{"href": "/css/projects.css"}]
TOML
File.write(File.join(dir, "projects.toml"), <<~TOML)
[[projects]]
name = "demo"
title = "demo"
description = "demo project"
url = "https://github.com/samsonjs/demo"
TOML
loader = Pressa::Config::Loader.new(source_path: dir)
site = loader.build_site
assert_equal("https://images.example.net/me.jpg", site.image_url)
assert_equal(["/js/site.js"], site.scripts.map(&:src))
assert_equal([false], site.scripts.map(&:defer))
assert_equal(["/css/site.css"], site.styles.map(&:href))
end
end
def test_build_site_rewraps_toml_parse_errors_as_validation_errors
Dir.mktmpdir do |dir|
File.write(File.join(dir, "site.toml"), "author = \"unterminated\n")
File.write(File.join(dir, "projects.toml"), <<~TOML)
[[projects]]
name = "demo"
title = "demo"
description = "demo project"
url = "https://github.com/samsonjs/demo"
TOML
loader = Pressa::Config::Loader.new(source_path: dir)
error = assert_raises(Pressa::Config::ValidationError) { loader.build_site }
assert_match(/Unterminated value for key 'author'/, error.message)
end
end
def test_build_site_rejects_non_boolean_defer_values
Dir.mktmpdir do |dir|
File.write(File.join(dir, "site.toml"), <<~TOML)
author = "Sami Samhuri"
email = "sami@samhuri.net"
title = "samhuri.net"
description = "blog"
url = "https://samhuri.net"
scripts = [{"src": "/js/site.js", "defer": "yes"}]
TOML
File.write(File.join(dir, "projects.toml"), <<~TOML)
[[projects]]
name = "demo"
title = "demo"
description = "demo project"
url = "https://github.com/samsonjs/demo"
TOML
loader = Pressa::Config::Loader.new(source_path: dir)
error = assert_raises(Pressa::Config::ValidationError) { loader.build_site }
assert_match(/Expected site\.toml scripts\[0\]\.defer to be a Boolean/, error.message)
end
end
def test_build_site_rejects_non_string_or_table_scripts_and_non_array_script_lists
Dir.mktmpdir do |dir|
File.write(File.join(dir, "site.toml"), <<~TOML)
author = "Sami Samhuri"
email = "sami@samhuri.net"
title = "samhuri.net"
description = "blog"
url = "https://samhuri.net"
scripts = [123]
TOML
File.write(File.join(dir, "projects.toml"), <<~TOML)
[[projects]]
name = "demo"
title = "demo"
description = "demo project"
url = "https://github.com/samsonjs/demo"
TOML
loader = Pressa::Config::Loader.new(source_path: dir)
invalid_item = assert_raises(Pressa::Config::ValidationError) { loader.build_site }
assert_match(/Expected site\.toml scripts\[0\] to be a String or table/, invalid_item.message)
File.write(File.join(dir, "site.toml"), <<~TOML)
author = "Sami Samhuri"
email = "sami@samhuri.net"
title = "samhuri.net"
description = "blog"
url = "https://samhuri.net"
[projects_plugin]
scripts = "js/projects.js"
TOML
non_array = assert_raises(Pressa::Config::ValidationError) { loader.build_site }
assert_match(/Expected site\.toml projects_plugin\.scripts to be an array/, non_array.message)
end
end
private
def with_temp_config

View file

@ -0,0 +1,147 @@
require "test_helper"
require "tmpdir"
class Pressa::Config::SimpleTomlTest < Minitest::Test
def parser
@parser ||= Pressa::Config::SimpleToml.new
end
def test_load_file_raises_parse_error_for_missing_file
Dir.mktmpdir do |dir|
missing = File.join(dir, "missing.toml")
error = assert_raises(Pressa::Config::ParseError) do
Pressa::Config::SimpleToml.load_file(missing)
end
assert_match(/Config file not found/, error.message)
end
end
def test_parse_supports_tables_array_tables_comments_and_multiline_arrays
content = <<~TOML
title = "samhuri # not a comment"
[projects_plugin]
scripts = ["js/a.js", "js/b.js"]
styles = [
"css/a.css",
"css/b.css"
]
[[projects]]
name = "alpha"
title = "Alpha"
description = "Project Alpha"
url = "https://github.com/samsonjs/alpha"
[[projects]]
name = "beta"
title = "Beta"
description = "Project Beta"
url = "https://github.com/samsonjs/beta"
TOML
parsed = parser.parse(content)
assert_equal("samhuri # not a comment", parsed["title"])
assert_equal(["js/a.js", "js/b.js"], parsed.dig("projects_plugin", "scripts"))
assert_equal(["css/a.css", "css/b.css"], parsed.dig("projects_plugin", "styles"))
assert_equal(2, parsed["projects"].length)
assert_equal("alpha", parsed["projects"][0]["name"])
assert_equal("beta", parsed["projects"][1]["name"])
end
def test_parse_rejects_duplicate_keys
content = <<~TOML
author = "Sami"
author = "Sam"
TOML
error = assert_raises(Pressa::Config::ParseError) { parser.parse(content) }
assert_match(/Duplicate key 'author'/, error.message)
end
def test_parse_rejects_invalid_assignment
error = assert_raises(Pressa::Config::ParseError) { parser.parse("invalid") }
assert_match(/Invalid assignment/, error.message)
end
def test_parse_rejects_invalid_key_names
error = assert_raises(Pressa::Config::ParseError) { parser.parse("bad-key = 1") }
assert_match(/Invalid key/, error.message)
end
def test_parse_rejects_missing_value
error = assert_raises(Pressa::Config::ParseError) { parser.parse("author = ") }
assert_match(/Missing value for key 'author'/, error.message)
end
def test_parse_rejects_invalid_table_paths
error = assert_raises(Pressa::Config::ParseError) { parser.parse("[projects..plugin]") }
assert_match(/Invalid table path/, error.message)
end
def test_parse_rejects_array_table_when_table_already_exists
content = <<~TOML
[projects]
title = "single"
[[projects]]
title = "array item"
TOML
error = assert_raises(Pressa::Config::ParseError) { parser.parse(content) }
assert_match(/Expected array for '\[\[projects\]\]'/, error.message)
end
def test_parse_rejects_nested_table_on_non_table_path
content = <<~TOML
projects = 1
[projects.plugin]
enabled = true
TOML
error = assert_raises(Pressa::Config::ParseError) { parser.parse(content) }
assert_match(/Expected table path 'projects.plugin'/, error.message)
end
def test_parse_rejects_unsupported_value_types
error = assert_raises(Pressa::Config::ParseError) do
parser.parse("published_at = 2025-01-01")
end
assert_match(/Unsupported TOML value/, error.message)
end
def test_parse_rejects_unterminated_multiline_value
content = <<~TOML
scripts = [
"a.js",
"b.js"
TOML
error = assert_raises(Pressa::Config::ParseError) { parser.parse(content) }
assert_match(/Unterminated value for key 'scripts'/, error.message)
end
def test_parse_ignores_comments_but_not_hashes_inside_strings
content = <<~TOML
url = "https://example.com/#anchor" # remove me
TOML
parsed = parser.parse(content)
assert_equal("https://example.com/#anchor", parsed["url"])
end
def test_private_parsing_helpers_handle_escaped_quotes_inside_strings
refute(parser.send(:needs_continuation?, "\"a\\\\\\\"b\""))
stripped = parser.send(:strip_comments, "title = \"a\\\\\\\"b # keep\" # drop\n")
assert_equal("title = \"a\\\\\\\"b # keep\" ", stripped)
source = "\"a\\\\\\\"=b\" = 1"
index = parser.send(:index_of_unquoted, source, "=")
refute_nil(index)
assert_equal("=", source[index])
assert(index > source.rindex('"'))
end
end

23
spec/plugin_test.rb Normal file
View file

@ -0,0 +1,23 @@
require "test_helper"
class Pressa::PluginTest < Minitest::Test
def test_setup_requires_subclass_implementation
plugin = Pressa::Plugin.new
error = assert_raises(NotImplementedError) do
plugin.setup(site: Object.new, source_path: "/tmp/source")
end
assert_match(/Pressa::Plugin#setup must be implemented/, error.message)
end
def test_render_requires_subclass_implementation
plugin = Pressa::Plugin.new
error = assert_raises(NotImplementedError) do
plugin.render(site: Object.new, target_path: "/tmp/target")
end
assert_match(/Pressa::Plugin#render must be implemented/, error.message)
end
end

View file

@ -57,4 +57,12 @@ class Pressa::Posts::PostMetadataTest < Minitest::Test
assert_equal([], metadata.tags)
assert_nil(metadata.link)
end
def test_parse_raises_error_when_front_matter_is_missing
error = assert_raises(StandardError) do
Pressa::Posts::PostMetadata.parse("just plain markdown")
end
assert_match(/No YAML front-matter found in post/, error.message)
end
end

76
spec/posts/models_test.rb Normal file
View file

@ -0,0 +1,76 @@
require "test_helper"
class Pressa::Posts::ModelsTest < Minitest::Test
def regular_post
@regular_post ||= Pressa::Posts::Post.new(
slug: "regular",
title: "Regular",
author: "Sami Samhuri",
date: DateTime.parse("2025-11-05T10:00:00-08:00"),
formatted_date: "5th November, 2025",
body: "<p>regular</p>",
excerpt: "regular...",
path: "/posts/2025/11/regular"
)
end
def link_post
@link_post ||= Pressa::Posts::Post.new(
slug: "linked",
title: "Linked",
author: "Sami Samhuri",
date: DateTime.parse("2024-10-01T10:00:00-07:00"),
formatted_date: "1st October, 2024",
link: "https://example.net/post",
body: "<p>linked</p>",
excerpt: "linked...",
path: "/posts/2024/10/linked"
)
end
def test_post_helpers_report_date_parts_and_link_state
assert_equal(2025, regular_post.year)
assert_equal(11, regular_post.month)
assert_equal("November", regular_post.formatted_month)
assert_equal("11", regular_post.padded_month)
refute(regular_post.link_post?)
assert(link_post.link_post?)
end
def test_month_from_date_creates_expected_values
month = Pressa::Posts::Month.from_date(DateTime.parse("2025-02-14T08:00:00-08:00"))
assert_equal("February", month.name)
assert_equal(2, month.number)
assert_equal("02", month.padded)
end
def test_month_posts_sorted_posts_returns_descending_by_date
month_posts = Pressa::Posts::MonthPosts.new(
month: Pressa::Posts::Month.new(name: "November", number: 11, padded: "11"),
posts: [link_post, regular_post]
)
assert_equal([regular_post, link_post], month_posts.sorted_posts)
end
def test_year_posts_and_posts_by_year_sorting_helpers
oct_posts = Pressa::Posts::MonthPosts.new(
month: Pressa::Posts::Month.new(name: "October", number: 10, padded: "10"),
posts: [link_post]
)
nov_posts = Pressa::Posts::MonthPosts.new(
month: Pressa::Posts::Month.new(name: "November", number: 11, padded: "11"),
posts: [regular_post]
)
year_2025 = Pressa::Posts::YearPosts.new(year: 2025, by_month: {11 => nov_posts, 10 => oct_posts})
year_2024 = Pressa::Posts::YearPosts.new(year: 2024, by_month: {10 => oct_posts})
posts_by_year = Pressa::Posts::PostsByYear.new(by_year: {2024 => year_2024, 2025 => year_2025})
assert_equal([11, 10], year_2025.sorted_months.map { |mp| mp.month.number })
assert_equal([regular_post, link_post], year_2025.all_posts)
assert_equal([2025, 2024], posts_by_year.sorted_years)
assert_equal(3, posts_by_year.all_posts.length)
assert_equal([regular_post], posts_by_year.recent_posts(1))
end
end

67
spec/posts/plugin_test.rb Normal file
View file

@ -0,0 +1,67 @@
require "test_helper"
require "fileutils"
require "tmpdir"
class Pressa::Posts::PluginTest < Minitest::Test
def site
@site ||= Pressa::Site.new(
author: "Sami Samhuri",
email: "sami@samhuri.net",
title: "samhuri.net",
description: "blog",
url: "https://samhuri.net"
)
end
def test_setup_skips_when_posts_directory_does_not_exist
Dir.mktmpdir do |source_path|
plugin = Pressa::Posts::Plugin.new
plugin.setup(site:, source_path:)
assert_nil(plugin.posts_by_year)
end
end
def test_render_skips_when_setup_did_not_load_posts
Dir.mktmpdir do |target_path|
plugin = Pressa::Posts::Plugin.new
plugin.render(site:, target_path:)
refute(File.exist?(File.join(target_path, "index.html")))
refute(File.exist?(File.join(target_path, "feed.json")))
refute(File.exist?(File.join(target_path, "feed.xml")))
end
end
def test_setup_and_render_write_post_indexes_and_feeds
Dir.mktmpdir do |root|
source_path = File.join(root, "source")
target_path = File.join(root, "target")
posts_dir = File.join(source_path, "posts", "2025", "11")
FileUtils.mkdir_p(posts_dir)
File.write(File.join(posts_dir, "shredding.md"), <<~MARKDOWN)
---
Title: Shredding in November
Author: Shaun White
Date: 5th November, 2025
Timestamp: 2025-11-05T10:00:00-08:00
---
Had an epic day at Whistler. The powder was deep and the lines were short.
MARKDOWN
plugin = Pressa::Posts::Plugin.new
plugin.setup(site:, source_path:)
plugin.render(site:, target_path:)
assert(File.exist?(File.join(target_path, "index.html")))
assert(File.exist?(File.join(target_path, "posts/index.html")))
assert(File.exist?(File.join(target_path, "posts/2025/index.html")))
assert(File.exist?(File.join(target_path, "posts/2025/11/index.html")))
assert(File.exist?(File.join(target_path, "posts/2025/11/shredding/index.html")))
assert(File.exist?(File.join(target_path, "feed.json")))
assert(File.exist?(File.join(target_path, "feed.xml")))
end
end
end

View file

@ -70,4 +70,39 @@ class Pressa::Posts::PostRepoTest < Minitest::Test
refute_includes(post.excerpt, "[link]")
end
end
def test_read_posts_merges_multiple_posts_in_same_month
Dir.mktmpdir do |tmpdir|
posts_dir = File.join(tmpdir, "posts", "2025", "11")
FileUtils.mkdir_p(posts_dir)
File.write(File.join(posts_dir, "first.md"), <<~MARKDOWN)
---
Title: First Post
Author: Sami Samhuri
Date: 5th November, 2025
Timestamp: 2025-11-05T10:00:00-08:00
---
First
MARKDOWN
File.write(File.join(posts_dir, "second.md"), <<~MARKDOWN)
---
Title: Second Post
Author: Sami Samhuri
Date: 6th November, 2025
Timestamp: 2025-11-06T10:00:00-08:00
---
Second
MARKDOWN
posts_by_year = repo.read_posts(File.join(tmpdir, "posts"))
month_posts = posts_by_year.by_year.fetch(2025).by_month.fetch(11)
assert_equal(2, month_posts.posts.length)
assert_equal(["Second Post", "First Post"], month_posts.sorted_posts.map(&:title))
end
end
end

View file

@ -0,0 +1,94 @@
require "test_helper"
require "tmpdir"
class Pressa::Posts::RSSFeedWriterTest < Minitest::Test
class PostsByYearStub
attr_accessor :posts
def initialize(posts)
@posts = posts
end
def recent_posts(_limit = 30)
@posts
end
end
def setup
@site = Pressa::Site.new(
author: "Sami Samhuri",
email: "sami@samhuri.net",
title: "samhuri.net",
description: "blog",
url: "https://samhuri.net"
)
@posts_by_year = PostsByYearStub.new([link_post])
@writer = Pressa::Posts::RSSFeedWriter.new(site: @site, posts_by_year: @posts_by_year)
end
def test_write_feed_for_link_post_uses_arrow_title_permalink_and_content
Dir.mktmpdir do |dir|
@writer.write_feed(target_path: dir, limit: 30)
xml = File.read(File.join(dir, "feed.xml"))
assert_includes(xml, "<title>→ GitHub Flow Like a Pro</title>")
assert_includes(xml, "<guid isPermaLink=\"true\">https://samhuri.net/posts/2015/05/github-flow-like-a-pro</guid>")
assert_includes(xml, "https://samhuri.net/feed.xml")
assert_includes(xml, "<author>Sami Samhuri</author>")
assert_match(%r{<content:encoded>\s*<!\[CDATA\[}m, xml)
end
end
def test_write_feed_for_regular_post_uses_plain_title
@posts_by_year.posts = [regular_post]
Dir.mktmpdir do |dir|
@writer.write_feed(target_path: dir, limit: 30)
xml = File.read(File.join(dir, "feed.xml"))
assert_includes(xml, "<title>Swift Optional OR</title>")
refute_includes(xml, "→ Swift Optional OR")
end
end
def test_write_feed_without_posts_skips_channel_pub_date
@posts_by_year.posts = []
Dir.mktmpdir do |dir|
@writer.write_feed(target_path: dir, limit: 30)
xml = File.read(File.join(dir, "feed.xml"))
assert_includes(xml, "<channel>")
refute_match(%r{<channel>.*?<pubDate>.*?</pubDate>}m, xml)
end
end
private
def link_post
Pressa::Posts::Post.new(
slug: "github-flow-like-a-pro",
title: "GitHub Flow Like a Pro",
author: "Sami Samhuri",
date: DateTime.parse("2015-05-28T07:42:27-07:00"),
formatted_date: "28th May, 2015",
link: "http://haacked.com/archive/2014/07/28/github-flow-aliases/",
body: "<p>hello</p>",
excerpt: "hello...",
path: "/posts/2015/05/github-flow-like-a-pro"
)
end
def regular_post
Pressa::Posts::Post.new(
slug: "swift-optional-or",
title: "Swift Optional OR",
author: "Sami Samhuri",
date: DateTime.parse("2017-10-01T10:00:00-07:00"),
formatted_date: "1st October, 2017",
body: "<p>hello</p>",
excerpt: "hello...",
path: "/posts/2017/10/swift-optional-or"
)
end
end

123
spec/posts/writer_test.rb Normal file
View file

@ -0,0 +1,123 @@
require "test_helper"
require "tmpdir"
class Pressa::Posts::PostWriterTest < Minitest::Test
def site
@site ||= Pressa::Site.new(
author: "Sami Samhuri",
email: "sami@samhuri.net",
title: "samhuri.net",
description: "blog",
url: "https://samhuri.net"
)
end
def posts_by_year
@posts_by_year ||= begin
link_post = Pressa::Posts::Post.new(
slug: "link-post",
title: "Linked",
author: "Sami Samhuri",
date: DateTime.parse("2025-11-05T10:00:00-08:00"),
formatted_date: "5th November, 2025",
link: "https://example.net/linked",
body: "<p>linked body</p>",
excerpt: "linked body...",
path: "/posts/2025/11/link-post"
)
regular_post = Pressa::Posts::Post.new(
slug: "regular-post",
title: "Regular",
author: "Sami Samhuri",
date: DateTime.parse("2024-10-01T10:00:00-07:00"),
formatted_date: "1st October, 2024",
body: "<p>regular body</p>",
excerpt: "regular body...",
path: "/posts/2024/10/regular-post"
)
nov_posts = Pressa::Posts::MonthPosts.new(
month: Pressa::Posts::Month.new(name: "November", number: 11, padded: "11"),
posts: [link_post]
)
oct_posts = Pressa::Posts::MonthPosts.new(
month: Pressa::Posts::Month.new(name: "October", number: 10, padded: "10"),
posts: [regular_post]
)
Pressa::Posts::PostsByYear.new(
by_year: {
2025 => Pressa::Posts::YearPosts.new(year: 2025, by_month: {11 => nov_posts}),
2024 => Pressa::Posts::YearPosts.new(year: 2024, by_month: {10 => oct_posts})
}
)
end
end
def writer
@writer ||= Pressa::Posts::PostWriter.new(site:, posts_by_year:)
end
def test_write_posts_writes_each_post_page
Dir.mktmpdir do |dir|
writer.write_posts(target_path: dir)
regular = File.join(dir, "posts/2024/10/regular-post/index.html")
linked = File.join(dir, "posts/2025/11/link-post/index.html")
assert(File.exist?(regular))
assert(File.exist?(linked))
assert_includes(File.read(regular), "Regular")
assert_includes(File.read(linked), "→ Linked")
end
end
def test_write_recent_posts_writes_index_page
Dir.mktmpdir do |dir|
writer.write_recent_posts(target_path: dir, limit: 1)
index_path = File.join(dir, "index.html")
assert(File.exist?(index_path))
html = File.read(index_path)
assert_includes(html, "Linked")
refute_includes(html, "Regular")
end
end
def test_write_archive_writes_archive_index
Dir.mktmpdir do |dir|
writer.write_archive(target_path: dir)
archive_path = File.join(dir, "posts/index.html")
assert(File.exist?(archive_path))
html = File.read(archive_path)
assert_includes(html, "Archive")
assert_includes(html, "https://samhuri.net/posts/2025/")
end
end
def test_write_year_indexes_writes_each_year_index
Dir.mktmpdir do |dir|
writer.write_year_indexes(target_path: dir)
path_2025 = File.join(dir, "posts/2025/index.html")
path_2024 = File.join(dir, "posts/2024/index.html")
assert(File.exist?(path_2025))
assert(File.exist?(path_2024))
assert_includes(File.read(path_2025), "November")
end
end
def test_write_month_rollups_writes_each_month_index
Dir.mktmpdir do |dir|
writer.write_month_rollups(target_path: dir)
nov = File.join(dir, "posts/2025/11/index.html")
oct = File.join(dir, "posts/2024/10/index.html")
assert(File.exist?(nov))
assert(File.exist?(oct))
assert_includes(File.read(nov), "November 2025")
assert_includes(File.read(oct), "October 2024")
end
end
end

View file

@ -0,0 +1,15 @@
require "test_helper"
class Pressa::Projects::ModelsTest < Minitest::Test
def test_project_helpers_compute_paths
project = Pressa::Projects::Project.new(
name: "demo",
title: "Demo",
description: "Demo project",
url: "https://github.com/samsonjs/demo"
)
assert_equal("samsonjs/demo", project.github_path)
assert_equal("/projects/demo", project.path)
end
end

View file

@ -0,0 +1,55 @@
require "test_helper"
require "tmpdir"
class Pressa::Projects::PluginTest < Minitest::Test
def site
@site ||= Pressa::Site.new(
author: "Sami Samhuri",
email: "sami@samhuri.net",
title: "samhuri.net",
description: "blog",
url: "https://samhuri.net"
)
end
def project
@project ||= Pressa::Projects::Project.new(
name: "demo",
title: "Demo",
description: "Demo project",
url: "https://github.com/samsonjs/demo"
)
end
def test_setup_is_a_no_op
plugin = Pressa::Projects::Plugin.new(projects: [project])
assert_nil(plugin.setup(site:, source_path: "/tmp/unused"))
end
def test_render_writes_projects_index_and_project_page
plugin = Pressa::Projects::Plugin.new(
projects: [project],
scripts: [Pressa::Script.new(src: "js/projects.js", defer: false)],
styles: [Pressa::Stylesheet.new(href: "css/projects.css")]
)
Dir.mktmpdir do |dir|
plugin.render(site:, target_path: dir)
index_path = File.join(dir, "projects/index.html")
project_path = File.join(dir, "projects/demo/index.html")
assert(File.exist?(index_path))
assert(File.exist?(project_path))
index_html = File.read(index_path)
details_html = File.read(project_path)
assert_includes(index_html, "Projects")
assert_includes(index_html, "Demo")
assert_includes(details_html, "Demo project")
assert_includes(details_html, "js/projects.js")
assert_includes(details_html, "css/projects.css")
end
end
end

View file

@ -0,0 +1,113 @@
require "test_helper"
require "fileutils"
require "tmpdir"
class Pressa::SiteGeneratorRenderingTest < Minitest::Test
class PluginSpy
attr_reader :calls
def initialize
@calls = []
end
def setup(site:, source_path:)
@calls << [:setup, site.title, source_path]
end
def render(site:, target_path:)
@calls << [:render, site.title, target_path]
File.write(File.join(target_path, "plugin-output.txt"), "plugin rendered")
end
end
class MarkdownRendererSpy
attr_reader :calls
def initialize
@calls = []
end
def can_render_file?(filename:, extension:)
extension == "md" && !filename.start_with?("_")
end
def render(site:, file_path:, target_dir:)
@calls << [site.title, file_path, target_dir]
FileUtils.mkdir_p(target_dir)
slug = File.basename(file_path, ".md")
File.write(File.join(target_dir, "#{slug}.html"), "rendered #{slug}")
end
end
def build_site(plugin:, renderer:)
Pressa::Site.new(
author: "Sami Samhuri",
email: "sami@samhuri.net",
title: "samhuri.net",
description: "blog",
url: "https://samhuri.net",
plugins: [plugin],
renderers: [renderer]
)
end
def test_generate_runs_plugins_copies_static_files_and_renders_supported_files
Dir.mktmpdir do |root|
source_path = File.join(root, "source")
target_path = File.join(root, "target")
public_dir = File.join(source_path, "public", "nested")
FileUtils.mkdir_p(public_dir)
File.write(File.join(source_path, "public", "plain.txt"), "copy me")
File.write(File.join(source_path, "public", "home.md"), "# home")
File.write(File.join(source_path, "public", ".hidden"), "skip me")
File.write(File.join(public_dir, "page.md"), "# title")
File.write(File.join(public_dir, "_ignore.md"), "# ignored")
plugin = PluginSpy.new
renderer = MarkdownRendererSpy.new
site = build_site(plugin:, renderer:)
Pressa::SiteGenerator.new(site:).generate(source_path:, target_path:)
assert_equal(2, plugin.calls.length)
assert_equal(:setup, plugin.calls[0][0])
assert_equal(:render, plugin.calls[1][0])
assert_equal("samhuri.net", renderer.calls.first[0])
assert(renderer.calls.any? do |call|
call[1].end_with?("/public/nested/page.md") &&
File.expand_path(call[2]) == File.expand_path(File.join(target_path, "nested"))
end)
assert(renderer.calls.any? do |call|
call[1].end_with?("/public/home.md") &&
File.expand_path(call[2]) == File.expand_path(target_path)
end)
assert(File.exist?(File.join(target_path, "plain.txt")))
assert_equal("copy me", File.read(File.join(target_path, "plain.txt")))
refute(File.exist?(File.join(target_path, ".hidden")))
refute(File.exist?(File.join(target_path, "nested", "page.md")))
assert(File.exist?(File.join(target_path, "home.html")))
assert(File.exist?(File.join(target_path, "nested", "page.html")))
refute(File.exist?(File.join(target_path, "nested", "_ignore.html")))
assert(File.exist?(File.join(target_path, "plugin-output.txt")))
end
end
def test_generate_handles_missing_public_directory
Dir.mktmpdir do |root|
source_path = File.join(root, "source")
target_path = File.join(root, "target")
FileUtils.mkdir_p(source_path)
plugin = PluginSpy.new
renderer = MarkdownRendererSpy.new
site = build_site(plugin:, renderer:)
Pressa::SiteGenerator.new(site:).generate(source_path:, target_path:)
assert(File.exist?(File.join(target_path, "plugin-output.txt")))
assert_empty(renderer.calls)
end
end
end

52
spec/site_test.rb Normal file
View file

@ -0,0 +1,52 @@
require "test_helper"
require "tmpdir"
class Pressa::SiteTest < Minitest::Test
def test_url_helpers
site = Pressa::Site.new(
author: "Sami Samhuri",
email: "sami@samhuri.net",
title: "samhuri.net",
description: "blog",
url: "https://samhuri.net",
image_url: "https://images.example.net"
)
assert_equal("https://samhuri.net/posts", site.url_for("/posts"))
assert_equal("https://images.example.net/avatar.png", site.image_url_for("/avatar.png"))
end
def test_image_url_for_returns_nil_when_image_url_not_configured
site = Pressa::Site.new(
author: "Sami Samhuri",
email: "sami@samhuri.net",
title: "samhuri.net",
description: "blog",
url: "https://samhuri.net"
)
assert_nil(site.image_url_for("/avatar.png"))
end
def test_create_site_builds_site_using_loader
Dir.mktmpdir do |dir|
File.write(File.join(dir, "site.toml"), <<~TOML)
author = "Sami Samhuri"
email = "sami@samhuri.net"
title = "samhuri.net"
description = "blog"
url = "https://samhuri.net"
TOML
File.write(File.join(dir, "projects.toml"), <<~TOML)
[[projects]]
name = "demo"
title = "demo"
description = "demo project"
url = "https://github.com/samsonjs/demo"
TOML
site = Pressa.create_site(source_path: dir, url_override: "https://beta.samhuri.net")
assert_equal("https://beta.samhuri.net", site.url)
end
end
end

View file

@ -0,0 +1,25 @@
require "test_helper"
require "tmpdir"
class Pressa::Utils::FileWriterTest < Minitest::Test
def test_write_creates_directories_writes_content_and_sets_permissions
Dir.mktmpdir do |dir|
path = File.join(dir, "nested", "file.txt")
Pressa::Utils::FileWriter.write(path:, content: "hello", permissions: 0o600)
assert_equal("hello", File.read(path))
assert_equal("600", format("%o", File.stat(path).mode & 0o777))
end
end
def test_write_data_writes_binary_content_and_sets_permissions
Dir.mktmpdir do |dir|
path = File.join(dir, "nested", "data.bin")
data = "\x00\xFFabc".b
Pressa::Utils::FileWriter.write_data(path:, data:, permissions: 0o640)
assert_equal(data, File.binread(path))
assert_equal("640", format("%o", File.stat(path).mode & 0o777))
end
end
end

View file

@ -0,0 +1,94 @@
require "test_helper"
require "fileutils"
require "tmpdir"
class Pressa::Utils::MarkdownRendererTest < Minitest::Test
def renderer
@renderer ||= Pressa::Utils::MarkdownRenderer.new
end
def site
@site ||= Pressa::Site.new(
author: "Sami Samhuri",
email: "sami@samhuri.net",
title: "samhuri.net",
description: "blog",
url: "https://samhuri.net"
)
end
def test_can_render_file_checks_md_extension
assert(renderer.can_render_file?(filename: "about.md", extension: "md"))
refute(renderer.can_render_file?(filename: "about.txt", extension: "txt"))
end
def test_render_writes_pretty_url_output_by_default
Dir.mktmpdir do |dir|
source_file = File.join(dir, "public", "about.md")
target_dir = File.join(dir, "www")
FileUtils.mkdir_p(File.dirname(source_file))
File.write(source_file, <<~MARKDOWN)
---
Title: About
Description: About page
---
This is [my bio](https://example.net).
MARKDOWN
renderer.render(site:, file_path: source_file, target_dir:)
output_file = File.join(target_dir, "about", "index.html")
assert(File.exist?(output_file))
html = File.read(output_file)
assert_includes(html, "<title>samhuri.net: About</title>")
assert_includes(html, "<meta name=\"description\" content=\"About page\">")
assert_includes(html, "<meta property=\"og:type\" content=\"website\">")
end
end
def test_render_writes_html_extension_when_enabled_and_uses_fallbacks
Dir.mktmpdir do |dir|
source_file = File.join(dir, "public", "docs", "readme.md")
target_dir = File.join(dir, "www", "docs")
FileUtils.mkdir_p(File.dirname(source_file))
File.write(source_file, <<~MARKDOWN)
---
Show extension: yes
Page type: article
---
Hello <strong>world</strong>. This is an ![img](x.png) excerpt with [a link](https://example.net).
MARKDOWN
renderer.render(site:, file_path: source_file, target_dir:)
output_file = File.join(target_dir, "readme.html")
assert(File.exist?(output_file))
html = File.read(output_file)
assert_includes(html, "<title>samhuri.net: Readme</title>")
assert_includes(html, "<meta property=\"og:type\" content=\"article\">")
assert_includes(html, "<meta name=\"description\" content=\"Hello world. This is an excerpt with a link....\">")
assert_includes(html, "<link rel=\"canonical\" href=\"https://samhuri.net/docs/readme.html\">")
end
end
def test_render_without_front_matter_uses_filename_title
Dir.mktmpdir do |dir|
source_file = File.join(dir, "public", "notes.md")
target_dir = File.join(dir, "www")
FileUtils.mkdir_p(File.dirname(source_file))
File.write(source_file, "hello from markdown")
renderer.render(site:, file_path: source_file, target_dir:)
html = File.read(File.join(target_dir, "notes", "index.html"))
assert_includes(html, "<title>samhuri.net: Notes</title>")
assert_includes(html, "<h1>Notes</h1>")
end
end
end

View file

@ -63,4 +63,14 @@ class Pressa::Views::LayoutTest < Minitest::Test
assert_includes(html, %(<link rel="stylesheet" type="text/css" href="https://cdn.example.com/site.css">))
end
def test_format_output_is_enabled
layout = Pressa::Views::Layout.new(
site:,
canonical_url: "https://samhuri.net/posts/",
content: content_view
)
assert(layout.format_output?)
end
end

View file

@ -0,0 +1,129 @@
require "test_helper"
class Pressa::Views::RenderingTest < Minitest::Test
def site
@site ||= Pressa::Site.new(
author: "Sami Samhuri",
email: "sami@samhuri.net",
title: "samhuri.net",
description: "blog",
url: "https://samhuri.net"
)
end
def regular_post
@regular_post ||= Pressa::Posts::Post.new(
slug: "swift-optional-or",
title: "Swift Optional OR",
author: "Sami Samhuri",
date: DateTime.parse("2017-10-01T10:00:00-07:00"),
formatted_date: "1st October, 2017",
body: "<p>hello</p>",
excerpt: "hello...",
path: "/posts/2017/10/swift-optional-or"
)
end
def link_post
@link_post ||= Pressa::Posts::Post.new(
slug: "github-flow-like-a-pro",
title: "GitHub Flow Like a Pro",
author: "Sami Samhuri",
date: DateTime.parse("2015-05-28T07:42:27-07:00"),
formatted_date: "28th May, 2015",
link: "http://haacked.com/archive/2014/07/28/github-flow-aliases/",
body: "<p>hello</p>",
excerpt: "hello...",
path: "/posts/2015/05/github-flow-like-a-pro"
)
end
def test_post_view_renders_regular_post_and_article_class
html = Pressa::Views::PostView.new(
post: regular_post,
site:,
article_class: "container"
).call
assert_includes(html, "<article class=\"container\">")
assert_includes(html, "<a href=\"/posts/2017/10/swift-optional-or\">Swift Optional OR</a>")
assert_includes(html, "<a href=\"/posts/2017/10/swift-optional-or\" class=\"permalink\">∞</a>")
end
def test_post_view_renders_link_post_title_with_arrow
html = Pressa::Views::PostView.new(post: link_post, site:).call
assert_includes(html, "→ GitHub Flow Like a Pro")
assert_includes(html, "http://haacked.com/archive/2014/07/28/github-flow-aliases/")
end
def test_feed_post_view_expands_root_relative_urls_only
post = Pressa::Posts::Post.new(
slug: "with-assets",
title: "With Assets",
author: "Sami Samhuri",
date: DateTime.parse("2017-10-01T10:00:00-07:00"),
formatted_date: "1st October, 2017",
body: '<p><a href="/posts/2010/01/basics-of-the-mach-o-file-format">read</a></p>' \
'<p><img src="/images/me.jpg" alt="me"></p>' \
'<p><a href="//cdn.example.net/app.js">cdn</a></p>',
excerpt: "hello...",
path: "/posts/2017/10/with-assets"
)
html = Pressa::Views::FeedPostView.new(post:, site:).call
assert_includes(html, 'href="https://samhuri.net/posts/2010/01/basics-of-the-mach-o-file-format"')
assert_includes(html, 'src="https://samhuri.net/images/me.jpg"')
assert_includes(html, 'href="//cdn.example.net/app.js"')
end
def test_project_and_projects_views_render_project_links_and_stats
project = Pressa::Projects::Project.new(
name: "demo",
title: "Demo Project",
description: "Demo project description",
url: "https://github.com/samsonjs/demo"
)
listing = Pressa::Views::ProjectsView.new(projects: [project], site:).call
details = Pressa::Views::ProjectView.new(project:, site:).call
assert_includes(listing, "Demo Project")
assert_includes(listing, "https://samhuri.net/projects/demo")
assert_includes(details, "https://github.com/samsonjs/demo/stargazers")
assert_includes(details, "https://github.com/samsonjs/demo/network/members")
end
def test_archive_views_render_year_month_and_both_post_types
may_posts = Pressa::Posts::MonthPosts.new(
month: Pressa::Posts::Month.new(name: "May", number: 5, padded: "05"),
posts: [link_post]
)
oct_posts = Pressa::Posts::MonthPosts.new(
month: Pressa::Posts::Month.new(name: "October", number: 10, padded: "10"),
posts: [regular_post]
)
by_year = {
2017 => Pressa::Posts::YearPosts.new(year: 2017, by_month: {10 => oct_posts}),
2015 => Pressa::Posts::YearPosts.new(year: 2015, by_month: {5 => may_posts})
}
posts_by_year = Pressa::Posts::PostsByYear.new(by_year:)
year_html = Pressa::Views::YearPostsView.new(year: 2015, year_posts: by_year[2015], site:).call
month_html = Pressa::Views::MonthPostsView.new(year: 2017, month_posts: oct_posts, site:).call
recent_html = Pressa::Views::RecentPostsView.new(posts: [regular_post], site:).call
archive_html = Pressa::Views::ArchiveView.new(posts_by_year:, site:).call
assert_includes(year_html, "https://samhuri.net/posts/2015/05/")
assert_includes(year_html, "→ GitHub Flow Like a Pro")
assert_match(%r{<a (?=[^>]*class="permalink")(?=[^>]*href="/posts/2015/05/github-flow-like-a-pro")[^>]*>∞</a>}, year_html)
assert_includes(month_html, "October 2017")
assert_includes(recent_html, "Swift Optional OR")
assert_includes(archive_html, "Archive")
assert_includes(archive_html, "https://samhuri.net/posts/2017/")
assert_includes(archive_html, "https://samhuri.net/posts/2015/")
end
end