Merge remote-tracking branch 'upstream/pull/6171'

This commit is contained in:
Tom Hughes
2025-07-07 17:28:38 +01:00
3 changed files with 491 additions and 504 deletions

View File

@ -373,71 +373,6 @@ module Api
# Test updating relations
# ------------------------------------
##
# test that, when tags are updated on a relation, the correct things
# happen to the correct tables and the API gives sensible results.
# this is to test a case that gregory marler noticed and posted to
# josm-dev.
## FIXME Move this to an integration test
def test_update_relation_tags
user = create(:user)
changeset = create(:changeset, :user => user)
relation = create(:relation)
create_list(:relation_tag, 4, :relation => relation)
auth_header = bearer_authorization_header user
with_relation(relation.id) do |rel|
# alter one of the tags
tag = rel.find("//osm/relation/tag").first
tag["v"] = "some changed value"
update_changeset(rel, changeset.id)
# check that the downloaded tags are the same as the uploaded tags...
new_version = with_update(rel, auth_header) do |new_rel|
assert_tags_equal rel, new_rel
end
# check the original one in the current_* table again
with_relation(relation.id) { |r| assert_tags_equal rel, r }
# now check the version in the history
with_relation(relation.id, new_version) { |r| assert_tags_equal rel, r }
end
end
##
# test that, when tags are updated on a relation when using the diff
# upload function, the correct things happen to the correct tables
# and the API gives sensible results. this is to test a case that
# gregory marler noticed and posted to josm-dev.
def test_update_relation_tags_via_upload
user = create(:user)
changeset = create(:changeset, :user => user)
relation = create(:relation)
create_list(:relation_tag, 4, :relation => relation)
auth_header = bearer_authorization_header user
with_relation(relation.id) do |rel|
# alter one of the tags
tag = rel.find("//osm/relation/tag").first
tag["v"] = "some changed value"
update_changeset(rel, changeset.id)
# check that the downloaded tags are the same as the uploaded tags...
new_version = with_update_diff(rel, auth_header) do |new_rel|
assert_tags_equal rel, new_rel
end
# check the original one in the current_* table again
with_relation(relation.id) { |r| assert_tags_equal rel, r }
# now check the version in the history
with_relation(relation.id, new_version) { |r| assert_tags_equal rel, r }
end
end
def test_update_wrong_id
user = create(:user)
changeset = create(:changeset, :user => user)
@ -445,11 +380,13 @@ module Api
other_relation = create(:relation)
auth_header = bearer_authorization_header user
with_relation(relation.id) do |rel|
update_changeset(rel, changeset.id)
put api_relation_path(other_relation), :params => rel.to_s, :headers => auth_header
assert_response :bad_request
end
get api_relation_path(relation)
assert_response :success
rel = XML::Parser.string(@response.body).parse
update_changeset(rel, changeset.id)
put api_relation_path(other_relation), :params => rel.to_s, :headers => auth_header
assert_response :bad_request
end
# -------------------------------------
@ -630,276 +567,6 @@ module Api
assert_response :not_found
end
##
# when a relation's tag is modified then it should put the bounding
# box of all its members into the changeset.
def test_tag_modify_bounding_box
relation = create(:relation)
node1 = create(:node, :lat => 0.3, :lon => 0.3)
node2 = create(:node, :lat => 0.5, :lon => 0.5)
way = create(:way)
create(:way_node, :way => way, :node => node1)
create(:relation_member, :relation => relation, :member => way)
create(:relation_member, :relation => relation, :member => node2)
# the relation contains nodes1 and node2 (node1
# indirectly via the way), so the bbox should be [0.3,0.3,0.5,0.5].
check_changeset_modify(BoundingBox.new(0.3, 0.3, 0.5, 0.5)) do |changeset_id, auth_header|
# add a tag to an existing relation
relation_xml = xml_for_relation(relation)
relation_element = relation_xml.find("//osm/relation").first
new_tag = XML::Node.new("tag")
new_tag["k"] = "some_new_tag"
new_tag["v"] = "some_new_value"
relation_element << new_tag
# update changeset ID to point to new changeset
update_changeset(relation_xml, changeset_id)
# upload the change
put api_relation_path(relation), :params => relation_xml.to_s, :headers => auth_header
assert_response :success, "can't update relation for tag/bbox test"
end
end
##
# add a member to a relation and check the bounding box is only that
# element.
def test_add_member_bounding_box
relation = create(:relation)
node1 = create(:node, :lat => 4, :lon => 4)
node2 = create(:node, :lat => 7, :lon => 7)
way1 = create(:way)
create(:way_node, :way => way1, :node => create(:node, :lat => 8, :lon => 8))
way2 = create(:way)
create(:way_node, :way => way2, :node => create(:node, :lat => 9, :lon => 9), :sequence_id => 1)
create(:way_node, :way => way2, :node => create(:node, :lat => 10, :lon => 10), :sequence_id => 2)
[node1, node2, way1, way2].each do |element|
bbox = element.bbox.to_unscaled
check_changeset_modify(bbox) do |changeset_id, auth_header|
relation_xml = xml_for_relation(Relation.find(relation.id))
relation_element = relation_xml.find("//osm/relation").first
new_member = XML::Node.new("member")
new_member["ref"] = element.id.to_s
new_member["type"] = element.class.to_s.downcase
new_member["role"] = "some_role"
relation_element << new_member
# update changeset ID to point to new changeset
update_changeset(relation_xml, changeset_id)
# upload the change
put api_relation_path(relation), :params => relation_xml.to_s, :headers => auth_header
assert_response :success, "can't update relation for add #{element.class}/bbox test: #{@response.body}"
# get it back and check the ordering
get api_relation_path(relation)
assert_response :success, "can't read back the relation: #{@response.body}"
check_ordering(relation_xml, @response.body)
end
end
end
##
# remove a member from a relation and check the bounding box is
# only that element.
def test_remove_member_bounding_box
relation = create(:relation)
node1 = create(:node, :lat => 3, :lon => 3)
node2 = create(:node, :lat => 5, :lon => 5)
create(:relation_member, :relation => relation, :member => node1)
create(:relation_member, :relation => relation, :member => node2)
check_changeset_modify(BoundingBox.new(5, 5, 5, 5)) do |changeset_id, auth_header|
# remove node 5 (5,5) from an existing relation
relation_xml = xml_for_relation(relation)
relation_xml
.find("//osm/relation/member[@type='node'][@ref='#{node2.id}']")
.first.remove!
# update changeset ID to point to new changeset
update_changeset(relation_xml, changeset_id)
# upload the change
put api_relation_path(relation), :params => relation_xml.to_s, :headers => auth_header
assert_response :success, "can't update relation for remove node/bbox test"
end
end
##
# check that relations are ordered
def test_relation_member_ordering
user = create(:user)
changeset = create(:changeset, :user => user)
node1 = create(:node)
node2 = create(:node)
node3 = create(:node)
way1 = create(:way_with_nodes, :nodes_count => 2)
way2 = create(:way_with_nodes, :nodes_count => 2)
auth_header = bearer_authorization_header user
doc_str = <<~OSM
<osm>
<relation changeset='#{changeset.id}'>
<member ref='#{node1.id}' type='node' role='first'/>
<member ref='#{node2.id}' type='node' role='second'/>
<member ref='#{way1.id}' type='way' role='third'/>
<member ref='#{way2.id}' type='way' role='fourth'/>
</relation>
</osm>
OSM
doc = XML::Parser.string(doc_str).parse
post api_relations_path, :params => doc.to_s, :headers => auth_header
assert_response :success, "can't create a relation: #{@response.body}"
relation_id = @response.body.to_i
# get it back and check the ordering
get api_relation_path(relation_id)
assert_response :success, "can't read back the relation: #{@response.body}"
check_ordering(doc, @response.body)
# insert a member at the front
new_member = XML::Node.new "member"
new_member["ref"] = node3.id.to_s
new_member["type"] = "node"
new_member["role"] = "new first"
doc.find("//osm/relation").first.child.prev = new_member
# update the version, should be 1?
doc.find("//osm/relation").first["id"] = relation_id.to_s
doc.find("//osm/relation").first["version"] = 1.to_s
# upload the next version of the relation
put api_relation_path(relation_id), :params => doc.to_s, :headers => auth_header
assert_response :success, "can't update relation: #{@response.body}"
assert_equal 2, @response.body.to_i
# get it back again and check the ordering again
get api_relation_path(relation_id)
assert_response :success, "can't read back the relation: #{@response.body}"
check_ordering(doc, @response.body)
# check the ordering in the history tables:
with_controller(OldRelationsController.new) do
get api_relation_version_path(relation_id, 2)
assert_response :success, "can't read back version 2 of the relation #{relation_id}"
check_ordering(doc, @response.body)
end
end
##
# check that relations can contain duplicate members
def test_relation_member_duplicates
private_user = create(:user, :data_public => false)
user = create(:user)
changeset = create(:changeset, :user => user)
node1 = create(:node)
node2 = create(:node)
doc_str = <<~OSM
<osm>
<relation changeset='#{changeset.id}'>
<member ref='#{node1.id}' type='node' role='forward'/>
<member ref='#{node2.id}' type='node' role='forward'/>
<member ref='#{node1.id}' type='node' role='forward'/>
<member ref='#{node2.id}' type='node' role='forward'/>
</relation>
</osm>
OSM
doc = XML::Parser.string(doc_str).parse
## First try with the private user
auth_header = bearer_authorization_header private_user
post api_relations_path, :params => doc.to_s, :headers => auth_header
assert_response :forbidden
## Now try with the public user
auth_header = bearer_authorization_header user
post api_relations_path, :params => doc.to_s, :headers => auth_header
assert_response :success, "can't create a relation: #{@response.body}"
relation_id = @response.body.to_i
# get it back and check the ordering
get api_relation_path(relation_id)
assert_response :success, "can't read back the relation: #{relation_id}"
check_ordering(doc, @response.body)
end
##
# test that the ordering of elements in the history is the same as in current.
def test_history_ordering
user = create(:user)
changeset = create(:changeset, :user => user)
node1 = create(:node)
node2 = create(:node)
node3 = create(:node)
node4 = create(:node)
doc_str = <<~OSM
<osm>
<relation changeset='#{changeset.id}'>
<member ref='#{node1.id}' type='node' role='forward'/>
<member ref='#{node4.id}' type='node' role='forward'/>
<member ref='#{node3.id}' type='node' role='forward'/>
<member ref='#{node2.id}' type='node' role='forward'/>
</relation>
</osm>
OSM
doc = XML::Parser.string(doc_str).parse
auth_header = bearer_authorization_header user
post api_relations_path, :params => doc.to_s, :headers => auth_header
assert_response :success, "can't create a relation: #{@response.body}"
relation_id = @response.body.to_i
# check the ordering in the current tables:
get api_relation_path(relation_id)
assert_response :success, "can't read back the relation: #{@response.body}"
check_ordering(doc, @response.body)
# check the ordering in the history tables:
with_controller(OldRelationsController.new) do
get api_relation_version_path(relation_id, 1)
assert_response :success, "can't read back version 1 of the relation: #{@response.body}"
check_ordering(doc, @response.body)
end
end
##
# remove all the members from a relation. the result is pretty useless, but
# still technically valid.
def test_remove_all_members
relation = create(:relation)
node1 = create(:node, :lat => 0.3, :lon => 0.3)
node2 = create(:node, :lat => 0.5, :lon => 0.5)
way = create(:way)
create(:way_node, :way => way, :node => node1)
create(:relation_member, :relation => relation, :member => way)
create(:relation_member, :relation => relation, :member => node2)
check_changeset_modify(BoundingBox.new(0.3, 0.3, 0.5, 0.5)) do |changeset_id, auth_header|
relation_xml = xml_for_relation(relation)
relation_xml
.find("//osm/relation/member")
.each(&:remove!)
# update changeset ID to point to new changeset
update_changeset(relation_xml, changeset_id)
# upload the change
put api_relation_path(relation), :params => relation_xml.to_s, :headers => auth_header
assert_response :success, "can't update relation for remove all members test"
checkrelation = Relation.find(relation.id)
assert_not_nil(checkrelation,
"uploaded relation not found in database after upload")
assert_equal(0, checkrelation.members.length,
"relation contains members but they should have all been deleted")
end
end
##
# test initial rate limit
def test_initial_rate_limit
@ -1013,160 +680,6 @@ module Api
private
##
# checks that the XML document and the string arguments have
# members in the same order.
def check_ordering(doc, xml)
new_doc = XML::Parser.string(xml).parse
doc_members = doc.find("//osm/relation/member").collect do |m|
[m["ref"].to_i, m["type"].to_sym, m["role"]]
end
new_members = new_doc.find("//osm/relation/member").collect do |m|
[m["ref"].to_i, m["type"].to_sym, m["role"]]
end
doc_members.zip(new_members).each do |d, n|
assert_equal d, n, "members are not equal - ordering is wrong? (#{doc}, #{xml})"
end
end
##
# create a changeset and yield to the caller to set it up, then assert
# that the changeset bounding box is +bbox+.
def check_changeset_modify(bbox)
## First test with the private user to check that you get a forbidden
auth_header = bearer_authorization_header create(:user, :data_public => false)
# create a new changeset for this operation, so we are assured
# that the bounding box will be newly-generated.
with_controller(Api::ChangesetsController.new) do
xml = "<osm><changeset/></osm>"
post api_changesets_path, :params => xml, :headers => auth_header
assert_response :forbidden, "shouldn't be able to create changeset for modify test, as should get forbidden"
end
## Now do the whole thing with the public user
auth_header = bearer_authorization_header
# create a new changeset for this operation, so we are assured
# that the bounding box will be newly-generated.
changeset_id = with_controller(Api::ChangesetsController.new) do
xml = "<osm><changeset/></osm>"
post api_changesets_path, :params => xml, :headers => auth_header
assert_response :success, "couldn't create changeset for modify test"
@response.body.to_i
end
# go back to the block to do the actual modifies
yield changeset_id, auth_header
# now download the changeset to check its bounding box
with_controller(Api::ChangesetsController.new) do
get api_changeset_path(changeset_id)
assert_response :success, "can't re-read changeset for modify test"
assert_select "osm>changeset", 1, "Changeset element doesn't exist in #{@response.body}"
assert_select "osm>changeset[id='#{changeset_id}']", 1, "Changeset id=#{changeset_id} doesn't exist in #{@response.body}"
assert_select "osm>changeset[min_lon='#{format('%<lon>.7f', :lon => bbox.min_lon)}']", 1, "Changeset min_lon wrong in #{@response.body}"
assert_select "osm>changeset[min_lat='#{format('%<lat>.7f', :lat => bbox.min_lat)}']", 1, "Changeset min_lat wrong in #{@response.body}"
assert_select "osm>changeset[max_lon='#{format('%<lon>.7f', :lon => bbox.max_lon)}']", 1, "Changeset max_lon wrong in #{@response.body}"
assert_select "osm>changeset[max_lat='#{format('%<lat>.7f', :lat => bbox.max_lat)}']", 1, "Changeset max_lat wrong in #{@response.body}"
end
end
##
# yields the relation with the given +id+ (and optional +version+
# to read from the history tables) into the block. the parsed XML
# doc is returned.
def with_relation(id, ver = nil)
if ver.nil?
get api_relation_path(id)
else
with_controller(OldRelationsController.new) do
get api_relation_version_path(id, ver)
end
end
assert_response :success
yield xml_parse(@response.body)
end
##
# updates the relation (XML) +rel+ and
# yields the new version of that relation into the block.
# the parsed XML doc is returned.
def with_update(rel, headers)
rel_id = rel.find("//osm/relation").first["id"].to_i
put api_relation_path(rel_id), :params => rel.to_s, :headers => headers
assert_response :success, "can't update relation: #{@response.body}"
version = @response.body.to_i
# now get the new version
get api_relation_path(rel_id)
assert_response :success
new_rel = xml_parse(@response.body)
yield new_rel
version
end
##
# updates the relation (XML) +rel+ via the diff-upload API and
# yields the new version of that relation into the block.
# the parsed XML doc is returned.
def with_update_diff(rel, headers)
rel_id = rel.find("//osm/relation").first["id"].to_i
cs_id = rel.find("//osm/relation").first["changeset"].to_i
version = nil
with_controller(Api::ChangesetsController.new) do
doc = OSM::API.new.xml_doc
change = XML::Node.new "osmChange"
doc.root = change
modify = XML::Node.new "modify"
change << modify
modify << doc.import(rel.find("//osm/relation").first)
post api_changeset_upload_path(cs_id), :params => doc.to_s, :headers => headers
assert_response :success, "can't upload diff relation: #{@response.body}"
version = xml_parse(@response.body).find("//diffResult/relation").first["new_version"].to_i
end
# now get the new version
get api_relation_path(rel_id)
assert_response :success
new_rel = xml_parse(@response.body)
yield new_rel
version
end
##
# returns a k->v hash of tags from an xml doc
def get_tags_as_hash(a)
a.find("//osm/relation/tag").sort_by { |v| v["k"] }.each_with_object({}) do |v, h|
h[v["k"]] = v["v"]
end
end
##
# assert that all tags on relation documents +a+ and +b+
# are equal
def assert_tags_equal(a, b)
# turn the XML doc into tags hashes
a_tags = get_tags_as_hash(a)
b_tags = get_tags_as_hash(b)
assert_equal a_tags.keys, b_tags.keys, "Tag keys should be identical."
a_tags.each do |k, v|
assert_equal v, b_tags[k],
"Tags which were not altered should be the same. " \
"#{a_tags.inspect} != #{b_tags.inspect}"
end
end
##
# update the changeset_id of a node element
def update_changeset(xml, changeset_id)
@ -1179,12 +692,5 @@ module Api
xml.find("//osm/relation").first[name] = value.to_s
xml
end
##
# parse some xml
def xml_parse(xml)
parser = XML::Parser.string(xml)
parser.parse
end
end
end

