Added tests for changeset upload code. Refactored diff reading code and put it into /lib. Changed the route of a changeset upload to explicitly refer to the changeset it applies to (i.e: resource).
This commit is contained in:
parent
136c5a7bf1
commit
dc2a959037
8 changed files with 596 additions and 96 deletions
|
@ -2,6 +2,7 @@
|
|||
|
||||
class ChangesetController < ApplicationController
|
||||
require 'xml/libxml'
|
||||
require 'diff_reader'
|
||||
|
||||
before_filter :authorize, :only => [:create, :update, :delete, :upload]
|
||||
before_filter :check_write_availability, :only => [:create, :update, :delete, :upload]
|
||||
|
@ -70,102 +71,37 @@ class ChangesetController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
##
|
||||
# Upload a diff in a single transaction.
|
||||
#
|
||||
# This means that each change within the diff must succeed, i.e: that
|
||||
# each version number mentioned is still current. Otherwise the entire
|
||||
# transaction *must* be rolled back.
|
||||
#
|
||||
# Furthermore, each element in the diff can only reference the current
|
||||
# changeset.
|
||||
#
|
||||
# Returns: ??? the new document? updated diffs?
|
||||
def upload
|
||||
unless request.put?
|
||||
# only allow POST requests, as the upload method is most definitely
|
||||
# not idempotent, as several uploads with placeholder IDs will have
|
||||
# different side-effects.
|
||||
# see http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1.2
|
||||
unless request.post?
|
||||
render :nothing => true, :status => :method_not_allowed
|
||||
return
|
||||
end
|
||||
|
||||
p = XML::Reader.new request.raw_post
|
||||
|
||||
node_ids, way_ids, rel_ids = {}, {}, {}
|
||||
ids = {"node"=>node_ids, "way"=>way_ids, "relation"=>rel_ids}
|
||||
|
||||
models = {"node"=>Node, "way"=>Way, "relation"=>Relation}
|
||||
|
||||
# FIXME shouldn't this be done through the
|
||||
# res = OSM::API.new.get_xml_doc
|
||||
# as everything else is?
|
||||
res = XML::Document.new
|
||||
res.encoding = 'UTF-8'
|
||||
root = XML::Node.new 'osm'
|
||||
root['version'] = API_VERSION
|
||||
root['creator'] = 'OpenStreetMap.org'
|
||||
res.root = root
|
||||
|
||||
root << XML::Node.new_comment(" Warning: this is a 0.6 result document, " +
|
||||
"not a normal OSM file. ")
|
||||
|
||||
changeset = Changeset.find(params[:id])
|
||||
|
||||
diff_reader = DiffReader.new(request.raw_post, changeset)
|
||||
Changeset.transaction do
|
||||
while p.read == 1
|
||||
break if p.node_type == 15 # end element
|
||||
next unless p.node_type == 1 # element
|
||||
|
||||
case p.name
|
||||
when 'create':
|
||||
while p.read == 1
|
||||
break if p.node_type == 15 # end element
|
||||
next unless p.node_type == 1 # element
|
||||
|
||||
model = models[p.name]
|
||||
next if model.nil?
|
||||
|
||||
elem = XML::Node.new p.name
|
||||
nd = p.expand; p.next
|
||||
osm = model.from_xml_node(nd, true)
|
||||
elem['old_id'] = nd['id']
|
||||
|
||||
case nd.name
|
||||
when 'way':
|
||||
fix_way(osm, node_ids)
|
||||
raise OSM::APIPreconditionFailedError.new if !osm.preconditions_ok?
|
||||
when 'relation':
|
||||
fix_rel(osm, ids)
|
||||
raise OSM::APIPreconditionFailedError.new if !osm.preconditions_ok?
|
||||
end
|
||||
|
||||
create_prim ids[nd.name], osm, nd
|
||||
elem['new_id'] = osm.id.to_s
|
||||
elem['new_version'] = osm.version.to_s
|
||||
root << elem
|
||||
end
|
||||
when 'modify':
|
||||
while p.read == 1
|
||||
break if p.node_type == 15 # end element
|
||||
next unless p.node_type == 1 # element
|
||||
|
||||
model = models[p.name]
|
||||
next if model.nil?
|
||||
|
||||
elem = XML::Node.new p.name
|
||||
new_osm = model.from_xml_node(p.expand); p.next
|
||||
osm = model.find(new_osm.id)
|
||||
osm.update_from new_osm, @user
|
||||
elem['old_id'] = elem['new_id'] = osm.id.to_s
|
||||
elem['new_version'] = osm.version.to_s
|
||||
root << elem
|
||||
end
|
||||
when 'delete':
|
||||
while p.read == 1
|
||||
break if p.node_type == 15 # end element
|
||||
next unless p.node_type == 1 # element
|
||||
|
||||
model = models[p.name]
|
||||
next if model.nil?
|
||||
|
||||
elem = XML::Node.new p.name
|
||||
osm = model.find(p.expand['id']); p.next
|
||||
osm.delete_with_history(@user)
|
||||
elem['old_id'] = elem['new_id'] = osm.id.to_s
|
||||
elem['new_version'] = osm.version.to_s
|
||||
root << elem
|
||||
end
|
||||
end
|
||||
end
|
||||
result = diff_reader.commit
|
||||
render :text => result.to_s, :content_type => "text/xml"
|
||||
end
|
||||
|
||||
render :text => res.to_s, :content_type => "text/xml"
|
||||
|
||||
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render :nothing => true, :status => :not_found
|
||||
rescue OSM::APIError => ex
|
||||
render ex.render_opts
|
||||
end
|
||||
|
|
|
@ -75,20 +75,25 @@ class Node < ActiveRecord::Base
|
|||
def self.from_xml_node(pt, create=false)
|
||||
node = Node.new
|
||||
|
||||
node.version = pt['version'].to_i
|
||||
node.lat = pt['lat'].to_f
|
||||
node.lon = pt['lon'].to_f
|
||||
node.changeset_id = pt['changeset'].to_i
|
||||
|
||||
return nil unless node.in_world?
|
||||
|
||||
# version must be present unless creating
|
||||
return nil unless create or not pt['version'].nil?
|
||||
node.version = pt['version'].to_i
|
||||
|
||||
unless create
|
||||
if pt['id'] != '0'
|
||||
node.id = pt['id'].to_i
|
||||
end
|
||||
end
|
||||
|
||||
node.visible = pt['visible'] and pt['visible'] == 'true'
|
||||
# visible if it says it is, or as the default if the attribute
|
||||
# is missing.
|
||||
node.visible = pt['visible'].nil? or pt['visible'] == 'true'
|
||||
|
||||
if create
|
||||
node.timestamp = Time.now
|
||||
|
@ -235,4 +240,11 @@ class Node < ActiveRecord::Base
|
|||
@tags[k] = v
|
||||
end
|
||||
|
||||
##
|
||||
# dummy method to make the interfaces of node, way and relation
|
||||
# more consistent.
|
||||
def fix_placeholders!(id_map)
|
||||
# nodes don't refer to anything, so there is nothing to do here
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -320,4 +320,22 @@ class Relation < ActiveRecord::Base
|
|||
def tags_as_hash
|
||||
return self.tags
|
||||
end
|
||||
|
||||
##
|
||||
# if any members are referenced by placeholder IDs (i.e: negative) then
|
||||
# this calling this method will fix them using the map from placeholders
|
||||
# to IDs +id_map+.
|
||||
def fix_placeholders!(id_map)
|
||||
self.members.map! do |type, id, role|
|
||||
old_id = id.to_i
|
||||
if old_id < 0
|
||||
new_id = id_map[type.to_sym][old_id]
|
||||
raise "invalid placeholder" if new_id.nil?
|
||||
[type, new_id, role]
|
||||
else
|
||||
[type, id, role]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -299,4 +299,21 @@ class Way < ActiveRecord::Base
|
|||
def tags_as_hash
|
||||
return self.tags
|
||||
end
|
||||
|
||||
##
|
||||
# if any referenced nodes are placeholder IDs (i.e: are negative) then
|
||||
# this calling this method will fix them using the map from placeholders
|
||||
# to IDs +id_map+.
|
||||
def fix_placeholders!(id_map)
|
||||
self.nds.map! do |node_id|
|
||||
if node_id < 0
|
||||
new_id = id_map[:node][node_id]
|
||||
raise "invalid placeholder for #{node_id.inspect}: #{new_id.inspect}" if new_id.nil?
|
||||
new_id
|
||||
else
|
||||
node_id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -4,7 +4,7 @@ ActionController::Routing::Routes.draw do |map|
|
|||
map.connect "api/capabilities", :controller => 'api', :action => 'capabilities'
|
||||
|
||||
map.connect "api/#{API_VERSION}/changeset/create", :controller => 'changeset', :action => 'create'
|
||||
map.connect "api/#{API_VERSION}/changeset/upload", :controller => 'changeset', :action => 'upload'
|
||||
map.connect "api/#{API_VERSION}/changeset/:id/upload", :controller => 'changeset', :action => 'upload', :id => /\d+/
|
||||
map.connect "api/#{API_VERSION}/changeset/:id", :controller => 'changeset', :action => 'read', :id => /\d+/
|
||||
map.connect "api/#{API_VERSION}/changeset/:id/close", :controller => 'changeset', :action => 'close', :id =>/\d+/
|
||||
|
||||
|
@ -22,6 +22,7 @@ ActionController::Routing::Routes.draw do |map|
|
|||
map.connect "api/#{API_VERSION}/way/:id/history", :controller => 'old_way', :action => 'history', :id => /\d+/
|
||||
map.connect "api/#{API_VERSION}/way/:id/full", :controller => 'way', :action => 'full', :id => /\d+/
|
||||
map.connect "api/#{API_VERSION}/way/:id/relations", :controller => 'relation', :action => 'relations_for_way', :id => /\d+/
|
||||
map.connect "api/#{API_VERSION}/way/:id/:version", :controller => 'old_way', :action => 'version', :id => /\d+/, :version => /\d+/
|
||||
map.connect "api/#{API_VERSION}/way/:id", :controller => 'way', :action => 'read', :id => /\d+/, :conditions => { :method => :get }
|
||||
map.connect "api/#{API_VERSION}/way/:id", :controller => 'way', :action => 'update', :id => /\d+/, :conditions => { :method => :put }
|
||||
map.connect "api/#{API_VERSION}/way/:id", :controller => 'way', :action => 'delete', :id => /\d+/, :conditions => { :method => :delete }
|
||||
|
|
167
lib/diff_reader.rb
Normal file
167
lib/diff_reader.rb
Normal file
|
@ -0,0 +1,167 @@
|
|||
##
|
||||
# DiffReader reads OSM diffs and applies them to the database.
|
||||
#
|
||||
# Uses the streaming LibXML "Reader" interface to cut down on memory
|
||||
# usage, so hopefully we can process fairly large diffs.
|
||||
class DiffReader
|
||||
include ConsistencyValidations
|
||||
|
||||
# maps each element type to the model class which handles it
|
||||
MODELS = {
|
||||
"node" => Node,
|
||||
"way" => Way,
|
||||
"relation" => Relation
|
||||
}
|
||||
|
||||
##
|
||||
# Construct a diff reader by giving it a bunch of XML +data+ to parse
|
||||
# in OsmChange format. All diffs must be limited to a single changeset
|
||||
# given in +changeset+.
|
||||
def initialize(data, changeset)
|
||||
@reader = XML::Reader.new data
|
||||
@changeset = changeset
|
||||
end
|
||||
|
||||
##
|
||||
# An element-block mapping for using the LibXML reader interface.
|
||||
#
|
||||
# Since a lot of LibXML reader usage is boilerplate iteration through
|
||||
# elements, it would be better to DRY and do this in a block. This
|
||||
# could also help with error handling...?
|
||||
def with_element
|
||||
# skip the first element, which is our opening element of the block
|
||||
@reader.read
|
||||
# loop over all elements.
|
||||
# NOTE: XML::Reader#read returns 0 for EOF and -1 for error.
|
||||
while @reader.read == 1
|
||||
break if @reader.node_type == 15 # end element
|
||||
next unless @reader.node_type == 1 # element
|
||||
yield @reader.name
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
# An element-block mapping for using the LibXML reader interface.
|
||||
#
|
||||
# Since a lot of LibXML reader usage is boilerplate iteration through
|
||||
# elements, it would be better to DRY and do this in a block. This
|
||||
# could also help with error handling...?
|
||||
def with_model
|
||||
with_element do |model_name|
|
||||
model = MODELS[model_name]
|
||||
raise "Unexpected element type #{model_name}, " +
|
||||
"expected node, way, relation." if model.nil?
|
||||
yield model, @reader.expand
|
||||
@reader.next
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
# Checks a few invariants. Others are checked in the model methods
|
||||
# such as save_ and delete_with_history.
|
||||
def check(model, xml, new)
|
||||
raise OSM::APIBadXMLError.new(model, xml) if new.nil?
|
||||
unless new.changeset_id == @changeset.id
|
||||
raise OSM::APIChangesetMismatchError.new(new.changeset_id, @changeset.id)
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
# Consume the XML diff and try to commit it to the database. This code
|
||||
# is *not* transactional, so code which calls it should ensure that the
|
||||
# appropriate transaction block is in place.
|
||||
#
|
||||
# On a failure to meet preconditions (e.g: optimistic locking fails)
|
||||
# an exception subclassing OSM::APIError will be thrown.
|
||||
def commit
|
||||
|
||||
node_ids, way_ids, rel_ids = {}, {}, {}
|
||||
ids = { :node => node_ids, :way => way_ids, :relation => rel_ids}
|
||||
|
||||
result = OSM::API.new.get_xml_doc
|
||||
|
||||
# loop at the top level, within the <osmChange> element (although we
|
||||
# don't actually check this...)
|
||||
with_element do |action_name|
|
||||
if action_name == 'create'
|
||||
# create a new element. this code is agnostic of the element type
|
||||
# because all the elements support the methods that we're using.
|
||||
with_model do |model, xml|
|
||||
new = model.from_xml_node(xml, true)
|
||||
check(model, xml, new)
|
||||
|
||||
# when this element is saved it will get a new ID, so we save it
|
||||
# to produce the mapping which is sent to other elements.
|
||||
placeholder_id = xml['id'].to_i
|
||||
raise OSM::APIBadXMLError.new(model, xml) if placeholder_id.nil?
|
||||
|
||||
# some elements may have placeholders for other elements in the
|
||||
# diff, so we must fix these before saving the element.
|
||||
new.fix_placeholders!(ids)
|
||||
|
||||
# set the initial version to zero and save (which increments it)
|
||||
new.version = 0
|
||||
new.save_with_history!
|
||||
|
||||
# save placeholder => allocated ID map
|
||||
ids[model.to_s.downcase.to_sym][placeholder_id] = new.id
|
||||
|
||||
# add the result to the document we're building for return.
|
||||
xml_result = XML::Node.new model.to_s.downcase
|
||||
xml_result["old_id"] = placeholder_id.to_s
|
||||
xml_result["new_id"] = new.id.to_s
|
||||
xml_result["new_version"] = new.version.to_s
|
||||
result.root << xml_result
|
||||
end
|
||||
|
||||
elsif action_name == 'modify'
|
||||
# modify an existing element. again, this code doesn't directly deal
|
||||
# with types, but uses duck typing to handle them transparently.
|
||||
with_model do |model, xml|
|
||||
# get the new element from the XML payload
|
||||
new = model.from_xml_node(xml, false)
|
||||
check(model, xml, new)
|
||||
|
||||
# and the old one from the database
|
||||
old = model.find(new.id)
|
||||
|
||||
new.fix_placeholders!(ids)
|
||||
old.update_from(new, @changeset.user)
|
||||
|
||||
xml_result = XML::Node.new model.to_s.downcase
|
||||
xml_result["old_id"] = old.id.to_s
|
||||
xml_result["new_id"] = new.id.to_s
|
||||
xml_result["new_version"] = new.version.to_s
|
||||
result.root << xml_result
|
||||
end
|
||||
|
||||
elsif action_name == 'delete'
|
||||
# delete action. this takes a payload in API 0.6, so we need to do
|
||||
# most of the same checks that are done for the modify.
|
||||
with_model do |model, xml|
|
||||
new = model.from_xml_node(xml, false)
|
||||
check(model, xml, new)
|
||||
|
||||
old = model.find(new.id)
|
||||
|
||||
# can a delete have placeholders under any circumstances?
|
||||
# if a way is modified, then deleted is that a valid diff?
|
||||
new.fix_placeholders!(ids)
|
||||
old.delete_with_history!(new, @changeset.user)
|
||||
|
||||
xml_result = XML::Node.new model.to_s.downcase
|
||||
xml_result["old_id"] = old.id.to_s
|
||||
result.root << xml_result
|
||||
end
|
||||
|
||||
else
|
||||
# no other actions to choose from, so it must be the users fault!
|
||||
raise "Unknown action #{action_name}, choices are create, modify, delete."
|
||||
end
|
||||
end
|
||||
|
||||
# return the XML document to be rendered back to the client
|
||||
return result
|
||||
end
|
||||
|
||||
end
|
26
lib/osm.rb
26
lib/osm.rb
|
@ -54,6 +54,32 @@ module OSM
|
|||
end
|
||||
end
|
||||
|
||||
# Raised when a diff is uploaded containing many changeset IDs which don't match
|
||||
# the changeset ID that the diff was uploaded to.
|
||||
class APIChangesetMismatchError < APIError
|
||||
def initialize(provided, allowed)
|
||||
@provided, @allowed = provided, allowed
|
||||
end
|
||||
|
||||
def render_opts
|
||||
{ :text => "Changeset mismatch: Provided #{@provided} but only " +
|
||||
"#{@allowed} is allowed.", :status => :conflict }
|
||||
end
|
||||
end
|
||||
|
||||
# Raised when bad XML is encountered which stops things parsing as
|
||||
# they should.
|
||||
class APIBadXMLError < APIError
|
||||
def initialize(model, xml)
|
||||
@model, @xml = model, xml
|
||||
end
|
||||
|
||||
def render_opts
|
||||
{ :text => "Cannot parse valid #{@model} from xml string #{@xml}",
|
||||
:status => :bad_request }
|
||||
end
|
||||
end
|
||||
|
||||
# Raised when the provided version is not equal to the latest in the db.
|
||||
class APIVersionMismatchError < APIError
|
||||
def initialize(provided, latest)
|
||||
|
|
|
@ -44,6 +44,7 @@ class ChangesetController; def rescue_action(e) raise e end; end
|
|||
basic_authorization "test@openstreetmap.org", "test"
|
||||
content "<osm><changeset></osm>"
|
||||
put :create
|
||||
assert_response :bad_request, "creating a invalid changeset should fail"
|
||||
end
|
||||
|
||||
def test_read
|
||||
|
@ -53,9 +54,331 @@ class ChangesetController; def rescue_action(e) raise e end; end
|
|||
def test_close
|
||||
|
||||
end
|
||||
|
||||
def test_upload
|
||||
|
||||
|
||||
##
|
||||
# upload something simple, but valid and check that it can
|
||||
# be read back ok.
|
||||
def test_upload_simple_valid
|
||||
basic_authorization "test@openstreetmap.org", "test"
|
||||
|
||||
# simple diff to change a node, way and relation by removing
|
||||
# their tags
|
||||
diff = <<EOF
|
||||
<osmChange>
|
||||
<modify>
|
||||
<node id='1' lon='0' lat='0' changeset='1' version='1'/>
|
||||
<way id='1' changeset='1' version='1'>
|
||||
<nd ref='3'/>
|
||||
</way>
|
||||
</modify>
|
||||
<modify>
|
||||
<relation id='1' changeset='1' version='1'>
|
||||
<member type='way' role='some' ref='3'/>
|
||||
<member type='node' role='some' ref='5'/>
|
||||
<member type='relation' role='some' ref='3'/>
|
||||
</relation>
|
||||
</modify>
|
||||
</osmChange>
|
||||
EOF
|
||||
|
||||
# upload it
|
||||
content diff
|
||||
post :upload, :id => 1
|
||||
assert_response :success,
|
||||
"can't upload a simple valid diff to changeset: #{@response.body}"
|
||||
|
||||
# check that the changes made it into the database
|
||||
assert_equal 0, Node.find(1).tags.size, "node 1 should now have no tags"
|
||||
assert_equal 0, Way.find(1).tags.size, "way 1 should now have no tags"
|
||||
assert_equal 0, Relation.find(1).tags.size, "relation 1 should now have no tags"
|
||||
end
|
||||
|
||||
|
||||
##
|
||||
# upload something which creates new objects using placeholders
|
||||
def test_upload_create_valid
|
||||
basic_authorization "test@openstreetmap.org", "test"
|
||||
|
||||
# simple diff to create a node way and relation using placeholders
|
||||
diff = <<EOF
|
||||
<osmChange>
|
||||
<create>
|
||||
<node id='-1' lon='0' lat='0' changeset='1'>
|
||||
<tag k='foo' v='bar'/>
|
||||
<tag k='baz' v='bat'/>
|
||||
</node>
|
||||
<way id='-1' changeset='1'>
|
||||
<nd ref='3'/>
|
||||
</way>
|
||||
</create>
|
||||
<create>
|
||||
<relation id='-1' changeset='1'>
|
||||
<member type='way' role='some' ref='3'/>
|
||||
<member type='node' role='some' ref='5'/>
|
||||
<member type='relation' role='some' ref='3'/>
|
||||
</relation>
|
||||
</create>
|
||||
</osmChange>
|
||||
EOF
|
||||
|
||||
# upload it
|
||||
content diff
|
||||
post :upload, :id => 1
|
||||
assert_response :success,
|
||||
"can't upload a simple valid creation to changeset: #{@response.body}"
|
||||
|
||||
# check the returned payload
|
||||
assert_select "osm[version=#{API_VERSION}][generator=\"OpenStreetMap server\"]", 1
|
||||
assert_select "osm>node", 1
|
||||
assert_select "osm>way", 1
|
||||
assert_select "osm>relation", 1
|
||||
|
||||
# inspect the response to find out what the new element IDs are
|
||||
doc = XML::Parser.string(@response.body).parse
|
||||
new_node_id = doc.find("//osm/node").first["new_id"].to_i
|
||||
new_way_id = doc.find("//osm/way").first["new_id"].to_i
|
||||
new_rel_id = doc.find("//osm/relation").first["new_id"].to_i
|
||||
|
||||
# check the old IDs are all present and negative one
|
||||
assert_equal -1, doc.find("//osm/node").first["old_id"].to_i
|
||||
assert_equal -1, doc.find("//osm/way").first["old_id"].to_i
|
||||
assert_equal -1, doc.find("//osm/relation").first["old_id"].to_i
|
||||
|
||||
# check the versions are present and equal one
|
||||
assert_equal 1, doc.find("//osm/node").first["new_version"].to_i
|
||||
assert_equal 1, doc.find("//osm/way").first["new_version"].to_i
|
||||
assert_equal 1, doc.find("//osm/relation").first["new_version"].to_i
|
||||
|
||||
# check that the changes made it into the database
|
||||
assert_equal 2, Node.find(new_node_id).tags.size, "new node should have two tags"
|
||||
assert_equal 0, Way.find(new_way_id).tags.size, "new way should have no tags"
|
||||
assert_equal 0, Relation.find(new_rel_id).tags.size, "new relation should have no tags"
|
||||
end
|
||||
|
||||
##
|
||||
# test a complex delete where we delete elements which rely on eachother
|
||||
# in the same transaction.
|
||||
def test_upload_delete
|
||||
basic_authorization "test@openstreetmap.org", "test"
|
||||
|
||||
diff = XML::Document.new
|
||||
diff.root = XML::Node.new "osmChange"
|
||||
delete = XML::Node.new "delete"
|
||||
diff.root << delete
|
||||
delete << current_relations(:visible_relation).to_xml_node
|
||||
delete << current_relations(:used_relation).to_xml_node
|
||||
delete << current_ways(:used_way).to_xml_node
|
||||
delete << current_nodes(:node_used_by_relationship).to_xml_node
|
||||
|
||||
# upload it
|
||||
content diff
|
||||
post :upload, :id => 1
|
||||
assert_response :success,
|
||||
"can't upload a deletion diff to changeset: #{@response.body}"
|
||||
|
||||
# check that everything was deleted
|
||||
assert_equal false, Node.find(current_nodes(:node_used_by_relationship).id).visible
|
||||
assert_equal false, Way.find(current_ways(:used_way).id).visible
|
||||
assert_equal false, Relation.find(current_relations(:visible_relation).id).visible
|
||||
assert_equal false, Relation.find(current_relations(:used_relation).id).visible
|
||||
end
|
||||
|
||||
##
|
||||
# test that deleting stuff in a transaction doesn't bypass the checks
|
||||
# to ensure that used elements are not deleted.
|
||||
def test_upload_delete_invalid
|
||||
basic_authorization "test@openstreetmap.org", "test"
|
||||
|
||||
diff = XML::Document.new
|
||||
diff.root = XML::Node.new "osmChange"
|
||||
delete = XML::Node.new "delete"
|
||||
diff.root << delete
|
||||
delete << current_relations(:visible_relation).to_xml_node
|
||||
delete << current_ways(:used_way).to_xml_node
|
||||
delete << current_nodes(:node_used_by_relationship).to_xml_node
|
||||
|
||||
# upload it
|
||||
content diff
|
||||
post :upload, :id => 1
|
||||
assert_response :precondition_failed,
|
||||
"shouldn't be able to upload a invalid deletion diff: #{@response.body}"
|
||||
|
||||
# check that nothing was, in fact, deleted
|
||||
assert_equal true, Node.find(current_nodes(:node_used_by_relationship).id).visible
|
||||
assert_equal true, Way.find(current_ways(:used_way).id).visible
|
||||
assert_equal true, Relation.find(current_relations(:visible_relation).id).visible
|
||||
end
|
||||
|
||||
##
|
||||
# upload something which creates new objects and inserts them into
|
||||
# existing containers using placeholders.
|
||||
def test_upload_complex
|
||||
basic_authorization "test@openstreetmap.org", "test"
|
||||
|
||||
# simple diff to create a node way and relation using placeholders
|
||||
diff = <<EOF
|
||||
<osmChange>
|
||||
<create>
|
||||
<node id='-1' lon='0' lat='0' changeset='1'>
|
||||
<tag k='foo' v='bar'/>
|
||||
<tag k='baz' v='bat'/>
|
||||
</node>
|
||||
</create>
|
||||
<modify>
|
||||
<way id='1' changeset='1' version='1'>
|
||||
<nd ref='-1'/>
|
||||
<nd ref='3'/>
|
||||
</way>
|
||||
<relation id='1' changeset='1' version='1'>
|
||||
<member type='way' role='some' ref='3'/>
|
||||
<member type='node' role='some' ref='-1'/>
|
||||
<member type='relation' role='some' ref='3'/>
|
||||
</relation>
|
||||
</modify>
|
||||
</osmChange>
|
||||
EOF
|
||||
|
||||
# upload it
|
||||
content diff
|
||||
post :upload, :id => 1
|
||||
assert_response :success,
|
||||
"can't upload a complex diff to changeset: #{@response.body}"
|
||||
|
||||
# check the returned payload
|
||||
assert_select "osm[version=#{API_VERSION}][generator=\"OpenStreetMap server\"]", 1
|
||||
assert_select "osm>node", 1
|
||||
assert_select "osm>way", 1
|
||||
assert_select "osm>relation", 1
|
||||
|
||||
# inspect the response to find out what the new element IDs are
|
||||
doc = XML::Parser.string(@response.body).parse
|
||||
new_node_id = doc.find("//osm/node").first["new_id"].to_i
|
||||
|
||||
# check that the changes made it into the database
|
||||
assert_equal 2, Node.find(new_node_id).tags.size, "new node should have two tags"
|
||||
assert_equal [new_node_id, 3], Way.find(1).nds, "way nodes should match"
|
||||
Relation.find(1).members.each do |type,id,role|
|
||||
if type == 'node'
|
||||
assert_equal new_node_id, id, "relation should contain new node"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
# create a diff which references several changesets, which should cause
|
||||
# a rollback and none of the diff gets committed
|
||||
def test_upload_invalid_changesets
|
||||
basic_authorization "test@openstreetmap.org", "test"
|
||||
|
||||
# simple diff to create a node way and relation using placeholders
|
||||
diff = <<EOF
|
||||
<osmChange>
|
||||
<modify>
|
||||
<node id='1' lon='0' lat='0' changeset='1' version='1'/>
|
||||
<way id='1' changeset='1' version='1'>
|
||||
<nd ref='3'/>
|
||||
</way>
|
||||
</modify>
|
||||
<modify>
|
||||
<relation id='1' changeset='1' version='1'>
|
||||
<member type='way' role='some' ref='3'/>
|
||||
<member type='node' role='some' ref='5'/>
|
||||
<member type='relation' role='some' ref='3'/>
|
||||
</relation>
|
||||
</modify>
|
||||
<create>
|
||||
<node id='-1' changeset='4'>
|
||||
<tag k='foo' v='bar'/>
|
||||
<tag k='baz' v='bat'/>
|
||||
</node>
|
||||
</create>
|
||||
</osmChange>
|
||||
EOF
|
||||
# cache the objects before uploading them
|
||||
node = current_nodes(:visible_node)
|
||||
way = current_ways(:visible_way)
|
||||
rel = current_relations(:visible_relation)
|
||||
|
||||
# upload it
|
||||
content diff
|
||||
post :upload, :id => 1
|
||||
assert_response :conflict,
|
||||
"uploading a diff with multiple changsets should have failed"
|
||||
|
||||
# check that objects are unmodified
|
||||
assert_nodes_are_equal(node, Node.find(1))
|
||||
assert_ways_are_equal(way, Way.find(1))
|
||||
end
|
||||
|
||||
##
|
||||
# upload multiple versions of the same element in the same diff.
|
||||
def test_upload_multiple_valid
|
||||
basic_authorization "test@openstreetmap.org", "test"
|
||||
|
||||
# change the location of a node multiple times, each time referencing
|
||||
# the last version. doesn't this depend on version numbers being
|
||||
# sequential?
|
||||
diff = <<EOF
|
||||
<osmChange>
|
||||
<modify>
|
||||
<node id='1' lon='0' lat='0' changeset='1' version='1'/>
|
||||
<node id='1' lon='1' lat='0' changeset='1' version='2'/>
|
||||
<node id='1' lon='1' lat='1' changeset='1' version='3'/>
|
||||
<node id='1' lon='1' lat='2' changeset='1' version='4'/>
|
||||
<node id='1' lon='2' lat='2' changeset='1' version='5'/>
|
||||
<node id='1' lon='3' lat='2' changeset='1' version='6'/>
|
||||
<node id='1' lon='3' lat='3' changeset='1' version='7'/>
|
||||
<node id='1' lon='9' lat='9' changeset='1' version='8'/>
|
||||
</modify>
|
||||
</osmChange>
|
||||
EOF
|
||||
|
||||
# upload it
|
||||
content diff
|
||||
post :upload, :id => 1
|
||||
assert_response :success,
|
||||
"can't upload multiple versions of an element in a diff: #{@response.body}"
|
||||
end
|
||||
|
||||
##
|
||||
# upload multiple versions of the same element in the same diff, but
|
||||
# keep the version numbers the same.
|
||||
def test_upload_multiple_duplicate
|
||||
basic_authorization "test@openstreetmap.org", "test"
|
||||
|
||||
diff = <<EOF
|
||||
<osmChange>
|
||||
<modify>
|
||||
<node id='1' lon='0' lat='0' changeset='1' version='1'/>
|
||||
<node id='1' lon='1' lat='1' changeset='1' version='1'/>
|
||||
</modify>
|
||||
</osmChange>
|
||||
EOF
|
||||
|
||||
# upload it
|
||||
content diff
|
||||
post :upload, :id => 1
|
||||
assert_response :conflict,
|
||||
"shouldn't be able to upload the same element twice in a diff: #{@response.body}"
|
||||
end
|
||||
|
||||
##
|
||||
# try to upload some elements without specifying the version
|
||||
def test_upload_missing_version
|
||||
basic_authorization "test@openstreetmap.org", "test"
|
||||
|
||||
diff = <<EOF
|
||||
<osmChange>
|
||||
<modify>
|
||||
<node id='1' lon='1' lat='1' changeset='1'/>
|
||||
</modify>
|
||||
</osmChange>
|
||||
EOF
|
||||
|
||||
# upload it
|
||||
content diff
|
||||
post :upload, :id => 1
|
||||
assert_response :bad_request,
|
||||
"shouldn't be able to upload an element without version: #{@response.body}"
|
||||
end
|
||||
|
||||
end
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue