GpxStore#tree now reports a "cached" flag per file (reusing the same mtime freshness check geojson() already does); the file tree shows a small curly-braces icon next to any file whose conversion is already on disk, so it's visible which files will toggle on instantly versus which still need to parse.
156 lines
5.4 KiB
Ruby
156 lines
5.4 KiB
Ruby
require_relative "test_helper"
|
|
require "maptool/gpx_store"
|
|
require "stringio"
|
|
|
|
class GpxStoreTest < Minitest::Test
|
|
def setup
|
|
@store = Maptool::GpxStore.new(Dir.mktmpdir)
|
|
end
|
|
|
|
def test_save_returns_the_relative_path_and_lists_it_in_the_tree
|
|
path = @store.save(folder: "", filename: "ride.gpx", io: StringIO.new("<gpx></gpx>"))
|
|
|
|
assert_equal "ride.gpx", path
|
|
assert_equal ["ride.gpx"], @store.tree["children"].map { |c| c["name"] }
|
|
end
|
|
|
|
def test_fit_extension_is_preserved_not_forced_to_gpx
|
|
path = @store.save(folder: "", filename: "activity.fit", io: StringIO.new("binary"))
|
|
assert_equal "activity.fit", path
|
|
end
|
|
|
|
def test_unrecognized_extension_gets_gpx_appended
|
|
path = @store.save(folder: "", filename: "notes.txt", io: StringIO.new("x"))
|
|
assert_equal "notes.txt.gpx", path
|
|
end
|
|
|
|
def test_colliding_filenames_are_disambiguated
|
|
@store.save(folder: "", filename: "ride.gpx", io: StringIO.new("a"))
|
|
path = @store.save(folder: "", filename: "ride.gpx", io: StringIO.new("b"))
|
|
|
|
assert_equal "ride-1.gpx", path
|
|
end
|
|
|
|
def test_save_creates_the_target_folder
|
|
@store.save(folder: "trips/2026", filename: "ride.gpx", io: StringIO.new("x"))
|
|
|
|
folder = @store.tree["children"].find { |c| c["name"] == "trips" }["children"].find { |c| c["name"] == "2026" }
|
|
assert_equal ["ride.gpx"], folder["children"].map { |c| c["name"] }
|
|
end
|
|
|
|
def test_path_traversal_is_rejected
|
|
assert_raises(Maptool::GpxStore::InvalidPath) { @store.resolve_file("../../etc/passwd") }
|
|
assert_raises(Maptool::GpxStore::InvalidPath) { @store.save(folder: "../escape", filename: "x.gpx", io: StringIO.new("x")) }
|
|
end
|
|
|
|
def test_delete_file
|
|
@store.save(folder: "", filename: "ride.gpx", io: StringIO.new("x"))
|
|
@store.delete_file("ride.gpx")
|
|
|
|
assert_empty @store.tree["children"]
|
|
end
|
|
|
|
def test_delete_missing_file_raises_enoent
|
|
assert_raises(Errno::ENOENT) { @store.delete_file("nope.gpx") }
|
|
end
|
|
|
|
def test_delete_folder_refuses_the_store_root
|
|
assert_raises(Maptool::GpxStore::InvalidPath) { @store.delete_folder("") }
|
|
end
|
|
|
|
def test_move_relocates_and_disambiguates
|
|
@store.save(folder: "", filename: "ride.gpx", io: StringIO.new("a"))
|
|
@store.save(folder: "archive", filename: "ride.gpx", io: StringIO.new("b"))
|
|
|
|
new_path = @store.move(from: "ride.gpx", to_folder: "archive")
|
|
|
|
assert_equal "archive/ride-1.gpx", new_path
|
|
assert_empty @store.tree["children"].reject { |c| c["name"] == "archive" }
|
|
end
|
|
|
|
def test_save_eagerly_caches_the_geojson_conversion
|
|
path = @store.save(folder: "", filename: "ride.gpx", io: StringIO.new(sample_gpx))
|
|
cache_path = @store.resolve_file(path) + ".geojson"
|
|
assert File.file?(cache_path)
|
|
|
|
first = @store.geojson(path)
|
|
|
|
# Corrupt the cache directly to prove the call above reused it instead of
|
|
# re-parsing the (still-valid) source file.
|
|
File.write(cache_path, first.sub("waypoint", "cached-waypoint"))
|
|
assert_includes @store.geojson(path), "cached-waypoint"
|
|
end
|
|
|
|
def test_save_of_an_unparseable_file_still_succeeds
|
|
path = @store.save(folder: "", filename: "bad.gpx", io: StringIO.new("not gpx at all"))
|
|
|
|
refute File.file?(@store.resolve_file(path) + ".geojson")
|
|
assert_raises(Maptool::Gpx::ParseError) { @store.geojson(path) }
|
|
end
|
|
|
|
def test_geojson_cache_is_invalidated_when_the_file_is_resaved
|
|
path = @store.save(folder: "", filename: "ride.gpx", io: StringIO.new(sample_gpx))
|
|
@store.geojson(path)
|
|
|
|
File.write(@store.resolve_file(path), sample_gpx.sub("Waypoint", "Renamed"))
|
|
FileUtils.touch(@store.resolve_file(path), mtime: Time.now + 1)
|
|
|
|
assert_includes @store.geojson(path), "Renamed"
|
|
end
|
|
|
|
def test_delete_file_removes_the_geojson_cache_too
|
|
path = @store.save(folder: "", filename: "ride.gpx", io: StringIO.new(sample_gpx))
|
|
@store.geojson(path)
|
|
cache_path = @store.resolve_file(path) + ".geojson"
|
|
|
|
@store.delete_file(path)
|
|
|
|
refute File.file?(cache_path)
|
|
end
|
|
|
|
def test_move_relocates_the_geojson_cache_too
|
|
path = @store.save(folder: "", filename: "ride.gpx", io: StringIO.new(sample_gpx))
|
|
@store.geojson(path)
|
|
|
|
new_path = @store.move(from: path, to_folder: "archive")
|
|
|
|
assert File.file?(@store.resolve_file(new_path) + ".geojson")
|
|
end
|
|
|
|
def test_geojson_raises_enoent_for_a_missing_file
|
|
assert_raises(Errno::ENOENT) { @store.geojson("nope.gpx") }
|
|
end
|
|
|
|
def test_tree_reports_cached_true_after_a_successful_save
|
|
@store.save(folder: "", filename: "ride.gpx", io: StringIO.new(sample_gpx))
|
|
|
|
file = @store.tree["children"].find { |c| c["name"] == "ride.gpx" }
|
|
assert file["cached"]
|
|
end
|
|
|
|
def test_tree_reports_cached_false_for_an_unparseable_file
|
|
@store.save(folder: "", filename: "bad.gpx", io: StringIO.new("not gpx at all"))
|
|
|
|
file = @store.tree["children"].find { |c| c["name"] == "bad.gpx" }
|
|
refute file["cached"]
|
|
end
|
|
|
|
def test_tree_reports_cached_false_once_the_source_is_resaved
|
|
path = @store.save(folder: "", filename: "ride.gpx", io: StringIO.new(sample_gpx))
|
|
FileUtils.touch(@store.resolve_file(path) + ".geojson", mtime: Time.now - 10)
|
|
File.write(@store.resolve_file(path), sample_gpx)
|
|
FileUtils.touch(@store.resolve_file(path), mtime: Time.now)
|
|
|
|
file = @store.tree["children"].find { |c| c["name"] == "ride.gpx" }
|
|
refute file["cached"]
|
|
end
|
|
|
|
private
|
|
|
|
def sample_gpx
|
|
<<~GPX
|
|
<gpx><wpt lat="48.1" lon="17.1"><name>Waypoint</name></wpt></gpx>
|
|
GPX
|
|
end
|
|
end
|