]> git.openstreetmap.org Git - rails.git/commitdiff
Move the api methods from changesets_controller into the api namespaced controller
authorAndy Allan <git@gravitystorm.co.uk>
Sun, 24 Feb 2019 11:18:31 +0000 (12:18 +0100)
committerAndy Allan <git@gravitystorm.co.uk>
Sun, 24 Feb 2019 11:18:31 +0000 (12:18 +0100)
app/controllers/api/changesets_controller.rb [new file with mode: 0644]
app/controllers/changesets_controller.rb
app/views/browse/changeset.html.erb
config/routes.rb
test/controllers/api/changesets_controller_test.rb [new file with mode: 0644]
test/controllers/changesets_controller_test.rb
test/controllers/relations_controller_test.rb

diff --git a/app/controllers/api/changesets_controller.rb b/app/controllers/api/changesets_controller.rb
new file mode 100644 (file)
index 0000000..853ee38
--- /dev/null
@@ -0,0 +1,422 @@
+# The ChangesetController is the RESTful interface to Changeset objects
+
+module Api
+  class ChangesetsController < ApplicationController
+    layout "site"
+    require "xml/libxml"
+
+    skip_before_action :verify_authenticity_token
+    before_action :authorize, :only => [:create, :update, :upload, :close, :subscribe, :unsubscribe]
+    before_action :api_deny_access_handler, :only => [:create, :update, :upload, :close, :subscribe, :unsubscribe, :expand_bbox]
+
+    authorize_resource
+
+    before_action :require_public_data, :only => [:create, :update, :upload, :close, :subscribe, :unsubscribe]
+    before_action :check_api_writable, :only => [:create, :update, :upload, :subscribe, :unsubscribe]
+    before_action :check_api_readable, :except => [:create, :update, :upload, :download, :query, :subscribe, :unsubscribe]
+    before_action(:only => [:index, :feed]) { |c| c.check_database_readable(true) }
+    around_action :api_call_handle_error
+    around_action :api_call_timeout, :except => [:upload]
+
+    # Helper methods for checking consistency
+    include ConsistencyValidations
+
+    # Create a changeset from XML.
+    def create
+      assert_method :put
+
+      cs = Changeset.from_xml(request.raw_post, true)
+
+      # Assume that Changeset.from_xml has thrown an exception if there is an error parsing the xml
+      cs.user = current_user
+      cs.save_with_tags!
+
+      # Subscribe user to changeset comments
+      cs.subscribers << current_user
+
+      render :plain => cs.id.to_s
+    end
+
+    ##
+    # Return XML giving the basic info about the changeset. Does not
+    # return anything about the nodes, ways and relations in the changeset.
+    def show
+      changeset = Changeset.find(params[:id])
+
+      render :xml => changeset.to_xml(params[:include_discussion].presence).to_s
+    end
+
+    ##
+    # marks a changeset as closed. this may be called multiple times
+    # on the same changeset, so is idempotent.
+    def close
+      assert_method :put
+
+      changeset = Changeset.find(params[:id])
+      check_changeset_consistency(changeset, current_user)
+
+      # to close the changeset, we'll just set its closed_at time to
+      # now. this might not be enough if there are concurrency issues,
+      # but we'll have to wait and see.
+      changeset.set_closed_time_now
+
+      changeset.save!
+      head :ok
+    end
+
+    ##
+    # insert a (set of) points into a changeset bounding box. this can only
+    # increase the size of the bounding box. this is a hint that clients can
+    # set either before uploading a large number of changes, or changes that
+    # the client (but not the server) knows will affect areas further away.
+    def expand_bbox
+      # only allow POST requests, because although this method is
+      # idempotent, there is no "document" to PUT really...
+      assert_method :post
+
+      cs = Changeset.find(params[:id])
+      check_changeset_consistency(cs, current_user)
+
+      # keep an array of lons and lats
+      lon = []
+      lat = []
+
+      # the request is in pseudo-osm format... this is kind-of an
+      # abuse, maybe should change to some other format?
+      doc = XML::Parser.string(request.raw_post, :options => XML::Parser::Options::NOERROR).parse
+      doc.find("//osm/node").each do |n|
+        lon << n["lon"].to_f * GeoRecord::SCALE
+        lat << n["lat"].to_f * GeoRecord::SCALE
+      end
+
+      # add the existing bounding box to the lon-lat array
+      lon << cs.min_lon unless cs.min_lon.nil?
+      lat << cs.min_lat unless cs.min_lat.nil?
+      lon << cs.max_lon unless cs.max_lon.nil?
+      lat << cs.max_lat unless cs.max_lat.nil?
+
+      # collapse the arrays to minimum and maximum
+      cs.min_lon = lon.min
+      cs.min_lat = lat.min
+      cs.max_lon = lon.max
+      cs.max_lat = lat.max
+
+      # save the larger bounding box and return the changeset, which
+      # will include the bigger bounding box.
+      cs.save!
+      render :xml => cs.to_xml.to_s
+    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: a diffResult document, as described in
+    # http://wiki.openstreetmap.org/wiki/OSM_Protocol_Version_0.6
+    def upload
+      # 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
+      assert_method :post
+
+      changeset = Changeset.find(params[:id])
+      check_changeset_consistency(changeset, current_user)
+
+      diff_reader = DiffReader.new(request.raw_post, changeset)
+      Changeset.transaction do
+        result = diff_reader.commit
+        render :xml => result.to_s
+      end
+    end
+
+    ##
+    # download the changeset as an osmChange document.
+    #
+    # to make it easier to revert diffs it would be better if the osmChange
+    # format were reversible, i.e: contained both old and new versions of
+    # modified elements. but it doesn't at the moment...
+    #
+    # this method cannot order the database changes fully (i.e: timestamp and
+    # version number may be too coarse) so the resulting diff may not apply
+    # to a different database. however since changesets are not atomic this
+    # behaviour cannot be guaranteed anyway and is the result of a design
+    # choice.
+    def download
+      changeset = Changeset.find(params[:id])
+
+      # get all the elements in the changeset which haven't been redacted
+      # and stick them in a big array.
+      elements = [changeset.old_nodes.unredacted,
+                  changeset.old_ways.unredacted,
+                  changeset.old_relations.unredacted].flatten
+
+      # sort the elements by timestamp and version number, as this is the
+      # almost sensible ordering available. this would be much nicer if
+      # global (SVN-style) versioning were used - then that would be
+      # unambiguous.
+      elements.sort! do |a, b|
+        if a.timestamp == b.timestamp
+          a.version <=> b.version
+        else
+          a.timestamp <=> b.timestamp
+        end
+      end
+
+      # create changeset and user caches
+      changeset_cache = {}
+      user_display_name_cache = {}
+
+      # create an osmChange document for the output
+      result = OSM::API.new.get_xml_doc
+      result.root.name = "osmChange"
+
+      # generate an output element for each operation. note: we avoid looking
+      # at the history because it is simpler - but it would be more correct to
+      # check these assertions.
+      elements.each do |elt|
+        result.root <<
+          if elt.version == 1
+            # first version, so it must be newly-created.
+            created = XML::Node.new "create"
+            created << elt.to_xml_node(changeset_cache, user_display_name_cache)
+          elsif elt.visible
+            # must be a modify
+            modified = XML::Node.new "modify"
+            modified << elt.to_xml_node(changeset_cache, user_display_name_cache)
+          else
+            # if the element isn't visible then it must have been deleted
+            deleted = XML::Node.new "delete"
+            deleted << elt.to_xml_node(changeset_cache, user_display_name_cache)
+          end
+      end
+
+      render :xml => result.to_s
+    end
+
+    ##
+    # query changesets by bounding box, time, user or open/closed status.
+    def query
+      # find any bounding box
+      bbox = BoundingBox.from_bbox_params(params) if params["bbox"]
+
+      # create the conditions that the user asked for. some or all of
+      # these may be nil.
+      changesets = Changeset.all
+      changesets = conditions_bbox(changesets, bbox)
+      changesets = conditions_user(changesets, params["user"], params["display_name"])
+      changesets = conditions_time(changesets, params["time"])
+      changesets = conditions_open(changesets, params["open"])
+      changesets = conditions_closed(changesets, params["closed"])
+      changesets = conditions_ids(changesets, params["changesets"])
+
+      # sort and limit the changesets
+      changesets = changesets.order("created_at DESC").limit(100)
+
+      # preload users, tags and comments
+      changesets = changesets.preload(:user, :changeset_tags, :comments)
+
+      # create the results document
+      results = OSM::API.new.get_xml_doc
+
+      # add all matching changesets to the XML results document
+      changesets.order("created_at DESC").limit(100).each do |cs|
+        results.root << cs.to_xml_node
+      end
+
+      render :xml => results.to_s
+    end
+
+    ##
+    # updates a changeset's tags. none of the changeset's attributes are
+    # user-modifiable, so they will be ignored.
+    #
+    # changesets are not (yet?) versioned, so we don't have to deal with
+    # history tables here. changesets are locked to a single user, however.
+    #
+    # after succesful update, returns the XML of the changeset.
+    def update
+      # request *must* be a PUT.
+      assert_method :put
+
+      changeset = Changeset.find(params[:id])
+      new_changeset = Changeset.from_xml(request.raw_post)
+
+      check_changeset_consistency(changeset, current_user)
+      changeset.update_from(new_changeset, current_user)
+      render :xml => changeset.to_xml.to_s
+    end
+
+    ##
+    # Adds a subscriber to the changeset
+    def subscribe
+      # Check the arguments are sane
+      raise OSM::APIBadUserInput, "No id was given" unless params[:id]
+
+      # Extract the arguments
+      id = params[:id].to_i
+
+      # Find the changeset and check it is valid
+      changeset = Changeset.find(id)
+      raise OSM::APIChangesetAlreadySubscribedError, changeset if changeset.subscribers.exists?(current_user.id)
+
+      # Add the subscriber
+      changeset.subscribers << current_user
+
+      # Return a copy of the updated changeset
+      render :xml => changeset.to_xml.to_s
+    end
+
+    ##
+    # Removes a subscriber from the changeset
+    def unsubscribe
+      # Check the arguments are sane
+      raise OSM::APIBadUserInput, "No id was given" unless params[:id]
+
+      # Extract the arguments
+      id = params[:id].to_i
+
+      # Find the changeset and check it is valid
+      changeset = Changeset.find(id)
+      raise OSM::APIChangesetNotSubscribedError, changeset unless changeset.subscribers.exists?(current_user.id)
+
+      # Remove the subscriber
+      changeset.subscribers.delete(current_user)
+
+      # Return a copy of the updated changeset
+      render :xml => changeset.to_xml.to_s
+    end
+
+    private
+
+    #------------------------------------------------------------
+    # utility functions below.
+    #------------------------------------------------------------
+
+    ##
+    # if a bounding box was specified do some sanity checks.
+    # restrict changesets to those enclosed by a bounding box
+    # we need to return both the changesets and the bounding box
+    def conditions_bbox(changesets, bbox)
+      if bbox
+        bbox.check_boundaries
+        bbox = bbox.to_scaled
+
+        changesets.where("min_lon < ? and max_lon > ? and min_lat < ? and max_lat > ?",
+                         bbox.max_lon.to_i, bbox.min_lon.to_i,
+                         bbox.max_lat.to_i, bbox.min_lat.to_i)
+      else
+        changesets
+      end
+    end
+
+    ##
+    # restrict changesets to those by a particular user
+    def conditions_user(changesets, user, name)
+      if user.nil? && name.nil?
+        changesets
+      else
+        # shouldn't provide both name and UID
+        raise OSM::APIBadUserInput, "provide either the user ID or display name, but not both" if user && name
+
+        # use either the name or the UID to find the user which we're selecting on.
+        u = if name.nil?
+              # user input checking, we don't have any UIDs < 1
+              raise OSM::APIBadUserInput, "invalid user ID" if user.to_i < 1
+
+              u = User.find(user.to_i)
+            else
+              u = User.find_by(:display_name => name)
+            end
+
+        # make sure we found a user
+        raise OSM::APINotFoundError if u.nil?
+
+        # should be able to get changesets of public users only, or
+        # our own changesets regardless of public-ness.
+        unless u.data_public?
+          # get optional user auth stuff so that users can see their own
+          # changesets if they're non-public
+          setup_user_auth
+
+          raise OSM::APINotFoundError if current_user.nil? || current_user != u
+        end
+
+        changesets.where(:user_id => u.id)
+      end
+    end
+
+    ##
+    # restrict changes to those closed during a particular time period
+    def conditions_time(changesets, time)
+      if time.nil?
+        changesets
+      elsif time.count(",") == 1
+        # if there is a range, i.e: comma separated, then the first is
+        # low, second is high - same as with bounding boxes.
+
+        # check that we actually have 2 elements in the array
+        times = time.split(/,/)
+        raise OSM::APIBadUserInput, "bad time range" if times.size != 2
+
+        from, to = times.collect { |t| Time.parse(t) }
+        changesets.where("closed_at >= ? and created_at <= ?", from, to)
+      else
+        # if there is no comma, assume its a lower limit on time
+        changesets.where("closed_at >= ?", Time.parse(time))
+      end
+      # stupid Time seems to throw both of these for bad parsing, so
+      # we have to catch both and ensure the correct code path is taken.
+    rescue ArgumentError => ex
+      raise OSM::APIBadUserInput, ex.message.to_s
+    rescue RuntimeError => ex
+      raise OSM::APIBadUserInput, ex.message.to_s
+    end
+
+    ##
+    # return changesets which are open (haven't been closed yet)
+    # we do this by seeing if the 'closed at' time is in the future. Also if we've
+    # hit the maximum number of changes then it counts as no longer open.
+    # if parameter 'open' is nill then open and closed changesets are returned
+    def conditions_open(changesets, open)
+      if open.nil?
+        changesets
+      else
+        changesets.where("closed_at >= ? and num_changes <= ?",
+                         Time.now.getutc, Changeset::MAX_ELEMENTS)
+      end
+    end
+
+    ##
+    # query changesets which are closed
+    # ('closed at' time has passed or changes limit is hit)
+    def conditions_closed(changesets, closed)
+      if closed.nil?
+        changesets
+      else
+        changesets.where("closed_at < ? or num_changes > ?",
+                         Time.now.getutc, Changeset::MAX_ELEMENTS)
+      end
+    end
+
+    ##
+    # query changesets by a list of ids
+    # (either specified as array or comma-separated string)
+    def conditions_ids(changesets, ids)
+      if ids.nil?
+        changesets
+      elsif ids.empty?
+        raise OSM::APIBadUserInput, "No changesets were given to search for"
+      else
+        ids = ids.split(",").collect(&:to_i)
+        changesets.where(:id => ids)
+      end
+    end
+  end
+end
index 80b8aff48c4e590c83bf655cff6fc34b4e982337..a69d0d57b7daaa14b9e78459a87f71de8187bc45 100644 (file)
@@ -5,256 +5,17 @@ class ChangesetsController < ApplicationController
   require "xml/libxml"
 
   skip_before_action :verify_authenticity_token, :except => [:index]
-  before_action :authorize_web, :only => [:index, :feed]
-  before_action :set_locale, :only => [:index, :feed]
-  before_action :authorize, :only => [:create, :update, :upload, :close, :subscribe, :unsubscribe]
-  before_action :api_deny_access_handler, :only => [:create, :update, :upload, :close, :subscribe, :unsubscribe, :expand_bbox]
+  before_action :authorize_web
+  before_action :set_locale
 
   authorize_resource
 
-  before_action :require_public_data, :only => [:create, :update, :upload, :close, :subscribe, :unsubscribe]
-  before_action :check_api_writable, :only => [:create, :update, :upload, :subscribe, :unsubscribe]
-  before_action :check_api_readable, :except => [:create, :update, :upload, :download, :query, :index, :feed, :subscribe, :unsubscribe]
   before_action(:only => [:index, :feed]) { |c| c.check_database_readable(true) }
-  around_action :api_call_handle_error, :except => [:index, :feed]
-  around_action :api_call_timeout, :except => [:index, :feed, :upload]
-  around_action :web_timeout, :only => [:index, :feed]
+  around_action :web_timeout
 
   # Helper methods for checking consistency
   include ConsistencyValidations
 
-  # Create a changeset from XML.
-  def create
-    assert_method :put
-
-    cs = Changeset.from_xml(request.raw_post, true)
-
-    # Assume that Changeset.from_xml has thrown an exception if there is an error parsing the xml
-    cs.user = current_user
-    cs.save_with_tags!
-
-    # Subscribe user to changeset comments
-    cs.subscribers << current_user
-
-    render :plain => cs.id.to_s
-  end
-
-  ##
-  # Return XML giving the basic info about the changeset. Does not
-  # return anything about the nodes, ways and relations in the changeset.
-  def show
-    changeset = Changeset.find(params[:id])
-
-    render :xml => changeset.to_xml(params[:include_discussion].presence).to_s
-  end
-
-  ##
-  # marks a changeset as closed. this may be called multiple times
-  # on the same changeset, so is idempotent.
-  def close
-    assert_method :put
-
-    changeset = Changeset.find(params[:id])
-    check_changeset_consistency(changeset, current_user)
-
-    # to close the changeset, we'll just set its closed_at time to
-    # now. this might not be enough if there are concurrency issues,
-    # but we'll have to wait and see.
-    changeset.set_closed_time_now
-
-    changeset.save!
-    head :ok
-  end
-
-  ##
-  # insert a (set of) points into a changeset bounding box. this can only
-  # increase the size of the bounding box. this is a hint that clients can
-  # set either before uploading a large number of changes, or changes that
-  # the client (but not the server) knows will affect areas further away.
-  def expand_bbox
-    # only allow POST requests, because although this method is
-    # idempotent, there is no "document" to PUT really...
-    assert_method :post
-
-    cs = Changeset.find(params[:id])
-    check_changeset_consistency(cs, current_user)
-
-    # keep an array of lons and lats
-    lon = []
-    lat = []
-
-    # the request is in pseudo-osm format... this is kind-of an
-    # abuse, maybe should change to some other format?
-    doc = XML::Parser.string(request.raw_post, :options => XML::Parser::Options::NOERROR).parse
-    doc.find("//osm/node").each do |n|
-      lon << n["lon"].to_f * GeoRecord::SCALE
-      lat << n["lat"].to_f * GeoRecord::SCALE
-    end
-
-    # add the existing bounding box to the lon-lat array
-    lon << cs.min_lon unless cs.min_lon.nil?
-    lat << cs.min_lat unless cs.min_lat.nil?
-    lon << cs.max_lon unless cs.max_lon.nil?
-    lat << cs.max_lat unless cs.max_lat.nil?
-
-    # collapse the arrays to minimum and maximum
-    cs.min_lon = lon.min
-    cs.min_lat = lat.min
-    cs.max_lon = lon.max
-    cs.max_lat = lat.max
-
-    # save the larger bounding box and return the changeset, which
-    # will include the bigger bounding box.
-    cs.save!
-    render :xml => cs.to_xml.to_s
-  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: a diffResult document, as described in
-  # http://wiki.openstreetmap.org/wiki/OSM_Protocol_Version_0.6
-  def upload
-    # 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
-    assert_method :post
-
-    changeset = Changeset.find(params[:id])
-    check_changeset_consistency(changeset, current_user)
-
-    diff_reader = DiffReader.new(request.raw_post, changeset)
-    Changeset.transaction do
-      result = diff_reader.commit
-      render :xml => result.to_s
-    end
-  end
-
-  ##
-  # download the changeset as an osmChange document.
-  #
-  # to make it easier to revert diffs it would be better if the osmChange
-  # format were reversible, i.e: contained both old and new versions of
-  # modified elements. but it doesn't at the moment...
-  #
-  # this method cannot order the database changes fully (i.e: timestamp and
-  # version number may be too coarse) so the resulting diff may not apply
-  # to a different database. however since changesets are not atomic this
-  # behaviour cannot be guaranteed anyway and is the result of a design
-  # choice.
-  def download
-    changeset = Changeset.find(params[:id])
-
-    # get all the elements in the changeset which haven't been redacted
-    # and stick them in a big array.
-    elements = [changeset.old_nodes.unredacted,
-                changeset.old_ways.unredacted,
-                changeset.old_relations.unredacted].flatten
-
-    # sort the elements by timestamp and version number, as this is the
-    # almost sensible ordering available. this would be much nicer if
-    # global (SVN-style) versioning were used - then that would be
-    # unambiguous.
-    elements.sort! do |a, b|
-      if a.timestamp == b.timestamp
-        a.version <=> b.version
-      else
-        a.timestamp <=> b.timestamp
-      end
-    end
-
-    # create changeset and user caches
-    changeset_cache = {}
-    user_display_name_cache = {}
-
-    # create an osmChange document for the output
-    result = OSM::API.new.get_xml_doc
-    result.root.name = "osmChange"
-
-    # generate an output element for each operation. note: we avoid looking
-    # at the history because it is simpler - but it would be more correct to
-    # check these assertions.
-    elements.each do |elt|
-      result.root <<
-        if elt.version == 1
-          # first version, so it must be newly-created.
-          created = XML::Node.new "create"
-          created << elt.to_xml_node(changeset_cache, user_display_name_cache)
-        elsif elt.visible
-          # must be a modify
-          modified = XML::Node.new "modify"
-          modified << elt.to_xml_node(changeset_cache, user_display_name_cache)
-        else
-          # if the element isn't visible then it must have been deleted
-          deleted = XML::Node.new "delete"
-          deleted << elt.to_xml_node(changeset_cache, user_display_name_cache)
-        end
-    end
-
-    render :xml => result.to_s
-  end
-
-  ##
-  # query changesets by bounding box, time, user or open/closed status.
-  def query
-    # find any bounding box
-    bbox = BoundingBox.from_bbox_params(params) if params["bbox"]
-
-    # create the conditions that the user asked for. some or all of
-    # these may be nil.
-    changesets = Changeset.all
-    changesets = conditions_bbox(changesets, bbox)
-    changesets = conditions_user(changesets, params["user"], params["display_name"])
-    changesets = conditions_time(changesets, params["time"])
-    changesets = conditions_open(changesets, params["open"])
-    changesets = conditions_closed(changesets, params["closed"])
-    changesets = conditions_ids(changesets, params["changesets"])
-
-    # sort and limit the changesets
-    changesets = changesets.order("created_at DESC").limit(100)
-
-    # preload users, tags and comments
-    changesets = changesets.preload(:user, :changeset_tags, :comments)
-
-    # create the results document
-    results = OSM::API.new.get_xml_doc
-
-    # add all matching changesets to the XML results document
-    changesets.order("created_at DESC").limit(100).each do |cs|
-      results.root << cs.to_xml_node
-    end
-
-    render :xml => results.to_s
-  end
-
-  ##
-  # updates a changeset's tags. none of the changeset's attributes are
-  # user-modifiable, so they will be ignored.
-  #
-  # changesets are not (yet?) versioned, so we don't have to deal with
-  # history tables here. changesets are locked to a single user, however.
-  #
-  # after succesful update, returns the XML of the changeset.
-  def update
-    # request *must* be a PUT.
-    assert_method :put
-
-    changeset = Changeset.find(params[:id])
-    new_changeset = Changeset.from_xml(request.raw_post)
-
-    check_changeset_consistency(changeset, current_user)
-    changeset.update_from(new_changeset, current_user)
-    render :xml => changeset.to_xml.to_s
-  end
-
   ##
   # list non-empty changesets in reverse chronological order
   def index
