mirror of
https://github.com/samsonjs/samhuri.net.git
synced 2026-04-27 14:57:40 +00:00
Migrate test suite from RSpec to Minitest
This commit is contained in:
parent
e7190a35f6
commit
c65dee7e91
16 changed files with 370 additions and 397 deletions
3
Gemfile
3
Gemfile
|
|
@ -12,8 +12,7 @@ gem "bake", "~> 0.20"
|
||||||
gem "nokogiri", "~> 1.18"
|
gem "nokogiri", "~> 1.18"
|
||||||
|
|
||||||
group :development, :test do
|
group :development, :test do
|
||||||
gem "rspec", "~> 3.13"
|
|
||||||
gem "guard", "~> 2.18"
|
gem "guard", "~> 2.18"
|
||||||
gem "guard-rspec", "~> 4.7"
|
gem "minitest", "~> 6.0"
|
||||||
gem "standard", "~> 1.43"
|
gem "standard", "~> 1.43"
|
||||||
end
|
end
|
||||||
|
|
|
||||||
25
Gemfile.lock
25
Gemfile.lock
|
|
@ -13,7 +13,6 @@ GEM
|
||||||
fiber-annotation
|
fiber-annotation
|
||||||
fiber-local (~> 1.1)
|
fiber-local (~> 1.1)
|
||||||
json
|
json
|
||||||
diff-lcs (1.6.2)
|
|
||||||
dry-core (1.2.0)
|
dry-core (1.2.0)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
logger
|
logger
|
||||||
|
|
@ -63,11 +62,6 @@ GEM
|
||||||
pry (>= 0.13.0)
|
pry (>= 0.13.0)
|
||||||
shellany (~> 0.0)
|
shellany (~> 0.0)
|
||||||
thor (>= 0.18.1)
|
thor (>= 0.18.1)
|
||||||
guard-compat (1.2.1)
|
|
||||||
guard-rspec (4.7.3)
|
|
||||||
guard (~> 2.1)
|
|
||||||
guard-compat (~> 1.1)
|
|
||||||
rspec (>= 2.99.0, < 4.0)
|
|
||||||
ice_nine (0.11.2)
|
ice_nine (0.11.2)
|
||||||
io-console (0.8.2)
|
io-console (0.8.2)
|
||||||
json (2.18.1)
|
json (2.18.1)
|
||||||
|
|
@ -86,6 +80,8 @@ GEM
|
||||||
mapping (1.1.3)
|
mapping (1.1.3)
|
||||||
method_source (1.1.0)
|
method_source (1.1.0)
|
||||||
mini_portile2 (2.8.9)
|
mini_portile2 (2.8.9)
|
||||||
|
minitest (6.0.0)
|
||||||
|
prism (~> 1.5)
|
||||||
nenv (0.3.0)
|
nenv (0.3.0)
|
||||||
nokogiri (1.19.0)
|
nokogiri (1.19.0)
|
||||||
mini_portile2 (~> 2.8.2)
|
mini_portile2 (~> 2.8.2)
|
||||||
|
|
@ -134,19 +130,6 @@ GEM
|
||||||
io-console (~> 0.5)
|
io-console (~> 0.5)
|
||||||
rexml (3.4.4)
|
rexml (3.4.4)
|
||||||
rouge (4.7.0)
|
rouge (4.7.0)
|
||||||
rspec (3.13.2)
|
|
||||||
rspec-core (~> 3.13.0)
|
|
||||||
rspec-expectations (~> 3.13.0)
|
|
||||||
rspec-mocks (~> 3.13.0)
|
|
||||||
rspec-core (3.13.6)
|
|
||||||
rspec-support (~> 3.13.0)
|
|
||||||
rspec-expectations (3.13.5)
|
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
|
||||||
rspec-support (~> 3.13.0)
|
|
||||||
rspec-mocks (3.13.7)
|
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
|
||||||
rspec-support (~> 3.13.0)
|
|
||||||
rspec-support (3.13.7)
|
|
||||||
rubocop (1.82.1)
|
rubocop (1.82.1)
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
language_server-protocol (~> 3.17.0.2)
|
language_server-protocol (~> 3.17.0.2)
|
||||||
|
|
@ -194,7 +177,6 @@ PLATFORMS
|
||||||
arm-linux-gnu
|
arm-linux-gnu
|
||||||
arm-linux-musl
|
arm-linux-musl
|
||||||
arm64-darwin
|
arm64-darwin
|
||||||
ruby
|
|
||||||
x86-linux-gnu
|
x86-linux-gnu
|
||||||
x86-linux-musl
|
x86-linux-musl
|
||||||
x86_64-darwin
|
x86_64-darwin
|
||||||
|
|
@ -206,13 +188,12 @@ DEPENDENCIES
|
||||||
builder (~> 3.3)
|
builder (~> 3.3)
|
||||||
dry-struct (~> 1.8)
|
dry-struct (~> 1.8)
|
||||||
guard (~> 2.18)
|
guard (~> 2.18)
|
||||||
guard-rspec (~> 4.7)
|
|
||||||
kramdown (~> 2.5)
|
kramdown (~> 2.5)
|
||||||
kramdown-parser-gfm (~> 1.1)
|
kramdown-parser-gfm (~> 1.1)
|
||||||
|
minitest (~> 6.0)
|
||||||
nokogiri (~> 1.18)
|
nokogiri (~> 1.18)
|
||||||
phlex (~> 2.3)
|
phlex (~> 2.3)
|
||||||
rouge (~> 4.6)
|
rouge (~> 4.6)
|
||||||
rspec (~> 3.13)
|
|
||||||
standard (~> 1.43)
|
standard (~> 1.43)
|
||||||
|
|
||||||
RUBY VERSION
|
RUBY VERSION
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ Published posts in `posts/YYYY/MM/*.md` require YAML front matter keys:
|
||||||
## Tests And Lint
|
## Tests And Lint
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
rbenv exec bundle exec rspec
|
rbenv exec bundle exec bake test
|
||||||
rbenv exec bundle exec standardrb
|
rbenv exec bundle exec standardrb
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
7
bake.rb
7
bake.rb
|
|
@ -136,9 +136,12 @@ def clean
|
||||||
puts "Cleaned www/ directory"
|
puts "Cleaned www/ directory"
|
||||||
end
|
end
|
||||||
|
|
||||||
# Run RSpec tests
|
# Run Minitest tests
|
||||||
def test
|
def test
|
||||||
exec "bundle exec rspec"
|
test_files = Dir.glob("spec/**/*_test.rb").sort
|
||||||
|
abort "Error: no tests found in spec/**/*_test.rb" if test_files.empty?
|
||||||
|
|
||||||
|
exec "ruby", "-Ilib", "-Ispec", "-e", "ARGV.each { |file| require File.expand_path(file) }", *test_files
|
||||||
end
|
end
|
||||||
|
|
||||||
# Run Guard for continuous testing
|
# Run Guard for continuous testing
|
||||||
|
|
|
||||||
|
|
@ -1,72 +0,0 @@
|
||||||
require "spec_helper"
|
|
||||||
require "fileutils"
|
|
||||||
require "tmpdir"
|
|
||||||
|
|
||||||
RSpec.describe Pressa::Config::Loader do
|
|
||||||
describe "#build_site" do
|
|
||||||
it "builds a site from site.toml and projects.toml" do
|
|
||||||
with_temp_config do |dir|
|
|
||||||
loader = described_class.new(source_path: dir)
|
|
||||||
site = loader.build_site
|
|
||||||
|
|
||||||
expect(site.author).to eq("Sami Samhuri")
|
|
||||||
expect(site.url).to eq("https://samhuri.net")
|
|
||||||
expect(site.image_url).to eq("https://samhuri.net/images/me.jpg")
|
|
||||||
expect(site.styles.map(&:href)).to eq(["css/style.css"])
|
|
||||||
|
|
||||||
projects_plugin = site.plugins.find { |plugin| plugin.is_a?(Pressa::Projects::Plugin) }
|
|
||||||
expect(projects_plugin).not_to be_nil
|
|
||||||
expect(projects_plugin.scripts.map(&:src)).to eq(["js/projects.js"])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it "applies url_override and rewrites relative image_url with override host" do
|
|
||||||
with_temp_config do |dir|
|
|
||||||
loader = described_class.new(source_path: dir)
|
|
||||||
site = loader.build_site(url_override: "https://beta.samhuri.net")
|
|
||||||
|
|
||||||
expect(site.url).to eq("https://beta.samhuri.net")
|
|
||||||
expect(site.image_url).to eq("https://beta.samhuri.net/images/me.jpg")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it "raises a validation error for missing required site keys" do
|
|
||||||
Dir.mktmpdir do |dir|
|
|
||||||
File.write(File.join(dir, "site.toml"), "title = \"x\"\n")
|
|
||||||
File.write(File.join(dir, "projects.toml"), "")
|
|
||||||
|
|
||||||
loader = described_class.new(source_path: dir)
|
|
||||||
expect { loader.build_site }.to raise_error(Pressa::Config::ValidationError, /Missing required site\.toml keys/)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def with_temp_config
|
|
||||||
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 = "/images/me.jpg"
|
|
||||||
scripts = []
|
|
||||||
styles = ["css/style.css"]
|
|
||||||
|
|
||||||
[projects_plugin]
|
|
||||||
scripts = ["js/projects.js"]
|
|
||||||
styles = []
|
|
||||||
TOML
|
|
||||||
|
|
||||||
File.write(File.join(dir, "projects.toml"), <<~TOML)
|
|
||||||
[[projects]]
|
|
||||||
name = "demo"
|
|
||||||
title = "demo"
|
|
||||||
description = "demo project"
|
|
||||||
url = "https://github.com/samsonjs/demo"
|
|
||||||
TOML
|
|
||||||
|
|
||||||
yield dir
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
73
spec/config/loader_test.rb
Normal file
73
spec/config/loader_test.rb
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
require "test_helper"
|
||||||
|
require "fileutils"
|
||||||
|
require "tmpdir"
|
||||||
|
|
||||||
|
class Pressa::Config::LoaderTest < Minitest::Test
|
||||||
|
def test_build_site_builds_a_site_from_site_toml_and_projects_toml
|
||||||
|
with_temp_config do |dir|
|
||||||
|
loader = Pressa::Config::Loader.new(source_path: dir)
|
||||||
|
site = loader.build_site
|
||||||
|
|
||||||
|
assert_equal("Sami Samhuri", site.author)
|
||||||
|
assert_equal("https://samhuri.net", site.url)
|
||||||
|
assert_equal("https://samhuri.net/images/me.jpg", site.image_url)
|
||||||
|
assert_equal(["css/style.css"], site.styles.map(&:href))
|
||||||
|
|
||||||
|
projects_plugin = site.plugins.find { |plugin| plugin.is_a?(Pressa::Projects::Plugin) }
|
||||||
|
refute_nil(projects_plugin)
|
||||||
|
assert_equal(["js/projects.js"], projects_plugin.scripts.map(&:src))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_build_site_applies_url_override_and_rewrites_relative_image_url_with_override_host
|
||||||
|
with_temp_config do |dir|
|
||||||
|
loader = Pressa::Config::Loader.new(source_path: dir)
|
||||||
|
site = loader.build_site(url_override: "https://beta.samhuri.net")
|
||||||
|
|
||||||
|
assert_equal("https://beta.samhuri.net", site.url)
|
||||||
|
assert_equal("https://beta.samhuri.net/images/me.jpg", site.image_url)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_build_site_raises_a_validation_error_for_missing_required_site_keys
|
||||||
|
Dir.mktmpdir do |dir|
|
||||||
|
File.write(File.join(dir, "site.toml"), "title = \"x\"\n")
|
||||||
|
File.write(File.join(dir, "projects.toml"), "")
|
||||||
|
|
||||||
|
loader = Pressa::Config::Loader.new(source_path: dir)
|
||||||
|
error = assert_raises(Pressa::Config::ValidationError) { loader.build_site }
|
||||||
|
assert_match(/Missing required site\.toml keys/, error.message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def with_temp_config
|
||||||
|
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 = "/images/me.jpg"
|
||||||
|
scripts = []
|
||||||
|
styles = ["css/style.css"]
|
||||||
|
|
||||||
|
[projects_plugin]
|
||||||
|
scripts = ["js/projects.js"]
|
||||||
|
styles = []
|
||||||
|
TOML
|
||||||
|
|
||||||
|
File.write(File.join(dir, "projects.toml"), <<~TOML)
|
||||||
|
[[projects]]
|
||||||
|
name = "demo"
|
||||||
|
title = "demo"
|
||||||
|
description = "demo project"
|
||||||
|
url = "https://github.com/samsonjs/demo"
|
||||||
|
TOML
|
||||||
|
|
||||||
|
yield dir
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
require "spec_helper"
|
|
||||||
require "json"
|
|
||||||
require "tmpdir"
|
|
||||||
|
|
||||||
RSpec.describe Pressa::Posts::JSONFeedWriter do
|
|
||||||
let(:site) do
|
|
||||||
Pressa::Site.new(
|
|
||||||
author: "Sami Samhuri",
|
|
||||||
email: "sami@samhuri.net",
|
|
||||||
title: "samhuri.net",
|
|
||||||
description: "blog",
|
|
||||||
url: "https://samhuri.net",
|
|
||||||
image_url: "https://samhuri.net/images/me.jpg"
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
let(:posts_by_year) { double("posts_by_year", recent_posts: [post]) }
|
|
||||||
let(:writer) { described_class.new(site:, posts_by_year:) }
|
|
||||||
|
|
||||||
context "for link posts" do
|
|
||||||
let(:post) do
|
|
||||||
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
|
|
||||||
|
|
||||||
it "uses permalink as url and keeps external_url for destination links" do
|
|
||||||
Dir.mktmpdir do |dir|
|
|
||||||
writer.write_feed(target_path: dir, limit: 30)
|
|
||||||
feed = JSON.parse(File.read(File.join(dir, "feed.json")))
|
|
||||||
item = feed.fetch("items").first
|
|
||||||
|
|
||||||
expect(item.fetch("id")).to eq("https://samhuri.net/posts/2015/05/github-flow-like-a-pro")
|
|
||||||
expect(item.fetch("url")).to eq("https://samhuri.net/posts/2015/05/github-flow-like-a-pro")
|
|
||||||
expect(item.fetch("external_url")).to eq("http://haacked.com/archive/2014/07/28/github-flow-aliases/")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context "for regular posts" do
|
|
||||||
let(:post) do
|
|
||||||
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
|
|
||||||
|
|
||||||
it "omits external_url" do
|
|
||||||
Dir.mktmpdir do |dir|
|
|
||||||
writer.write_feed(target_path: dir, limit: 30)
|
|
||||||
feed = JSON.parse(File.read(File.join(dir, "feed.json")))
|
|
||||||
item = feed.fetch("items").first
|
|
||||||
|
|
||||||
expect(item.fetch("url")).to eq("https://samhuri.net/posts/2017/10/swift-optional-or")
|
|
||||||
expect(item).not_to have_key("external_url")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it "expands root-relative links in content_html to absolute URLs" do
|
|
||||||
post_with_assets = 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><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/swift-optional-or"
|
|
||||||
)
|
|
||||||
allow(posts_by_year).to receive(:recent_posts).and_return([post_with_assets])
|
|
||||||
|
|
||||||
Dir.mktmpdir do |dir|
|
|
||||||
writer.write_feed(target_path: dir, limit: 30)
|
|
||||||
feed = JSON.parse(File.read(File.join(dir, "feed.json")))
|
|
||||||
item = feed.fetch("items").first
|
|
||||||
content_html = item.fetch("content_html")
|
|
||||||
|
|
||||||
expect(content_html).to include('href="https://samhuri.net/posts/2010/01/basics-of-the-mach-o-file-format"')
|
|
||||||
expect(content_html).to include('src="https://samhuri.net/images/me.jpg"')
|
|
||||||
expect(content_html).to include('href="//cdn.example.net/app.js"')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
115
spec/posts/json_feed_test.rb
Normal file
115
spec/posts/json_feed_test.rb
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
require "test_helper"
|
||||||
|
require "json"
|
||||||
|
require "tmpdir"
|
||||||
|
|
||||||
|
class Pressa::Posts::JSONFeedWriterTest < 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",
|
||||||
|
image_url: "https://samhuri.net/images/me.jpg"
|
||||||
|
)
|
||||||
|
|
||||||
|
@posts_by_year = PostsByYearStub.new([link_post])
|
||||||
|
@writer = Pressa::Posts::JSONFeedWriter.new(site: @site, posts_by_year: @posts_by_year)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_write_feed_for_link_posts_uses_permalink_as_url_and_keeps_external_url
|
||||||
|
Dir.mktmpdir do |dir|
|
||||||
|
@writer.write_feed(target_path: dir, limit: 30)
|
||||||
|
feed = JSON.parse(File.read(File.join(dir, "feed.json")))
|
||||||
|
item = feed.fetch("items").first
|
||||||
|
|
||||||
|
assert_equal("https://samhuri.net/posts/2015/05/github-flow-like-a-pro", item.fetch("id"))
|
||||||
|
assert_equal("https://samhuri.net/posts/2015/05/github-flow-like-a-pro", item.fetch("url"))
|
||||||
|
assert_equal("http://haacked.com/archive/2014/07/28/github-flow-aliases/", item.fetch("external_url"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_write_feed_for_regular_posts_omits_external_url
|
||||||
|
@posts_by_year.posts = [regular_post]
|
||||||
|
|
||||||
|
Dir.mktmpdir do |dir|
|
||||||
|
@writer.write_feed(target_path: dir, limit: 30)
|
||||||
|
feed = JSON.parse(File.read(File.join(dir, "feed.json")))
|
||||||
|
item = feed.fetch("items").first
|
||||||
|
|
||||||
|
assert_equal("https://samhuri.net/posts/2017/10/swift-optional-or", item.fetch("url"))
|
||||||
|
refute(item.key?("external_url"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_write_feed_expands_root_relative_links_in_content_html
|
||||||
|
@posts_by_year.posts = [post_with_assets]
|
||||||
|
|
||||||
|
Dir.mktmpdir do |dir|
|
||||||
|
@writer.write_feed(target_path: dir, limit: 30)
|
||||||
|
feed = JSON.parse(File.read(File.join(dir, "feed.json")))
|
||||||
|
item = feed.fetch("items").first
|
||||||
|
content_html = item.fetch("content_html")
|
||||||
|
|
||||||
|
assert_includes(content_html, 'href="https://samhuri.net/posts/2010/01/basics-of-the-mach-o-file-format"')
|
||||||
|
assert_includes(content_html, 'src="https://samhuri.net/images/me.jpg"')
|
||||||
|
assert_includes(content_html, 'href="//cdn.example.net/app.js"')
|
||||||
|
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
|
||||||
|
|
||||||
|
def post_with_assets
|
||||||
|
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><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/swift-optional-or"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
require "spec_helper"
|
|
||||||
|
|
||||||
RSpec.describe Pressa::Posts::PostMetadata do
|
|
||||||
describe ".parse" do
|
|
||||||
it "parses valid YAML front-matter" do
|
|
||||||
content = <<~MARKDOWN
|
|
||||||
---
|
|
||||||
Title: Test Post
|
|
||||||
Author: Trent Reznor
|
|
||||||
Date: 5th November, 2025
|
|
||||||
Timestamp: 2025-11-05T10:00:00-08:00
|
|
||||||
Tags: Ruby, Testing
|
|
||||||
Scripts: highlight.js
|
|
||||||
Styles: code.css
|
|
||||||
Link: https://example.net/external
|
|
||||||
---
|
|
||||||
|
|
||||||
This is the post body.
|
|
||||||
MARKDOWN
|
|
||||||
|
|
||||||
metadata = described_class.parse(content)
|
|
||||||
|
|
||||||
expect(metadata.title).to eq("Test Post")
|
|
||||||
expect(metadata.author).to eq("Trent Reznor")
|
|
||||||
expect(metadata.formatted_date).to eq("5th November, 2025")
|
|
||||||
expect(metadata.date.year).to eq(2025)
|
|
||||||
expect(metadata.date.month).to eq(11)
|
|
||||||
expect(metadata.date.day).to eq(5)
|
|
||||||
expect(metadata.link).to eq("https://example.net/external")
|
|
||||||
expect(metadata.tags).to eq(["Ruby", "Testing"])
|
|
||||||
expect(metadata.scripts.map(&:src)).to eq(["js/highlight.js"])
|
|
||||||
expect(metadata.styles.map(&:href)).to eq(["css/code.css"])
|
|
||||||
end
|
|
||||||
|
|
||||||
it "raises error when required fields are missing" do
|
|
||||||
content = <<~MARKDOWN
|
|
||||||
---
|
|
||||||
Title: Incomplete Post
|
|
||||||
---
|
|
||||||
|
|
||||||
Body content
|
|
||||||
MARKDOWN
|
|
||||||
|
|
||||||
expect {
|
|
||||||
described_class.parse(content)
|
|
||||||
}.to raise_error(/Missing required fields/)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "handles posts without optional fields" do
|
|
||||||
content = <<~MARKDOWN
|
|
||||||
---
|
|
||||||
Title: Simple Post
|
|
||||||
Author: Fat Mike
|
|
||||||
Date: 1st January, 2025
|
|
||||||
Timestamp: 2025-01-01T12:00:00-08:00
|
|
||||||
---
|
|
||||||
|
|
||||||
Simple content
|
|
||||||
MARKDOWN
|
|
||||||
|
|
||||||
metadata = described_class.parse(content)
|
|
||||||
|
|
||||||
expect(metadata.tags).to eq([])
|
|
||||||
expect(metadata.scripts).to eq([])
|
|
||||||
expect(metadata.styles).to eq([])
|
|
||||||
expect(metadata.link).to be_nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
66
spec/posts/metadata_test.rb
Normal file
66
spec/posts/metadata_test.rb
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class Pressa::Posts::PostMetadataTest < Minitest::Test
|
||||||
|
def test_parse_parses_valid_yaml_front_matter
|
||||||
|
content = <<~MARKDOWN
|
||||||
|
---
|
||||||
|
Title: Test Post
|
||||||
|
Author: Trent Reznor
|
||||||
|
Date: 5th November, 2025
|
||||||
|
Timestamp: 2025-11-05T10:00:00-08:00
|
||||||
|
Tags: Ruby, Testing
|
||||||
|
Scripts: highlight.js
|
||||||
|
Styles: code.css
|
||||||
|
Link: https://example.net/external
|
||||||
|
---
|
||||||
|
|
||||||
|
This is the post body.
|
||||||
|
MARKDOWN
|
||||||
|
|
||||||
|
metadata = Pressa::Posts::PostMetadata.parse(content)
|
||||||
|
|
||||||
|
assert_equal("Test Post", metadata.title)
|
||||||
|
assert_equal("Trent Reznor", metadata.author)
|
||||||
|
assert_equal("5th November, 2025", metadata.formatted_date)
|
||||||
|
assert_equal(2025, metadata.date.year)
|
||||||
|
assert_equal(11, metadata.date.month)
|
||||||
|
assert_equal(5, metadata.date.day)
|
||||||
|
assert_equal("https://example.net/external", metadata.link)
|
||||||
|
assert_equal(["Ruby", "Testing"], metadata.tags)
|
||||||
|
assert_equal(["js/highlight.js"], metadata.scripts.map(&:src))
|
||||||
|
assert_equal(["css/code.css"], metadata.styles.map(&:href))
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_parse_raises_error_when_required_fields_are_missing
|
||||||
|
content = <<~MARKDOWN
|
||||||
|
---
|
||||||
|
Title: Incomplete Post
|
||||||
|
---
|
||||||
|
|
||||||
|
Body content
|
||||||
|
MARKDOWN
|
||||||
|
|
||||||
|
error = assert_raises(StandardError) { Pressa::Posts::PostMetadata.parse(content) }
|
||||||
|
assert_match(/Missing required fields/, error.message)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_parse_handles_posts_without_optional_fields
|
||||||
|
content = <<~MARKDOWN
|
||||||
|
---
|
||||||
|
Title: Simple Post
|
||||||
|
Author: Fat Mike
|
||||||
|
Date: 1st January, 2025
|
||||||
|
Timestamp: 2025-01-01T12:00:00-08:00
|
||||||
|
---
|
||||||
|
|
||||||
|
Simple content
|
||||||
|
MARKDOWN
|
||||||
|
|
||||||
|
metadata = Pressa::Posts::PostMetadata.parse(content)
|
||||||
|
|
||||||
|
assert_equal([], metadata.tags)
|
||||||
|
assert_equal([], metadata.scripts)
|
||||||
|
assert_equal([], metadata.styles)
|
||||||
|
assert_nil(metadata.link)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
require "spec_helper"
|
|
||||||
require "fileutils"
|
|
||||||
require "tmpdir"
|
|
||||||
|
|
||||||
RSpec.describe Pressa::Posts::PostRepo do
|
|
||||||
let(:repo) { described_class.new }
|
|
||||||
|
|
||||||
describe "#read_posts" do
|
|
||||||
it "reads and organizes posts by year and month" do
|
|
||||||
Dir.mktmpdir do |tmpdir|
|
|
||||||
posts_dir = File.join(tmpdir, "posts", "2025", "11")
|
|
||||||
FileUtils.mkdir_p(posts_dir)
|
|
||||||
|
|
||||||
post_content = <<~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
|
|
||||||
|
|
||||||
File.write(File.join(posts_dir, "shredding.md"), post_content)
|
|
||||||
|
|
||||||
posts_by_year = repo.read_posts(File.join(tmpdir, "posts"))
|
|
||||||
|
|
||||||
expect(posts_by_year.all_posts.length).to eq(1)
|
|
||||||
|
|
||||||
post = posts_by_year.all_posts.first
|
|
||||||
expect(post.title).to eq("Shredding in November")
|
|
||||||
expect(post.author).to eq("Shaun White")
|
|
||||||
expect(post.slug).to eq("shredding")
|
|
||||||
expect(post.year).to eq(2025)
|
|
||||||
expect(post.month).to eq(11)
|
|
||||||
expect(post.path).to eq("/posts/2025/11/shredding")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it "generates excerpts from post content" do
|
|
||||||
Dir.mktmpdir do |tmpdir|
|
|
||||||
posts_dir = File.join(tmpdir, "posts", "2025", "11")
|
|
||||||
FileUtils.mkdir_p(posts_dir)
|
|
||||||
|
|
||||||
post_content = <<~MARKDOWN
|
|
||||||
---
|
|
||||||
Title: Test Post
|
|
||||||
Author: Greg Graffin
|
|
||||||
Date: 5th November, 2025
|
|
||||||
Timestamp: 2025-11-05T10:00:00-08:00
|
|
||||||
---
|
|
||||||
|
|
||||||
This is a test post with some content. It should generate an excerpt.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
More content with a [link](https://example.net).
|
|
||||||
MARKDOWN
|
|
||||||
|
|
||||||
File.write(File.join(posts_dir, "test.md"), post_content)
|
|
||||||
|
|
||||||
posts_by_year = repo.read_posts(File.join(tmpdir, "posts"))
|
|
||||||
post = posts_by_year.all_posts.first
|
|
||||||
|
|
||||||
expect(post.excerpt).to include("test post")
|
|
||||||
expect(post.excerpt).not_to include("![")
|
|
||||||
expect(post.excerpt).to include("link")
|
|
||||||
expect(post.excerpt).not_to include("[link]")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
73
spec/posts/repo_test.rb
Normal file
73
spec/posts/repo_test.rb
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
require "test_helper"
|
||||||
|
require "fileutils"
|
||||||
|
require "tmpdir"
|
||||||
|
|
||||||
|
class Pressa::Posts::PostRepoTest < Minitest::Test
|
||||||
|
def repo
|
||||||
|
@repo ||= Pressa::Posts::PostRepo.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_read_posts_reads_and_organizes_posts_by_year_and_month
|
||||||
|
Dir.mktmpdir do |tmpdir|
|
||||||
|
posts_dir = File.join(tmpdir, "posts", "2025", "11")
|
||||||
|
FileUtils.mkdir_p(posts_dir)
|
||||||
|
|
||||||
|
post_content = <<~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
|
||||||
|
|
||||||
|
File.write(File.join(posts_dir, "shredding.md"), post_content)
|
||||||
|
|
||||||
|
posts_by_year = repo.read_posts(File.join(tmpdir, "posts"))
|
||||||
|
|
||||||
|
assert_equal(1, posts_by_year.all_posts.length)
|
||||||
|
|
||||||
|
post = posts_by_year.all_posts.first
|
||||||
|
assert_equal("Shredding in November", post.title)
|
||||||
|
assert_equal("Shaun White", post.author)
|
||||||
|
assert_equal("shredding", post.slug)
|
||||||
|
assert_equal(2025, post.year)
|
||||||
|
assert_equal(11, post.month)
|
||||||
|
assert_equal("/posts/2025/11/shredding", post.path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_read_posts_generates_excerpts_from_post_content
|
||||||
|
Dir.mktmpdir do |tmpdir|
|
||||||
|
posts_dir = File.join(tmpdir, "posts", "2025", "11")
|
||||||
|
FileUtils.mkdir_p(posts_dir)
|
||||||
|
|
||||||
|
post_content = <<~MARKDOWN
|
||||||
|
---
|
||||||
|
Title: Test Post
|
||||||
|
Author: Greg Graffin
|
||||||
|
Date: 5th November, 2025
|
||||||
|
Timestamp: 2025-11-05T10:00:00-08:00
|
||||||
|
---
|
||||||
|
|
||||||
|
This is a test post with some content. It should generate an excerpt.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
More content with a [link](https://example.net).
|
||||||
|
MARKDOWN
|
||||||
|
|
||||||
|
File.write(File.join(posts_dir, "test.md"), post_content)
|
||||||
|
|
||||||
|
posts_by_year = repo.read_posts(File.join(tmpdir, "posts"))
|
||||||
|
post = posts_by_year.all_posts.first
|
||||||
|
|
||||||
|
assert_includes(post.excerpt, "test post")
|
||||||
|
refute_includes(post.excerpt, "![")
|
||||||
|
assert_includes(post.excerpt, "link")
|
||||||
|
refute_includes(post.excerpt, "[link]")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
require "spec_helper"
|
require "test_helper"
|
||||||
require "fileutils"
|
require "fileutils"
|
||||||
require "tmpdir"
|
require "tmpdir"
|
||||||
|
|
||||||
RSpec.describe Pressa::SiteGenerator do
|
class Pressa::SiteGeneratorTest < Minitest::Test
|
||||||
let(:site) do
|
def site
|
||||||
Pressa::Site.new(
|
@site ||= Pressa::Site.new(
|
||||||
author: "Sami Samhuri",
|
author: "Sami Samhuri",
|
||||||
email: "sami@samhuri.net",
|
email: "sami@samhuri.net",
|
||||||
title: "samhuri.net",
|
title: "samhuri.net",
|
||||||
|
|
@ -15,23 +15,23 @@ RSpec.describe Pressa::SiteGenerator do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "rejects a target path that matches the source path" do
|
def test_rejects_a_target_path_that_matches_the_source_path
|
||||||
Dir.mktmpdir do |dir|
|
Dir.mktmpdir do |dir|
|
||||||
FileUtils.mkdir_p(File.join(dir, "public"))
|
FileUtils.mkdir_p(File.join(dir, "public"))
|
||||||
source_file = File.join(dir, "public", "keep.txt")
|
source_file = File.join(dir, "public", "keep.txt")
|
||||||
File.write(source_file, "safe")
|
File.write(source_file, "safe")
|
||||||
|
|
||||||
generator = described_class.new(site:)
|
generator = Pressa::SiteGenerator.new(site:)
|
||||||
|
error = assert_raises(ArgumentError) do
|
||||||
expect {
|
|
||||||
generator.generate(source_path: dir, target_path: dir)
|
generator.generate(source_path: dir, target_path: dir)
|
||||||
}.to raise_error(ArgumentError, /must not be the same as or contain source_path/)
|
end
|
||||||
|
|
||||||
expect(File.read(source_file)).to eq("safe")
|
assert_match(/must not be the same as or contain source_path/, error.message)
|
||||||
|
assert_equal("safe", File.read(source_file))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it "does not copy ignored dotfiles from public" do
|
def test_does_not_copy_ignored_dotfiles_from_public
|
||||||
Dir.mktmpdir do |dir|
|
Dir.mktmpdir do |dir|
|
||||||
source_path = File.join(dir, "source")
|
source_path = File.join(dir, "source")
|
||||||
target_path = File.join(dir, "target")
|
target_path = File.join(dir, "target")
|
||||||
|
|
@ -42,11 +42,11 @@ RSpec.describe Pressa::SiteGenerator do
|
||||||
File.write(File.join(public_path, ".gitkeep"), "")
|
File.write(File.join(public_path, ".gitkeep"), "")
|
||||||
File.write(File.join(public_path, "visible.txt"), "ok")
|
File.write(File.join(public_path, "visible.txt"), "ok")
|
||||||
|
|
||||||
described_class.new(site:).generate(source_path:, target_path:)
|
Pressa::SiteGenerator.new(site:).generate(source_path:, target_path:)
|
||||||
|
|
||||||
expect(File.exist?(File.join(target_path, "visible.txt"))).to be(true)
|
assert(File.exist?(File.join(target_path, "visible.txt")))
|
||||||
expect(File.exist?(File.join(target_path, ".DS_Store"))).to be(false)
|
refute(File.exist?(File.join(target_path, ".DS_Store")))
|
||||||
expect(File.exist?(File.join(target_path, ".gitkeep"))).to be(false)
|
refute(File.exist?(File.join(target_path, ".gitkeep")))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
require_relative "../lib/pressa"
|
|
||||||
|
|
||||||
RSpec.configure do |config|
|
|
||||||
config.expect_with :rspec do |expectations|
|
|
||||||
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
|
||||||
end
|
|
||||||
|
|
||||||
config.mock_with :rspec do |mocks|
|
|
||||||
mocks.verify_partial_doubles = true
|
|
||||||
end
|
|
||||||
|
|
||||||
config.shared_context_metadata_behavior = :apply_to_host_groups
|
|
||||||
config.filter_run_when_matching :focus
|
|
||||||
config.example_status_persistence_file_path = "spec/examples.txt"
|
|
||||||
config.disable_monkey_patching!
|
|
||||||
config.warnings = true
|
|
||||||
|
|
||||||
if config.files_to_run.one?
|
|
||||||
config.default_formatter = "doc"
|
|
||||||
end
|
|
||||||
|
|
||||||
config.profile_examples = 10
|
|
||||||
config.order = :random
|
|
||||||
Kernel.srand config.seed
|
|
||||||
end
|
|
||||||
2
spec/test_helper.rb
Normal file
2
spec/test_helper.rb
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
require_relative "../lib/pressa"
|
||||||
|
require "minitest/autorun"
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
require "spec_helper"
|
require "test_helper"
|
||||||
|
|
||||||
RSpec.describe Pressa::Views::Layout do
|
class Pressa::Views::LayoutTest < Minitest::Test
|
||||||
let(:test_content_view) do
|
def test_content_view
|
||||||
Class.new(Phlex::HTML) do
|
Class.new(Phlex::HTML) do
|
||||||
def view_template
|
def view_template
|
||||||
article do
|
article do
|
||||||
|
|
@ -11,8 +11,8 @@ RSpec.describe Pressa::Views::Layout do
|
||||||
end.new
|
end.new
|
||||||
end
|
end
|
||||||
|
|
||||||
let(:site) do
|
def site
|
||||||
Pressa::Site.new(
|
@site ||= Pressa::Site.new(
|
||||||
author: "Sami Samhuri",
|
author: "Sami Samhuri",
|
||||||
email: "sami@samhuri.net",
|
email: "sami@samhuri.net",
|
||||||
title: "samhuri.net",
|
title: "samhuri.net",
|
||||||
|
|
@ -21,31 +21,31 @@ RSpec.describe Pressa::Views::Layout do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "renders child components as HTML instead of escaped text" do
|
def test_rendering_child_components_as_html_instead_of_escaped_text
|
||||||
html = described_class.new(
|
html = Pressa::Views::Layout.new(
|
||||||
site:,
|
site:,
|
||||||
canonical_url: "https://samhuri.net/posts/",
|
canonical_url: "https://samhuri.net/posts/",
|
||||||
content: test_content_view
|
content: test_content_view
|
||||||
).call
|
).call
|
||||||
|
|
||||||
expect(html).to include("<article>")
|
assert_includes(html, "<article>")
|
||||||
expect(html).to include("<h1>Hello</h1>")
|
assert_includes(html, "<h1>Hello</h1>")
|
||||||
expect(html).not_to include("<article>")
|
refute_includes(html, "<article>")
|
||||||
end
|
end
|
||||||
|
|
||||||
it "keeps escaping enabled for untrusted string fields" do
|
def test_keeps_escaping_enabled_for_untrusted_string_fields
|
||||||
subtitle = "<img src=x onerror=alert(1)>"
|
subtitle = "<img src=x onerror=alert(1)>"
|
||||||
html = described_class.new(
|
html = Pressa::Views::Layout.new(
|
||||||
site:,
|
site:,
|
||||||
canonical_url: "https://samhuri.net/posts/",
|
canonical_url: "https://samhuri.net/posts/",
|
||||||
page_subtitle: subtitle,
|
page_subtitle: subtitle,
|
||||||
content: test_content_view
|
content: test_content_view
|
||||||
).call
|
).call
|
||||||
|
|
||||||
expect(html).to include("<title>samhuri.net: <img src=x onerror=alert(1)></title>")
|
assert_includes(html, "<title>samhuri.net: <img src=x onerror=alert(1)></title>")
|
||||||
end
|
end
|
||||||
|
|
||||||
it "preserves absolute stylesheet URLs" do
|
def test_preserves_absolute_stylesheet_urls
|
||||||
cdn_site = Pressa::Site.new(
|
cdn_site = Pressa::Site.new(
|
||||||
author: "Sami Samhuri",
|
author: "Sami Samhuri",
|
||||||
email: "sami@samhuri.net",
|
email: "sami@samhuri.net",
|
||||||
|
|
@ -55,12 +55,12 @@ RSpec.describe Pressa::Views::Layout do
|
||||||
styles: [Pressa::Stylesheet.new(href: "https://cdn.example.com/site.css")]
|
styles: [Pressa::Stylesheet.new(href: "https://cdn.example.com/site.css")]
|
||||||
)
|
)
|
||||||
|
|
||||||
html = described_class.new(
|
html = Pressa::Views::Layout.new(
|
||||||
site: cdn_site,
|
site: cdn_site,
|
||||||
canonical_url: "https://samhuri.net/posts/",
|
canonical_url: "https://samhuri.net/posts/",
|
||||||
content: test_content_view
|
content: test_content_view
|
||||||
).call
|
).call
|
||||||
|
|
||||||
expect(html).to include(%(<link rel="stylesheet" type="text/css" href="https://cdn.example.com/site.css">))
|
assert_includes(html, %(<link rel="stylesheet" type="text/css" href="https://cdn.example.com/site.css">))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
Loading…
Reference in a new issue