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.
95 lines
3.4 KiB
Ruby
95 lines
3.4 KiB
Ruby
ENV["RACK_ENV"] = "test"
|
|
ENV["LIVETRACK_WEBHOOK_SECRET"] ||= "test-webhook-secret"
|
|
|
|
require "fileutils"
|
|
require "tmpdir"
|
|
require "minitest/autorun"
|
|
require "rack"
|
|
require "json"
|
|
|
|
# A fresh throwaway directory per test run, not the real data/ — every
|
|
# GpxStore/LiveTrackStore write app.rb's routes make during the run lands
|
|
# here and is gone when the process exits, so running the suite never
|
|
# touches (or needs to clean up after itself in) real uploaded data.
|
|
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"
|
|
|
|
# Available in every test via Minitest::Test below — fixture_path/json_body
|
|
# are generic enough to be worth having everywhere, not just in app.rb's
|
|
# own HTTP-level tests (see AppTestHelper for those).
|
|
module TestHelpers
|
|
def fixture_path(name)
|
|
File.join(__dir__, "fixtures", name)
|
|
end
|
|
|
|
def json_body(response)
|
|
JSON.parse(response.body)
|
|
end
|
|
end
|
|
|
|
class Minitest::Test
|
|
include TestHelpers
|
|
end
|
|
|
|
# Only included where needed (test_app.rb) — drives app.rb's Sinatra routes
|
|
# through Rack::MockRequest, the same way every ad-hoc verification during
|
|
# development did.
|
|
module AppTestHelper
|
|
def app
|
|
Maptool::App
|
|
end
|
|
|
|
def request
|
|
@request ||= Rack::MockRequest.new(app)
|
|
end
|
|
|
|
# Sinatra's session is read from env["rack.session"] (populated by the
|
|
# Rack::Session::Cookie middleware on a real request) — setting it
|
|
# directly here is equivalent to already having a valid session cookie,
|
|
# without needing to drive the actual Gitea OAuth2 login flow.
|
|
def logged_in_env(user: "testuser")
|
|
{ "rack.session" => { user: user } }
|
|
end
|
|
|
|
# GPX_STORE/LIVETRACK_STORE are process-wide constants pointing at the
|
|
# one throwaway MAPTOOL_DATA_DIR (see above) — without this, uploads from
|
|
# one test would still be there (and show up in tree/list assertions) in
|
|
# the next one, since minitest runs a class's tests in one process.
|
|
def teardown
|
|
FileUtils.rm_rf(Dir.glob(File.join(Maptool::App::DATA_DIR, "gpx", "*")))
|
|
File.write(File.join(Maptool::App::DATA_DIR, "livetrack.json"), "[]")
|
|
super
|
|
end
|
|
|
|
def multipart_body(boundary, filename, content, content_type: "application/octet-stream", field_name: "file")
|
|
"--#{boundary}\r\n" \
|
|
"Content-Disposition: form-data; name=\"#{field_name}\"; filename=\"#{filename}\"\r\n" \
|
|
"Content-Type: #{content_type}\r\n\r\n" \
|
|
"#{content}\r\n" \
|
|
"--#{boundary}--\r\n"
|
|
end
|
|
|
|
# POSTs a single file as a multipart/form-data upload; pass folder in
|
|
# `path`'s query string (e.g. "/api/gpx?folder=trips") the same way the
|
|
# real upload form does.
|
|
def post_file(path, filename:, content:, content_type: "application/octet-stream", env: {})
|
|
boundary = "TestBoundary#{rand(1_000_000)}"
|
|
body = multipart_body(boundary, filename, content, content_type: content_type)
|
|
request.post(
|
|
path,
|
|
input: body,
|
|
"CONTENT_TYPE" => "multipart/form-data; boundary=#{boundary}",
|
|
"CONTENT_LENGTH" => body.bytesize.to_s,
|
|
**env,
|
|
)
|
|
end
|
|
end
|