@@ -312,46 +73,6 @@ class ChangesetsController < ApplicationController
     index
   end
 
-  ##
-  # Adds a subscriber to the changeset
-  def subscribe
-    # Check the arguments are sane
-    raise OSM::APIBadUserInput, "No id was given" unless params[:id]
-
-    # Extract the arguments
-    id = params[:id].to_i
-
-    # Find the changeset and check it is valid
-    changeset = Changeset.find(id)
-    raise OSM::APIChangesetAlreadySubscribedError, changeset if changeset.subscribers.exists?(current_user.id)
-
-    # Add the subscriber
-    changeset.subscribers << current_user
-
-    # Return a copy of the updated changeset
-    render :xml => changeset.to_xml.to_s
-  end
-
-  ##
-  # Removes a subscriber from the changeset
-  def unsubscribe
-    # Check the arguments are sane
-    raise OSM::APIBadUserInput, "No id was given" unless params[:id]
-
-    # Extract the arguments
-    id = params[:id].to_i
-
-    # Find the changeset and check it is valid
-    changeset = Changeset.find(id)
-    raise OSM::APIChangesetNotSubscribedError, changeset unless changeset.subscribers.exists?(current_user.id)
-
-    # Remove the subscriber
-    changeset.subscribers.delete(current_user)
-
-    # Return a copy of the updated changeset
-    render :xml => changeset.to_xml.to_s
-  end
-
   private
 
   #------------------------------------------------------------
@@ -375,109 +96,6 @@ class ChangesetsController < ApplicationController
     end
   end
 
-  ##
-  # restrict changesets to those by a particular user
-  def conditions_user(changesets, user, name)
-    if user.nil? && name.nil?
-      changesets
-    else
-      # shouldn't provide both name and UID
-      raise OSM::APIBadUserInput, "provide either the user ID or display name, but not both" if user && name
-
-      # use either the name or the UID to find the user which we're selecting on.
-      u = if name.nil?
-            # user input checking, we don't have any UIDs < 1
-            raise OSM::APIBadUserInput, "invalid user ID" if user.to_i < 1
-
-            u = User.find(user.to_i)
-          else
-            u = User.find_by(:display_name => name)
-          end
-
-      # make sure we found a user
-      raise OSM::APINotFoundError if u.nil?
-
-      # should be able to get changesets of public users only, or
-      # our own changesets regardless of public-ness.
-      unless u.data_public?
-        # get optional user auth stuff so that users can see their own
-        # changesets if they're non-public
-        setup_user_auth
-
-        raise OSM::APINotFoundError if current_user.nil? || current_user != u
-      end
-
-      changesets.where(:user_id => u.id)
-    end
-  end
-
-  ##
-  # restrict changes to those closed during a particular time period
-  def conditions_time(changesets, time)
-    if time.nil?
-      changesets
-    elsif time.count(",") == 1
-      # if there is a range, i.e: comma separated, then the first is
-      # low, second is high - same as with bounding boxes.
-
-      # check that we actually have 2 elements in the array
-      times = time.split(/,/)
-      raise OSM::APIBadUserInput, "bad time range" if times.size != 2
-
-      from, to = times.collect { |t| Time.parse(t) }
-      changesets.where("closed_at >= ? and created_at <= ?", from, to)
-    else
-      # if there is no comma, assume its a lower limit on time
-      changesets.where("closed_at >= ?", Time.parse(time))
-    end
-    # stupid Time seems to throw both of these for bad parsing, so
-    # we have to catch both and ensure the correct code path is taken.
-  rescue ArgumentError => ex
-    raise OSM::APIBadUserInput, ex.message.to_s
-  rescue RuntimeError => ex
-    raise OSM::APIBadUserInput, ex.message.to_s
-  end
-
-  ##
-  # return changesets which are open (haven't been closed yet)
-  # we do this by seeing if the 'closed at' time is in the future. Also if we've
-  # hit the maximum number of changes then it counts as no longer open.
-  # if parameter 'open' is nill then open and closed changesets are returned
-  def conditions_open(changesets, open)
-    if open.nil?
-      changesets
-    else
-      changesets.where("closed_at >= ? and num_changes <= ?",
-                       Time.now.getutc, Changeset::MAX_ELEMENTS)
-    end
-  end
-
-  ##
-  # query changesets which are closed
-  # ('closed at' time has passed or changes limit is hit)
-  def conditions_closed(changesets, closed)
-    if closed.nil?
-      changesets
-    else
-      changesets.where("closed_at < ? or num_changes > ?",
-                       Time.now.getutc, Changeset::MAX_ELEMENTS)
-    end
-  end
-
-  ##
-  # query changesets by a list of ids
-  # (either specified as array or comma-separated string)
-  def conditions_ids(changesets, ids)
-    if ids.nil?
-      changesets
-    elsif ids.empty?
-      raise OSM::APIBadUserInput, "No changesets were given to search for"
-    else
-      ids = ids.split(",").collect(&:to_i)
-      changesets.where(:id => ids)
-    end
-  end
-
   ##
   # eliminate empty changesets (where the bbox has not been set)
   # this should be applied to all changeset list displays
index 0b73fa083d62bdaea53a90148abc799b4eab448d..1328461d77c0e3c574061f80b195f959775dacc5 100644 (file)
 <% end %>
 
 <div class='secondary-actions'>
-  <%= link_to(t('.changesetxml'), :controller => "changesets", :action => "show") %>
+  <%= link_to(t('.changesetxml'), :controller => "api/changesets", :action => "show") %>
   &middot;
-  <%= link_to(t('.osmchangexml'), :controller => "changesets", :action => "download") %>
+  <%= link_to(t('.osmchangexml'), :controller => "api/changesets", :action => "download") %>
 </div>
index 3c0cf61f1587c6050109bf54bf8452f9d5b760e3..95142a49f254c0bfd019d10e77329c7f9bff93e2 100644 (file)
@@ -8,16 +8,16 @@ OpenStreetMap::Application.routes.draw do
     get "capabilities" => "api/capabilities#show"
     get "permissions" => "api/permissions#show"
 
-    put "changeset/create" => "changesets#create"
-    post "changeset/:id/upload" => "changesets#upload", :id => /\d+/
-    get "changeset/:id/download" => "changesets#download", :as => :changeset_download, :id => /\d+/
-    post "changeset/:id/expand_bbox" => "changesets#expand_bbox", :id => /\d+/
-    get "changeset/:id" => "changesets#show", :as => :changeset_show, :id => /\d+/
-    post "changeset/:id/subscribe" => "changesets#subscribe", :as => :changeset_subscribe, :id => /\d+/
-    post "changeset/:id/unsubscribe" => "changesets#unsubscribe", :as => :changeset_unsubscribe, :id => /\d+/
-    put "changeset/:id" => "changesets#update", :id => /\d+/
-    put "changeset/:id/close" => "changesets#close", :id => /\d+/
-    get "changesets" => "changesets#query"
+    put "changeset/create" => "api/changesets#create"
+    post "changeset/:id/upload" => "api/changesets#upload", :id => /\d+/
+    get "changeset/:id/download" => "api/changesets#download", :as => :changeset_download, :id => /\d+/
+    post "changeset/:id/expand_bbox" => "api/changesets#expand_bbox", :id => /\d+/
+    get "changeset/:id" => "api/changesets#show", :as => :changeset_show, :id => /\d+/
+    post "changeset/:id/subscribe" => "api/changesets#subscribe", :as => :changeset_subscribe, :id => /\d+/
+    post "changeset/:id/unsubscribe" => "api/changesets#unsubscribe", :as => :changeset_unsubscribe, :id => /\d+/
+    put "changeset/:id" => "api/changesets#update", :id => /\d+/
+    put "changeset/:id/close" => "api/changesets#close", :id => /\d+/
+    get "changesets" => "api/changesets#query"
     post "changeset/:id/comment" => "changeset_comments#create", :as => :changeset_comment, :id => /\d+/
     post "changeset/comment/:id/hide" => "changeset_comments#destroy", :as => :changeset_comment_hide, :id => /\d+/
     post "changeset/comment/:id/unhide" => "changeset_comments#restore", :as => :changeset_comment_unhide, :id => /\d+/
