
diff --git a/spec/config/loader_test.rb b/spec/config/loader_test.rb index 640af06..144e890 100644 --- a/spec/config/loader_test.rb +++ b/spec/config/loader_test.rb @@ -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 diff --git a/spec/config/simple_toml_test.rb b/spec/config/simple_toml_test.rb new file mode 100644 index 0000000..582b82f --- /dev/null +++ b/spec/config/simple_toml_test.rb @@ -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 diff --git a/spec/plugin_test.rb b/spec/plugin_test.rb new file mode 100644 index 0000000..15c6fbf --- /dev/null +++ b/spec/plugin_test.rb @@ -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 diff --git a/spec/posts/metadata_test.rb b/spec/posts/metadata_test.rb index 4346c8e..2bd8a72 100644 --- a/spec/posts/metadata_test.rb +++ b/spec/posts/metadata_test.rb @@ -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 diff --git a/spec/posts/models_test.rb b/spec/posts/models_test.rb new file mode 100644 index 0000000..fca5f6f --- /dev/null +++ b/spec/posts/models_test.rb @@ -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: "
regular
", + 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: "linked
", + 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 diff --git a/spec/posts/plugin_test.rb b/spec/posts/plugin_test.rb new file mode 100644 index 0000000..8caf419 --- /dev/null +++ b/spec/posts/plugin_test.rb @@ -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 diff --git a/spec/posts/repo_test.rb b/spec/posts/repo_test.rb index f741c62..6d60e25 100644 --- a/spec/posts/repo_test.rb +++ b/spec/posts/repo_test.rb @@ -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 diff --git a/spec/posts/rss_feed_test.rb b/spec/posts/rss_feed_test.rb new file mode 100644 index 0000000..a3b3155 --- /dev/null +++ b/spec/posts/rss_feed_test.rb @@ -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, "hello
", + 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: "hello
", + excerpt: "hello...", + path: "/posts/2017/10/swift-optional-or" + ) + end +end diff --git a/spec/posts/writer_test.rb b/spec/posts/writer_test.rb new file mode 100644 index 0000000..15fd95f --- /dev/null +++ b/spec/posts/writer_test.rb @@ -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: "linked body
", + 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: "regular body
", + 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 diff --git a/spec/projects/models_test.rb b/spec/projects/models_test.rb new file mode 100644 index 0000000..0fc2cb7 --- /dev/null +++ b/spec/projects/models_test.rb @@ -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 diff --git a/spec/projects/plugin_test.rb b/spec/projects/plugin_test.rb new file mode 100644 index 0000000..708f241 --- /dev/null +++ b/spec/projects/plugin_test.rb @@ -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 diff --git a/spec/site_generator_rendering_test.rb b/spec/site_generator_rendering_test.rb new file mode 100644 index 0000000..6e14044 --- /dev/null +++ b/spec/site_generator_rendering_test.rb @@ -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 diff --git a/spec/site_test.rb b/spec/site_test.rb new file mode 100644 index 0000000..c042954 --- /dev/null +++ b/spec/site_test.rb @@ -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 diff --git a/spec/utils/file_writer_test.rb b/spec/utils/file_writer_test.rb new file mode 100644 index 0000000..eb7bad8 --- /dev/null +++ b/spec/utils/file_writer_test.rb @@ -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 diff --git a/spec/utils/markdown_renderer_test.rb b/spec/utils/markdown_renderer_test.rb new file mode 100644 index 0000000..4ec69f4 --- /dev/null +++ b/spec/utils/markdown_renderer_test.rb @@ -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, "hello
", + 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: "hello
", + 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, "