From 1778fa3d9c25ae1a98386d3dcbb426eda5e62fbf Mon Sep 17 00:00:00 2001 From: Andy Allan Date: Sun, 24 Feb 2019 12:18:31 +0100 Subject: [PATCH] Move the api methods from changesets_controller into the api namespaced controller --- app/controllers/api/changesets_controller.rb | 422 ++++ app/controllers/changesets_controller.rb | 388 +--- app/views/browse/changeset.html.erb | 4 +- config/routes.rb | 20 +- .../api/changesets_controller_test.rb | 1963 +++++++++++++++++ .../controllers/changesets_controller_test.rb | 1951 ---------------- test/controllers/relations_controller_test.rb | 8 +- 7 files changed, 2404 insertions(+), 2352 deletions(-) create mode 100644 app/controllers/api/changesets_controller.rb create mode 100644 test/controllers/api/changesets_controller_test.rb diff --git a/app/controllers/api/changesets_controller.rb b/app/controllers/api/changesets_controller.rb new file mode 100644 index 000000000..853ee389f --- /dev/null +++ b/app/controllers/api/changesets_controller.rb @@ -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 diff --git a/app/controllers/changesets_controller.rb b/app/controllers/changesets_controller.rb index 80b8aff48..a69d0d57b 100644 --- a/app/controllers/changesets_controller.rb +++ b/app/controllers/changesets_controller.rb @@ -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 diff --git a/app/views/browse/changeset.html.erb b/app/views/browse/changeset.html.erb index 0b73fa083..1328461d7 100644 --- a/app/views/browse/changeset.html.erb +++ b/app/views/browse/changeset.html.erb @@ -137,7 +137,7 @@ <% end %>
- <%= link_to(t('.changesetxml'), :controller => "changesets", :action => "show") %> + <%= link_to(t('.changesetxml'), :controller => "api/changesets", :action => "show") %> · - <%= link_to(t('.osmchangexml'), :controller => "changesets", :action => "download") %> + <%= link_to(t('.osmchangexml'), :controller => "api/changesets", :action => "download") %>
diff --git a/config/routes.rb b/config/routes.rb index 3c0cf61f1..95142a49f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 index 000000000..75896b202 --- /dev/null +++ b/test/controllers/api/changesets_controller_test.rb @@ -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 = "" \ + "" \ + "" + put :create, :body => xml + assert_require_public_data + + basic_authorization create(:user).email, "test" + # Create the first user's changeset + xml = "" \ + "" \ + "" + 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 = "" + put :create, :body => xml + assert_require_public_data + + ## Try the public user + basic_authorization create(:user).email, "test" + xml = "" + 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 + + # 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 + + # 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 + + # 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 + + # 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 = "" + + # 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 = "" \ + "" \ + "" + 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 => "" + 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 + + # 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 + + # 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 + + # 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 + + # 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 + + # 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 + + # 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 + + # 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 + 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 + + # 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 + + # 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 + + # 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 + + # 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 + + # 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 + + # 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 + + # 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 = "" \ + "" \ + "" + 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 = "" \ + "" \ + "" + 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" + + ["", + "", + "", + ""].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 = "" \ + "" \ + "" + put :create, :body => xml + assert_response :forbidden + + ## Now try with a normal user + basic_authorization create(:user).email, "test" + + # create a temporary changeset + xml = "" \ + "" \ + "" + put :create, :body => xml + assert_response :success + changeset_id = @response.body.to_i + + # add a diff to it + diff = < + + + + + + + + + + + +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 = "" \ + "" \ + "" + put :create, :body => xml + assert_response :success + changeset_id = @response.body.to_i + + diff = < + + + + + + + + + + + + + + + + + + + + + + + + + +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 = "" \ + "" \ + "" + put :create, :body => xml + assert_response :success + changeset_id = @response.body.to_i + + # add a diff to it + diff = < + + + + + + + + + + + + + + + + + + +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 = "" + 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 = "" + 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 = "" + 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 => "" + 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 = "" + 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 = "" + 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 = "" + 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 = "" + 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 = "" + 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 = "" + 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 = "" + 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 diff --git a/test/controllers/changesets_controller_test.rb b/test/controllers/changesets_controller_test.rb index a3c8947ca..fad7a97d8 100644 --- a/test/controllers/changesets_controller_test.rb +++ b/test/controllers/changesets_controller_test.rb @@ -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 = "" \ - "" \ - "" - put :create, :body => xml - assert_require_public_data - - basic_authorization create(:user).email, "test" - # Create the first user's changeset - xml = "" \ - "" \ - "" - 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 = "" - put :create, :body => xml - assert_require_public_data - - ## Try the public user - basic_authorization create(:user).email, "test" - xml = "" - 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 - - # 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 - - # 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 - - # 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 - - # 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 = "" - - # 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 = "" \ - "" \ - "" - 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 => "" - 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 - - # 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 - - # 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 - - # 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 - - # 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 - - # 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 - - # 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 - - # 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 - 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 - - # 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 - - # 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 - - # 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 - - # 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 - - # 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 - - # 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 - - # 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 = "" \ - "" \ - "" - 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 = "" \ - "" \ - "" - 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" - - ["", - "", - "", - ""].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 = "" \ - "" \ - "" - put :create, :body => xml - assert_response :forbidden - - ## Now try with a normal user - basic_authorization create(:user).email, "test" - - # create a temporary changeset - xml = "" \ - "" \ - "" - put :create, :body => xml - assert_response :success - changeset_id = @response.body.to_i - - # add a diff to it - diff = < - - - - - - - - - - - -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 = "" \ - "" \ - "" - put :create, :body => xml - assert_response :success - changeset_id = @response.body.to_i - - diff = < - - - - - - - - - - - - - - - - - - - - - - - - - -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 = "" \ - "" \ - "" - put :create, :body => xml - assert_response :success - changeset_id = @response.body.to_i - - # add a diff to it - diff = < - - - - - - - - - - - - - - - - - - -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 = "" - 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 = "" - 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 = "" - 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 => "" - 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 = "" - 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 = "" - 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 = "" - 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 = "" - 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 = "" - 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 = "" - 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 = "" - 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) diff --git a/test/controllers/relations_controller_test.rb b/test/controllers/relations_controller_test.rb index a8b0ed2df..ff26af7e8 100644 --- a/test/controllers/relations_controller_test.rb +++ b/test/controllers/relations_controller_test.rb @@ -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 = "" 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 = "" 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 -- 2.39.5