diff --git a/test/controllers/api/changesets_controller_test.rb b/test/controllers/api/changesets_controller_test.rb
new file mode 100644 (file)
index 0000000..75896b2
--- /dev/null
@@ -0,0 +1,1963 @@
+require "test_helper"
+
+module Api
+  class ChangesetsControllerTest < ActionController::TestCase
+    ##
+    # test all routes which lead to this controller
+    def test_routes
+      assert_routing(
+        { :path => "/api/0.6/changeset/create", :method => :put },
+        { :controller => "api/changesets", :action => "create" }
+      )
+      assert_routing(
+        { :path => "/api/0.6/changeset/1/upload", :method => :post },
+        { :controller => "api/changesets", :action => "upload", :id => "1" }
+      )
+      assert_routing(
+        { :path => "/api/0.6/changeset/1/download", :method => :get },
+        { :controller => "api/changesets", :action => "download", :id => "1" }
+      )
+      assert_routing(
+        { :path => "/api/0.6/changeset/1/expand_bbox", :method => :post },
+        { :controller => "api/changesets", :action => "expand_bbox", :id => "1" }
+      )
+      assert_routing(
+        { :path => "/api/0.6/changeset/1", :method => :get },
+        { :controller => "api/changesets", :action => "show", :id => "1" }
+      )
+      assert_routing(
+        { :path => "/api/0.6/changeset/1/subscribe", :method => :post },
+        { :controller => "api/changesets", :action => "subscribe", :id => "1" }
+      )
+      assert_routing(
+        { :path => "/api/0.6/changeset/1/unsubscribe", :method => :post },
+        { :controller => "api/changesets", :action => "unsubscribe", :id => "1" }
+      )
+      assert_routing(
+        { :path => "/api/0.6/changeset/1", :method => :put },
+        { :controller => "api/changesets", :action => "update", :id => "1" }
+      )
+      assert_routing(
+        { :path => "/api/0.6/changeset/1/close", :method => :put },
+        { :controller => "api/changesets", :action => "close", :id => "1" }
+      )
+      assert_routing(
+        { :path => "/api/0.6/changesets", :method => :get },
+        { :controller => "api/changesets", :action => "query" }
+      )
+    end
+
+    # -----------------------
+    # Test simple changeset creation
+    # -----------------------
+
+    def test_create
+      basic_authorization create(:user, :data_public => false).email, "test"
+      # Create the first user's changeset
+      xml = "<osm><changeset>" \
+            "<tag k='created_by' v='osm test suite checking changesets'/>" \
+            "</changeset></osm>"
+      put :create, :body => xml
+      assert_require_public_data
+
+      basic_authorization create(:user).email, "test"
+      # Create the first user's changeset
+      xml = "<osm><changeset>" \
+            "<tag k='created_by' v='osm test suite checking changesets'/>" \
+            "</changeset></osm>"
+      put :create, :body => xml
+
+      assert_response :success, "Creation of changeset did not return sucess status"
+      newid = @response.body.to_i
+
+      # check end time, should be an hour ahead of creation time
+      cs = Changeset.find(newid)
+      duration = cs.closed_at - cs.created_at
+      # the difference can either be a rational, or a floating point number
+      # of seconds, depending on the code path taken :-(
+      if duration.class == Rational
+        assert_equal Rational(1, 24), duration, "initial idle timeout should be an hour (#{cs.created_at} -> #{cs.closed_at})"
+      else
+        # must be number of seconds...
+        assert_equal 3600, duration.round, "initial idle timeout should be an hour (#{cs.created_at} -> #{cs.closed_at})"
+      end
+
+      # checks if uploader was subscribed
+      assert_equal 1, cs.subscribers.length
+    end
+
+    def test_create_invalid
+      basic_authorization create(:user, :data_public => false).email, "test"
+      xml = "<osm><changeset></osm>"
+      put :create, :body => xml
+      assert_require_public_data
+
+      ## Try the public user
+      basic_authorization create(:user).email, "test"
+      xml = "<osm><changeset></osm>"
+      put :create, :body => xml
+      assert_response :bad_request, "creating a invalid changeset should fail"
+    end
+
+    def test_create_invalid_no_content
+      ## First check with no auth
+      put :create
+      assert_response :unauthorized, "shouldn't be able to create a changeset with no auth"
+
+      ## Now try to with a non-public user
+      basic_authorization create(:user, :data_public => false).email, "test"
+      put :create
+      assert_require_public_data
+
+      ## Try an inactive user
+      basic_authorization create(:user, :pending).email, "test"
+      put :create
+      assert_inactive_user
+
+      ## Now try to use a normal user
+      basic_authorization create(:user).email, "test"
+      put :create
+      assert_response :bad_request, "creating a changeset with no content should fail"
+    end
+
+    def test_create_wrong_method
+      basic_authorization create(:user).email, "test"
+      get :create
+      assert_response :method_not_allowed
+      post :create
+      assert_response :method_not_allowed
+    end
+
+    ##
+    # check that the changeset can be shown and returns the correct
+    # document structure.
+    def test_show
+      changeset_id = create(:changeset).id
+
+      get :show, :params => { :id => changeset_id }
+      assert_response :success, "cannot get first changeset"
+
+      assert_select "osm[version='#{API_VERSION}'][generator='OpenStreetMap server']", 1
+      assert_select "osm>changeset[id='#{changeset_id}']", 1
+      assert_select "osm>changeset>discussion", 0
+
+      get :show, :params => { :id => changeset_id, :include_discussion => true }
+      assert_response :success, "cannot get first changeset with comments"
+
+      assert_select "osm[version='#{API_VERSION}'][generator='OpenStreetMap server']", 1
+      assert_select "osm>changeset[id='#{changeset_id}']", 1
+      assert_select "osm>changeset>discussion", 1
+      assert_select "osm>changeset>discussion>comment", 0
+
+      changeset_id = create(:changeset, :closed).id
+      create_list(:changeset_comment, 3, :changeset_id => changeset_id)
+
+      get :show, :params => { :id => changeset_id, :include_discussion => true }
+      assert_response :success, "cannot get closed changeset with comments"
+
+      assert_select "osm[version='#{API_VERSION}'][generator='OpenStreetMap server']", 1
+      assert_select "osm>changeset[id='#{changeset_id}']", 1
+      assert_select "osm>changeset>discussion", 1
+      assert_select "osm>changeset>discussion>comment", 3
+    end
+
+    ##
+    # check that a changeset that doesn't exist returns an appropriate message
+    def test_show_not_found
+      [0, -32, 233455644, "afg", "213"].each do |id|
+        begin
+          get :show, :params => { :id => id }
+          assert_response :not_found, "should get a not found"
+        rescue ActionController::UrlGenerationError => ex
+          assert_match(/No route matches/, ex.to_s)
+        end
+      end
+    end
+
+    ##
+    # test that the user who opened a change can close it
+    def test_close
+      private_user = create(:user, :data_public => false)
+      private_changeset = create(:changeset, :user => private_user)
+      user = create(:user)
+      changeset = create(:changeset, :user => user)
+
+      ## Try without authentication
+      put :close, :params => { :id => changeset.id }
+      assert_response :unauthorized
+
+      ## Try using the non-public user
+      basic_authorization private_user.email, "test"
+      put :close, :params => { :id => private_changeset.id }
+      assert_require_public_data
+
+      ## The try with the public user
+      basic_authorization user.email, "test"
+
+      cs_id = changeset.id
+      put :close, :params => { :id => cs_id }
+      assert_response :success
+
+      # test that it really is closed now
+      cs = Changeset.find(cs_id)
+      assert_not(cs.is_open?,
+                 "changeset should be closed now (#{cs.closed_at} > #{Time.now.getutc}.")
+    end
+
+    ##
+    # test that a different user can't close another user's changeset
+    def test_close_invalid
+      user = create(:user)
+      changeset = create(:changeset)
+
+      basic_authorization user.email, "test"
+
+      put :close, :params => { :id => changeset.id }
+      assert_response :conflict
+      assert_equal "The user doesn't own that changeset", @response.body
+    end
+
+    ##
+    # test that you can't close using another method
+    def test_close_method_invalid
+      user = create(:user)
+      changeset = create(:changeset, :user => user)
+
+      basic_authorization user.email, "test"
+
+      get :close, :params => { :id => changeset.id }
+      assert_response :method_not_allowed
+
+      post :close, :params => { :id => changeset.id }
+      assert_response :method_not_allowed
+    end
+
+    ##
+    # check that you can't close a changeset that isn't found
+    def test_close_not_found
+      cs_ids = [0, -132, "123"]
+
+      # First try to do it with no auth
+      cs_ids.each do |id|
+        begin
+          put :close, :params => { :id => id }
+          assert_response :unauthorized, "Shouldn't be able close the non-existant changeset #{id}, when not authorized"
+        rescue ActionController::UrlGenerationError => ex
+          assert_match(/No route matches/, ex.to_s)
+        end
+      end
+
+      # Now try with auth
+      basic_authorization create(:user).email, "test"
+      cs_ids.each do |id|
+        begin
+          put :close, :params => { :id => id }
+          assert_response :not_found, "The changeset #{id} doesn't exist, so can't be closed"
+        rescue ActionController::UrlGenerationError => ex
+          assert_match(/No route matches/, ex.to_s)
+        end
+      end
+    end
+
+    ##
+    # upload something simple, but valid and check that it can
+    # be read back ok
+    # Also try without auth and another user.
+    def test_upload_simple_valid
+      private_user = create(:user, :data_public => false)
+      private_changeset = create(:changeset, :user => private_user)
+      user = create(:user)
+      changeset = create(:changeset, :user => user)
+
+      node = create(:node)
+      way = create(:way)
+      relation = create(:relation)
+      other_relation = create(:relation)
+      # create some tags, since we test that they are removed later
+      create(:node_tag, :node => node)
+      create(:way_tag, :way => way)
+      create(:relation_tag, :relation => relation)
+
+      ## Try with no auth
+      changeset_id = changeset.id
+
+      # simple diff to change a node, way and relation by removing
+      # their tags
+      diff = <<CHANGESET.strip_heredoc
+        <osmChange>
+         <modify>
+          <node id='#{node.id}' lon='0' lat='0' changeset='#{changeset_id}' version='1'/>
+          <way id='#{way.id}' changeset='#{changeset_id}' version='1'>
+           <nd ref='#{node.id}'/>
+          </way>
+         </modify>
+         <modify>
+          <relation id='#{relation.id}' changeset='#{changeset_id}' version='1'>
+           <member type='way' role='some' ref='#{way.id}'/>
+           <member type='node' role='some' ref='#{node.id}'/>
+           <member type='relation' role='some' ref='#{other_relation.id}'/>
+          </relation>
+         </modify>
+        </osmChange>
+CHANGESET
+
+      # upload it
+      post :upload, :params => { :id => changeset_id }, :body => diff
+      assert_response :unauthorized,
+                      "shouldn't be able to upload a simple valid diff to changeset: #{@response.body}"
+
+      ## Now try with a private user
+      basic_authorization private_user.email, "test"
+      changeset_id = private_changeset.id
+
+      # simple diff to change a node, way and relation by removing
+      # their tags
+      diff = <<CHANGESET.strip_heredoc
+        <osmChange>
+         <modify>
+          <node id='#{node.id}' lon='0' lat='0' changeset='#{changeset_id}' version='1'/>
+          <way id='#{way.id}' changeset='#{changeset_id}' version='1'>
+           <nd ref='#{node.id}'/>
+          </way>
+         </modify>
+         <modify>
+          <relation id='#{relation.id}' changeset='#{changeset_id}' version='1'>
+           <member type='way' role='some' ref='#{way.id}'/>
+           <member type='node' role='some' ref='#{node.id}'/>
+           <member type='relation' role='some' ref='#{other_relation.id}'/>
+          </relation>
+         </modify>
+        </osmChange>
+CHANGESET
+
+      # upload it
+      post :upload, :params => { :id => changeset_id }, :body => diff
+      assert_response :forbidden,
+                      "can't upload a simple valid diff to changeset: #{@response.body}"
+
+      ## Now try with the public user
+      basic_authorization user.email, "test"
+      changeset_id = changeset.id
+
+      # simple diff to change a node, way and relation by removing
+      # their tags
+      diff = <<CHANGESET.strip_heredoc
+        <osmChange>
+         <modify>
+          <node id='#{node.id}' lon='0' lat='0' changeset='#{changeset_id}' version='1'/>
+          <way id='#{way.id}' changeset='#{changeset_id}' version='1'>
+           <nd ref='#{node.id}'/>
+          </way>
+         </modify>
+         <modify>
+          <relation id='#{relation.id}' changeset='#{changeset_id}' version='1'>
+           <member type='way' role='some' ref='#{way.id}'/>
+           <member type='node' role='some' ref='#{node.id}'/>
+           <member type='relation' role='some' ref='#{other_relation.id}'/>
+          </relation>
+         </modify>
+        </osmChange>
+CHANGESET
+
+      # upload it
+      post :upload, :params => { :id => changeset_id }, :body => diff
+      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(node.id).tags.size, "node #{node.id} should now have no tags"
+      assert_equal 0, Way.find(way.id).tags.size, "way #{way.id} should now have no tags"
+      assert_equal 0, Relation.find(relation.id).tags.size, "relation #{relation.id} should now have no tags"
+    end
+
+    ##
+    # upload something which creates new objects using placeholders
+    def test_upload_create_valid
+      user = create(:user)
+      changeset = create(:changeset, :user => user)
+      node = create(:node)
+      way = create(:way_with_nodes, :nodes_count => 2)
+      relation = create(:relation)
+
+      basic_authorization user.email, "test"
+
+      # simple diff to create a node way and relation using placeholders
+      diff = <<CHANGESET.strip_heredoc
+        <osmChange>
+         <create>
+          <node id='-1' lon='0' lat='0' changeset='#{changeset.id}'>
+           <tag k='foo' v='bar'/>
+           <tag k='baz' v='bat'/>
+          </node>
+          <way id='-1' changeset='#{changeset.id}'>
+           <nd ref='#{node.id}'/>
+          </way>
+         </create>
+         <create>
+          <relation id='-1' changeset='#{changeset.id}'>
+           <member type='way' role='some' ref='#{way.id}'/>
+           <member type='node' role='some' ref='#{node.id}'/>
+           <member type='relation' role='some' ref='#{relation.id}'/>
+          </relation>
+         </create>
+        </osmChange>
+CHANGESET
+
+      # upload it
+      post :upload, :params => { :id => changeset.id }, :body => diff
+      assert_response :success,
+                      "can't upload a simple valid creation to changeset: #{@response.body}"
+
+      # check the returned payload
+      assert_select "diffResult[version='#{API_VERSION}'][generator='OpenStreetMap server']", 1
+      assert_select "diffResult>node", 1
+      assert_select "diffResult>way", 1
+      assert_select "diffResult>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("//diffResult/node").first["new_id"].to_i
+      new_way_id = doc.find("//diffResult/way").first["new_id"].to_i
+      new_rel_id = doc.find("//diffResult/relation").first["new_id"].to_i
+
+      # check the old IDs are all present and negative one
+      assert_equal(-1, doc.find("//diffResult/node").first["old_id"].to_i)
+      assert_equal(-1, doc.find("//diffResult/way").first["old_id"].to_i)
+      assert_equal(-1, doc.find("//diffResult/relation").first["old_id"].to_i)
+
+      # check the versions are present and equal one
+      assert_equal 1, doc.find("//diffResult/node").first["new_version"].to_i
+      assert_equal 1, doc.find("//diffResult/way").first["new_version"].to_i
+      assert_equal 1, doc.find("//diffResult/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
+      changeset = create(:changeset)
+      super_relation = create(:relation)
+      used_relation = create(:relation)
+      used_way = create(:way)
+      used_node = create(:node)
+      create(:relation_member, :relation => super_relation, :member => used_relation)
+      create(:relation_member, :relation => super_relation, :member => used_way)
+      create(:relation_member, :relation => super_relation, :member => used_node)
+
+      basic_authorization changeset.user.display_name, "test"
+
+      diff = XML::Document.new
+      diff.root = XML::Node.new "osmChange"
+      delete = XML::Node.new "delete"
+      diff.root << delete
+      delete << super_relation.to_xml_node
+      delete << used_relation.to_xml_node
+      delete << used_way.to_xml_node
+      delete << used_node.to_xml_node
+
+      # update the changeset to one that this user owns
+      %w[node way relation].each do |type|
+        delete.find("//osmChange/delete/#{type}").each do |n|
+          n["changeset"] = changeset.id.to_s
+        end
+      end
+
+      # upload it
+      post :upload, :params => { :id => changeset.id }, :body => diff.to_s
+      assert_response :success,
+                      "can't upload a deletion diff to changeset: #{@response.body}"
+
+      # check the response is well-formed
+      assert_select "diffResult>node", 1
+      assert_select "diffResult>way", 1
+      assert_select "diffResult>relation", 2
+
+      # check that everything was deleted
+      assert_equal false, Node.find(used_node.id).visible
+      assert_equal false, Way.find(used_way.id).visible
+      assert_equal false, Relation.find(super_relation.id).visible
+      assert_equal false, Relation.find(used_relation.id).visible
+    end
+
+    ##
+    # test uploading a delete with no lat/lon, as they are optional in
+    # the osmChange spec.
+    def test_upload_nolatlon_delete
+      node = create(:node)
+      changeset = create(:changeset)
+
+      basic_authorization changeset.user.display_name, "test"
+      diff = "<osmChange><delete><node id='#{node.id}' version='#{node.version}' changeset='#{changeset.id}'/></delete></osmChange>"
+
+      # upload it
+      post :upload, :params => { :id => changeset.id }, :body => diff
+      assert_response :success,
+                      "can't upload a deletion diff to changeset: #{@response.body}"
+
+      # check the response is well-formed
+      assert_select "diffResult>node", 1
+
+      # check that everything was deleted
+      assert_equal false, Node.find(node.id).visible
+    end
+
+    def test_repeated_changeset_create
+      3.times do
+        basic_authorization create(:user).email, "test"
+
+        # create a temporary changeset
+        xml = "<osm><changeset>" \
+              "<tag k='created_by' v='osm test suite checking changesets'/>" \
+              "</changeset></osm>"
+        assert_difference "Changeset.count", 1 do
+          put :create, :body => xml
+        end
+        assert_response :success
+      end
+    end
+
+    def test_upload_large_changeset
+      basic_authorization create(:user).email, "test"
+
+      # create a changeset
+      put :create, :body => "<osm><changeset/></osm>"
+      assert_response :success, "Should be able to create a changeset: #{@response.body}"
+      changeset_id = @response.body.to_i
+
+      # upload some widely-spaced nodes, spiralling positive and negative
+      diff = <<CHANGESET.strip_heredoc
+        <osmChange>
+         <create>
+          <node id='-1' lon='-20' lat='-10' changeset='#{changeset_id}'/>
+          <node id='-10' lon='20'  lat='10' changeset='#{changeset_id}'/>
+          <node id='-2' lon='-40' lat='-20' changeset='#{changeset_id}'/>
+          <node id='-11' lon='40'  lat='20' changeset='#{changeset_id}'/>
+          <node id='-3' lon='-60' lat='-30' changeset='#{changeset_id}'/>
+          <node id='-12' lon='60'  lat='30' changeset='#{changeset_id}'/>
+          <node id='-4' lon='-80' lat='-40' changeset='#{changeset_id}'/>
+          <node id='-13' lon='80'  lat='40' changeset='#{changeset_id}'/>
+          <node id='-5' lon='-100' lat='-50' changeset='#{changeset_id}'/>
+          <node id='-14' lon='100'  lat='50' changeset='#{changeset_id}'/>
+          <node id='-6' lon='-120' lat='-60' changeset='#{changeset_id}'/>
+          <node id='-15' lon='120'  lat='60' changeset='#{changeset_id}'/>
+          <node id='-7' lon='-140' lat='-70' changeset='#{changeset_id}'/>
+          <node id='-16' lon='140'  lat='70' changeset='#{changeset_id}'/>
+          <node id='-8' lon='-160' lat='-80' changeset='#{changeset_id}'/>
+          <node id='-17' lon='160'  lat='80' changeset='#{changeset_id}'/>
+          <node id='-9' lon='-179.9' lat='-89.9' changeset='#{changeset_id}'/>
+          <node id='-18' lon='179.9'  lat='89.9' changeset='#{changeset_id}'/>
+         </create>
+        </osmChange>
+CHANGESET
+
+      # upload it, which used to cause an error like "PGError: ERROR:
+      # integer out of range" (bug #2152). but shouldn't any more.
+      post :upload, :params => { :id => changeset_id }, :body => diff
+      assert_response :success,
+                      "can't upload a spatially-large diff to changeset: #{@response.body}"
+
+      # check that the changeset bbox is within bounds
+      cs = Changeset.find(changeset_id)
+      assert cs.min_lon >= -180 * GeoRecord::SCALE, "Minimum longitude (#{cs.min_lon / GeoRecord::SCALE}) should be >= -180 to be valid."
+      assert cs.max_lon <= 180 * GeoRecord::SCALE, "Maximum longitude (#{cs.max_lon / GeoRecord::SCALE}) should be <= 180 to be valid."
+      assert cs.min_lat >= -90 * GeoRecord::SCALE, "Minimum latitude (#{cs.min_lat / GeoRecord::SCALE}) should be >= -90 to be valid."
+      assert cs.max_lat <= 90 * GeoRecord::SCALE, "Maximum latitude (#{cs.max_lat / GeoRecord::SCALE}) should be <= 90 to be valid."
+    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
+      changeset = create(:changeset)
+      relation = create(:relation)
+      other_relation = create(:relation)
+      used_way = create(:way)
+      used_node = create(:node)
+      create(:relation_member, :relation => relation, :member => used_way)
+      create(:relation_member, :relation => relation, :member => used_node)
+
+      basic_authorization changeset.user.email, "test"
+
+      diff = XML::Document.new
+      diff.root = XML::Node.new "osmChange"
+      delete = XML::Node.new "delete"
+      diff.root << delete
+      delete << other_relation.to_xml_node
+      delete << used_way.to_xml_node
+      delete << used_node.to_xml_node
+
+      # update the changeset to one that this user owns
+      %w[node way relation].each do |type|
+        delete.find("//osmChange/delete/#{type}").each do |n|
+          n["changeset"] = changeset.id.to_s
+        end
+      end
+
+      # upload it
+      post :upload, :params => { :id => changeset.id }, :body => diff.to_s
+      assert_response :precondition_failed,
+                      "shouldn't be able to upload a invalid deletion diff: #{@response.body}"
+      assert_equal "Precondition failed: Way #{used_way.id} is still used by relations #{relation.id}.", @response.body
+
+      # check that nothing was, in fact, deleted
+      assert_equal true, Node.find(used_node.id).visible
+      assert_equal true, Way.find(used_way.id).visible
+      assert_equal true, Relation.find(relation.id).visible
+      assert_equal true, Relation.find(other_relation.id).visible
+    end
+
+    ##
+    # test that a conditional delete of an in use object works.
+    def test_upload_delete_if_unused
+      changeset = create(:changeset)
+      super_relation = create(:relation)
+      used_relation = create(:relation)
+      used_way = create(:way)
+      used_node = create(:node)
+      create(:relation_member, :relation => super_relation, :member => used_relation)
+      create(:relation_member, :relation => super_relation, :member => used_way)
+      create(:relation_member, :relation => super_relation, :member => used_node)
+
+      basic_authorization changeset.user.email, "test"
+
+      diff = XML::Document.new
+      diff.root = XML::Node.new "osmChange"
+      delete = XML::Node.new "delete"
+      diff.root << delete
+      delete["if-unused"] = ""
+      delete << used_relation.to_xml_node
+      delete << used_way.to_xml_node
+      delete << used_node.to_xml_node
+
+      # update the changeset to one that this user owns
+      %w[node way relation].each do |type|
+        delete.find("//osmChange/delete/#{type}").each do |n|
+          n["changeset"] = changeset.id.to_s
+        end
+      end
+
+      # upload it
+      post :upload, :params => { :id => changeset.id }, :body => diff.to_s
+      assert_response :success,
+                      "can't do a conditional delete of in use objects: #{@response.body}"
+
+      # check the returned payload
+      assert_select "diffResult[version='#{API_VERSION}'][generator='OpenStreetMap server']", 1
+      assert_select "diffResult>node", 1
+      assert_select "diffResult>way", 1
+      assert_select "diffResult>relation", 1
+
+      # parse the response
+      doc = XML::Parser.string(@response.body).parse
+
+      # check the old IDs are all present and what we expect
+      assert_equal used_node.id, doc.find("//diffResult/node").first["old_id"].to_i
+      assert_equal used_way.id, doc.find("//diffResult/way").first["old_id"].to_i
+      assert_equal used_relation.id, doc.find("//diffResult/relation").first["old_id"].to_i
+
+      # check the new IDs are all present and unchanged
+      assert_equal used_node.id, doc.find("//diffResult/node").first["new_id"].to_i
+      assert_equal used_way.id, doc.find("//diffResult/way").first["new_id"].to_i
+      assert_equal used_relation.id, doc.find("//diffResult/relation").first["new_id"].to_i
+
+      # check the new versions are all present and unchanged
+      assert_equal used_node.version, doc.find("//diffResult/node").first["new_version"].to_i
+      assert_equal used_way.version, doc.find("//diffResult/way").first["new_version"].to_i
+      assert_equal used_relation.version, doc.find("//diffResult/relation").first["new_version"].to_i
+
+      # check that nothing was, in fact, deleted
+      assert_equal true, Node.find(used_node.id).visible
+      assert_equal true, Way.find(used_way.id).visible
+      assert_equal true, Relation.find(used_relation.id).visible
+    end
+
+    ##
+    # upload an element with a really long tag value
+    def test_upload_invalid_too_long_tag
+      changeset = create(:changeset)
+
+      basic_authorization changeset.user.email, "test"
+
+      # simple diff to create a node way and relation using placeholders
+      diff = <<CHANGESET.strip_heredoc
+        <osmChange>
+         <create>
+          <node id='-1' lon='0' lat='0' changeset='#{changeset.id}'>
+           <tag k='foo' v='#{'x' * 256}'/>
+          </node>
+         </create>
+        </osmChange>
+CHANGESET
+
+      # upload it
+      post :upload, :params => { :id => changeset.id }, :body => diff
+      assert_response :bad_request,
+                      "shoudln't be able to upload too long a tag to changeset: #{@response.body}"
+    end
+
+    ##
+    # upload something which creates new objects and inserts them into
+    # existing containers using placeholders.
+    def test_upload_complex
+      way = create(:way)
+      node = create(:node)
+      relation = create(:relation)
+      create(:way_node, :way => way, :node => node)
+
+      changeset = create(:changeset)
+
+      basic_authorization changeset.user.email, "test"
+
+      # simple diff to create a node way and relation using placeholders
+      diff = <<CHANGESET.strip_heredoc
+        <osmChange>
+         <create>
+          <node id='-1' lon='0' lat='0' changeset='#{changeset.id}'>
+           <tag k='foo' v='bar'/>
+           <tag k='baz' v='bat'/>
+          </node>
+         </create>
+         <modify>
+          <way id='#{way.id}' changeset='#{changeset.id}' version='1'>
+           <nd ref='-1'/>
+           <nd ref='#{node.id}'/>
+          </way>
+          <relation id='#{relation.id}' changeset='#{changeset.id}' version='1'>
+           <member type='way' role='some' ref='#{way.id}'/>
+           <member type='node' role='some' ref='-1'/>
+           <member type='relation' role='some' ref='#{relation.id}'/>
+          </relation>
+         </modify>
+        </osmChange>
+CHANGESET
+
+      # upload it
+      post :upload, :params => { :id => changeset.id }, :body => diff
+      assert_response :success,
+                      "can't upload a complex diff to changeset: #{@response.body}"
+
+      # check the returned payload
+      assert_select "diffResult[version='#{API_VERSION}'][generator='#{GENERATOR}']", 1
+      assert_select "diffResult>node", 1
+      assert_select "diffResult>way", 1
+      assert_select "diffResult>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("//diffResult/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, node.id], Way.find(way.id).nds, "way nodes should match"
+      Relation.find(relation.id).members.each do |type, id, _role|
+        assert_equal new_node_id, id, "relation should contain new node" if type == "node"
+      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
+      changeset = create(:changeset)
+      other_changeset = create(:changeset, :user => changeset.user)
+      node = create(:node)
+      way = create(:way)
+      relation = create(:relation)
+      other_relation = create(:relation)
+
+      basic_authorization changeset.user.email, "test"
+
+      # simple diff to create a node way and relation using placeholders
+      diff = <<CHANGESET.strip_heredoc
+        <osmChange>
+         <modify>
+          <node id='#{node.id}' lon='0' lat='0' changeset='#{changeset.id}' version='1'/>
+          <way id='#{way.id}' changeset='#{changeset.id}' version='1'>
+           <nd ref='#{node.id}'/>
+          </way>
+         </modify>
+         <modify>
+          <relation id='#{relation.id}' changeset='#{changeset.id}' version='1'>
+           <member type='way' role='some' ref='#{way.id}'/>
+           <member type='node' role='some' ref='#{node.id}'/>
+           <member type='relation' role='some' ref='#{other_relation.id}'/>
+          </relation>
+         </modify>
+         <create>
+          <node id='-1' lon='0' lat='0' changeset='#{other_changeset.id}'>
+           <tag k='foo' v='bar'/>
+           <tag k='baz' v='bat'/>
+          </node>
+         </create>
+        </osmChange>
+CHANGESET
+
+      # upload it
+      post :upload, :params => { :id => changeset.id }, :body => diff
+      assert_response :conflict,
+                      "uploading a diff with multiple changesets should have failed"
+
+      # check that objects are unmodified
+      assert_nodes_are_equal(node, Node.find(node.id))
+      assert_ways_are_equal(way, Way.find(way.id))
+      assert_relations_are_equal(relation, Relation.find(relation.id))
+    end
+
+    ##
+    # upload multiple versions of the same element in the same diff.
+    def test_upload_multiple_valid
+      node = create(:node)
+      changeset = create(:changeset)
+      basic_authorization changeset.user.email, "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 = <<CHANGESET.strip_heredoc
+        <osmChange>
+         <modify>
+          <node id='#{node.id}' lon='0' lat='0' changeset='#{changeset.id}' version='1'/>
+          <node id='#{node.id}' lon='1' lat='0' changeset='#{changeset.id}' version='2'/>
+          <node id='#{node.id}' lon='1' lat='1' changeset='#{changeset.id}' version='3'/>
+          <node id='#{node.id}' lon='1' lat='2' changeset='#{changeset.id}' version='4'/>
+          <node id='#{node.id}' lon='2' lat='2' changeset='#{changeset.id}' version='5'/>
+          <node id='#{node.id}' lon='3' lat='2' changeset='#{changeset.id}' version='6'/>
+          <node id='#{node.id}' lon='3' lat='3' changeset='#{changeset.id}' version='7'/>
+          <node id='#{node.id}' lon='9' lat='9' changeset='#{changeset.id}' version='8'/>
+         </modify>
+        </osmChange>
+CHANGESET
+
+      # upload it
+      post :upload, :params => { :id => changeset.id }, :body => diff
+      assert_response :success,
+                      "can't upload multiple versions of an element in a diff: #{@response.body}"
+
+      # check the response is well-formed. its counter-intuitive, but the
+      # API will return multiple elements with the same ID and different
+      # version numbers for each change we made.
+      assert_select "diffResult>node", 8
+    end
+
+    ##
+    # upload multiple versions of the same element in the same diff, but
+    # keep the version numbers the same.
+    def test_upload_multiple_duplicate
+      node = create(:node)
+      changeset = create(:changeset)
+
+      basic_authorization changeset.user.email, "test"
+
+      diff = <<CHANGESET.strip_heredoc
+        <osmChange>
+         <modify>
+          <node id='#{node.id}' lon='0' lat='0' changeset='#{changeset.id}' version='1'/>
+          <node id='#{node.id}' lon='1' lat='1' changeset='#{changeset.id}' version='1'/>
+         </modify>
+        </osmChange>
+CHANGESET
+
+      # upload it
+      post :upload, :params => { :id => changeset.id }, :body => diff
+      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
+      changeset = create(:changeset)
+
+      basic_authorization changeset.user.email, "test"
+
+      diff = <<CHANGESET.strip_heredoc
+        <osmChange>
+         <modify>
+         <node id='1' lon='1' lat='1' changeset='#{changeset.id}'/>
+         </modify>
+        </osmChange>
+CHANGESET
+
+      # upload it
+      post :upload, :params => { :id => changeset.id }, :body => diff
+      assert_response :bad_request,
+                      "shouldn't be able to upload an element without version: #{@response.body}"
+    end
+
+    ##
+    # try to upload with commands other than create, modify, or delete
+    def test_action_upload_invalid
+      changeset = create(:changeset)
+
+      basic_authorization changeset.user.email, "test"
+
+      diff = <<CHANGESET.strip_heredoc
+        <osmChange>
+          <ping>
+           <node id='1' lon='1' lat='1' changeset='#{changeset.id}' />
+          </ping>
+        </osmChange>
+CHANGESET
+      post :upload, :params => { :id => changeset.id }, :body => diff
+      assert_response :bad_request, "Shouldn't be able to upload a diff with the action ping"
+      assert_equal @response.body, "Unknown action ping, choices are create, modify, delete"
+    end
+
+    ##
+    # upload a valid changeset which has a mixture of whitespace
+    # to check a bug reported by ivansanchez (#1565).
+    def test_upload_whitespace_valid
+      changeset = create(:changeset)
+      node = create(:node)
+      way = create(:way_with_nodes, :nodes_count => 2)
+      relation = create(:relation)
+      other_relation = create(:relation)
+      create(:relation_tag, :relation => relation)
+
+      basic_authorization changeset.user.email, "test"
+
+      diff = <<CHANGESET.strip_heredoc
+        <osmChange>
+         <modify><node id='#{node.id}' lon='0' lat='0' changeset='#{changeset.id}'
+          version='1'></node>
+          <node id='#{node.id}' lon='1' lat='1' changeset='#{changeset.id}' version='2'><tag k='k' v='v'/></node></modify>
+         <modify>
+         <relation id='#{relation.id}' changeset='#{changeset.id}' version='1'><member
+           type='way' role='some' ref='#{way.id}'/><member
+            type='node' role='some' ref='#{node.id}'/>
+           <member type='relation' role='some' ref='#{other_relation.id}'/>
+          </relation>
+         </modify></osmChange>
+CHANGESET
+
+      # upload it
+      post :upload, :params => { :id => changeset.id }, :body => diff
+      assert_response :success,
+                      "can't upload a valid diff with whitespace variations to changeset: #{@response.body}"
+
+      # check the response is well-formed
+      assert_select "diffResult>node", 2
+      assert_select "diffResult>relation", 1
+
+      # check that the changes made it into the database
+      assert_equal 1, Node.find(node.id).tags.size, "node #{node.id} should now have one tag"
+      assert_equal 0, Relation.find(relation.id).tags.size, "relation #{relation.id} should now have no tags"
+    end
+
+    ##
+    # test that a placeholder can be reused within the same upload.
+    def test_upload_reuse_placeholder_valid
+      changeset = create(:changeset)
+
+      basic_authorization changeset.user.email, "test"
+
+      diff = <<CHANGESET.strip_heredoc
+        <osmChange>
+         <create>
+          <node id='-1' lon='0' lat='0' changeset='#{changeset.id}'>
+           <tag k="foo" v="bar"/>
+          </node>
+         </create>
+         <modify>
+          <node id='-1' lon='1' lat='1' changeset='#{changeset.id}' version='1'/>
+         </modify>
+         <delete>
+          <node id='-1' lon='2' lat='2' changeset='#{changeset.id}' version='2'/>
+         </delete>
+        </osmChange>
+CHANGESET
+
+      # upload it
+      post :upload, :params => { :id => changeset.id }, :body => diff
+      assert_response :success,
+                      "can't upload a valid diff with re-used placeholders to changeset: #{@response.body}"
+
+      # check the response is well-formed
+      assert_select "diffResult>node", 3
+      assert_select "diffResult>node[old_id='-1']", 3
+    end
+
+    ##
+    # test what happens if a diff upload re-uses placeholder IDs in an
+    # illegal way.
+    def test_upload_placeholder_invalid
+      changeset = create(:changeset)
+
+      basic_authorization changeset.user.email, "test"
+
+      diff = <<CHANGESET.strip_heredoc
+        <osmChange>
+         <create>
+          <node id='-1' lon='0' lat='0' changeset='#{changeset.id}' version='1'/>
+          <node id='-1' lon='1' lat='1' changeset='#{changeset.id}' version='1'/>
+          <node id='-1' lon='2' lat='2' changeset='#{changeset.id}' version='2'/>
+         </create>
+        </osmChange>
+CHANGESET
+
+      # upload it
+      post :upload, :params => { :id => changeset.id }, :body => diff
+      assert_response :bad_request,
+                      "shouldn't be able to re-use placeholder IDs"
+    end
+
+    ##
+    # test that uploading a way referencing invalid placeholders gives a
+    # proper error, not a 500.
+    def test_upload_placeholder_invalid_way
+      changeset = create(:changeset)
+      way = create(:way)
+
+      basic_authorization changeset.user.email, "test"
+
+      diff = <<CHANGESET.strip_heredoc
+        <osmChange>
+         <create>
+          <node id="-1" lon="0" lat="0" changeset="#{changeset.id}" version="1"/>
+          <node id="-2" lon="1" lat="1" changeset="#{changeset.id}" version="1"/>
+          <node id="-3" lon="2" lat="2" changeset="#{changeset.id}" version="1"/>
+          <way id="-1" changeset="#{changeset.id}" version="1">
+           <nd ref="-1"/>
+           <nd ref="-2"/>
+           <nd ref="-3"/>
+           <nd ref="-4"/>
+          </way>
+         </create>
+        </osmChange>
+CHANGESET
+
+      # upload it
+      post :upload, :params => { :id => changeset.id }, :body => diff
+      assert_response :bad_request,
+                      "shouldn't be able to use invalid placeholder IDs"
+      assert_equal "Placeholder node not found for reference -4 in way -1", @response.body
+
+      # the same again, but this time use an existing way
+      diff = <<CHANGESET.strip_heredoc
+        <osmChange>
+         <create>
+          <node id="-1" lon="0" lat="0" changeset="#{changeset.id}" version="1"/>
+          <node id="-2" lon="1" lat="1" changeset="#{changeset.id}" version="1"/>
+          <node id="-3" lon="2" lat="2" changeset="#{changeset.id}" version="1"/>
+          <way id="#{way.id}" changeset="#{changeset.id}" version="1">
+           <nd ref="-1"/>
+           <nd ref="-2"/>
+           <nd ref="-3"/>
+           <nd ref="-4"/>
+          </way>
+         </create>
+        </osmChange>
+CHANGESET
+
+      # upload it
+      post :upload, :params => { :id => changeset.id }, :body => diff
+      assert_response :bad_request,
+                      "shouldn't be able to use invalid placeholder IDs"
+      assert_equal "Placeholder node not found for reference -4 in way #{way.id}", @response.body
+    end
+
+    ##
+    # test that uploading a relation referencing invalid placeholders gives a
+    # proper error, not a 500.
+    def test_upload_placeholder_invalid_relation
+      changeset = create(:changeset)
+      relation = create(:relation)
+
+      basic_authorization changeset.user.email, "test"
+
+      diff = <<CHANGESET.strip_heredoc
+        <osmChange>
+         <create>
+          <node id="-1" lon="0" lat="0" changeset="#{changeset.id}" version="1"/>
+          <node id="-2" lon="1" lat="1" changeset="#{changeset.id}" version="1"/>
+          <node id="-3" lon="2" lat="2" changeset="#{changeset.id}" version="1"/>
+          <relation id="-1" changeset="#{changeset.id}" version="1">
+           <member type="node" role="foo" ref="-1"/>
+           <member type="node" role="foo" ref="-2"/>
+           <member type="node" role="foo" ref="-3"/>
+           <member type="node" role="foo" ref="-4"/>
+          </relation>
+         </create>
+        </osmChange>
+CHANGESET
+
+      # upload it
+      post :upload, :params => { :id => changeset.id }, :body => diff
+      assert_response :bad_request,
+                      "shouldn't be able to use invalid placeholder IDs"
+      assert_equal "Placeholder Node not found for reference -4 in relation -1.", @response.body
+
+      # the same again, but this time use an existing relation
+      diff = <<CHANGESET.strip_heredoc
+        <osmChange>
+         <create>
+          <node id="-1" lon="0" lat="0" changeset="#{changeset.id}" version="1"/>
+          <node id="-2" lon="1" lat="1" changeset="#{changeset.id}" version="1"/>
+          <node id="-3" lon="2" lat="2" changeset="#{changeset.id}" version="1"/>
+          <relation id="#{relation.id}" changeset="#{changeset.id}" version="1">
+           <member type="node" role="foo" ref="-1"/>
+           <member type="node" role="foo" ref="-2"/>
+           <member type="node" role="foo" ref="-3"/>
+           <member type="way" role="bar" ref="-1"/>
+          </relation>
+         </create>
+        </osmChange>
+CHANGESET
+
+      # upload it
+      post :upload, :params => { :id => changeset.id }, :body => diff
+      assert_response :bad_request,
+                      "shouldn't be able to use invalid placeholder IDs"
+      assert_equal "Placeholder Way not found for reference -1 in relation #{relation.id}.", @response.body
+    end
+
+    ##
+    # test what happens if a diff is uploaded containing only a node
+    # move.
+    def test_upload_node_move
+      basic_authorization create(:user).email, "test"
+
+      xml = "<osm><changeset>" \
+            "<tag k='created_by' v='osm test suite checking changesets'/>" \
+            "</changeset></osm>"
+      put :create, :body => xml
+      assert_response :success
+      changeset_id = @response.body.to_i
+
+      old_node = create(:node, :lat => 1, :lon => 1)
+
+      diff = XML::Document.new
+      diff.root = XML::Node.new "osmChange"
+      modify = XML::Node.new "modify"
+      xml_old_node = old_node.to_xml_node
+      xml_old_node["lat"] = 2.0.to_s
+      xml_old_node["lon"] = 2.0.to_s
+      xml_old_node["changeset"] = changeset_id.to_s
+      modify << xml_old_node
+      diff.root << modify
+
+      # upload it
+      post :upload, :params => { :id => changeset_id }, :body => diff.to_s
+      assert_response :success,
+                      "diff should have uploaded OK"
+
+      # check the bbox
+      changeset = Changeset.find(changeset_id)
+      assert_equal 1 * GeoRecord::SCALE, changeset.min_lon, "min_lon should be 1 degree"
+      assert_equal 2 * GeoRecord::SCALE, changeset.max_lon, "max_lon should be 2 degrees"
+      assert_equal 1 * GeoRecord::SCALE, changeset.min_lat, "min_lat should be 1 degree"
+      assert_equal 2 * GeoRecord::SCALE, changeset.max_lat, "max_lat should be 2 degrees"
+    end
+
+    ##
+    # test what happens if a diff is uploaded adding a node to a way.
+    def test_upload_way_extend
+      basic_authorization create(:user).email, "test"
+
+      xml = "<osm><changeset>" \
+            "<tag k='created_by' v='osm test suite checking changesets'/>" \
+            "</changeset></osm>"
+      put :create, :body => xml
+      assert_response :success
+      changeset_id = @response.body.to_i
+
+      old_way = create(:way)
+      create(:way_node, :way => old_way, :node => create(:node, :lat => 1, :lon => 1))
+
+      diff = XML::Document.new
+      diff.root = XML::Node.new "osmChange"
+      modify = XML::Node.new "modify"
+      xml_old_way = old_way.to_xml_node
+      nd_ref = XML::Node.new "nd"
+      nd_ref["ref"] = create(:node, :lat => 3, :lon => 3).id.to_s
+      xml_old_way << nd_ref
+      xml_old_way["changeset"] = changeset_id.to_s
+      modify << xml_old_way
+      diff.root << modify
+
+      # upload it
+      post :upload, :params => { :id => changeset_id }, :body => diff.to_s
+      assert_response :success,
+                      "diff should have uploaded OK"
+
+      # check the bbox
+      changeset = Changeset.find(changeset_id)
+      assert_equal 1 * GeoRecord::SCALE, changeset.min_lon, "min_lon should be 1 degree"
+      assert_equal 3 * GeoRecord::SCALE, changeset.max_lon, "max_lon should be 3 degrees"
+      assert_equal 1 * GeoRecord::SCALE, changeset.min_lat, "min_lat should be 1 degree"
+      assert_equal 3 * GeoRecord::SCALE, changeset.max_lat, "max_lat should be 3 degrees"
+    end
+
+    ##
+    # test for more issues in #1568
+    def test_upload_empty_invalid
+      changeset = create(:changeset)
+
+      basic_authorization changeset.user.email, "test"
+
+      ["<osmChange/>",
+       "<osmChange></osmChange>",
+       "<osmChange><modify/></osmChange>",
+       "<osmChange><modify></modify></osmChange>"].each do |diff|
+        # upload it
+        post :upload, :params => { :id => changeset.id }, :body => diff
+        assert_response(:success, "should be able to upload " \
+                        "empty changeset: " + diff)
+      end
+    end
+
+    ##
+    # test that the X-Error-Format header works to request XML errors
+    def test_upload_xml_errors
+      changeset = create(:changeset)
+      node = create(:node)
+      create(:relation_member, :member => node)
+
+      basic_authorization changeset.user.email, "test"
+
+      # try and delete a node that is in use
+      diff = XML::Document.new
+      diff.root = XML::Node.new "osmChange"
+      delete = XML::Node.new "delete"
+      diff.root << delete
+      delete << node.to_xml_node
+
+      # upload it
+      error_format "xml"
+      post :upload, :params => { :id => changeset.id }, :body => diff.to_s
+      assert_response :success,
+                      "failed to return error in XML format"
+
+      # check the returned payload
+      assert_select "osmError[version='#{API_VERSION}'][generator='OpenStreetMap server']", 1
+      assert_select "osmError>status", 1
+      assert_select "osmError>message", 1
+    end
+
+    ##
+    # when we make some simple changes we get the same changes back from the
+    # diff download.
+    def test_diff_download_simple
+      node = create(:node)
+
+      ## First try with a non-public user, which should get a forbidden
+      basic_authorization create(:user, :data_public => false).email, "test"
+
+      # create a temporary changeset
+      xml = "<osm><changeset>" \
+            "<tag k='created_by' v='osm test suite checking changesets'/>" \
+            "</changeset></osm>"
+      put :create, :body => xml
+      assert_response :forbidden
+
+      ## Now try with a normal user
+      basic_authorization create(:user).email, "test"
+
+      # create a temporary changeset
+      xml = "<osm><changeset>" \
+            "<tag k='created_by' v='osm test suite checking changesets'/>" \
+            "</changeset></osm>"
+      put :create, :body => xml
+      assert_response :success
+      changeset_id = @response.body.to_i
+
+      # add a diff to it
+      diff = <<CHANGESET.strip_heredoc
+        <osmChange>
+         <modify>
+          <node id='#{node.id}' lon='0' lat='0' changeset='#{changeset_id}' version='1'/>
+          <node id='#{node.id}' lon='1' lat='0' changeset='#{changeset_id}' version='2'/>
+          <node id='#{node.id}' lon='1' lat='1' changeset='#{changeset_id}' version='3'/>
+          <node id='#{node.id}' lon='1' lat='2' changeset='#{changeset_id}' version='4'/>
+          <node id='#{node.id}' lon='2' lat='2' changeset='#{changeset_id}' version='5'/>
+          <node id='#{node.id}' lon='3' lat='2' changeset='#{changeset_id}' version='6'/>
+          <node id='#{node.id}' lon='3' lat='3' changeset='#{changeset_id}' version='7'/>
+          <node id='#{node.id}' lon='9' lat='9' changeset='#{changeset_id}' version='8'/>
+         </modify>
+        </osmChange>
+CHANGESET
+
+      # upload it
+      post :upload, :params => { :id => changeset_id }, :body => diff
+      assert_response :success,
+                      "can't upload multiple versions of an element in a diff: #{@response.body}"
+
+      get :download, :params => { :id => changeset_id }
+      assert_response :success
+
+      assert_select "osmChange", 1
+      assert_select "osmChange>modify", 8
+      assert_select "osmChange>modify>node", 8
+    end
+
+    ##
+    # culled this from josm to ensure that nothing in the way that josm
+    # is formatting the request is causing it to fail.
+    #
+    # NOTE: the error turned out to be something else completely!
+    def test_josm_upload
+      basic_authorization create(:user).email, "test"
+
+      # create a temporary changeset
+      xml = "<osm><changeset>" \
+            "<tag k='created_by' v='osm test suite checking changesets'/>" \
+            "</changeset></osm>"
+      put :create, :body => xml
+      assert_response :success
+      changeset_id = @response.body.to_i
+
+      diff = <<OSMFILE.strip_heredoc
+        <osmChange version="0.6" generator="JOSM">
+        <create version="0.6" generator="JOSM">
+          <node id='-1' visible='true' changeset='#{changeset_id}' lat='51.49619982187321' lon='-0.18722061869438314' />
+          <node id='-2' visible='true' changeset='#{changeset_id}' lat='51.496359883909605' lon='-0.18653093576241928' />
+          <node id='-3' visible='true' changeset='#{changeset_id}' lat='51.49598132358285' lon='-0.18719613290981638' />
+          <node id='-4' visible='true' changeset='#{changeset_id}' lat='51.4961591711078' lon='-0.18629015888084607' />
+          <node id='-5' visible='true' changeset='#{changeset_id}' lat='51.49582126021711' lon='-0.18708186591517145' />
+          <node id='-6' visible='true' changeset='#{changeset_id}' lat='51.49591018437858' lon='-0.1861432441734455' />
+          <node id='-7' visible='true' changeset='#{changeset_id}' lat='51.49560784152179' lon='-0.18694719410005425' />
+          <node id='-8' visible='true' changeset='#{changeset_id}' lat='51.49567389979617' lon='-0.1860289771788006' />
+          <node id='-9' visible='true' changeset='#{changeset_id}' lat='51.49543761398892' lon='-0.186820684213126' />
+          <way id='-10' action='modiy' visible='true' changeset='#{changeset_id}'>
+            <nd ref='-1' />
+            <nd ref='-2' />
+            <nd ref='-3' />
+            <nd ref='-4' />
+            <nd ref='-5' />
+            <nd ref='-6' />
+            <nd ref='-7' />
+            <nd ref='-8' />
+            <nd ref='-9' />
+            <tag k='highway' v='residential' />
+            <tag k='name' v='Foobar Street' />
+          </way>
+        </create>
+        </osmChange>
+OSMFILE
+
+      # upload it
+      post :upload, :params => { :id => changeset_id }, :body => diff
+      assert_response :success,
+                      "can't upload a diff from JOSM: #{@response.body}"
+
+      get :download, :params => { :id => changeset_id }
+      assert_response :success
+
+      assert_select "osmChange", 1
+      assert_select "osmChange>create>node", 9
+      assert_select "osmChange>create>way", 1
+      assert_select "osmChange>create>way>nd", 9
+      assert_select "osmChange>create>way>tag", 2
+    end
+
+    ##
+    # when we make some complex changes we get the same changes back from the
+    # diff download.
+    def test_diff_download_complex
+      node = create(:node)
+      node2 = create(:node)
+      way = create(:way)
+      basic_authorization create(:user).email, "test"
+
+      # create a temporary changeset
+      xml = "<osm><changeset>" \
+            "<tag k='created_by' v='osm test suite checking changesets'/>" \
+            "</changeset></osm>"
+      put :create, :body => xml
+      assert_response :success
+      changeset_id = @response.body.to_i
+
+      # add a diff to it
+      diff = <<CHANGESET.strip_heredoc
+        <osmChange>
+         <delete>
+          <node id='#{node.id}' lon='0' lat='0' changeset='#{changeset_id}' version='1'/>
+         </delete>
+         <create>
+          <node id='-1' lon='9' lat='9' changeset='#{changeset_id}' version='0'/>
+          <node id='-2' lon='8' lat='9' changeset='#{changeset_id}' version='0'/>
+          <node id='-3' lon='7' lat='9' changeset='#{changeset_id}' version='0'/>
+         </create>
+         <modify>
+          <node id='#{node2.id}' lon='20' lat='15' changeset='#{changeset_id}' version='1'/>
+          <way id='#{way.id}' changeset='#{changeset_id}' version='1'>
+           <nd ref='#{node2.id}'/>
+           <nd ref='-1'/>
+           <nd ref='-2'/>
+           <nd ref='-3'/>
+          </way>
+         </modify>
+        </osmChange>
+CHANGESET
+
+      # upload it
+      post :upload, :params => { :id => changeset_id }, :body => diff
+      assert_response :success,
+                      "can't upload multiple versions of an element in a diff: #{@response.body}"
+
+      get :download, :params => { :id => changeset_id }
+      assert_response :success
+
+      assert_select "osmChange", 1
+      assert_select "osmChange>create", 3
+      assert_select "osmChange>delete", 1
+      assert_select "osmChange>modify", 2
+      assert_select "osmChange>create>node", 3
+      assert_select "osmChange>delete>node", 1
+      assert_select "osmChange>modify>node", 1
+      assert_select "osmChange>modify>way", 1
+    end
+
+    def test_changeset_download
+      changeset = create(:changeset)
+      node = create(:node, :with_history, :version => 1, :changeset => changeset)
+      tag = create(:old_node_tag, :old_node => node.old_nodes.find_by(:version => 1))
+      node2 = create(:node, :with_history, :version => 1, :changeset => changeset)
+      _node3 = create(:node, :with_history, :deleted, :version => 1, :changeset => changeset)
+      _relation = create(:relation, :with_history, :version => 1, :changeset => changeset)
+      _relation2 = create(:relation, :with_history, :deleted, :version => 1, :changeset => changeset)
+
+      get :download, :params => { :id => changeset.id }
+
+      assert_response :success
+      assert_template nil
+      # print @response.body
+      # FIXME: needs more assert_select tests
+      assert_select "osmChange[version='#{API_VERSION}'][generator='#{GENERATOR}']" do
+        assert_select "create", :count => 5
+        assert_select "create>node[id='#{node.id}'][visible='#{node.visible?}'][version='#{node.version}']" do
+          assert_select "tag[k='#{tag.k}'][v='#{tag.v}']"
+        end
+        assert_select "create>node[id='#{node2.id}']"
+      end
+    end
+
+    ##
+    # check that the bounding box of a changeset gets updated correctly
+    # FIXME: This should really be moded to a integration test due to the with_controller
+    def test_changeset_bbox
+      way = create(:way)
+      create(:way_node, :way => way, :node => create(:node, :lat => 3, :lon => 3))
+
+      basic_authorization create(:user).email, "test"
+
+      # create a new changeset
+      xml = "<osm><changeset/></osm>"
+      put :create, :body => xml
+      assert_response :success, "Creating of changeset failed."
+      changeset_id = @response.body.to_i
+
+      # add a single node to it
+      with_controller(NodesController.new) do
+        xml = "<osm><node lon='1' lat='2' changeset='#{changeset_id}'/></osm>"
+        put :create, :body => xml
+        assert_response :success, "Couldn't create node."
+      end
+
+      # get the bounding box back from the changeset
+      get :show, :params => { :id => changeset_id }
+      assert_response :success, "Couldn't read back changeset."
+      assert_select "osm>changeset[min_lon='1.0000000']", 1
+      assert_select "osm>changeset[max_lon='1.0000000']", 1
+      assert_select "osm>changeset[min_lat='2.0000000']", 1
+      assert_select "osm>changeset[max_lat='2.0000000']", 1
+
+      # add another node to it
+      with_controller(NodesController.new) do
+        xml = "<osm><node lon='2' lat='1' changeset='#{changeset_id}'/></osm>"
+        put :create, :body => xml
+        assert_response :success, "Couldn't create second node."
+      end
+
+      # get the bounding box back from the changeset
+      get :show, :params => { :id => changeset_id }
+      assert_response :success, "Couldn't read back changeset for the second time."
+      assert_select "osm>changeset[min_lon='1.0000000']", 1
+      assert_select "osm>changeset[max_lon='2.0000000']", 1
+      assert_select "osm>changeset[min_lat='1.0000000']", 1
+      assert_select "osm>changeset[max_lat='2.0000000']", 1
+
+      # add (delete) a way to it, which contains a point at (3,3)
+      with_controller(WaysController.new) do
+        xml = update_changeset(way.to_xml, changeset_id)
+        put :delete, :params => { :id => way.id }, :body => xml.to_s
+        assert_response :success, "Couldn't delete a way."
+      end
+
+      # get the bounding box back from the changeset
+      get :show, :params => { :id => changeset_id }
+      assert_response :success, "Couldn't read back changeset for the third time."
+      assert_select "osm>changeset[min_lon='1.0000000']", 1
+      assert_select "osm>changeset[max_lon='3.0000000']", 1
+      assert_select "osm>changeset[min_lat='1.0000000']", 1
+      assert_select "osm>changeset[max_lat='3.0000000']", 1
+    end
+
+    ##
+    # test that the changeset :include method works as it should
+    def test_changeset_include
+      basic_authorization create(:user).display_name, "test"
+
+      # create a new changeset
+      put :create, :body => "<osm><changeset/></osm>"
+      assert_response :success, "Creating of changeset failed."
+      changeset_id = @response.body.to_i
+
+      # NOTE: the include method doesn't over-expand, like inserting
+      # a real method does. this is because we expect the client to
+      # know what it is doing!
+      check_after_include(changeset_id, 1, 1, [1, 1, 1, 1])
+      check_after_include(changeset_id, 3, 3, [1, 1, 3, 3])
+      check_after_include(changeset_id, 4, 2, [1, 1, 4, 3])
+      check_after_include(changeset_id, 2, 2, [1, 1, 4, 3])
+      check_after_include(changeset_id, -1, -1, [-1, -1, 4, 3])
+      check_after_include(changeset_id, -2, 5, [-2, -1, 4, 5])
+    end
+
+    ##
+    # test that a not found, wrong method with the expand bbox works as expected
+    def test_changeset_expand_bbox_error
+      basic_authorization create(:user).display_name, "test"
+
+      # create a new changeset
+      xml = "<osm><changeset/></osm>"
+      put :create, :body => xml
+      assert_response :success, "Creating of changeset failed."
+      changeset_id = @response.body.to_i
+
+      lon = 58.2
+      lat = -0.45
+
+      # Try and put
+      xml = "<osm><node lon='#{lon}' lat='#{lat}'/></osm>"
+      put :expand_bbox, :params => { :id => changeset_id }, :body => xml
+      assert_response :method_not_allowed, "shouldn't be able to put a bbox expand"
+
+      # Try to get the update
+      xml = "<osm><node lon='#{lon}' lat='#{lat}'/></osm>"
+      get :expand_bbox, :params => { :id => changeset_id }, :body => xml
+      assert_response :method_not_allowed, "shouldn't be able to get a bbox expand"
+
+      # Try to use a hopefully missing changeset
+      xml = "<osm><node lon='#{lon}' lat='#{lat}'/></osm>"
+      post :expand_bbox, :params => { :id => changeset_id + 13245 }, :body => xml
+      assert_response :not_found, "shouldn't be able to do a bbox expand on a nonexistant changeset"
+    end
+
+    ##
+    # test the query functionality of changesets
+    def test_query
+      private_user = create(:user, :data_public => false)
+      private_user_changeset = create(:changeset, :user => private_user)
+      private_user_closed_changeset = create(:changeset, :closed, :user => private_user)
+      user = create(:user)
+      changeset = create(:changeset, :user => user)
+      closed_changeset = create(:changeset, :closed, :user => user, :created_at => Time.utc(2008, 1, 1, 0, 0, 0), :closed_at => Time.utc(2008, 1, 2, 0, 0, 0))
+      changeset2 = create(:changeset, :min_lat => 5 * GeoRecord::SCALE, :min_lon => 5 * GeoRecord::SCALE, :max_lat => 15 * GeoRecord::SCALE, :max_lon => 15 * GeoRecord::SCALE)
+      changeset3 = create(:changeset, :min_lat => 4.5 * GeoRecord::SCALE, :min_lon => 4.5 * GeoRecord::SCALE, :max_lat => 5 * GeoRecord::SCALE, :max_lon => 5 * GeoRecord::SCALE)
+
+      get :query, :params => { :bbox => "-10,-10, 10, 10" }
+      assert_response :success, "can't get changesets in bbox"
+      assert_changesets [changeset2, changeset3]
+
+      get :query, :params => { :bbox => "4.5,4.5,4.6,4.6" }
+      assert_response :success, "can't get changesets in bbox"
+      assert_changesets [changeset3]
+
+      # not found when looking for changesets of non-existing users
+      get :query, :params => { :user => User.maximum(:id) + 1 }
+      assert_response :not_found
+      get :query, :params => { :display_name => " " }
+      assert_response :not_found
+
+      # can't get changesets of user 1 without authenticating
+      get :query, :params => { :user => private_user.id }
+      assert_response :not_found, "shouldn't be able to get changesets by non-public user (ID)"
+      get :query, :params => { :display_name => private_user.display_name }
+      assert_response :not_found, "shouldn't be able to get changesets by non-public user (name)"
+
+      # but this should work
+      basic_authorization private_user.email, "test"
+      get :query, :params => { :user => private_user.id }
+      assert_response :success, "can't get changesets by user ID"
+      assert_changesets [private_user_changeset, private_user_closed_changeset]
+
+      get :query, :params => { :display_name => private_user.display_name }
+      assert_response :success, "can't get changesets by user name"
+      assert_changesets [private_user_changeset, private_user_closed_changeset]
+
+      # check that the correct error is given when we provide both UID and name
+      get :query, :params => { :user => private_user.id,
+                               :display_name => private_user.display_name }
+      assert_response :bad_request, "should be a bad request to have both ID and name specified"
+
+      get :query, :params => { :user => private_user.id, :open => true }
+      assert_response :success, "can't get changesets by user and open"
+      assert_changesets [private_user_changeset]
+
+      get :query, :params => { :time => "2007-12-31" }
+      assert_response :success, "can't get changesets by time-since"
+      assert_changesets [private_user_changeset, private_user_closed_changeset, changeset, closed_changeset, changeset2, changeset3]
+
+      get :query, :params => { :time => "2008-01-01T12:34Z" }
+      assert_response :success, "can't get changesets by time-since with hour"
+      assert_changesets [private_user_changeset, private_user_closed_changeset, changeset, closed_changeset, changeset2, changeset3]
+
+      get :query, :params => { :time => "2007-12-31T23:59Z,2008-01-02T00:01Z" }
+      assert_response :success, "can't get changesets by time-range"
+      assert_changesets [closed_changeset]
+
+      get :query, :params => { :open => "true" }
+      assert_response :success, "can't get changesets by open-ness"
+      assert_changesets [private_user_changeset, changeset, changeset2, changeset3]
+
+      get :query, :params => { :closed => "true" }
+      assert_response :success, "can't get changesets by closed-ness"
+      assert_changesets [private_user_closed_changeset, closed_changeset]
+
+      get :query, :params => { :closed => "true", :user => private_user.id }
+      assert_response :success, "can't get changesets by closed-ness and user"
+      assert_changesets [private_user_closed_changeset]
+
+      get :query, :params => { :closed => "true", :user => user.id }
+      assert_response :success, "can't get changesets by closed-ness and user"
+      assert_changesets [closed_changeset]
+
+      get :query, :params => { :changesets => "#{private_user_changeset.id},#{changeset.id},#{closed_changeset.id}" }
+      assert_response :success, "can't get changesets by id (as comma-separated string)"
+      assert_changesets [private_user_changeset, changeset, closed_changeset]
+
+      get :query, :params => { :changesets => "" }
+      assert_response :bad_request, "should be a bad request since changesets is empty"
+    end
+
+    ##
+    # check that errors are returned if garbage is inserted
+    # into query strings
+    def test_query_invalid
+      ["abracadabra!",
+       "1,2,3,F",
+       ";drop table users;"].each do |bbox|
+        get :query, :params => { :bbox => bbox }
+        assert_response :bad_request, "'#{bbox}' isn't a bbox"
+      end
+
+      ["now()",
+       "00-00-00",
+       ";drop table users;",
+       ",",
+       "-,-"].each do |time|
+        get :query, :params => { :time => time }
+        assert_response :bad_request, "'#{time}' isn't a valid time range"
+      end
+
+      ["me",
+       "foobar",
+       "-1",
+       "0"].each do |uid|
+        get :query, :params => { :user => uid }
+        assert_response :bad_request, "'#{uid}' isn't a valid user ID"
+      end
+    end
+
+    ##
+    # check updating tags on a changeset
+    def test_changeset_update
+      private_user = create(:user, :data_public => false)
+      private_changeset = create(:changeset, :user => private_user)
+      user = create(:user)
+      changeset = create(:changeset, :user => user)
+
+      ## First try with a non-public user
+      new_changeset = private_changeset.to_xml
+      new_tag = XML::Node.new "tag"
+      new_tag["k"] = "tagtesting"
+      new_tag["v"] = "valuetesting"
+      new_changeset.find("//osm/changeset").first << new_tag
+
+      # try without any authorization
+      put :update, :params => { :id => private_changeset.id }, :body => new_changeset.to_s
+      assert_response :unauthorized
+
+      # try with the wrong authorization
+      basic_authorization create(:user).email, "test"
+      put :update, :params => { :id => private_changeset.id }, :body => new_changeset.to_s
+      assert_response :conflict
+
+      # now this should get an unauthorized
+      basic_authorization private_user.email, "test"
+      put :update, :params => { :id => private_changeset.id }, :body => new_changeset.to_s
+      assert_require_public_data "user with their data non-public, shouldn't be able to edit their changeset"
+
+      ## Now try with the public user
+      create(:changeset_tag, :changeset => changeset)
+      new_changeset = changeset.to_xml
+      new_tag = XML::Node.new "tag"
+      new_tag["k"] = "tagtesting"
+      new_tag["v"] = "valuetesting"
+      new_changeset.find("//osm/changeset").first << new_tag
+
+      # try without any authorization
+      @request.env["HTTP_AUTHORIZATION"] = nil
+      put :update, :params => { :id => changeset.id }, :body => new_changeset.to_s
+      assert_response :unauthorized
+
+      # try with the wrong authorization
+      basic_authorization create(:user).email, "test"
+      put :update, :params => { :id => changeset.id }, :body => new_changeset.to_s
+      assert_response :conflict
+
+      # now this should work...
+      basic_authorization user.email, "test"
+      put :update, :params => { :id => changeset.id }, :body => new_changeset.to_s
+      assert_response :success
+
+      assert_select "osm>changeset[id='#{changeset.id}']", 1
+      assert_select "osm>changeset>tag", 2
+      assert_select "osm>changeset>tag[k='tagtesting'][v='valuetesting']", 1
+    end
+
+    ##
+    # check that a user different from the one who opened the changeset
+    # can't modify it.
+    def test_changeset_update_invalid
+      basic_authorization create(:user).email, "test"
+
+      changeset = create(:changeset)
+      new_changeset = changeset.to_xml
+      new_tag = XML::Node.new "tag"
+      new_tag["k"] = "testing"
+      new_tag["v"] = "testing"
+      new_changeset.find("//osm/changeset").first << new_tag
+
+      put :update, :params => { :id => changeset.id }, :body => new_changeset.to_s
+      assert_response :conflict
+    end
+
+    ##
+    # check that a changeset can contain a certain max number of changes.
+    ## FIXME should be changed to an integration test due to the with_controller
+    def test_changeset_limits
+      basic_authorization create(:user).email, "test"
+
+      # open a new changeset
+      xml = "<osm><changeset/></osm>"
+      put :create, :body => xml
+      assert_response :success, "can't create a new changeset"
+      cs_id = @response.body.to_i
+
+      # start the counter just short of where the changeset should finish.
+      offset = 10
+      # alter the database to set the counter on the changeset directly,
+      # otherwise it takes about 6 minutes to fill all of them.
+      changeset = Changeset.find(cs_id)
+      changeset.num_changes = Changeset::MAX_ELEMENTS - offset
+      changeset.save!
+
+      with_controller(NodesController.new) do
+        # create a new node
+        xml = "<osm><node changeset='#{cs_id}' lat='0.0' lon='0.0'/></osm>"
+        put :create, :body => xml
+        assert_response :success, "can't create a new node"
+        node_id = @response.body.to_i
+
+        get :show, :params => { :id => node_id }
+        assert_response :success, "can't read back new node"
+        node_doc = XML::Parser.string(@response.body).parse
+        node_xml = node_doc.find("//osm/node").first
+
+        # loop until we fill the changeset with nodes
+        offset.times do |i|
+          node_xml["lat"] = rand.to_s
+          node_xml["lon"] = rand.to_s
+          node_xml["version"] = (i + 1).to_s
+
+          put :update, :params => { :id => node_id }, :body => node_doc.to_s
+          assert_response :success, "attempt #{i} should have succeeded"
+        end
+
+        # trying again should fail
+        node_xml["lat"] = rand.to_s
+        node_xml["lon"] = rand.to_s
+        node_xml["version"] = offset.to_s
+
+        put :update, :params => { :id => node_id }, :body => node_doc.to_s
+        assert_response :conflict, "final attempt should have failed"
+      end
+
+      changeset = Changeset.find(cs_id)
+      assert_equal Changeset::MAX_ELEMENTS + 1, changeset.num_changes
+
+      # check that the changeset is now closed as well
+      assert_not(changeset.is_open?,
+                 "changeset should have been auto-closed by exceeding " \
+                 "element limit.")
+    end
+
+    ##
+    # check that the changeset download for a changeset with a redacted
+    # element in it doesn't contain that element.
+    def test_diff_download_redacted
+      changeset = create(:changeset)
+      node = create(:node, :with_history, :version => 2, :changeset => changeset)
+      node_v1 = node.old_nodes.find_by(:version => 1)
+      node_v1.redact!(create(:redaction))
+
+      get :download, :params => { :id => changeset.id }
+      assert_response :success
+
+      assert_select "osmChange", 1
+      # this changeset contains the node in versions 1 & 2, but 1 should
+      # be hidden.
+      assert_select "osmChange node[id='#{node.id}']", 1
+      assert_select "osmChange node[id='#{node.id}'][version='1']", 0
+    end
+
+    ##
+    # test subscribe success
+    def test_subscribe_success
+      basic_authorization create(:user).email, "test"
+      changeset = create(:changeset, :closed)
+
+      assert_difference "changeset.subscribers.count", 1 do
+        post :subscribe, :params => { :id => changeset.id }
+      end
+      assert_response :success
+
+      # not closed changeset
+      changeset = create(:changeset)
+      assert_difference "changeset.subscribers.count", 1 do
+        post :subscribe, :params => { :id => changeset.id }
+      end
+      assert_response :success
+    end
+
+    ##
+    # test subscribe fail
+    def test_subscribe_fail
+      user = create(:user)
+
+      # unauthorized
+      changeset = create(:changeset, :closed)
+      assert_no_difference "changeset.subscribers.count" do
+        post :subscribe, :params => { :id => changeset.id }
+      end
+      assert_response :unauthorized
+
+      basic_authorization user.email, "test"
+
+      # bad changeset id
+      assert_no_difference "changeset.subscribers.count" do
+        post :subscribe, :params => { :id => 999111 }
+      end
+      assert_response :not_found
+
+      # trying to subscribe when already subscribed
+      changeset = create(:changeset, :closed)
+      changeset.subscribers.push(user)
+      assert_no_difference "changeset.subscribers.count" do
+        post :subscribe, :params => { :id => changeset.id }
+      end
+      assert_response :conflict
+    end
+
+    ##
+    # test unsubscribe success
+    def test_unsubscribe_success
+      user = create(:user)
+      basic_authorization user.email, "test"
+      changeset = create(:changeset, :closed)
+      changeset.subscribers.push(user)
+
+      assert_difference "changeset.subscribers.count", -1 do
+        post :unsubscribe, :params => { :id => changeset.id }
+      end
+      assert_response :success
+
+      # not closed changeset
+      changeset = create(:changeset)
+      changeset.subscribers.push(user)
+
+      assert_difference "changeset.subscribers.count", -1 do
+        post :unsubscribe, :params => { :id => changeset.id }
+      end
+      assert_response :success
+    end
+
+    ##
+    # test unsubscribe fail
+    def test_unsubscribe_fail
+      # unauthorized
+      changeset = create(:changeset, :closed)
+      assert_no_difference "changeset.subscribers.count" do
+        post :unsubscribe, :params => { :id => changeset.id }
+      end
+      assert_response :unauthorized
+
+      basic_authorization create(:user).email, "test"
+
+      # bad changeset id
+      assert_no_difference "changeset.subscribers.count" do
+        post :unsubscribe, :params => { :id => 999111 }
+      end
+      assert_response :not_found
+
+      # trying to unsubscribe when not subscribed
+      changeset = create(:changeset, :closed)
+      assert_no_difference "changeset.subscribers.count" do
+        post :unsubscribe, :params => { :id => changeset.id }
+      end
+      assert_response :not_found
+    end
+
+    private
+
+    ##
+    # boilerplate for checking that certain changesets exist in the
+    # output.
+    def assert_changesets(changesets)
+      assert_select "osm>changeset", changesets.size
+      changesets.each do |changeset|
+        assert_select "osm>changeset[id='#{changeset.id}']", 1
+      end
+    end
+
+    ##
+    # call the include method and assert properties of the bbox
+    def check_after_include(changeset_id, lon, lat, bbox)
+      xml = "<osm><node lon='#{lon}' lat='#{lat}'/></osm>"
+      post :expand_bbox, :params => { :id => changeset_id }, :body => xml
+      assert_response :success, "Setting include of changeset failed: #{@response.body}"
+
+      # check exactly one changeset
+      assert_select "osm>changeset", 1
+      assert_select "osm>changeset[id='#{changeset_id}']", 1
+
+      # check the bbox
+      doc = XML::Parser.string(@response.body).parse
+      changeset = doc.find("//osm/changeset").first
+      assert_equal bbox[0], changeset["min_lon"].to_f, "min lon"
+      assert_equal bbox[1], changeset["min_lat"].to_f, "min lat"
+      assert_equal bbox[2], changeset["max_lon"].to_f, "max lon"
+      assert_equal bbox[3], changeset["max_lat"].to_f, "max lat"
+    end
+
+    ##
+    # update the changeset_id of a way element
+    def update_changeset(xml, changeset_id)
+      xml_attr_rewrite(xml, "changeset", changeset_id)
+    end
+
+    ##
+    # update an attribute in a way element
+    def xml_attr_rewrite(xml, name, value)
+      xml.find("//osm/way").first[name] = value.to_s
+      xml
+    end
+  end
+end
index a3c8947ca6e2eec779aa58152cd249bd559444fe..fad7a97d8387d5e5cc6977fea0f59779accc89f0 100644 (file)
@@ -4,46 +4,6 @@ class ChangesetsControllerTest < ActionController::TestCase
   ##
   # test all routes which lead to this controller
   def test_routes
-    assert_routing(
-      { :path => "/api/0.6/changeset/create", :method => :put },
-      { :controller => "changesets", :action => "create" }
-    )
-    assert_routing(
-      { :path => "/api/0.6/changeset/1/upload", :method => :post },
-      { :controller => "changesets", :action => "upload", :id => "1" }
-    )
-    assert_routing(
-      { :path => "/api/0.6/changeset/1/download", :method => :get },
-      { :controller => "changesets", :action => "download", :id => "1" }
-    )
-    assert_routing(
-      { :path => "/api/0.6/changeset/1/expand_bbox", :method => :post },
-      { :controller => "changesets", :action => "expand_bbox", :id => "1" }
-    )
-    assert_routing(
-      { :path => "/api/0.6/changeset/1", :method => :get },
-      { :controller => "changesets", :action => "show", :id => "1" }
-    )
-    assert_routing(
-      { :path => "/api/0.6/changeset/1/subscribe", :method => :post },
-      { :controller => "changesets", :action => "subscribe", :id => "1" }
-    )
-    assert_routing(
-      { :path => "/api/0.6/changeset/1/unsubscribe", :method => :post },
-      { :controller => "changesets", :action => "unsubscribe", :id => "1" }
-    )
-    assert_routing(
-      { :path => "/api/0.6/changeset/1", :method => :put },
-      { :controller => "changesets", :action => "update", :id => "1" }
-    )
-    assert_routing(
-      { :path => "/api/0.6/changeset/1/close", :method => :put },
-      { :controller => "changesets", :action => "close", :id => "1" }
-    )
-    assert_routing(
-      { :path => "/api/0.6/changesets", :method => :get },
-      { :controller => "changesets", :action => "query" }
-    )
     assert_routing(
       { :path => "/user/name/history", :method => :get },
       { :controller => "changesets", :action => "index", :display_name => "name" }
@@ -70,1758 +30,6 @@ class ChangesetsControllerTest < ActionController::TestCase
     )
   end
 
-  # -----------------------
-  # Test simple changeset creation
-  # -----------------------
-
-  def test_create
-    basic_authorization create(:user, :data_public => false).email, "test"
-    # Create the first user's changeset
-    xml = "<osm><changeset>" \
-          "<tag k='created_by' v='osm test suite checking changesets'/>" \
-          "</changeset></osm>"
-    put :create, :body => xml
-    assert_require_public_data
-
-    basic_authorization create(:user).email, "test"
-    # Create the first user's changeset
-    xml = "<osm><changeset>" \
-          "<tag k='created_by' v='osm test suite checking changesets'/>" \
-          "</changeset></osm>"
-    put :create, :body => xml
-
-    assert_response :success, "Creation of changeset did not return sucess status"
-    newid = @response.body.to_i
-
-    # check end time, should be an hour ahead of creation time
-    cs = Changeset.find(newid)
-    duration = cs.closed_at - cs.created_at
-    # the difference can either be a rational, or a floating point number
-    # of seconds, depending on the code path taken :-(
-    if duration.class == Rational
-      assert_equal Rational(1, 24), duration, "initial idle timeout should be an hour (#{cs.created_at} -> #{cs.closed_at})"
-    else
-      # must be number of seconds...
-      assert_equal 3600, duration.round, "initial idle timeout should be an hour (#{cs.created_at} -> #{cs.closed_at})"
-    end
-
-    # checks if uploader was subscribed
-    assert_equal 1, cs.subscribers.length
-  end
-
-  def test_create_invalid
-    basic_authorization create(:user, :data_public => false).email, "test"
-    xml = "<osm><changeset></osm>"
-    put :create, :body => xml
-    assert_require_public_data
-
-    ## Try the public user
-    basic_authorization create(:user).email, "test"
-    xml = "<osm><changeset></osm>"
-    put :create, :body => xml
-    assert_response :bad_request, "creating a invalid changeset should fail"
-  end
-
-  def test_create_invalid_no_content
-    ## First check with no auth
-    put :create
-    assert_response :unauthorized, "shouldn't be able to create a changeset with no auth"
-
-    ## Now try to with a non-public user
-    basic_authorization create(:user, :data_public => false).email, "test"
-    put :create
-    assert_require_public_data
-
-    ## Try an inactive user
-    basic_authorization create(:user, :pending).email, "test"
-    put :create
-    assert_inactive_user
-
-    ## Now try to use a normal user
-    basic_authorization create(:user).email, "test"
-    put :create
-    assert_response :bad_request, "creating a changeset with no content should fail"
-  end
-
-  def test_create_wrong_method
-    basic_authorization create(:user).email, "test"
-    get :create
-    assert_response :method_not_allowed
-    post :create
-    assert_response :method_not_allowed
-  end
-
-  ##
-  # check that the changeset can be shown and returns the correct
-  # document structure.
-  def test_show
-    changeset_id = create(:changeset).id
-
-    get :show, :params => { :id => changeset_id }
-    assert_response :success, "cannot get first changeset"
-
-    assert_select "osm[version='#{API_VERSION}'][generator='OpenStreetMap server']", 1
-    assert_select "osm>changeset[id='#{changeset_id}']", 1
-    assert_select "osm>changeset>discussion", 0
-
-    get :show, :params => { :id => changeset_id, :include_discussion => true }
-    assert_response :success, "cannot get first changeset with comments"
-
-    assert_select "osm[version='#{API_VERSION}'][generator='OpenStreetMap server']", 1
-    assert_select "osm>changeset[id='#{changeset_id}']", 1
-    assert_select "osm>changeset>discussion", 1
-    assert_select "osm>changeset>discussion>comment", 0
-
-    changeset_id = create(:changeset, :closed).id
-    create_list(:changeset_comment, 3, :changeset_id => changeset_id)
-
-    get :show, :params => { :id => changeset_id, :include_discussion => true }
-    assert_response :success, "cannot get closed changeset with comments"
-
-    assert_select "osm[version='#{API_VERSION}'][generator='OpenStreetMap server']", 1
-    assert_select "osm>changeset[id='#{changeset_id}']", 1
-    assert_select "osm>changeset>discussion", 1
-    assert_select "osm>changeset>discussion>comment", 3
-  end
-
-  ##
-  # check that a changeset that doesn't exist returns an appropriate message
-  def test_show_not_found
-    [0, -32, 233455644, "afg", "213"].each do |id|
-      begin
-        get :show, :params => { :id => id }
-        assert_response :not_found, "should get a not found"
-      rescue ActionController::UrlGenerationError => ex
-        assert_match(/No route matches/, ex.to_s)
-      end
-    end
-  end
-
-  ##
-  # test that the user who opened a change can close it
-  def test_close
-    private_user = create(:user, :data_public => false)
-    private_changeset = create(:changeset, :user => private_user)
-    user = create(:user)
-    changeset = create(:changeset, :user => user)
-
-    ## Try without authentication
-    put :close, :params => { :id => changeset.id }
-    assert_response :unauthorized
-
-    ## Try using the non-public user
-    basic_authorization private_user.email, "test"
-    put :close, :params => { :id => private_changeset.id }
-    assert_require_public_data
-
-    ## The try with the public user
-    basic_authorization user.email, "test"
-
-    cs_id = changeset.id
-    put :close, :params => { :id => cs_id }
-    assert_response :success
-
-    # test that it really is closed now
-    cs = Changeset.find(cs_id)
-    assert_not(cs.is_open?,
-               "changeset should be closed now (#{cs.closed_at} > #{Time.now.getutc}.")
-  end
-
-  ##
-  # test that a different user can't close another user's changeset
-  def test_close_invalid
-    user = create(:user)
-    changeset = create(:changeset)
-
-    basic_authorization user.email, "test"
-
-    put :close, :params => { :id => changeset.id }
-    assert_response :conflict
-    assert_equal "The user doesn't own that changeset", @response.body
-  end
-
-  ##
-  # test that you can't close using another method
-  def test_close_method_invalid
-    user = create(:user)
-    changeset = create(:changeset, :user => user)
-
-    basic_authorization user.email, "test"
-
-    get :close, :params => { :id => changeset.id }
-    assert_response :method_not_allowed
-
-    post :close, :params => { :id => changeset.id }
-    assert_response :method_not_allowed
-  end
-
-  ##
-  # check that you can't close a changeset that isn't found
-  def test_close_not_found
-    cs_ids = [0, -132, "123"]
-
-    # First try to do it with no auth
-    cs_ids.each do |id|
-      begin
-        put :close, :params => { :id => id }
-        assert_response :unauthorized, "Shouldn't be able close the non-existant changeset #{id}, when not authorized"
-      rescue ActionController::UrlGenerationError => ex
-        assert_match(/No route matches/, ex.to_s)
-      end
-    end
-
-    # Now try with auth
-    basic_authorization create(:user).email, "test"
-    cs_ids.each do |id|
-      begin
-        put :close, :params => { :id => id }
-        assert_response :not_found, "The changeset #{id} doesn't exist, so can't be closed"
-      rescue ActionController::UrlGenerationError => ex
-        assert_match(/No route matches/, ex.to_s)
-      end
-    end
-  end
-
-  ##
-  # upload something simple, but valid and check that it can
-  # be read back ok
-  # Also try without auth and another user.
-  def test_upload_simple_valid
-    private_user = create(:user, :data_public => false)
-    private_changeset = create(:changeset, :user => private_user)
-    user = create(:user)
-    changeset = create(:changeset, :user => user)
-
-    node = create(:node)
-    way = create(:way)
-    relation = create(:relation)
-    other_relation = create(:relation)
-    # create some tags, since we test that they are removed later
-    create(:node_tag, :node => node)
-    create(:way_tag, :way => way)
-    create(:relation_tag, :relation => relation)
-
-    ## Try with no auth
-    changeset_id = changeset.id
-
-    # simple diff to change a node, way and relation by removing
-    # their tags
-    diff = <<CHANGESET.strip_heredoc
-      <osmChange>
-       <modify>
-        <node id='#{node.id}' lon='0' lat='0' changeset='#{changeset_id}' version='1'/>
-        <way id='#{way.id}' changeset='#{changeset_id}' version='1'>
-         <nd ref='#{node.id}'/>
-        </way>
-       </modify>
-       <modify>
-        <relation id='#{relation.id}' changeset='#{changeset_id}' version='1'>
-         <member type='way' role='some' ref='#{way.id}'/>
-         <member type='node' role='some' ref='#{node.id}'/>
-         <member type='relation' role='some' ref='#{other_relation.id}'/>
-        </relation>
-       </modify>
-      </osmChange>
-CHANGESET
-
-    # upload it
-    post :upload, :params => { :id => changeset_id }, :body => diff
-    assert_response :unauthorized,
-                    "shouldn't be able to upload a simple valid diff to changeset: #{@response.body}"
-
-    ## Now try with a private user
-    basic_authorization private_user.email, "test"
-    changeset_id = private_changeset.id
-
-    # simple diff to change a node, way and relation by removing
-    # their tags
-    diff = <<CHANGESET.strip_heredoc
-      <osmChange>
-       <modify>
-        <node id='#{node.id}' lon='0' lat='0' changeset='#{changeset_id}' version='1'/>
-        <way id='#{way.id}' changeset='#{changeset_id}' version='1'>
-         <nd ref='#{node.id}'/>
-        </way>
-       </modify>
-       <modify>
-        <relation id='#{relation.id}' changeset='#{changeset_id}' version='1'>
-         <member type='way' role='some' ref='#{way.id}'/>
-         <member type='node' role='some' ref='#{node.id}'/>
-         <member type='relation' role='some' ref='#{other_relation.id}'/>
-        </relation>
-       </modify>
-      </osmChange>
-CHANGESET
-
-    # upload it
-    post :upload, :params => { :id => changeset_id }, :body => diff
-    assert_response :forbidden,
-                    "can't upload a simple valid diff to changeset: #{@response.body}"
-
-    ## Now try with the public user
-    basic_authorization user.email, "test"
-    changeset_id = changeset.id
-
-    # simple diff to change a node, way and relation by removing
-    # their tags
-    diff = <<CHANGESET.strip_heredoc
-      <osmChange>
-       <modify>
-        <node id='#{node.id}' lon='0' lat='0' changeset='#{changeset_id}' version='1'/>
-        <way id='#{way.id}' changeset='#{changeset_id}' version='1'>
-         <nd ref='#{node.id}'/>
-        </way>
-       </modify>
-       <modify>
-        <relation id='#{relation.id}' changeset='#{changeset_id}' version='1'>
-         <member type='way' role='some' ref='#{way.id}'/>
-         <member type='node' role='some' ref='#{node.id}'/>
-         <member type='relation' role='some' ref='#{other_relation.id}'/>
-        </relation>
-       </modify>
-      </osmChange>
-CHANGESET
-
-    # upload it
-    post :upload, :params => { :id => changeset_id }, :body => diff
-    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(node.id).tags.size, "node #{node.id} should now have no tags"
-    assert_equal 0, Way.find(way.id).tags.size, "way #{way.id} should now have no tags"
-    assert_equal 0, Relation.find(relation.id).tags.size, "relation #{relation.id} should now have no tags"
-  end
-
-  ##
-  # upload something which creates new objects using placeholders
-  def test_upload_create_valid
-    user = create(:user)
-    changeset = create(:changeset, :user => user)
-    node = create(:node)
-    way = create(:way_with_nodes, :nodes_count => 2)
-    relation = create(:relation)
-
-    basic_authorization user.email, "test"
-
-    # simple diff to create a node way and relation using placeholders
-    diff = <<CHANGESET.strip_heredoc
-      <osmChange>
-       <create>
-        <node id='-1' lon='0' lat='0' changeset='#{changeset.id}'>
-         <tag k='foo' v='bar'/>
-         <tag k='baz' v='bat'/>
-        </node>
-        <way id='-1' changeset='#{changeset.id}'>
-         <nd ref='#{node.id}'/>
-        </way>
-       </create>
-       <create>
-        <relation id='-1' changeset='#{changeset.id}'>
-         <member type='way' role='some' ref='#{way.id}'/>
-         <member type='node' role='some' ref='#{node.id}'/>
-         <member type='relation' role='some' ref='#{relation.id}'/>
-        </relation>
-       </create>
-      </osmChange>
-CHANGESET
-
-    # upload it
-    post :upload, :params => { :id => changeset.id }, :body => diff
-    assert_response :success,
-                    "can't upload a simple valid creation to changeset: #{@response.body}"
-
-    # check the returned payload
-    assert_select "diffResult[version='#{API_VERSION}'][generator='OpenStreetMap server']", 1
-    assert_select "diffResult>node", 1
-    assert_select "diffResult>way", 1
-    assert_select "diffResult>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("//diffResult/node").first["new_id"].to_i
-    new_way_id = doc.find("//diffResult/way").first["new_id"].to_i
-    new_rel_id = doc.find("//diffResult/relation").first["new_id"].to_i
-
-    # check the old IDs are all present and negative one
-    assert_equal(-1, doc.find("//diffResult/node").first["old_id"].to_i)
-    assert_equal(-1, doc.find("//diffResult/way").first["old_id"].to_i)
-    assert_equal(-1, doc.find("//diffResult/relation").first["old_id"].to_i)
-
-    # check the versions are present and equal one
-    assert_equal 1, doc.find("//diffResult/node").first["new_version"].to_i
-    assert_equal 1, doc.find("//diffResult/way").first["new_version"].to_i
-    assert_equal 1, doc.find("//diffResult/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
-    changeset = create(:changeset)
-    super_relation = create(:relation)
-    used_relation = create(:relation)
-    used_way = create(:way)
-    used_node = create(:node)
-    create(:relation_member, :relation => super_relation, :member => used_relation)
-    create(:relation_member, :relation => super_relation, :member => used_way)
-    create(:relation_member, :relation => super_relation, :member => used_node)
-
-    basic_authorization changeset.user.display_name, "test"
-
-    diff = XML::Document.new
-    diff.root = XML::Node.new "osmChange"
-    delete = XML::Node.new "delete"
-    diff.root << delete
-    delete << super_relation.to_xml_node
-    delete << used_relation.to_xml_node
-    delete << used_way.to_xml_node
-    delete << used_node.to_xml_node
-
-    # update the changeset to one that this user owns
-    %w[node way relation].each do |type|
-      delete.find("//osmChange/delete/#{type}").each do |n|
-        n["changeset"] = changeset.id.to_s
-      end
-    end
-
-    # upload it
-    post :upload, :params => { :id => changeset.id }, :body => diff.to_s
-    assert_response :success,
-                    "can't upload a deletion diff to changeset: #{@response.body}"
-
-    # check the response is well-formed
-    assert_select "diffResult>node", 1
-    assert_select "diffResult>way", 1
-    assert_select "diffResult>relation", 2
-
-    # check that everything was deleted
-    assert_equal false, Node.find(used_node.id).visible
-    assert_equal false, Way.find(used_way.id).visible
-    assert_equal false, Relation.find(super_relation.id).visible
-    assert_equal false, Relation.find(used_relation.id).visible
-  end
-
-  ##
-  # test uploading a delete with no lat/lon, as they are optional in
-  # the osmChange spec.
-  def test_upload_nolatlon_delete
-    node = create(:node)
-    changeset = create(:changeset)
-
-    basic_authorization changeset.user.display_name, "test"
-    diff = "<osmChange><delete><node id='#{node.id}' version='#{node.version}' changeset='#{changeset.id}'/></delete></osmChange>"
-
-    # upload it
-    post :upload, :params => { :id => changeset.id }, :body => diff
-    assert_response :success,
-                    "can't upload a deletion diff to changeset: #{@response.body}"
-
-    # check the response is well-formed
-    assert_select "diffResult>node", 1
-
-    # check that everything was deleted
-    assert_equal false, Node.find(node.id).visible
-  end
-
-  def test_repeated_changeset_create
-    3.times do
-      basic_authorization create(:user).email, "test"
-
-      # create a temporary changeset
-      xml = "<osm><changeset>" \
-            "<tag k='created_by' v='osm test suite checking changesets'/>" \
-            "</changeset></osm>"
-      assert_difference "Changeset.count", 1 do
-        put :create, :body => xml
-      end
-      assert_response :success
-    end
-  end
-
-  def test_upload_large_changeset
-    basic_authorization create(:user).email, "test"
-
-    # create a changeset
-    put :create, :body => "<osm><changeset/></osm>"
-    assert_response :success, "Should be able to create a changeset: #{@response.body}"
-    changeset_id = @response.body.to_i
-
-    # upload some widely-spaced nodes, spiralling positive and negative
-    diff = <<CHANGESET.strip_heredoc
-      <osmChange>
-       <create>
-        <node id='-1' lon='-20' lat='-10' changeset='#{changeset_id}'/>
-        <node id='-10' lon='20'  lat='10' changeset='#{changeset_id}'/>
-        <node id='-2' lon='-40' lat='-20' changeset='#{changeset_id}'/>
-        <node id='-11' lon='40'  lat='20' changeset='#{changeset_id}'/>
-        <node id='-3' lon='-60' lat='-30' changeset='#{changeset_id}'/>
-        <node id='-12' lon='60'  lat='30' changeset='#{changeset_id}'/>
-        <node id='-4' lon='-80' lat='-40' changeset='#{changeset_id}'/>
-        <node id='-13' lon='80'  lat='40' changeset='#{changeset_id}'/>
-        <node id='-5' lon='-100' lat='-50' changeset='#{changeset_id}'/>
-        <node id='-14' lon='100'  lat='50' changeset='#{changeset_id}'/>
-        <node id='-6' lon='-120' lat='-60' changeset='#{changeset_id}'/>
-        <node id='-15' lon='120'  lat='60' changeset='#{changeset_id}'/>
-        <node id='-7' lon='-140' lat='-70' changeset='#{changeset_id}'/>
-        <node id='-16' lon='140'  lat='70' changeset='#{changeset_id}'/>
-        <node id='-8' lon='-160' lat='-80' changeset='#{changeset_id}'/>
-        <node id='-17' lon='160'  lat='80' changeset='#{changeset_id}'/>
-        <node id='-9' lon='-179.9' lat='-89.9' changeset='#{changeset_id}'/>
-        <node id='-18' lon='179.9'  lat='89.9' changeset='#{changeset_id}'/>
-       </create>
-      </osmChange>
-CHANGESET
-
-    # upload it, which used to cause an error like "PGError: ERROR:
-    # integer out of range" (bug #2152). but shouldn't any more.
-    post :upload, :params => { :id => changeset_id }, :body => diff
-    assert_response :success,
-                    "can't upload a spatially-large diff to changeset: #{@response.body}"
-
-    # check that the changeset bbox is within bounds
-    cs = Changeset.find(changeset_id)
-    assert cs.min_lon >= -180 * GeoRecord::SCALE, "Minimum longitude (#{cs.min_lon / GeoRecord::SCALE}) should be >= -180 to be valid."
-    assert cs.max_lon <= 180 * GeoRecord::SCALE, "Maximum longitude (#{cs.max_lon / GeoRecord::SCALE}) should be <= 180 to be valid."
-    assert cs.min_lat >= -90 * GeoRecord::SCALE, "Minimum latitude (#{cs.min_lat / GeoRecord::SCALE}) should be >= -90 to be valid."
-    assert cs.max_lat <= 90 * GeoRecord::SCALE, "Maximum latitude (#{cs.max_lat / GeoRecord::SCALE}) should be <= 90 to be valid."
-  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
-    changeset = create(:changeset)
-    relation = create(:relation)
-    other_relation = create(:relation)
-    used_way = create(:way)
-    used_node = create(:node)
-    create(:relation_member, :relation => relation, :member => used_way)
-    create(:relation_member, :relation => relation, :member => used_node)
-
-    basic_authorization changeset.user.email, "test"
-
-    diff = XML::Document.new
-    diff.root = XML::Node.new "osmChange"
-    delete = XML::Node.new "delete"
-    diff.root << delete
-    delete << other_relation.to_xml_node
-    delete << used_way.to_xml_node
-    delete << used_node.to_xml_node
-
-    # update the changeset to one that this user owns
-    %w[node way relation].each do |type|
-      delete.find("//osmChange/delete/#{type}").each do |n|
-        n["changeset"] = changeset.id.to_s
-      end
-    end
-
-    # upload it
-    post :upload, :params => { :id => changeset.id }, :body => diff.to_s
-    assert_response :precondition_failed,
-                    "shouldn't be able to upload a invalid deletion diff: #{@response.body}"
-    assert_equal "Precondition failed: Way #{used_way.id} is still used by relations #{relation.id}.", @response.body
-
-    # check that nothing was, in fact, deleted
-    assert_equal true, Node.find(used_node.id).visible
-    assert_equal true, Way.find(used_way.id).visible
-    assert_equal true, Relation.find(relation.id).visible
-    assert_equal true, Relation.find(other_relation.id).visible
-  end
-
-  ##
-  # test that a conditional delete of an in use object works.
-  def test_upload_delete_if_unused
-    changeset = create(:changeset)
-    super_relation = create(:relation)
-    used_relation = create(:relation)
-    used_way = create(:way)
-    used_node = create(:node)
-    create(:relation_member, :relation => super_relation, :member => used_relation)
-    create(:relation_member, :relation => super_relation, :member => used_way)
-    create(:relation_member, :relation => super_relation, :member => used_node)
-
-    basic_authorization changeset.user.email, "test"
-
-    diff = XML::Document.new
-    diff.root = XML::Node.new "osmChange"
-    delete = XML::Node.new "delete"
-    diff.root << delete
-    delete["if-unused"] = ""
-    delete << used_relation.to_xml_node
-    delete << used_way.to_xml_node
-    delete << used_node.to_xml_node
-
-    # update the changeset to one that this user owns
-    %w[node way relation].each do |type|
-      delete.find("//osmChange/delete/#{type}").each do |n|
-        n["changeset"] = changeset.id.to_s
-      end
-    end
-
-    # upload it
-    post :upload, :params => { :id => changeset.id }, :body => diff.to_s
-    assert_response :success,
-                    "can't do a conditional delete of in use objects: #{@response.body}"
-
-    # check the returned payload
-    assert_select "diffResult[version='#{API_VERSION}'][generator='OpenStreetMap server']", 1
-    assert_select "diffResult>node", 1
-    assert_select "diffResult>way", 1
-    assert_select "diffResult>relation", 1
-
-    # parse the response
-    doc = XML::Parser.string(@response.body).parse
-
-    # check the old IDs are all present and what we expect
-    assert_equal used_node.id, doc.find("//diffResult/node").first["old_id"].to_i
-    assert_equal used_way.id, doc.find("//diffResult/way").first["old_id"].to_i
-    assert_equal used_relation.id, doc.find("//diffResult/relation").first["old_id"].to_i
-
-    # check the new IDs are all present and unchanged
-    assert_equal used_node.id, doc.find("//diffResult/node").first["new_id"].to_i
-    assert_equal used_way.id, doc.find("//diffResult/way").first["new_id"].to_i
-    assert_equal used_relation.id, doc.find("//diffResult/relation").first["new_id"].to_i
-
-    # check the new versions are all present and unchanged
-    assert_equal used_node.version, doc.find("//diffResult/node").first["new_version"].to_i
-    assert_equal used_way.version, doc.find("//diffResult/way").first["new_version"].to_i
-    assert_equal used_relation.version, doc.find("//diffResult/relation").first["new_version"].to_i
-
-    # check that nothing was, in fact, deleted
-    assert_equal true, Node.find(used_node.id).visible
-    assert_equal true, Way.find(used_way.id).visible
-    assert_equal true, Relation.find(used_relation.id).visible
-  end
-
-  ##
-  # upload an element with a really long tag value
-  def test_upload_invalid_too_long_tag
-    changeset = create(:changeset)
-
-    basic_authorization changeset.user.email, "test"
-
-    # simple diff to create a node way and relation using placeholders
-    diff = <<CHANGESET.strip_heredoc
-      <osmChange>
-       <create>
-        <node id='-1' lon='0' lat='0' changeset='#{changeset.id}'>
-         <tag k='foo' v='#{'x' * 256}'/>
-        </node>
-       </create>
-      </osmChange>
-CHANGESET
-
-    # upload it
-    post :upload, :params => { :id => changeset.id }, :body => diff
-    assert_response :bad_request,
-                    "shoudln't be able to upload too long a tag to changeset: #{@response.body}"
-  end
-
-  ##
-  # upload something which creates new objects and inserts them into
-  # existing containers using placeholders.
-  def test_upload_complex
-    way = create(:way)
-    node = create(:node)
-    relation = create(:relation)
-    create(:way_node, :way => way, :node => node)
-
-    changeset = create(:changeset)
-
-    basic_authorization changeset.user.email, "test"
-
-    # simple diff to create a node way and relation using placeholders
-    diff = <<CHANGESET.strip_heredoc
-      <osmChange>
-       <create>
-        <node id='-1' lon='0' lat='0' changeset='#{changeset.id}'>
-         <tag k='foo' v='bar'/>
-         <tag k='baz' v='bat'/>
-        </node>
-       </create>
-       <modify>
-        <way id='#{way.id}' changeset='#{changeset.id}' version='1'>
-         <nd ref='-1'/>
-         <nd ref='#{node.id}'/>
-        </way>
-        <relation id='#{relation.id}' changeset='#{changeset.id}' version='1'>
-         <member type='way' role='some' ref='#{way.id}'/>
-         <member type='node' role='some' ref='-1'/>
-         <member type='relation' role='some' ref='#{relation.id}'/>
-        </relation>
-       </modify>
-      </osmChange>
-CHANGESET
-
-    # upload it
-    post :upload, :params => { :id => changeset.id }, :body => diff
-    assert_response :success,
-                    "can't upload a complex diff to changeset: #{@response.body}"
-
-    # check the returned payload
-    assert_select "diffResult[version='#{API_VERSION}'][generator='#{GENERATOR}']", 1
-    assert_select "diffResult>node", 1
-    assert_select "diffResult>way", 1
-    assert_select "diffResult>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("//diffResult/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, node.id], Way.find(way.id).nds, "way nodes should match"
-    Relation.find(relation.id).members.each do |type, id, _role|
-      assert_equal new_node_id, id, "relation should contain new node" if type == "node"
-    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
-    changeset = create(:changeset)
-    other_changeset = create(:changeset, :user => changeset.user)
-    node = create(:node)
-    way = create(:way)
-    relation = create(:relation)
-    other_relation = create(:relation)
-
-    basic_authorization changeset.user.email, "test"
-
-    # simple diff to create a node way and relation using placeholders
-    diff = <<CHANGESET.strip_heredoc
-      <osmChange>
-       <modify>
-        <node id='#{node.id}' lon='0' lat='0' changeset='#{changeset.id}' version='1'/>
-        <way id='#{way.id}' changeset='#{changeset.id}' version='1'>
-         <nd ref='#{node.id}'/>
-        </way>
-       </modify>
-       <modify>
-        <relation id='#{relation.id}' changeset='#{changeset.id}' version='1'>
-         <member type='way' role='some' ref='#{way.id}'/>
-         <member type='node' role='some' ref='#{node.id}'/>
-         <member type='relation' role='some' ref='#{other_relation.id}'/>
-        </relation>
-       </modify>
-       <create>
-        <node id='-1' lon='0' lat='0' changeset='#{other_changeset.id}'>
-         <tag k='foo' v='bar'/>
-         <tag k='baz' v='bat'/>
-        </node>
-       </create>
-      </osmChange>
-CHANGESET
-
-    # upload it
-    post :upload, :params => { :id => changeset.id }, :body => diff
-    assert_response :conflict,
-                    "uploading a diff with multiple changesets should have failed"
-
-    # check that objects are unmodified
-    assert_nodes_are_equal(node, Node.find(node.id))
-    assert_ways_are_equal(way, Way.find(way.id))
-    assert_relations_are_equal(relation, Relation.find(relation.id))
-  end
-
-  ##
-  # upload multiple versions of the same element in the same diff.
-  def test_upload_multiple_valid
-    node = create(:node)
-    changeset = create(:changeset)
-    basic_authorization changeset.user.email, "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 = <<CHANGESET.strip_heredoc
-      <osmChange>
-       <modify>
-        <node id='#{node.id}' lon='0' lat='0' changeset='#{changeset.id}' version='1'/>
-        <node id='#{node.id}' lon='1' lat='0' changeset='#{changeset.id}' version='2'/>
-        <node id='#{node.id}' lon='1' lat='1' changeset='#{changeset.id}' version='3'/>
-        <node id='#{node.id}' lon='1' lat='2' changeset='#{changeset.id}' version='4'/>
-        <node id='#{node.id}' lon='2' lat='2' changeset='#{changeset.id}' version='5'/>
-        <node id='#{node.id}' lon='3' lat='2' changeset='#{changeset.id}' version='6'/>
-        <node id='#{node.id}' lon='3' lat='3' changeset='#{changeset.id}' version='7'/>
-        <node id='#{node.id}' lon='9' lat='9' changeset='#{changeset.id}' version='8'/>
-       </modify>
-      </osmChange>
-CHANGESET
-
-    # upload it
-    post :upload, :params => { :id => changeset.id }, :body => diff
-    assert_response :success,
-                    "can't upload multiple versions of an element in a diff: #{@response.body}"
-
-    # check the response is well-formed. its counter-intuitive, but the
-    # API will return multiple elements with the same ID and different
-    # version numbers for each change we made.
-    assert_select "diffResult>node", 8
-  end
-
-  ##
-  # upload multiple versions of the same element in the same diff, but
-  # keep the version numbers the same.
-  def test_upload_multiple_duplicate
-    node = create(:node)
-    changeset = create(:changeset)
-
-    basic_authorization changeset.user.email, "test"
-
-    diff = <<CHANGESET.strip_heredoc
-      <osmChange>
-       <modify>
-        <node id='#{node.id}' lon='0' lat='0' changeset='#{changeset.id}' version='1'/>
-        <node id='#{node.id}' lon='1' lat='1' changeset='#{changeset.id}' version='1'/>
-       </modify>
-      </osmChange>
-CHANGESET
-
-    # upload it
-    post :upload, :params => { :id => changeset.id }, :body => diff
-    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
-    changeset = create(:changeset)
-
-    basic_authorization changeset.user.email, "test"
-
-    diff = <<CHANGESET.strip_heredoc
-      <osmChange>
-       <modify>
-       <node id='1' lon='1' lat='1' changeset='#{changeset.id}'/>
-       </modify>
-      </osmChange>
-CHANGESET
-
-    # upload it
-    post :upload, :params => { :id => changeset.id }, :body => diff
-    assert_response :bad_request,
-                    "shouldn't be able to upload an element without version: #{@response.body}"
-  end
-
-  ##
-  # try to upload with commands other than create, modify, or delete
-  def test_action_upload_invalid
-    changeset = create(:changeset)
-
-    basic_authorization changeset.user.email, "test"
-
-    diff = <<CHANGESET.strip_heredoc
-      <osmChange>
-        <ping>
-         <node id='1' lon='1' lat='1' changeset='#{changeset.id}' />
-        </ping>
-      </osmChange>
-CHANGESET
-    post :upload, :params => { :id => changeset.id }, :body => diff
-    assert_response :bad_request, "Shouldn't be able to upload a diff with the action ping"
-    assert_equal @response.body, "Unknown action ping, choices are create, modify, delete"
-  end
-
-  ##
-  # upload a valid changeset which has a mixture of whitespace
-  # to check a bug reported by ivansanchez (#1565).
-  def test_upload_whitespace_valid
-    changeset = create(:changeset)
-    node = create(:node)
-    way = create(:way_with_nodes, :nodes_count => 2)
-    relation = create(:relation)
-    other_relation = create(:relation)
-    create(:relation_tag, :relation => relation)
-
-    basic_authorization changeset.user.email, "test"
-
-    diff = <<CHANGESET.strip_heredoc
-      <osmChange>
-       <modify><node id='#{node.id}' lon='0' lat='0' changeset='#{changeset.id}'
-        version='1'></node>
-        <node id='#{node.id}' lon='1' lat='1' changeset='#{changeset.id}' version='2'><tag k='k' v='v'/></node></modify>
-       <modify>
-       <relation id='#{relation.id}' changeset='#{changeset.id}' version='1'><member
-         type='way' role='some' ref='#{way.id}'/><member
-          type='node' role='some' ref='#{node.id}'/>
-         <member type='relation' role='some' ref='#{other_relation.id}'/>
-        </relation>
-       </modify></osmChange>
-CHANGESET
-
-    # upload it
-    post :upload, :params => { :id => changeset.id }, :body => diff
-    assert_response :success,
-                    "can't upload a valid diff with whitespace variations to changeset: #{@response.body}"
-
-    # check the response is well-formed
-    assert_select "diffResult>node", 2
-    assert_select "diffResult>relation", 1
-
-    # check that the changes made it into the database
-    assert_equal 1, Node.find(node.id).tags.size, "node #{node.id} should now have one tag"
-    assert_equal 0, Relation.find(relation.id).tags.size, "relation #{relation.id} should now have no tags"
-  end
-
-  ##
-  # test that a placeholder can be reused within the same upload.
-  def test_upload_reuse_placeholder_valid
-    changeset = create(:changeset)
-
-    basic_authorization changeset.user.email, "test"
-
-    diff = <<CHANGESET.strip_heredoc
-      <osmChange>
-       <create>
-        <node id='-1' lon='0' lat='0' changeset='#{changeset.id}'>
-         <tag k="foo" v="bar"/>
-        </node>
-       </create>
-       <modify>
-        <node id='-1' lon='1' lat='1' changeset='#{changeset.id}' version='1'/>
-       </modify>
-       <delete>
-        <node id='-1' lon='2' lat='2' changeset='#{changeset.id}' version='2'/>
-       </delete>
-      </osmChange>
-CHANGESET
-
-    # upload it
-    post :upload, :params => { :id => changeset.id }, :body => diff
-    assert_response :success,
-                    "can't upload a valid diff with re-used placeholders to changeset: #{@response.body}"
-
-    # check the response is well-formed
-    assert_select "diffResult>node", 3
-    assert_select "diffResult>node[old_id='-1']", 3
-  end
-
-  ##
-  # test what happens if a diff upload re-uses placeholder IDs in an
-  # illegal way.
-  def test_upload_placeholder_invalid
-    changeset = create(:changeset)
-
-    basic_authorization changeset.user.email, "test"
-
-    diff = <<CHANGESET.strip_heredoc
-      <osmChange>
-       <create>
-        <node id='-1' lon='0' lat='0' changeset='#{changeset.id}' version='1'/>
-        <node id='-1' lon='1' lat='1' changeset='#{changeset.id}' version='1'/>
-        <node id='-1' lon='2' lat='2' changeset='#{changeset.id}' version='2'/>
-       </create>
-      </osmChange>
-CHANGESET
-
-    # upload it
-    post :upload, :params => { :id => changeset.id }, :body => diff
-    assert_response :bad_request,
-                    "shouldn't be able to re-use placeholder IDs"
-  end
-
-  ##
-  # test that uploading a way referencing invalid placeholders gives a
-  # proper error, not a 500.
-  def test_upload_placeholder_invalid_way
-    changeset = create(:changeset)
-    way = create(:way)
-
-    basic_authorization changeset.user.email, "test"
-
-    diff = <<CHANGESET.strip_heredoc
-      <osmChange>
-       <create>
-        <node id="-1" lon="0" lat="0" changeset="#{changeset.id}" version="1"/>
-        <node id="-2" lon="1" lat="1" changeset="#{changeset.id}" version="1"/>
-        <node id="-3" lon="2" lat="2" changeset="#{changeset.id}" version="1"/>
-        <way id="-1" changeset="#{changeset.id}" version="1">
-         <nd ref="-1"/>
-         <nd ref="-2"/>
-         <nd ref="-3"/>
-         <nd ref="-4"/>
-        </way>
-       </create>
-      </osmChange>
-CHANGESET
-
-    # upload it
-    post :upload, :params => { :id => changeset.id }, :body => diff
-    assert_response :bad_request,
-                    "shouldn't be able to use invalid placeholder IDs"
-    assert_equal "Placeholder node not found for reference -4 in way -1", @response.body
-
-    # the same again, but this time use an existing way
-    diff = <<CHANGESET.strip_heredoc
-      <osmChange>
-       <create>
-        <node id="-1" lon="0" lat="0" changeset="#{changeset.id}" version="1"/>
-        <node id="-2" lon="1" lat="1" changeset="#{changeset.id}" version="1"/>
-        <node id="-3" lon="2" lat="2" changeset="#{changeset.id}" version="1"/>
-        <way id="#{way.id}" changeset="#{changeset.id}" version="1">
-         <nd ref="-1"/>
-         <nd ref="-2"/>
-         <nd ref="-3"/>
-         <nd ref="-4"/>
-        </way>
-       </create>
-      </osmChange>
-CHANGESET
-
-    # upload it
-    post :upload, :params => { :id => changeset.id }, :body => diff
-    assert_response :bad_request,
-                    "shouldn't be able to use invalid placeholder IDs"
-    assert_equal "Placeholder node not found for reference -4 in way #{way.id}", @response.body
-  end
-
-  ##
-  # test that uploading a relation referencing invalid placeholders gives a
-  # proper error, not a 500.
-  def test_upload_placeholder_invalid_relation
-    changeset = create(:changeset)
-    relation = create(:relation)
-
-    basic_authorization changeset.user.email, "test"
-
-    diff = <<CHANGESET.strip_heredoc
-      <osmChange>
-       <create>
-        <node id="-1" lon="0" lat="0" changeset="#{changeset.id}" version="1"/>
-        <node id="-2" lon="1" lat="1" changeset="#{changeset.id}" version="1"/>
-        <node id="-3" lon="2" lat="2" changeset="#{changeset.id}" version="1"/>
-        <relation id="-1" changeset="#{changeset.id}" version="1">
-         <member type="node" role="foo" ref="-1"/>
-         <member type="node" role="foo" ref="-2"/>
-         <member type="node" role="foo" ref="-3"/>
-         <member type="node" role="foo" ref="-4"/>
-        </relation>
-       </create>
-      </osmChange>
-CHANGESET
-
-    # upload it
-    post :upload, :params => { :id => changeset.id }, :body => diff
-    assert_response :bad_request,
-                    "shouldn't be able to use invalid placeholder IDs"
-    assert_equal "Placeholder Node not found for reference -4 in relation -1.", @response.body
-
-    # the same again, but this time use an existing relation
-    diff = <<CHANGESET.strip_heredoc
-      <osmChange>
-       <create>
-        <node id="-1" lon="0" lat="0" changeset="#{changeset.id}" version="1"/>
-        <node id="-2" lon="1" lat="1" changeset="#{changeset.id}" version="1"/>
-        <node id="-3" lon="2" lat="2" changeset="#{changeset.id}" version="1"/>
-        <relation id="#{relation.id}" changeset="#{changeset.id}" version="1">
-         <member type="node" role="foo" ref="-1"/>
-         <member type="node" role="foo" ref="-2"/>
-         <member type="node" role="foo" ref="-3"/>
-         <member type="way" role="bar" ref="-1"/>
-        </relation>
-       </create>
-      </osmChange>
-CHANGESET
-
-    # upload it
-    post :upload, :params => { :id => changeset.id }, :body => diff
-    assert_response :bad_request,
-                    "shouldn't be able to use invalid placeholder IDs"
-    assert_equal "Placeholder Way not found for reference -1 in relation #{relation.id}.", @response.body
-  end
-
-  ##
-  # test what happens if a diff is uploaded containing only a node
-  # move.
-  def test_upload_node_move
-    basic_authorization create(:user).email, "test"
-
-    xml = "<osm><changeset>" \
-          "<tag k='created_by' v='osm test suite checking changesets'/>" \
-          "</changeset></osm>"
-    put :create, :body => xml
-    assert_response :success
-    changeset_id = @response.body.to_i
-
-    old_node = create(:node, :lat => 1, :lon => 1)
-
-    diff = XML::Document.new
-    diff.root = XML::Node.new "osmChange"
-    modify = XML::Node.new "modify"
-    xml_old_node = old_node.to_xml_node
-    xml_old_node["lat"] = 2.0.to_s
-    xml_old_node["lon"] = 2.0.to_s
-    xml_old_node["changeset"] = changeset_id.to_s
-    modify << xml_old_node
-    diff.root << modify
-
-    # upload it
-    post :upload, :params => { :id => changeset_id }, :body => diff.to_s
-    assert_response :success,
-                    "diff should have uploaded OK"
-
-    # check the bbox
-    changeset = Changeset.find(changeset_id)
-    assert_equal 1 * GeoRecord::SCALE, changeset.min_lon, "min_lon should be 1 degree"
-    assert_equal 2 * GeoRecord::SCALE, changeset.max_lon, "max_lon should be 2 degrees"
-    assert_equal 1 * GeoRecord::SCALE, changeset.min_lat, "min_lat should be 1 degree"
-    assert_equal 2 * GeoRecord::SCALE, changeset.max_lat, "max_lat should be 2 degrees"
-  end
-
-  ##
-  # test what happens if a diff is uploaded adding a node to a way.
-  def test_upload_way_extend
-    basic_authorization create(:user).email, "test"
-
-    xml = "<osm><changeset>" \
-          "<tag k='created_by' v='osm test suite checking changesets'/>" \
-          "</changeset></osm>"
-    put :create, :body => xml
-    assert_response :success
-    changeset_id = @response.body.to_i
-
-    old_way = create(:way)
-    create(:way_node, :way => old_way, :node => create(:node, :lat => 1, :lon => 1))
-
-    diff = XML::Document.new
-    diff.root = XML::Node.new "osmChange"
-    modify = XML::Node.new "modify"
-    xml_old_way = old_way.to_xml_node
-    nd_ref = XML::Node.new "nd"
-    nd_ref["ref"] = create(:node, :lat => 3, :lon => 3).id.to_s
-    xml_old_way << nd_ref
-    xml_old_way["changeset"] = changeset_id.to_s
-    modify << xml_old_way
-    diff.root << modify
-
-    # upload it
-    post :upload, :params => { :id => changeset_id }, :body => diff.to_s
-    assert_response :success,
-                    "diff should have uploaded OK"
-
-    # check the bbox
-    changeset = Changeset.find(changeset_id)
-    assert_equal 1 * GeoRecord::SCALE, changeset.min_lon, "min_lon should be 1 degree"
-    assert_equal 3 * GeoRecord::SCALE, changeset.max_lon, "max_lon should be 3 degrees"
-    assert_equal 1 * GeoRecord::SCALE, changeset.min_lat, "min_lat should be 1 degree"
-    assert_equal 3 * GeoRecord::SCALE, changeset.max_lat, "max_lat should be 3 degrees"
-  end
-
-  ##
-  # test for more issues in #1568
-  def test_upload_empty_invalid
-    changeset = create(:changeset)
-
-    basic_authorization changeset.user.email, "test"
-
-    ["<osmChange/>",
-     "<osmChange></osmChange>",
-     "<osmChange><modify/></osmChange>",
-     "<osmChange><modify></modify></osmChange>"].each do |diff|
-      # upload it
-      post :upload, :params => { :id => changeset.id }, :body => diff
-      assert_response(:success, "should be able to upload " \
-                      "empty changeset: " + diff)
-    end
-  end
-
-  ##
-  # test that the X-Error-Format header works to request XML errors
-  def test_upload_xml_errors
-    changeset = create(:changeset)
-    node = create(:node)
-    create(:relation_member, :member => node)
-
-    basic_authorization changeset.user.email, "test"
-
-    # try and delete a node that is in use
-    diff = XML::Document.new
-    diff.root = XML::Node.new "osmChange"
-    delete = XML::Node.new "delete"
-    diff.root << delete
-    delete << node.to_xml_node
-
-    # upload it
-    error_format "xml"
-    post :upload, :params => { :id => changeset.id }, :body => diff.to_s
-    assert_response :success,
-                    "failed to return error in XML format"
-
-    # check the returned payload
-    assert_select "osmError[version='#{API_VERSION}'][generator='OpenStreetMap server']", 1
-    assert_select "osmError>status", 1
-    assert_select "osmError>message", 1
-  end
-
-  ##
-  # when we make some simple changes we get the same changes back from the
-  # diff download.
-  def test_diff_download_simple
-    node = create(:node)
-
-    ## First try with a non-public user, which should get a forbidden
-    basic_authorization create(:user, :data_public => false).email, "test"
-
-    # create a temporary changeset
-    xml = "<osm><changeset>" \
-          "<tag k='created_by' v='osm test suite checking changesets'/>" \
-          "</changeset></osm>"
-    put :create, :body => xml
-    assert_response :forbidden
-
-    ## Now try with a normal user
-    basic_authorization create(:user).email, "test"
-
-    # create a temporary changeset
-    xml = "<osm><changeset>" \
-          "<tag k='created_by' v='osm test suite checking changesets'/>" \
-          "</changeset></osm>"
-    put :create, :body => xml
-    assert_response :success
-    changeset_id = @response.body.to_i
-
-    # add a diff to it
-    diff = <<CHANGESET.strip_heredoc
-      <osmChange>
-       <modify>
-        <node id='#{node.id}' lon='0' lat='0' changeset='#{changeset_id}' version='1'/>
-        <node id='#{node.id}' lon='1' lat='0' changeset='#{changeset_id}' version='2'/>
-        <node id='#{node.id}' lon='1' lat='1' changeset='#{changeset_id}' version='3'/>
-        <node id='#{node.id}' lon='1' lat='2' changeset='#{changeset_id}' version='4'/>
-        <node id='#{node.id}' lon='2' lat='2' changeset='#{changeset_id}' version='5'/>
-        <node id='#{node.id}' lon='3' lat='2' changeset='#{changeset_id}' version='6'/>
-        <node id='#{node.id}' lon='3' lat='3' changeset='#{changeset_id}' version='7'/>
-        <node id='#{node.id}' lon='9' lat='9' changeset='#{changeset_id}' version='8'/>
-       </modify>
-      </osmChange>
-CHANGESET
-
-    # upload it
-    post :upload, :params => { :id => changeset_id }, :body => diff
-    assert_response :success,
-                    "can't upload multiple versions of an element in a diff: #{@response.body}"
-
-    get :download, :params => { :id => changeset_id }
-    assert_response :success
-
-    assert_select "osmChange", 1
-    assert_select "osmChange>modify", 8
-    assert_select "osmChange>modify>node", 8
-  end
-
-  ##
-  # culled this from josm to ensure that nothing in the way that josm
-  # is formatting the request is causing it to fail.
-  #
-  # NOTE: the error turned out to be something else completely!
-  def test_josm_upload
-    basic_authorization create(:user).email, "test"
-
-    # create a temporary changeset
-    xml = "<osm><changeset>" \
-          "<tag k='created_by' v='osm test suite checking changesets'/>" \
-          "</changeset></osm>"
-    put :create, :body => xml
-    assert_response :success
-    changeset_id = @response.body.to_i
-
-    diff = <<OSMFILE.strip_heredoc
-      <osmChange version="0.6" generator="JOSM">
-      <create version="0.6" generator="JOSM">
-        <node id='-1' visible='true' changeset='#{changeset_id}' lat='51.49619982187321' lon='-0.18722061869438314' />
-        <node id='-2' visible='true' changeset='#{changeset_id}' lat='51.496359883909605' lon='-0.18653093576241928' />
-        <node id='-3' visible='true' changeset='#{changeset_id}' lat='51.49598132358285' lon='-0.18719613290981638' />
-        <node id='-4' visible='true' changeset='#{changeset_id}' lat='51.4961591711078' lon='-0.18629015888084607' />
-        <node id='-5' visible='true' changeset='#{changeset_id}' lat='51.49582126021711' lon='-0.18708186591517145' />
-        <node id='-6' visible='true' changeset='#{changeset_id}' lat='51.49591018437858' lon='-0.1861432441734455' />
-        <node id='-7' visible='true' changeset='#{changeset_id}' lat='51.49560784152179' lon='-0.18694719410005425' />
-        <node id='-8' visible='true' changeset='#{changeset_id}' lat='51.49567389979617' lon='-0.1860289771788006' />
-        <node id='-9' visible='true' changeset='#{changeset_id}' lat='51.49543761398892' lon='-0.186820684213126' />
-        <way id='-10' action='modiy' visible='true' changeset='#{changeset_id}'>
-          <nd ref='-1' />
-          <nd ref='-2' />
-          <nd ref='-3' />
-          <nd ref='-4' />
-          <nd ref='-5' />
-          <nd ref='-6' />
-          <nd ref='-7' />
-          <nd ref='-8' />
-          <nd ref='-9' />
-          <tag k='highway' v='residential' />
-          <tag k='name' v='Foobar Street' />
-        </way>
-      </create>
-      </osmChange>
-OSMFILE
-
-    # upload it
-    post :upload, :params => { :id => changeset_id }, :body => diff
-    assert_response :success,
-                    "can't upload a diff from JOSM: #{@response.body}"
-
-    get :download, :params => { :id => changeset_id }
-    assert_response :success
-
-    assert_select "osmChange", 1
-    assert_select "osmChange>create>node", 9
-    assert_select "osmChange>create>way", 1
-    assert_select "osmChange>create>way>nd", 9
-    assert_select "osmChange>create>way>tag", 2
-  end
-
-  ##
-  # when we make some complex changes we get the same changes back from the
-  # diff download.
-  def test_diff_download_complex
-    node = create(:node)
-    node2 = create(:node)
-    way = create(:way)
-    basic_authorization create(:user).email, "test"
-
-    # create a temporary changeset
-    xml = "<osm><changeset>" \
-          "<tag k='created_by' v='osm test suite checking changesets'/>" \
-          "</changeset></osm>"
-    put :create, :body => xml
-    assert_response :success
-    changeset_id = @response.body.to_i
-
-    # add a diff to it
-    diff = <<CHANGESET.strip_heredoc
-      <osmChange>
-       <delete>
-        <node id='#{node.id}' lon='0' lat='0' changeset='#{changeset_id}' version='1'/>
-       </delete>
-       <create>
-        <node id='-1' lon='9' lat='9' changeset='#{changeset_id}' version='0'/>
-        <node id='-2' lon='8' lat='9' changeset='#{changeset_id}' version='0'/>
-        <node id='-3' lon='7' lat='9' changeset='#{changeset_id}' version='0'/>
-       </create>
-       <modify>
-        <node id='#{node2.id}' lon='20' lat='15' changeset='#{changeset_id}' version='1'/>
-        <way id='#{way.id}' changeset='#{changeset_id}' version='1'>
-         <nd ref='#{node2.id}'/>
-         <nd ref='-1'/>
-         <nd ref='-2'/>
-         <nd ref='-3'/>
-        </way>
-       </modify>
-      </osmChange>
-CHANGESET
-
-    # upload it
-    post :upload, :params => { :id => changeset_id }, :body => diff
-    assert_response :success,
-                    "can't upload multiple versions of an element in a diff: #{@response.body}"
-
-    get :download, :params => { :id => changeset_id }
-    assert_response :success
-
-    assert_select "osmChange", 1
-    assert_select "osmChange>create", 3
-    assert_select "osmChange>delete", 1
-    assert_select "osmChange>modify", 2
-    assert_select "osmChange>create>node", 3
-    assert_select "osmChange>delete>node", 1
-    assert_select "osmChange>modify>node", 1
-    assert_select "osmChange>modify>way", 1
-  end
-
-  def test_changeset_download
-    changeset = create(:changeset)
-    node = create(:node, :with_history, :version => 1, :changeset => changeset)
-    tag = create(:old_node_tag, :old_node => node.old_nodes.find_by(:version => 1))
-    node2 = create(:node, :with_history, :version => 1, :changeset => changeset)
-    _node3 = create(:node, :with_history, :deleted, :version => 1, :changeset => changeset)
-    _relation = create(:relation, :with_history, :version => 1, :changeset => changeset)
-    _relation2 = create(:relation, :with_history, :deleted, :version => 1, :changeset => changeset)
-
-    get :download, :params => { :id => changeset.id }
-
-    assert_response :success
-    assert_template nil
-    # print @response.body
-    # FIXME: needs more assert_select tests
-    assert_select "osmChange[version='#{API_VERSION}'][generator='#{GENERATOR}']" do
-      assert_select "create", :count => 5
-      assert_select "create>node[id='#{node.id}'][visible='#{node.visible?}'][version='#{node.version}']" do
-        assert_select "tag[k='#{tag.k}'][v='#{tag.v}']"
-      end
-      assert_select "create>node[id='#{node2.id}']"
-    end
-  end
-
-  ##
-  # check that the bounding box of a changeset gets updated correctly
-  # FIXME: This should really be moded to a integration test due to the with_controller
-  def test_changeset_bbox
-    way = create(:way)
-    create(:way_node, :way => way, :node => create(:node, :lat => 3, :lon => 3))
-
-    basic_authorization create(:user).email, "test"
-
-    # create a new changeset
-    xml = "<osm><changeset/></osm>"
-    put :create, :body => xml
-    assert_response :success, "Creating of changeset failed."
-    changeset_id = @response.body.to_i
-
-    # add a single node to it
-    with_controller(NodesController.new) do
-      xml = "<osm><node lon='1' lat='2' changeset='#{changeset_id}'/></osm>"
-      put :create, :body => xml
-      assert_response :success, "Couldn't create node."
-    end
-
-    # get the bounding box back from the changeset
-    get :show, :params => { :id => changeset_id }
-    assert_response :success, "Couldn't read back changeset."
-    assert_select "osm>changeset[min_lon='1.0000000']", 1
-    assert_select "osm>changeset[max_lon='1.0000000']", 1
-    assert_select "osm>changeset[min_lat='2.0000000']", 1
-    assert_select "osm>changeset[max_lat='2.0000000']", 1
-
-    # add another node to it
-    with_controller(NodesController.new) do
-      xml = "<osm><node lon='2' lat='1' changeset='#{changeset_id}'/></osm>"
-      put :create, :body => xml
-      assert_response :success, "Couldn't create second node."
-    end
-
-    # get the bounding box back from the changeset
-    get :show, :params => { :id => changeset_id }
-    assert_response :success, "Couldn't read back changeset for the second time."
-    assert_select "osm>changeset[min_lon='1.0000000']", 1
-    assert_select "osm>changeset[max_lon='2.0000000']", 1
-    assert_select "osm>changeset[min_lat='1.0000000']", 1
-    assert_select "osm>changeset[max_lat='2.0000000']", 1
-
-    # add (delete) a way to it, which contains a point at (3,3)
-    with_controller(WaysController.new) do
-      xml = update_changeset(way.to_xml, changeset_id)
-      put :delete, :params => { :id => way.id }, :body => xml.to_s
-      assert_response :success, "Couldn't delete a way."
-    end
-
-    # get the bounding box back from the changeset
-    get :show, :params => { :id => changeset_id }
-    assert_response :success, "Couldn't read back changeset for the third time."
-    assert_select "osm>changeset[min_lon='1.0000000']", 1
-    assert_select "osm>changeset[max_lon='3.0000000']", 1
-    assert_select "osm>changeset[min_lat='1.0000000']", 1
-    assert_select "osm>changeset[max_lat='3.0000000']", 1
-  end
-
-  ##
-  # test that the changeset :include method works as it should
-  def test_changeset_include
-    basic_authorization create(:user).display_name, "test"
-
-    # create a new changeset
-    put :create, :body => "<osm><changeset/></osm>"
-    assert_response :success, "Creating of changeset failed."
-    changeset_id = @response.body.to_i
-
-    # NOTE: the include method doesn't over-expand, like inserting
-    # a real method does. this is because we expect the client to
-    # know what it is doing!
-    check_after_include(changeset_id, 1, 1, [1, 1, 1, 1])
-    check_after_include(changeset_id, 3, 3, [1, 1, 3, 3])
-    check_after_include(changeset_id, 4, 2, [1, 1, 4, 3])
-    check_after_include(changeset_id, 2, 2, [1, 1, 4, 3])
-    check_after_include(changeset_id, -1, -1, [-1, -1, 4, 3])
-    check_after_include(changeset_id, -2, 5, [-2, -1, 4, 5])
-  end
-
-  ##
-  # test that a not found, wrong method with the expand bbox works as expected
-  def test_changeset_expand_bbox_error
-    basic_authorization create(:user).display_name, "test"
-
-    # create a new changeset
-    xml = "<osm><changeset/></osm>"
-    put :create, :body => xml
-    assert_response :success, "Creating of changeset failed."
-    changeset_id = @response.body.to_i
-
-    lon = 58.2
-    lat = -0.45
-
-    # Try and put
-    xml = "<osm><node lon='#{lon}' lat='#{lat}'/></osm>"
-    put :expand_bbox, :params => { :id => changeset_id }, :body => xml
-    assert_response :method_not_allowed, "shouldn't be able to put a bbox expand"
-
-    # Try to get the update
-    xml = "<osm><node lon='#{lon}' lat='#{lat}'/></osm>"
-    get :expand_bbox, :params => { :id => changeset_id }, :body => xml
-    assert_response :method_not_allowed, "shouldn't be able to get a bbox expand"
-
-    # Try to use a hopefully missing changeset
-    xml = "<osm><node lon='#{lon}' lat='#{lat}'/></osm>"
-    post :expand_bbox, :params => { :id => changeset_id + 13245 }, :body => xml
-    assert_response :not_found, "shouldn't be able to do a bbox expand on a nonexistant changeset"
-  end
-
-  ##
-  # test the query functionality of changesets
-  def test_query
-    private_user = create(:user, :data_public => false)
-    private_user_changeset = create(:changeset, :user => private_user)
-    private_user_closed_changeset = create(:changeset, :closed, :user => private_user)
-    user = create(:user)
-    changeset = create(:changeset, :user => user)
-    closed_changeset = create(:changeset, :closed, :user => user, :created_at => Time.utc(2008, 1, 1, 0, 0, 0), :closed_at => Time.utc(2008, 1, 2, 0, 0, 0))
-    changeset2 = create(:changeset, :min_lat => 5 * GeoRecord::SCALE, :min_lon => 5 * GeoRecord::SCALE, :max_lat => 15 * GeoRecord::SCALE, :max_lon => 15 * GeoRecord::SCALE)
-    changeset3 = create(:changeset, :min_lat => 4.5 * GeoRecord::SCALE, :min_lon => 4.5 * GeoRecord::SCALE, :max_lat => 5 * GeoRecord::SCALE, :max_lon => 5 * GeoRecord::SCALE)
-
-    get :query, :params => { :bbox => "-10,-10, 10, 10" }
-    assert_response :success, "can't get changesets in bbox"
-    assert_changesets [changeset2, changeset3]
-
-    get :query, :params => { :bbox => "4.5,4.5,4.6,4.6" }
-    assert_response :success, "can't get changesets in bbox"
-    assert_changesets [changeset3]
-
-    # not found when looking for changesets of non-existing users
-    get :query, :params => { :user => User.maximum(:id) + 1 }
-    assert_response :not_found
-    get :query, :params => { :display_name => " " }
-    assert_response :not_found
-
-    # can't get changesets of user 1 without authenticating
-    get :query, :params => { :user => private_user.id }
-    assert_response :not_found, "shouldn't be able to get changesets by non-public user (ID)"
-    get :query, :params => { :display_name => private_user.display_name }
-    assert_response :not_found, "shouldn't be able to get changesets by non-public user (name)"
-
-    # but this should work
-    basic_authorization private_user.email, "test"
-    get :query, :params => { :user => private_user.id }
-    assert_response :success, "can't get changesets by user ID"
-    assert_changesets [private_user_changeset, private_user_closed_changeset]
-
-    get :query, :params => { :display_name => private_user.display_name }
-    assert_response :success, "can't get changesets by user name"
-    assert_changesets [private_user_changeset, private_user_closed_changeset]
-
-    # check that the correct error is given when we provide both UID and name
-    get :query, :params => { :user => private_user.id,
-                             :display_name => private_user.display_name }
-    assert_response :bad_request, "should be a bad request to have both ID and name specified"
-
-    get :query, :params => { :user => private_user.id, :open => true }
-    assert_response :success, "can't get changesets by user and open"
-    assert_changesets [private_user_changeset]
-
-    get :query, :params => { :time => "2007-12-31" }
-    assert_response :success, "can't get changesets by time-since"
-    assert_changesets [private_user_changeset, private_user_closed_changeset, changeset, closed_changeset, changeset2, changeset3]
-
-    get :query, :params => { :time => "2008-01-01T12:34Z" }
-    assert_response :success, "can't get changesets by time-since with hour"
-    assert_changesets [private_user_changeset, private_user_closed_changeset, changeset, closed_changeset, changeset2, changeset3]
-
-    get :query, :params => { :time => "2007-12-31T23:59Z,2008-01-02T00:01Z" }
-    assert_response :success, "can't get changesets by time-range"
-    assert_changesets [closed_changeset]
-
-    get :query, :params => { :open => "true" }
-    assert_response :success, "can't get changesets by open-ness"
-    assert_changesets [private_user_changeset, changeset, changeset2, changeset3]
-
-    get :query, :params => { :closed => "true" }
-    assert_response :success, "can't get changesets by closed-ness"
-    assert_changesets [private_user_closed_changeset, closed_changeset]
-
-    get :query, :params => { :closed => "true", :user => private_user.id }
-    assert_response :success, "can't get changesets by closed-ness and user"
-    assert_changesets [private_user_closed_changeset]
-
-    get :query, :params => { :closed => "true", :user => user.id }
-    assert_response :success, "can't get changesets by closed-ness and user"
-    assert_changesets [closed_changeset]
-
-    get :query, :params => { :changesets => "#{private_user_changeset.id},#{changeset.id},#{closed_changeset.id}" }
-    assert_response :success, "can't get changesets by id (as comma-separated string)"
-    assert_changesets [private_user_changeset, changeset, closed_changeset]
-
-    get :query, :params => { :changesets => "" }
-    assert_response :bad_request, "should be a bad request since changesets is empty"
-  end
-
-  ##
-  # check that errors are returned if garbage is inserted
-  # into query strings
-  def test_query_invalid
-    ["abracadabra!",
-     "1,2,3,F",
-     ";drop table users;"].each do |bbox|
-      get :query, :params => { :bbox => bbox }
-      assert_response :bad_request, "'#{bbox}' isn't a bbox"
-    end
-
-    ["now()",
-     "00-00-00",
-     ";drop table users;",
-     ",",
-     "-,-"].each do |time|
-      get :query, :params => { :time => time }
-      assert_response :bad_request, "'#{time}' isn't a valid time range"
-    end
-
-    ["me",
-     "foobar",
-     "-1",
-     "0"].each do |uid|
-      get :query, :params => { :user => uid }
-      assert_response :bad_request, "'#{uid}' isn't a valid user ID"
-    end
-  end
-
-  ##
-  # check updating tags on a changeset
-  def test_changeset_update
-    private_user = create(:user, :data_public => false)
-    private_changeset = create(:changeset, :user => private_user)
-    user = create(:user)
-    changeset = create(:changeset, :user => user)
-
-    ## First try with a non-public user
-    new_changeset = private_changeset.to_xml
-    new_tag = XML::Node.new "tag"
-    new_tag["k"] = "tagtesting"
-    new_tag["v"] = "valuetesting"
-    new_changeset.find("//osm/changeset").first << new_tag
-
-    # try without any authorization
-    put :update, :params => { :id => private_changeset.id }, :body => new_changeset.to_s
-    assert_response :unauthorized
-
-    # try with the wrong authorization
-    basic_authorization create(:user).email, "test"
-    put :update, :params => { :id => private_changeset.id }, :body => new_changeset.to_s
-    assert_response :conflict
-
-    # now this should get an unauthorized
-    basic_authorization private_user.email, "test"
-    put :update, :params => { :id => private_changeset.id }, :body => new_changeset.to_s
-    assert_require_public_data "user with their data non-public, shouldn't be able to edit their changeset"
-
-    ## Now try with the public user
-    create(:changeset_tag, :changeset => changeset)
-    new_changeset = changeset.to_xml
-    new_tag = XML::Node.new "tag"
-    new_tag["k"] = "tagtesting"
-    new_tag["v"] = "valuetesting"
-    new_changeset.find("//osm/changeset").first << new_tag
-
-    # try without any authorization
-    @request.env["HTTP_AUTHORIZATION"] = nil
-    put :update, :params => { :id => changeset.id }, :body => new_changeset.to_s
-    assert_response :unauthorized
-
-    # try with the wrong authorization
-    basic_authorization create(:user).email, "test"
-    put :update, :params => { :id => changeset.id }, :body => new_changeset.to_s
-    assert_response :conflict
-
-    # now this should work...
-    basic_authorization user.email, "test"
-    put :update, :params => { :id => changeset.id }, :body => new_changeset.to_s
-    assert_response :success
-
-    assert_select "osm>changeset[id='#{changeset.id}']", 1
-    assert_select "osm>changeset>tag", 2
-    assert_select "osm>changeset>tag[k='tagtesting'][v='valuetesting']", 1
-  end
-
-  ##
-  # check that a user different from the one who opened the changeset
-  # can't modify it.
-  def test_changeset_update_invalid
-    basic_authorization create(:user).email, "test"
-
-    changeset = create(:changeset)
-    new_changeset = changeset.to_xml
-    new_tag = XML::Node.new "tag"
-    new_tag["k"] = "testing"
-    new_tag["v"] = "testing"
-    new_changeset.find("//osm/changeset").first << new_tag
-
-    put :update, :params => { :id => changeset.id }, :body => new_changeset.to_s
-    assert_response :conflict
-  end
-
-  ##
-  # check that a changeset can contain a certain max number of changes.
-  ## FIXME should be changed to an integration test due to the with_controller
-  def test_changeset_limits
-    basic_authorization create(:user).email, "test"
-
-    # open a new changeset
-    xml = "<osm><changeset/></osm>"
-    put :create, :body => xml
-    assert_response :success, "can't create a new changeset"
-    cs_id = @response.body.to_i
-
-    # start the counter just short of where the changeset should finish.
-    offset = 10
-    # alter the database to set the counter on the changeset directly,
-    # otherwise it takes about 6 minutes to fill all of them.
-    changeset = Changeset.find(cs_id)
-    changeset.num_changes = Changeset::MAX_ELEMENTS - offset
-    changeset.save!
-
-    with_controller(NodesController.new) do
-      # create a new node
-      xml = "<osm><node changeset='#{cs_id}' lat='0.0' lon='0.0'/></osm>"
-      put :create, :body => xml
-      assert_response :success, "can't create a new node"
-      node_id = @response.body.to_i
-
-      get :show, :params => { :id => node_id }
-      assert_response :success, "can't read back new node"
-      node_doc = XML::Parser.string(@response.body).parse
-      node_xml = node_doc.find("//osm/node").first
-
-      # loop until we fill the changeset with nodes
-      offset.times do |i|
-        node_xml["lat"] = rand.to_s
-        node_xml["lon"] = rand.to_s
-        node_xml["version"] = (i + 1).to_s
-
-        put :update, :params => { :id => node_id }, :body => node_doc.to_s
-        assert_response :success, "attempt #{i} should have succeeded"
-      end
-
-      # trying again should fail
-      node_xml["lat"] = rand.to_s
-      node_xml["lon"] = rand.to_s
-      node_xml["version"] = offset.to_s
-
-      put :update, :params => { :id => node_id }, :body => node_doc.to_s
-      assert_response :conflict, "final attempt should have failed"
-    end
-
-    changeset = Changeset.find(cs_id)
-    assert_equal Changeset::MAX_ELEMENTS + 1, changeset.num_changes
-
-    # check that the changeset is now closed as well
-    assert_not(changeset.is_open?,
-               "changeset should have been auto-closed by exceeding " \
-               "element limit.")
-  end
-
   ##
   # This should display the last 20 changesets closed
   def test_index
@@ -2062,167 +270,8 @@ CHANGESET
     assert_redirected_to :action => :feed
   end
 
-  ##
-  # check that the changeset download for a changeset with a redacted
-  # element in it doesn't contain that element.
-  def test_diff_download_redacted
-    changeset = create(:changeset)
-    node = create(:node, :with_history, :version => 2, :changeset => changeset)
-    node_v1 = node.old_nodes.find_by(:version => 1)
-    node_v1.redact!(create(:redaction))
-
-    get :download, :params => { :id => changeset.id }
-    assert_response :success
-
-    assert_select "osmChange", 1
-    # this changeset contains the node in versions 1 & 2, but 1 should
-    # be hidden.
-    assert_select "osmChange node[id='#{node.id}']", 1
-    assert_select "osmChange node[id='#{node.id}'][version='1']", 0
-  end
-
-  ##
-  # test subscribe success
-  def test_subscribe_success
-    basic_authorization create(:user).email, "test"
-    changeset = create(:changeset, :closed)
-
-    assert_difference "changeset.subscribers.count", 1 do
-      post :subscribe, :params => { :id => changeset.id }
-    end
-    assert_response :success
-
-    # not closed changeset
-    changeset = create(:changeset)
-    assert_difference "changeset.subscribers.count", 1 do
-      post :subscribe, :params => { :id => changeset.id }
-    end
-    assert_response :success
-  end
-
-  ##
-  # test subscribe fail
-  def test_subscribe_fail
-    user = create(:user)
-
-    # unauthorized
-    changeset = create(:changeset, :closed)
-    assert_no_difference "changeset.subscribers.count" do
-      post :subscribe, :params => { :id => changeset.id }
-    end
-    assert_response :unauthorized
-
-    basic_authorization user.email, "test"
-
-    # bad changeset id
-    assert_no_difference "changeset.subscribers.count" do
-      post :subscribe, :params => { :id => 999111 }
-    end
-    assert_response :not_found
-
-    # trying to subscribe when already subscribed
-    changeset = create(:changeset, :closed)
-    changeset.subscribers.push(user)
-    assert_no_difference "changeset.subscribers.count" do
-      post :subscribe, :params => { :id => changeset.id }
-    end
-    assert_response :conflict
-  end
-
-  ##
-  # test unsubscribe success
-  def test_unsubscribe_success
-    user = create(:user)
-    basic_authorization user.email, "test"
-    changeset = create(:changeset, :closed)
-    changeset.subscribers.push(user)
-
-    assert_difference "changeset.subscribers.count", -1 do
-      post :unsubscribe, :params => { :id => changeset.id }
-    end
-    assert_response :success
-
-    # not closed changeset
-    changeset = create(:changeset)
-    changeset.subscribers.push(user)
-
-    assert_difference "changeset.subscribers.count", -1 do
-      post :unsubscribe, :params => { :id => changeset.id }
-    end
-    assert_response :success
-  end
-
-  ##
-  # test unsubscribe fail
-  def test_unsubscribe_fail
-    # unauthorized
-    changeset = create(:changeset, :closed)
-    assert_no_difference "changeset.subscribers.count" do
-      post :unsubscribe, :params => { :id => changeset.id }
-    end
-    assert_response :unauthorized
-
-    basic_authorization create(:user).email, "test"
-
-    # bad changeset id
-    assert_no_difference "changeset.subscribers.count" do
-      post :unsubscribe, :params => { :id => 999111 }
-    end
-    assert_response :not_found
-
-    # trying to unsubscribe when not subscribed
-    changeset = create(:changeset, :closed)
-    assert_no_difference "changeset.subscribers.count" do
-      post :unsubscribe, :params => { :id => changeset.id }
-    end
-    assert_response :not_found
-  end
-
   private
 
-  ##
-  # boilerplate for checking that certain changesets exist in the
-  # output.
-  def assert_changesets(changesets)
-    assert_select "osm>changeset", changesets.size
-    changesets.each do |changeset|
-      assert_select "osm>changeset[id='#{changeset.id}']", 1
-    end
-  end
-
-  ##
-  # call the include method and assert properties of the bbox
-  def check_after_include(changeset_id, lon, lat, bbox)
-    xml = "<osm><node lon='#{lon}' lat='#{lat}'/></osm>"
-    post :expand_bbox, :params => { :id => changeset_id }, :body => xml
-    assert_response :success, "Setting include of changeset failed: #{@response.body}"
-
-    # check exactly one changeset
-    assert_select "osm>changeset", 1
-    assert_select "osm>changeset[id='#{changeset_id}']", 1
-
-    # check the bbox
-    doc = XML::Parser.string(@response.body).parse
-    changeset = doc.find("//osm/changeset").first
-    assert_equal bbox[0], changeset["min_lon"].to_f, "min lon"
-    assert_equal bbox[1], changeset["min_lat"].to_f, "min lat"
-    assert_equal bbox[2], changeset["max_lon"].to_f, "max lon"
-    assert_equal bbox[3], changeset["max_lat"].to_f, "max lat"
-  end
-
-  ##
-  # update the changeset_id of a way element
-  def update_changeset(xml, changeset_id)
-    xml_attr_rewrite(xml, "changeset", changeset_id)
-  end
-
-  ##
-  # update an attribute in a way element
-  def xml_attr_rewrite(xml, name, value)
-    xml.find("//osm/way").first[name] = value.to_s
-    xml
-  end
-
   ##
   # check the result of a index
   def check_index_result(changesets)
index a8b0ed2dffed80eddddb8c059453fa18dfac471d..ff26af7e82a4aa527e07844deade84908e12ba27 100644 (file)
@@ -929,7 +929,7 @@ OSM
 
     # create a new changeset for this operation, so we are assured
     # that the bounding box will be newly-generated.
-    changeset_id = with_controller(ChangesetsController.new) do
+    changeset_id = with_controller(Api::ChangesetsController.new) do
       xml = "<osm><changeset/></osm>"
       put :create, :body => xml
       assert_response :forbidden, "shouldn't be able to create changeset for modify test, as should get forbidden"
@@ -940,7 +940,7 @@ OSM
 
     # create a new changeset for this operation, so we are assured
     # that the bounding box will be newly-generated.
-    changeset_id = with_controller(ChangesetsController.new) do
+    changeset_id = with_controller(Api::ChangesetsController.new) do
       xml = "<osm><changeset/></osm>"
       put :create, :body => xml
       assert_response :success, "couldn't create changeset for modify test"
@@ -951,7 +951,7 @@ OSM
     yield changeset_id
 
     # now download the changeset to check its bounding box
-    with_controller(ChangesetsController.new) do
+    with_controller(Api::ChangesetsController.new) do
       get :show, :params => { :id => 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}"
@@ -1008,7 +1008,7 @@ OSM
     cs_id = rel.find("//osm/relation").first["changeset"].to_i
     version = nil
 
-    with_controller(ChangesetsController.new) do
+    with_controller(Api::ChangesetsController.new) do
       doc = OSM::API.new.get_xml_doc
       change = XML::Node.new "osmChange"
       doc.root = change