View File

@ -61,18 +61,206 @@ class ChangesetBboxTest < ActionDispatch::IntegrationTest
assert_dom "osm>changeset[max_lat='0.3000000']", 1
end
##
# when a relation's tag is modified then it should put the bounding
# box of all its members into the changeset.
def test_relation_tag_modify_bounding_box
relation = create(:relation)
node1 = create(:node, :lat => 0.3, :lon => 0.3)
node2 = create(:node, :lat => 0.5, :lon => 0.5)
way = create(:way)
create(:way_node, :way => way, :node => node1)
create(:relation_member, :relation => relation, :member => way)
create(:relation_member, :relation => relation, :member => node2)
# the relation contains nodes1 and node2 (node1
# indirectly via the way), so the bbox should be [0.3,0.3,0.5,0.5].
check_changeset_modify(BoundingBox.new(0.3, 0.3, 0.5, 0.5)) do |changeset_id, auth_header|
# add a tag to an existing relation
relation_xml = xml_for_relation(relation)
relation_element = relation_xml.find("//osm/relation").first
new_tag = XML::Node.new("tag")
new_tag["k"] = "some_new_tag"
new_tag["v"] = "some_new_value"
relation_element << new_tag
# update changeset ID to point to new changeset
update_changeset(relation_xml, changeset_id)
# upload the change
put api_relation_path(relation), :params => relation_xml.to_s, :headers => auth_header
assert_response :success, "can't update relation for tag/bbox test"
end
end
##
# add a member to a relation and check the bounding box is only that
# element.
def test_relation_add_member_bounding_box
relation = create(:relation)
node1 = create(:node, :lat => 4, :lon => 4)
node2 = create(:node, :lat => 7, :lon => 7)
way1 = create(:way)
create(:way_node, :way => way1, :node => create(:node, :lat => 8, :lon => 8))
way2 = create(:way)
create(:way_node, :way => way2, :node => create(:node, :lat => 9, :lon => 9), :sequence_id => 1)
create(:way_node, :way => way2, :node => create(:node, :lat => 10, :lon => 10), :sequence_id => 2)
[node1, node2, way1, way2].each do |element|
bbox = element.bbox.to_unscaled
check_changeset_modify(bbox) do |changeset_id, auth_header|
relation_xml = xml_for_relation(Relation.find(relation.id))
relation_element = relation_xml.find("//osm/relation").first
new_member = XML::Node.new("member")
new_member["ref"] = element.id.to_s
new_member["type"] = element.class.to_s.downcase
new_member["role"] = "some_role"
relation_element << new_member
# update changeset ID to point to new changeset
update_changeset(relation_xml, changeset_id)
# upload the change
put api_relation_path(relation), :params => relation_xml.to_s, :headers => auth_header
assert_response :success, "can't update relation for add #{element.class}/bbox test: #{@response.body}"
# get it back and check the ordering
get api_relation_path(relation)
assert_members_equal_response relation_xml
end
end
end
##
# remove a member from a relation and check the bounding box is
# only that element.
def test_relation_remove_member_bounding_box
relation = create(:relation)
node1 = create(:node, :lat => 3, :lon => 3)
node2 = create(:node, :lat => 5, :lon => 5)
create(:relation_member, :relation => relation, :member => node1)
create(:relation_member, :relation => relation, :member => node2)
check_changeset_modify(BoundingBox.new(5, 5, 5, 5)) do |changeset_id, auth_header|
# remove node 5 (5,5) from an existing relation
relation_xml = xml_for_relation(relation)
relation_xml
.find("//osm/relation/member[@type='node'][@ref='#{node2.id}']")
.first.remove!
# update changeset ID to point to new changeset
update_changeset(relation_xml, changeset_id)
# upload the change
put api_relation_path(relation), :params => relation_xml.to_s, :headers => auth_header
assert_response :success, "can't update relation for remove node/bbox test"
end
end
##
# remove all the members from a relation. the result is pretty useless, but
# still technically valid.
def test_relation_remove_all_members
relation = create(:relation)
node1 = create(:node, :lat => 0.3, :lon => 0.3)
node2 = create(:node, :lat => 0.5, :lon => 0.5)
way = create(:way)
create(:way_node, :way => way, :node => node1)
create(:relation_member, :relation => relation, :member => way)
create(:relation_member, :relation => relation, :member => node2)
check_changeset_modify(BoundingBox.new(0.3, 0.3, 0.5, 0.5)) do |changeset_id, auth_header|
relation_xml = xml_for_relation(relation)
relation_xml
.find("//osm/relation/member")
.each(&:remove!)
# update changeset ID to point to new changeset
update_changeset(relation_xml, changeset_id)
# upload the change
put api_relation_path(relation), :params => relation_xml.to_s, :headers => auth_header
assert_response :success, "can't update relation for remove all members test"
checkrelation = Relation.find(relation.id)
assert_not_nil(checkrelation,
"uploaded relation not found in database after upload")
assert_equal(0, checkrelation.members.length,
"relation contains members but they should have all been deleted")
end
end
private
##
# update the changeset_id of a way element
# create a changeset and yield to the caller to set it up, then assert
# that the changeset bounding box is +bbox+.
def check_changeset_modify(bbox)
## First test with the private user to check that you get a forbidden
auth_header = bearer_authorization_header create(:user, :data_public => false)
# create a new changeset for this operation, so we are assured
# that the bounding box will be newly-generated.
with_controller(Api::ChangesetsController.new) do
xml = "<osm><changeset/></osm>"
post api_changesets_path, :params => xml, :headers => auth_header
assert_response :forbidden, "shouldn't be able to create changeset for modify test, as should get forbidden"
end
## Now do the whole thing with the public user
auth_header = bearer_authorization_header
# create a new changeset for this operation, so we are assured
# that the bounding box will be newly-generated.
changeset_id = with_controller(Api::ChangesetsController.new) do
xml = "<osm><changeset/></osm>"
post api_changesets_path, :params => xml, :headers => auth_header
assert_response :success, "couldn't create changeset for modify test"
@response.body.to_i
end
# go back to the block to do the actual modifies
yield changeset_id, auth_header
# now download the changeset to check its bounding box
with_controller(Api::ChangesetsController.new) do
get api_changeset_path(changeset_id)
assert_response :success, "can't re-read changeset for modify test"
assert_select "osm>changeset", 1, "Changeset element doesn't exist in #{@response.body}"
assert_select "osm>changeset[id='#{changeset_id}']", 1, "Changeset id=#{changeset_id} doesn't exist in #{@response.body}"
assert_select "osm>changeset[min_lon='#{format('%<lon>.7f', :lon => bbox.min_lon)}']", 1, "Changeset min_lon wrong in #{@response.body}"
assert_select "osm>changeset[min_lat='#{format('%<lat>.7f', :lat => bbox.min_lat)}']", 1, "Changeset min_lat wrong in #{@response.body}"
assert_select "osm>changeset[max_lon='#{format('%<lon>.7f', :lon => bbox.max_lon)}']", 1, "Changeset max_lon wrong in #{@response.body}"
assert_select "osm>changeset[max_lat='#{format('%<lat>.7f', :lat => bbox.max_lat)}']", 1, "Changeset max_lat wrong in #{@response.body}"
end
end
##
# checks that the XML document and the response have
# members in the same order.
def assert_members_equal_response(doc, response_message = "can't read back the relation")
assert_response :success, "#{response_message}: #{@response.body}"
new_doc = XML::Parser.string(@response.body).parse
doc_members = doc.find("//osm/relation/member").collect do |m|
[m["ref"].to_i, m["type"].to_sym, m["role"]]
end
new_members = new_doc.find("//osm/relation/member").collect do |m|
[m["ref"].to_i, m["type"].to_sym, m["role"]]
end
assert_equal doc_members, new_members, "members are not equal - ordering is wrong? (#{doc}, #{@response.body})"
end
##
# update the changeset_id of an element
def update_changeset(xml, changeset_id)
xml_attr_rewrite(xml, "changeset", changeset_id)
end
##
# update an attribute in a way element
# update an attribute in an element
def xml_attr_rewrite(xml, name, value)
xml.find("//osm/way").first[name] = value.to_s
xml.find("//osm/*[self::node or self::way or self::relation]").first[name] = value.to_s
xml
end
end

