A plugin is a directory under plugins/ with a plugin.json manifest
({"label": "...", "command": "./run.sh"}) plus a script that reads
--south/--west/--north/--east and prints a GeoJSON FeatureCollection
to stdout (lib/maptool/plugins.rb) — that's the entire interface a
plugin needs to know about this app. The sidebar's new import icon
(logged in only, GET /api/plugins + POST /api/plugins/:name/run) lists
whatever's found and runs one for the current map viewport, saving its
output as a new .geojson file the same way an upload would be.
The point: a scraper for a site whose ToS forbids that (Park4Night,
iOverlander, Freecampsites) doesn't need to live in this public repo
to work here — its own directory just goes under plugins/ locally and
is never git add'ed, no special-casing needed anywhere.
plugins/overpass-pois wraps the existing rake overpass:pois query as
the first, in-repo, reference example of the contract.
Also fills in four modules (auth.js, locate.js, measure.js, notes.js)
missing from layout.erb's cache-busting importmap alongside adding
plugins.js's own entry — a pre-existing gap that meant they could keep
serving a stale cached copy across a deploy despite the importmap
existing specifically to prevent that.
56 lines
2.1 KiB
Ruby
56 lines
2.1 KiB
Ruby
require_relative "test_helper"
|
|
require "maptool/plugins"
|
|
require "tmpdir"
|
|
require "fileutils"
|
|
|
|
class PluginsTest < Minitest::Test
|
|
def setup
|
|
@dir = Dir.mktmpdir("maptool-plugins-test")
|
|
@plugin_dir = File.join(@dir, "hello")
|
|
FileUtils.mkdir_p(@plugin_dir)
|
|
File.write(File.join(@plugin_dir, "plugin.json"), JSON.generate(label: "Hello", command: "./run.sh"))
|
|
File.write(File.join(@plugin_dir, "run.sh"), "#!/bin/sh\necho '{\"type\":\"FeatureCollection\",\"features\":[]}'\n")
|
|
File.chmod(0o755, File.join(@plugin_dir, "run.sh"))
|
|
end
|
|
|
|
def teardown
|
|
FileUtils.remove_entry(@dir)
|
|
end
|
|
|
|
def test_list_finds_plugins_by_their_manifest
|
|
assert_equal [{ "name" => "hello", "label" => "Hello" }], Maptool::Plugins.list(@dir)
|
|
end
|
|
|
|
def test_list_is_empty_for_a_directory_with_no_plugins
|
|
assert_empty Maptool::Plugins.list(Dir.mktmpdir)
|
|
end
|
|
|
|
def test_run_returns_the_plugins_geojson_output
|
|
result = Maptool::Plugins.run(@dir, "hello", [1, 2, 3, 4])
|
|
assert_equal({ "type" => "FeatureCollection", "features" => [] }, result)
|
|
end
|
|
|
|
def test_run_raises_for_an_unknown_plugin
|
|
assert_raises(Maptool::Plugins::RunError) { Maptool::Plugins.run(@dir, "nope", [1, 2, 3, 4]) }
|
|
end
|
|
|
|
# A directory-name-shaped ".." would otherwise resolve one level above
|
|
# dir via File.join — the same class of path-escape GpxStore guards
|
|
# against for uploaded file paths.
|
|
def test_run_rejects_a_path_traversal_attempt_as_the_name
|
|
assert_raises(Maptool::Plugins::RunError) { Maptool::Plugins.run(@dir, "..", [1, 2, 3, 4]) }
|
|
end
|
|
|
|
def test_run_raises_when_the_script_exits_non_zero
|
|
File.write(File.join(@plugin_dir, "run.sh"), "#!/bin/sh\nexit 1\n")
|
|
File.chmod(0o755, File.join(@plugin_dir, "run.sh"))
|
|
assert_raises(Maptool::Plugins::RunError) { Maptool::Plugins.run(@dir, "hello", [1, 2, 3, 4]) }
|
|
end
|
|
|
|
def test_run_raises_when_the_script_does_not_print_a_feature_collection
|
|
File.write(File.join(@plugin_dir, "run.sh"), "#!/bin/sh\necho 'not json'\n")
|
|
File.chmod(0o755, File.join(@plugin_dir, "run.sh"))
|
|
assert_raises(Maptool::Plugins::RunError) { Maptool::Plugins.run(@dir, "hello", [1, 2, 3, 4]) }
|
|
end
|
|
end
|