Add an import plugin system, with the Overpass POI query as its first plugin
CI / check (push) Successful in 40s
CI / deploy (push) Successful in 13s

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.
This commit is contained in:
randy committed 2026-09-09 16:23:31 +02:00
1 parent 2b49eda7cb
commit 72ffd0ef18
17 files changed
+434 -14

No files matched your search

+4
View File
@@ -0,0 +1,4 @@
{
"label": "Test plugin",
"command": "./run.sh"
}
+5
View File
@@ -0,0 +1,5 @@
#!/bin/sh
# A fixture plugin for test_app.rb's /api/plugins route tests — no
# network, just echoes a single fixed point back as GeoJSON so those tests
# stay deterministic and offline like the rest of the suite.
echo '{"type":"FeatureCollection","features":[{"type":"Feature","properties":{"name":"Test POI"},"geometry":{"type":"Point","coordinates":[17.0,48.0]}}]}'
+37
View File
@@ -49,6 +49,7 @@ class AppTest < Minitest::Test
assert_equal 401, request.delete("/api/gpx/file/x.gpx").status
assert_equal 401, request.delete("/api/gpx/folder/x").status
assert_equal 401, request.post("/api/gpx/move").status
assert_equal 401, request.post("/api/plugins/test-plugin/run").status
end
# --- GPX/FIT upload and the tree ---
@@ -290,6 +291,42 @@ class AppTest < Minitest::Test
assert_equal 404, response.status
end
# --- Import plugins (lib/maptool/plugins.rb) ---
def test_plugins_lists_whatever_is_on_disk
response = request.get("/api/plugins")
assert_equal 200, response.status
assert_equal [{ "name" => "test-plugin", "label" => "Test plugin" }], json_body(response)
end
def test_plugins_run_saves_the_output_as_a_new_geojson_file
response = request.post(
"/api/plugins/test-plugin/run",
params: { south: "48.0", west: "17.0", north: "48.1", east: "17.1" },
**logged_in_env,
)
assert_equal 200, response.status
body = json_body(response)
assert_equal "test-plugin.geojson", body["path"]
saved = json_body(request.get("/api/gpx/file/#{body["path"]}"))
assert_equal "Test POI", saved["features"][0]["properties"]["name"]
end
def test_plugins_run_requires_a_bbox
response = request.post("/api/plugins/test-plugin/run", **logged_in_env)
assert_equal 400, response.status
end
def test_plugins_run_unknown_plugin_is_502
response = request.post(
"/api/plugins/nope/run",
params: { south: "48.0", west: "17.0", north: "48.1", east: "17.1" },
**logged_in_env,
)
assert_equal 502, response.status
end
# --- LiveTrack webhook ---
def test_livetrack_webhook_requires_the_configured_secret
+5
View File
@@ -14,6 +14,11 @@ require "json"
ENV["MAPTOOL_DATA_DIR"] = Dir.mktmpdir("maptool-test-data")
Minitest.after_run { FileUtils.remove_entry(ENV["MAPTOOL_DATA_DIR"]) }
# A fixture plugin (test/fixtures/plugins/test-plugin), not the real
# plugins/ — keeps /api/plugins route tests offline/deterministic instead
# of actually running plugins/overpass-pois against the real Overpass API.
ENV["MAPTOOL_PLUGINS_DIR"] = File.join(__dir__, "fixtures", "plugins")
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
require_relative "../app"
+55
View File
@@ -0,0 +1,55 @@
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