View File

@ -0,0 +1,293 @@
require "test_helper"
class RelationVersionsTest < ActionDispatch::IntegrationTest
##
# test that, when tags are updated on a relation, the correct things
# happen to the correct tables and the API gives sensible results.
# this is to test a case that gregory marler noticed and posted to
# josm-dev.
def test_update_relation_tags
user = create(:user)
changeset = create(:changeset, :user => user)
relation = create(:relation)
create_list(:relation_tag, 4, :relation => relation)
auth_header = bearer_authorization_header user
get api_relation_path(relation)
assert_response :success
rel = xml_parse(@response.body)
rel_id = rel.find("//osm/relation").first["id"].to_i
# alter one of the tags
tag = rel.find("//osm/relation/tag").first
tag["v"] = "some changed value"
update_changeset(rel, changeset.id)
put api_relation_path(rel_id), :params => rel.to_s, :headers => auth_header
assert_response :success, "can't update relation: #{@response.body}"
new_version = @response.body.to_i
# check that the downloaded tags are the same as the uploaded tags...
get api_relation_path(rel_id)
assert_tags_equal_response rel
# check the original one in the current_* table again
get api_relation_path(relation)
assert_tags_equal_response rel
# now check the version in the history
get api_relation_version_path(relation, new_version)
assert_tags_equal_response rel
end
##
# test that, when tags are updated on a relation when using the diff
# upload function, the correct things happen to the correct tables
# and the API gives sensible results. this is to test a case that
# gregory marler noticed and posted to josm-dev.
def test_update_relation_tags_via_upload
user = create(:user)
changeset = create(:changeset, :user => user)
relation = create(:relation)
create_list(:relation_tag, 4, :relation => relation)
auth_header = bearer_authorization_header user
get api_relation_path(relation)
assert_response :success
rel = xml_parse(@response.body)
rel_id = rel.find("//osm/relation").first["id"].to_i
# alter one of the tags
tag = rel.find("//osm/relation/tag").first
tag["v"] = "some changed value"
update_changeset(rel, changeset.id)
new_version = nil
with_controller(Api::ChangesetsController.new) do
doc = OSM::API.new.xml_doc
change = XML::Node.new "osmChange"
doc.root = change
modify = XML::Node.new "modify"
change << modify
modify << doc.import(rel.find("//osm/relation").first)
post api_changeset_upload_path(changeset), :params => doc.to_s, :headers => auth_header
assert_response :success, "can't upload diff relation: #{@response.body}"
new_version = xml_parse(@response.body).find("//diffResult/relation").first["new_version"].to_i
end
# check that the downloaded tags are the same as the uploaded tags...
get api_relation_path(rel_id)
assert_tags_equal_response rel
# check the original one in the current_* table again
get api_relation_path(relation)
assert_tags_equal_response rel
# now check the version in the history
get api_relation_version_path(relation, new_version)
assert_tags_equal_response rel
end
##
# check that relations are ordered
def test_relation_member_ordering
user = create(:user)
changeset = create(:changeset, :user => user)
node1 = create(:node)
node2 = create(:node)
node3 = create(:node)
way1 = create(:way_with_nodes, :nodes_count => 2)
way2 = create(:way_with_nodes, :nodes_count => 2)
auth_header = bearer_authorization_header user
doc_str = <<~OSM
<osm>
<relation changeset='#{changeset.id}'>
<member ref='#{node1.id}' type='node' role='first'/>
<member ref='#{node2.id}' type='node' role='second'/>
<member ref='#{way1.id}' type='way' role='third'/>
<member ref='#{way2.id}' type='way' role='fourth'/>
</relation>
</osm>
OSM
doc = XML::Parser.string(doc_str).parse
post api_relations_path, :params => doc.to_s, :headers => auth_header
assert_response :success, "can't create a relation: #{@response.body}"
relation_id = @response.body.to_i
# get it back and check the ordering
get api_relation_path(relation_id)
assert_members_equal_response doc
# check the ordering in the history tables:
get api_relation_version_path(relation_id, 1)
assert_members_equal_response doc, "can't read back version 2 of the relation"
# insert a member at the front
new_member = XML::Node.new "member"
new_member["ref"] = node3.id.to_s
new_member["type"] = "node"
new_member["role"] = "new first"
doc.find("//osm/relation").first.child.prev = new_member
# update the version, should be 1?
doc.find("//osm/relation").first["id"] = relation_id.to_s
doc.find("//osm/relation").first["version"] = 1.to_s
# upload the next version of the relation
put api_relation_path(relation_id), :params => doc.to_s, :headers => auth_header
assert_response :success, "can't update relation: #{@response.body}"
assert_equal 2, @response.body.to_i
# get it back again and check the ordering again
get api_relation_path(relation_id)
assert_members_equal_response doc
# check the ordering in the history tables:
get api_relation_version_path(relation_id, 2)
assert_members_equal_response doc, "can't read back version 2 of the relation"
end
##
# check that relations can contain duplicate members
def test_relation_member_duplicates
private_user = create(:user, :data_public => false)
user = create(:user)
changeset = create(:changeset, :user => user)
node1 = create(:node)
node2 = create(:node)
doc_str = <<~OSM
<osm>
<relation changeset='#{changeset.id}'>
<member ref='#{node1.id}' type='node' role='forward'/>
<member ref='#{node2.id}' type='node' role='forward'/>
<member ref='#{node1.id}' type='node' role='forward'/>
<member ref='#{node2.id}' type='node' role='forward'/>
</relation>
</osm>
OSM
doc = XML::Parser.string(doc_str).parse
## First try with the private user
auth_header = bearer_authorization_header private_user
post api_relations_path, :params => doc.to_s, :headers => auth_header
assert_response :forbidden
## Now try with the public user
auth_header = bearer_authorization_header user
post api_relations_path, :params => doc.to_s, :headers => auth_header
assert_response :success, "can't create a relation: #{@response.body}"
relation_id = @response.body.to_i
# get it back and check the ordering
get api_relation_path(relation_id)
assert_members_equal_response doc
# check the ordering in the history tables:
get api_relation_version_path(relation_id, 1)
assert_members_equal_response doc, "can't read back version 1 of the relation"
end
##
# test that the ordering of elements in the history is the same as in current.
def test_history_ordering
user = create(:user)
changeset = create(:changeset, :user => user)
node1 = create(:node)
node2 = create(:node)
node3 = create(:node)
node4 = create(:node)
doc_str = <<~OSM
<osm>
<relation changeset='#{changeset.id}'>
<member ref='#{node1.id}' type='node' role='forward'/>
<member ref='#{node4.id}' type='node' role='forward'/>
<member ref='#{node3.id}' type='node' role='forward'/>
<member ref='#{node2.id}' type='node' role='forward'/>
</relation>
</osm>
OSM
doc = XML::Parser.string(doc_str).parse
auth_header = bearer_authorization_header user
post api_relations_path, :params => doc.to_s, :headers => auth_header
assert_response :success, "can't create a relation: #{@response.body}"
relation_id = @response.body.to_i
# check the ordering in the current tables:
get api_relation_path(relation_id)
assert_members_equal_response doc
# check the ordering in the history tables:
get api_relation_version_path(relation_id, 1)
assert_members_equal_response doc, "can't read back version 1 of the relation"
end
private
##
# assert that tags on relation document +rel+
# are equal to tags in response
def assert_tags_equal_response(rel)
assert_response :success
response_xml = xml_parse(@response.body)
# turn the XML doc into tags hashes
rel_tags = get_tags_as_hash(rel)
response_tags = get_tags_as_hash(response_xml)
assert_equal rel_tags, response_tags, "Tags should be identical."
end
##
# checks that the XML document and the response have
# members in the same order.
def assert_members_equal_response(doc, response_message = "can't read back the relation")
assert_response :success, "#{response_message}: #{@response.body}"
new_doc = XML::Parser.string(@response.body).parse
doc_members = doc.find("//osm/relation/member").collect do |m|
[m["ref"].to_i, m["type"].to_sym, m["role"]]
end
new_members = new_doc.find("//osm/relation/member").collect do |m|
[m["ref"].to_i, m["type"].to_sym, m["role"]]
end
assert_equal doc_members, new_members, "members are not equal - ordering is wrong? (#{doc}, #{@response.body})"
end
##
# returns a k->v hash of tags from an xml doc
def get_tags_as_hash(a)
a.find("//osm/relation/tag").to_h do |tag|
[tag["k"], tag["v"]]
end
end
##
# update the changeset_id of a node element
def update_changeset(xml, changeset_id)
xml_attr_rewrite(xml, "changeset", changeset_id)
end
##
# update an attribute in the node element
def xml_attr_rewrite(xml, name, value)
xml.find("//osm/relation").first[name] = value.to_s
xml
end
##
# parse some xml
def xml_parse(xml)
parser = XML::Parser.string(xml)
parser.parse
end
end