]> git.openstreetmap.org Git - rails.git/blobdiff - test/controllers/api/changesets_controller_test.rb
Merge pull request #4887 from AntonKhorev/full-encode-wikipedia-value
[rails.git] / test / controllers / api / changesets_controller_test.rb
index c9ec9c9feeacff0c83a3e214366c6c5a89799481..17a9ad9d35ec901c289c6684ca2f94a839a33445 100644 (file)
@@ -51,11 +51,11 @@ module Api
       )
       assert_routing(
         { :path => "/api/0.6/changesets", :method => :get },
-        { :controller => "api/changesets", :action => "query" }
+        { :controller => "api/changesets", :action => "index" }
       )
       assert_routing(
         { :path => "/api/0.6/changesets.json", :method => :get },
-        { :controller => "api/changesets", :action => "query", :format => "json" }
+        { :controller => "api/changesets", :action => "index", :format => "json" }
       )
     end
 
@@ -134,12 +134,14 @@ module Api
 
     def test_create_wrong_method
       auth_header = basic_authorization_header create(:user).email, "test"
-      assert_raise ActionController::RoutingError do
-        get changeset_create_path, :headers => auth_header
-      end
-      assert_raise ActionController::RoutingError do
-        post changeset_create_path, :headers => auth_header
-      end
+
+      get changeset_create_path, :headers => auth_header
+      assert_response :not_found
+      assert_template "rescues/routing_error"
+
+      post changeset_create_path, :headers => auth_header
+      assert_response :not_found
+      assert_template "rescues/routing_error"
     end
 
     ##
@@ -152,36 +154,83 @@ module Api
       assert_response :success, "cannot get first changeset"
 
       assert_select "osm[version='#{Settings.api_version}'][generator='#{Settings.generator}']", 1
-      assert_select "osm>changeset[id='#{changeset.id}']", 1
-      assert_select "osm>changeset>@open", "true"
-      assert_select "osm>changeset>@created_at", changeset.created_at.xmlschema
-      assert_select "osm>changeset>@closed_at", 0
+      assert_single_changeset changeset
       assert_select "osm>changeset>discussion", 0
 
       get changeset_show_path(changeset), :params => { :include_discussion => true }
       assert_response :success, "cannot get first changeset with comments"
 
       assert_select "osm[version='#{Settings.api_version}'][generator='#{Settings.generator}']", 1
-      assert_select "osm>changeset[id='#{changeset.id}']", 1
-      assert_select "osm>changeset>@open", "true"
-      assert_select "osm>changeset>@created_at", changeset.created_at.xmlschema
-      assert_select "osm>changeset>@closed_at", 0
+      assert_single_changeset changeset
       assert_select "osm>changeset>discussion", 1
       assert_select "osm>changeset>discussion>comment", 0
+    end
 
+    def test_show_comments
+      # all comments visible
       changeset = create(:changeset, :closed)
-      create_list(:changeset_comment, 3, :changeset_id => changeset.id)
+      comment1, comment2, comment3 = create_list(:changeset_comment, 3, :changeset_id => changeset.id)
 
       get changeset_show_path(changeset), :params => { :include_discussion => true }
       assert_response :success, "cannot get closed changeset with comments"
 
       assert_select "osm[version='#{Settings.api_version}'][generator='#{Settings.generator}']", 1
-      assert_select "osm>changeset[id='#{changeset.id}']", 1
-      assert_select "osm>changeset>@open", "false"
-      assert_select "osm>changeset>@created_at", changeset.created_at.xmlschema
-      assert_select "osm>changeset>@closed_at", changeset.closed_at.xmlschema
+      assert_single_changeset changeset
       assert_select "osm>changeset>discussion", 1
       assert_select "osm>changeset>discussion>comment", 3
+      assert_select "osm>changeset>discussion>comment:nth-child(1)>@id", comment1.id.to_s
+      assert_select "osm>changeset>discussion>comment:nth-child(1)>@visible", "true"
+      assert_select "osm>changeset>discussion>comment:nth-child(2)>@id", comment2.id.to_s
+      assert_select "osm>changeset>discussion>comment:nth-child(2)>@visible", "true"
+      assert_select "osm>changeset>discussion>comment:nth-child(3)>@id", comment3.id.to_s
+      assert_select "osm>changeset>discussion>comment:nth-child(3)>@visible", "true"
+
+      # one hidden comment not included because not asked for
+      comment2.update(:visible => false)
+
+      get changeset_show_path(changeset), :params => { :include_discussion => true }
+      assert_response :success, "cannot get closed changeset with comments"
+
+      assert_select "osm[version='#{Settings.api_version}'][generator='#{Settings.generator}']", 1
+      assert_single_changeset changeset
+      assert_select "osm>changeset>discussion", 1
+      assert_select "osm>changeset>discussion>comment", 2
+      assert_select "osm>changeset>discussion>comment:nth-child(1)>@id", comment1.id.to_s
+      assert_select "osm>changeset>discussion>comment:nth-child(1)>@visible", "true"
+      assert_select "osm>changeset>discussion>comment:nth-child(2)>@id", comment3.id.to_s
+      assert_select "osm>changeset>discussion>comment:nth-child(2)>@visible", "true"
+
+      # one hidden comment not included because no permissions
+      get changeset_show_path(changeset), :params => { :include_discussion => true, :show_hidden_comments => true }
+      assert_response :success, "cannot get closed changeset with comments"
+
+      assert_select "osm[version='#{Settings.api_version}'][generator='#{Settings.generator}']", 1
+      assert_single_changeset changeset
+      assert_select "osm>changeset>discussion", 1
+      assert_select "osm>changeset>discussion>comment", 2
+      assert_select "osm>changeset>discussion>comment:nth-child(1)>@id", comment1.id.to_s
+      assert_select "osm>changeset>discussion>comment:nth-child(1)>@visible", "true"
+      # maybe will show an empty comment element with visible=false in the future
+      assert_select "osm>changeset>discussion>comment:nth-child(2)>@id", comment3.id.to_s
+      assert_select "osm>changeset>discussion>comment:nth-child(2)>@visible", "true"
+
+      # one hidden comment shown to moderators
+      moderator_user = create(:moderator_user)
+      auth_header = basic_authorization_header moderator_user.email, "test"
+      get changeset_show_path(changeset), :params => { :include_discussion => true, :show_hidden_comments => true },
+                                          :headers => auth_header
+      assert_response :success, "cannot get closed changeset with comments"
+
+      assert_select "osm[version='#{Settings.api_version}'][generator='#{Settings.generator}']", 1
+      assert_single_changeset changeset
+      assert_select "osm>changeset>discussion", 1
+      assert_select "osm>changeset>discussion>comment", 3
+      assert_select "osm>changeset>discussion>comment:nth-child(1)>@id", comment1.id.to_s
+      assert_select "osm>changeset>discussion>comment:nth-child(1)>@visible", "true"
+      assert_select "osm>changeset>discussion>comment:nth-child(2)>@id", comment2.id.to_s
+      assert_select "osm>changeset>discussion>comment:nth-child(2)>@visible", "false"
+      assert_select "osm>changeset>discussion>comment:nth-child(3)>@id", comment3.id.to_s
+      assert_select "osm>changeset>discussion>comment:nth-child(3)>@visible", "true"
     end
 
     def test_show_json
@@ -195,10 +244,7 @@ module Api
 
       assert_equal Settings.api_version, js["version"]
       assert_equal Settings.generator, js["generator"]
-      assert_equal changeset.id, js["changeset"]["id"]
-      assert js["changeset"]["open"]
-      assert_equal changeset.created_at.xmlschema, js["changeset"]["created_at"]
-      assert_nil js["changeset"]["closed_at"]
+      assert_single_changeset_json changeset, js
       assert_nil js["changeset"]["tags"]
       assert_nil js["changeset"]["comments"]
       assert_equal changeset.user.id, js["changeset"]["uid"]
@@ -211,10 +257,7 @@ module Api
       assert_not_nil js
       assert_equal Settings.api_version, js["version"]
       assert_equal Settings.generator, js["generator"]
-      assert_equal changeset.id, js["changeset"]["id"]
-      assert js["changeset"]["open"]
-      assert_equal changeset.created_at.xmlschema, js["changeset"]["created_at"]
-      assert_nil js["changeset"]["closed_at"]
+      assert_single_changeset_json changeset, js
       assert_nil js["changeset"]["tags"]
       assert_nil js["changeset"]["min_lat"]
       assert_nil js["changeset"]["min_lon"]
@@ -223,6 +266,81 @@ module Api
       assert_equal 0, js["changeset"]["comments"].count
     end
 
+    def test_show_comments_json
+      # all comments visible
+      changeset = create(:changeset, :closed)
+      comment0, comment1, comment2 = create_list(:changeset_comment, 3, :changeset_id => changeset.id)
+
+      get changeset_show_path(changeset), :params => { :format => "json", :include_discussion => true }
+      assert_response :success, "cannot get closed changeset with comments"
+
+      js = ActiveSupport::JSON.decode(@response.body)
+      assert_not_nil js
+      assert_equal Settings.api_version, js["version"]
+      assert_equal Settings.generator, js["generator"]
+      assert_single_changeset_json changeset, js
+      assert_equal 3, js["changeset"]["comments"].count
+      assert_equal comment0.id, js["changeset"]["comments"][0]["id"]
+      assert js["changeset"]["comments"][0]["visible"]
+      assert_equal comment1.id, js["changeset"]["comments"][1]["id"]
+      assert js["changeset"]["comments"][1]["visible"]
+      assert_equal comment2.id, js["changeset"]["comments"][2]["id"]
+      assert js["changeset"]["comments"][2]["visible"]
+
+      # one hidden comment not included because not asked for
+      comment1.update(:visible => false)
+
+      get changeset_show_path(changeset), :params => { :format => "json", :include_discussion => true }
+      assert_response :success, "cannot get closed changeset with comments"
+
+      js = ActiveSupport::JSON.decode(@response.body)
+      assert_not_nil js
+      assert_equal Settings.api_version, js["version"]
+      assert_equal Settings.generator, js["generator"]
+      assert_single_changeset_json changeset, js
+      assert_equal 2, js["changeset"]["comments"].count
+      assert_equal comment0.id, js["changeset"]["comments"][0]["id"]
+      assert js["changeset"]["comments"][0]["visible"]
+      assert_equal comment2.id, js["changeset"]["comments"][1]["id"]
+      assert js["changeset"]["comments"][1]["visible"]
+
+      # one hidden comment not included because no permissions
+      get changeset_show_path(changeset), :params => { :format => "json", :include_discussion => true, :show_hidden_comments => true }
+      assert_response :success, "cannot get closed changeset with comments"
+
+      js = ActiveSupport::JSON.decode(@response.body)
+      assert_not_nil js
+      assert_equal Settings.api_version, js["version"]
+      assert_equal Settings.generator, js["generator"]
+      assert_single_changeset_json changeset, js
+      assert_equal 2, js["changeset"]["comments"].count
+      assert_equal comment0.id, js["changeset"]["comments"][0]["id"]
+      assert js["changeset"]["comments"][0]["visible"]
+      # maybe will show an empty comment element with visible=false in the future
+      assert_equal comment2.id, js["changeset"]["comments"][1]["id"]
+      assert js["changeset"]["comments"][1]["visible"]
+
+      # one hidden comment shown to moderators
+      moderator_user = create(:moderator_user)
+      auth_header = basic_authorization_header moderator_user.email, "test"
+      get changeset_show_path(changeset), :params => { :format => "json", :include_discussion => true, :show_hidden_comments => true },
+                                          :headers => auth_header
+      assert_response :success, "cannot get closed changeset with comments"
+
+      js = ActiveSupport::JSON.decode(@response.body)
+      assert_not_nil js
+      assert_equal Settings.api_version, js["version"]
+      assert_equal Settings.generator, js["generator"]
+      assert_single_changeset_json changeset, js
+      assert_equal 3, js["changeset"]["comments"].count
+      assert_equal comment0.id, js["changeset"]["comments"][0]["id"]
+      assert js["changeset"]["comments"][0]["visible"]
+      assert_equal comment1.id, js["changeset"]["comments"][1]["id"]
+      assert_not js["changeset"]["comments"][1]["visible"]
+      assert_equal comment2.id, js["changeset"]["comments"][2]["id"]
+      assert js["changeset"]["comments"][2]["visible"]
+    end
+
     def test_show_tag_and_discussion_json
       changeset = create(:changeset, :closed)
 
@@ -248,10 +366,7 @@ module Api
       assert_not_nil js
       assert_equal Settings.api_version, js["version"]
       assert_equal Settings.generator, js["generator"]
-      assert_equal changeset.id, js["changeset"]["id"]
-      assert_not js["changeset"]["open"]
-      assert_equal changeset.created_at.xmlschema, js["changeset"]["created_at"]
-      assert_equal changeset.closed_at.xmlschema, js["changeset"]["closed_at"]
+      assert_single_changeset_json changeset, js
       assert_equal 2, js["changeset"]["tags"].count
       assert_equal 3, js["changeset"]["comments"].count
       assert_equal 3, js["changeset"]["comments_count"]
@@ -339,13 +454,13 @@ module Api
 
       auth_header = basic_authorization_header user.email, "test"
 
-      assert_raise ActionController::RoutingError do
-        get changeset_close_path(changeset), :headers => auth_header
-      end
+      get changeset_close_path(changeset), :headers => auth_header
+      assert_response :not_found
+      assert_template "rescues/routing_error"
 
-      assert_raise ActionController::RoutingError do
-        post changeset_close_path(changeset), :headers => auth_header
-      end
+      post changeset_close_path(changeset), :headers => auth_header
+      assert_response :not_found
+      assert_template "rescues/routing_error"
     end
 
     ##
@@ -634,7 +749,11 @@ module Api
     end
 
     def test_upload_large_changeset
-      auth_header = basic_authorization_header create(:user).email, "test"
+      user = create(:user)
+      auth_header = basic_authorization_header user.email, "test"
+
+      # create an old changeset to ensure we have the maximum rate limit
+      create(:changeset, :user => user, :created_at => Time.now.utc - 28.days)
 
       # create a changeset
       put changeset_create_path, :params => "<osm><changeset/></osm>", :headers => auth_header
@@ -675,10 +794,10 @@ module Api
 
       # 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."
+      assert_operator cs.min_lon, :>=, -180 * GeoRecord::SCALE, "Minimum longitude (#{cs.min_lon / GeoRecord::SCALE}) should be >= -180 to be valid."
+      assert_operator cs.max_lon, :<=, 180 * GeoRecord::SCALE, "Maximum longitude (#{cs.max_lon / GeoRecord::SCALE}) should be <= 180 to be valid."
+      assert_operator cs.min_lat, :>=, -90 * GeoRecord::SCALE, "Minimum latitude (#{cs.min_lat / GeoRecord::SCALE}) should be >= -90 to be valid."
+      assert_operator cs.max_lat, :<=, 90 * GeoRecord::SCALE, "Maximum latitude (#{cs.max_lat / GeoRecord::SCALE}) should be <= 90 to be valid."
     end
 
     ##
@@ -933,14 +1052,14 @@ module Api
       diff = <<~CHANGESET
         <osmChange>
          <modify>
-          <node id='#{node.id}' lon='0' lat='0' changeset='#{changeset.id}' version='1'/>
-          <node id='#{node.id}' lon='1' lat='0' changeset='#{changeset.id}' version='2'/>
-          <node id='#{node.id}' lon='1' lat='1' changeset='#{changeset.id}' version='3'/>
-          <node id='#{node.id}' lon='1' lat='2' changeset='#{changeset.id}' version='4'/>
-          <node id='#{node.id}' lon='2' lat='2' changeset='#{changeset.id}' version='5'/>
-          <node id='#{node.id}' lon='3' lat='2' changeset='#{changeset.id}' version='6'/>
-          <node id='#{node.id}' lon='3' lat='3' changeset='#{changeset.id}' version='7'/>
-          <node id='#{node.id}' lon='9' lat='9' changeset='#{changeset.id}' version='8'/>
+          <node id='#{node.id}' lon='0.0' lat='0.0' changeset='#{changeset.id}' version='1'/>
+          <node id='#{node.id}' lon='0.1' lat='0.0' changeset='#{changeset.id}' version='2'/>
+          <node id='#{node.id}' lon='0.1' lat='0.1' changeset='#{changeset.id}' version='3'/>
+          <node id='#{node.id}' lon='0.1' lat='0.2' changeset='#{changeset.id}' version='4'/>
+          <node id='#{node.id}' lon='0.2' lat='0.2' changeset='#{changeset.id}' version='5'/>
+          <node id='#{node.id}' lon='0.3' lat='0.2' changeset='#{changeset.id}' version='6'/>
+          <node id='#{node.id}' lon='0.3' lat='0.3' changeset='#{changeset.id}' version='7'/>
+          <node id='#{node.id}' lon='0.9' lat='0.9' changeset='#{changeset.id}' version='8'/>
          </modify>
         </osmChange>
       CHANGESET
@@ -1214,9 +1333,9 @@ module Api
       diff = <<~CHANGESET
         <osmChange>
          <create>
-          <node id="-1" lon="0" lat="0" changeset="#{changeset.id}" version="1"/>
-          <node id="-2" lon="1" lat="1" changeset="#{changeset.id}" version="1"/>
-          <node id="-3" lon="2" lat="2" changeset="#{changeset.id}" version="1"/>
+          <node id="-1" lon="0.0" lat="0.0" changeset="#{changeset.id}" version="1"/>
+          <node id="-2" lon="0.1" lat="0.1" changeset="#{changeset.id}" version="1"/>
+          <node id="-3" lon="0.2" lat="0.2" changeset="#{changeset.id}" version="1"/>
           <way id="-1" changeset="#{changeset.id}" version="1">
            <nd ref="-1"/>
            <nd ref="-2"/>
@@ -1237,9 +1356,9 @@ module Api
       diff = <<~CHANGESET
         <osmChange>
          <create>
-          <node id="-1" lon="0" lat="0" changeset="#{changeset.id}" version="1"/>
-          <node id="-2" lon="1" lat="1" changeset="#{changeset.id}" version="1"/>
-          <node id="-3" lon="2" lat="2" changeset="#{changeset.id}" version="1"/>
+          <node id="-1" lon="0.0" lat="0.0" changeset="#{changeset.id}" version="1"/>
+          <node id="-2" lon="0.1" lat="0.1" changeset="#{changeset.id}" version="1"/>
+          <node id="-3" lon="0.2" lat="0.2" changeset="#{changeset.id}" version="1"/>
           <way id="#{way.id}" changeset="#{changeset.id}" version="1">
            <nd ref="-1"/>
            <nd ref="-2"/>
@@ -1269,9 +1388,9 @@ module Api
       diff = <<~CHANGESET
         <osmChange>
          <create>
-          <node id="-1" lon="0" lat="0" changeset="#{changeset.id}" version="1"/>
-          <node id="-2" lon="1" lat="1" changeset="#{changeset.id}" version="1"/>
-          <node id="-3" lon="2" lat="2" changeset="#{changeset.id}" version="1"/>
+          <node id="-1" lon="0.0" lat="0.0" changeset="#{changeset.id}" version="1"/>
+          <node id="-2" lon="0.1" lat="0.1" changeset="#{changeset.id}" version="1"/>
+          <node id="-3" lon="0.2" lat="0.2" changeset="#{changeset.id}" version="1"/>
           <relation id="-1" changeset="#{changeset.id}" version="1">
            <member type="node" role="foo" ref="-1"/>
            <member type="node" role="foo" ref="-2"/>
@@ -1292,9 +1411,9 @@ module Api
       diff = <<~CHANGESET
         <osmChange>
          <create>
-          <node id="-1" lon="0" lat="0" changeset="#{changeset.id}" version="1"/>
-          <node id="-2" lon="1" lat="1" changeset="#{changeset.id}" version="1"/>
-          <node id="-3" lon="2" lat="2" changeset="#{changeset.id}" version="1"/>
+          <node id="-1" lon="0.0" lat="0.0" changeset="#{changeset.id}" version="1"/>
+          <node id="-2" lon="0.1" lat="0.1" changeset="#{changeset.id}" version="1"/>
+          <node id="-3" lon="0.2" lat="0.2" changeset="#{changeset.id}" version="1"/>
           <relation id="#{relation.id}" changeset="#{changeset.id}" version="1">
            <member type="node" role="foo" ref="-1"/>
            <member type="node" role="foo" ref="-2"/>
@@ -1363,14 +1482,14 @@ module Api
       changeset_id = @response.body.to_i
 
       old_way = create(:way)
-      create(:way_node, :way => old_way, :node => create(:node, :lat => 1, :lon => 1))
+      create(:way_node, :way => old_way, :node => create(:node, :lat => 0.1, :lon => 0.1))
 
       diff = XML::Document.new
       diff.root = XML::Node.new "osmChange"
       modify = XML::Node.new "modify"
       xml_old_way = xml_node_for_way(old_way)
       nd_ref = XML::Node.new "nd"
-      nd_ref["ref"] = create(:node, :lat => 3, :lon => 3).id.to_s
+      nd_ref["ref"] = create(:node, :lat => 0.3, :lon => 0.3).id.to_s
       xml_old_way << nd_ref
       xml_old_way["changeset"] = changeset_id.to_s
       modify << xml_old_way
@@ -1383,10 +1502,10 @@ module Api
 
       # 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"
+      assert_equal 0.1 * GeoRecord::SCALE, changeset.min_lon, "min_lon should be 0.1 degree"
+      assert_equal 0.3 * GeoRecord::SCALE, changeset.max_lon, "max_lon should be 0.3 degrees"
+      assert_equal 0.1 * GeoRecord::SCALE, changeset.min_lat, "min_lat should be 0.1 degree"
+      assert_equal 0.3 * GeoRecord::SCALE, changeset.max_lat, "max_lat should be 0.3 degrees"
     end
 
     ##
@@ -1582,6 +1701,172 @@ module Api
       assert_equal "Precondition failed: Node #{node.id} is still used by ways #{way.id}.", @response.body
     end
 
+    ##
+    # test initial rate limit
+    def test_upload_initial_rate_limit
+      # create a user
+      user = create(:user)
+
+      # create some objects to use
+      node = create(:node)
+      way = create(:way_with_nodes, :nodes_count => 2)
+      relation = create(:relation)
+
+      # create a changeset that puts us near the initial rate limit
+      changeset = create(:changeset, :user => user,
+                                     :created_at => Time.now.utc - 5.minutes,
+                                     :num_changes => Settings.initial_changes_per_hour - 2)
+
+      # create authentication header
+      auth_header = basic_authorization_header user.email, "test"
+
+      # simple diff to create a node way and relation using placeholders
+      diff = <<~CHANGESET
+        <osmChange>
+         <create>
+          <node id='-1' lon='0' lat='0' changeset='#{changeset.id}'>
+           <tag k='foo' v='bar'/>
+           <tag k='baz' v='bat'/>
+          </node>
+          <way id='-1' changeset='#{changeset.id}'>
+           <nd ref='#{node.id}'/>
+          </way>
+         </create>
+         <create>
+          <relation id='-1' changeset='#{changeset.id}'>
+           <member type='way' role='some' ref='#{way.id}'/>
+           <member type='node' role='some' ref='#{node.id}'/>
+           <member type='relation' role='some' ref='#{relation.id}'/>
+          </relation>
+         </create>
+        </osmChange>
+      CHANGESET
+
+      # upload it
+      post changeset_upload_path(changeset), :params => diff, :headers => auth_header
+      assert_response :too_many_requests, "upload did not hit rate limit"
+    end
+
+    ##
+    # test maximum rate limit
+    def test_upload_maximum_rate_limit
+      # create a user
+      user = create(:user)
+
+      # create some objects to use
+      node = create(:node)
+      way = create(:way_with_nodes, :nodes_count => 2)
+      relation = create(:relation)
+
+      # create a changeset to establish our initial edit time
+      changeset = create(:changeset, :user => user,
+                                     :created_at => Time.now.utc - 28.days)
+
+      # create changeset to put us near the maximum rate limit
+      total_changes = Settings.max_changes_per_hour - 2
+      while total_changes.positive?
+        changes = [total_changes, Changeset::MAX_ELEMENTS].min
+        changeset = create(:changeset, :user => user,
+                                       :created_at => Time.now.utc - 5.minutes,
+                                       :num_changes => changes)
+        total_changes -= changes
+      end
+
+      # create authentication header
+      auth_header = basic_authorization_header user.email, "test"
+
+      # simple diff to create a node way and relation using placeholders
+      diff = <<~CHANGESET
+        <osmChange>
+         <create>
+          <node id='-1' lon='0' lat='0' changeset='#{changeset.id}'>
+           <tag k='foo' v='bar'/>
+           <tag k='baz' v='bat'/>
+          </node>
+          <way id='-1' changeset='#{changeset.id}'>
+           <nd ref='#{node.id}'/>
+          </way>
+         </create>
+         <create>
+          <relation id='-1' changeset='#{changeset.id}'>
+           <member type='way' role='some' ref='#{way.id}'/>
+           <member type='node' role='some' ref='#{node.id}'/>
+           <member type='relation' role='some' ref='#{relation.id}'/>
+          </relation>
+         </create>
+        </osmChange>
+      CHANGESET
+
+      # upload it
+      post changeset_upload_path(changeset), :params => diff, :headers => auth_header
+      assert_response :too_many_requests, "upload did not hit rate limit"
+    end
+
+    ##
+    # test initial size limit
+    def test_upload_initial_size_limit
+      # create a user
+      user = create(:user)
+
+      # create a changeset that puts us near the initial size limit
+      changeset = create(:changeset, :user => user,
+                                     :min_lat => (-0.5 * GeoRecord::SCALE).round, :min_lon => (0.5 * GeoRecord::SCALE).round,
+                                     :max_lat => (0.5 * GeoRecord::SCALE).round, :max_lon => (2.5 * GeoRecord::SCALE).round)
+
+      # create authentication header
+      auth_header = basic_authorization_header user.email, "test"
+
+      # simple diff to create a node
+      diff = <<~CHANGESET
+        <osmChange>
+         <create>
+          <node id='-1' lon='0.9' lat='2.9' changeset='#{changeset.id}'>
+           <tag k='foo' v='bar'/>
+           <tag k='baz' v='bat'/>
+          </node>
+         </create>
+        </osmChange>
+      CHANGESET
+
+      # upload it
+      post changeset_upload_path(changeset), :params => diff, :headers => auth_header
+      assert_response :payload_too_large, "upload did not hit size limit"
+    end
+
+    ##
+    # test size limit after one week
+    def test_upload_week_size_limit
+      # create a user
+      user = create(:user)
+
+      # create a changeset to establish our initial edit time
+      create(:changeset, :user => user, :created_at => Time.now.utc - 7.days)
+
+      # create a changeset that puts us near the initial size limit
+      changeset = create(:changeset, :user => user,
+                                     :min_lat => (-0.5 * GeoRecord::SCALE).round, :min_lon => (0.5 * GeoRecord::SCALE).round,
+                                     :max_lat => (0.5 * GeoRecord::SCALE).round, :max_lon => (2.5 * GeoRecord::SCALE).round)
+
+      # create authentication header
+      auth_header = basic_authorization_header user.email, "test"
+
+      # simple diff to create a node way and relation using placeholders
+      diff = <<~CHANGESET
+        <osmChange>
+         <create>
+          <node id='-1' lon='35' lat='35' changeset='#{changeset.id}'>
+           <tag k='foo' v='bar'/>
+           <tag k='baz' v='bat'/>
+          </node>
+         </create>
+        </osmChange>
+      CHANGESET
+
+      # upload it
+      post changeset_upload_path(changeset), :params => diff, :headers => auth_header
+      assert_response :payload_too_large, "upload did not hit size limit"
+    end
+
     ##
     # when we make some simple changes we get the same changes back from the
     # diff download.
@@ -1613,14 +1898,14 @@ module Api
       diff = <<~CHANGESET
         <osmChange>
          <modify>
-          <node id='#{node.id}' lon='0' lat='0' changeset='#{changeset_id}' version='1'/>
-          <node id='#{node.id}' lon='1' lat='0' changeset='#{changeset_id}' version='2'/>
-          <node id='#{node.id}' lon='1' lat='1' changeset='#{changeset_id}' version='3'/>
-          <node id='#{node.id}' lon='1' lat='2' changeset='#{changeset_id}' version='4'/>
-          <node id='#{node.id}' lon='2' lat='2' changeset='#{changeset_id}' version='5'/>
-          <node id='#{node.id}' lon='3' lat='2' changeset='#{changeset_id}' version='6'/>
-          <node id='#{node.id}' lon='3' lat='3' changeset='#{changeset_id}' version='7'/>
-          <node id='#{node.id}' lon='9' lat='9' changeset='#{changeset_id}' version='8'/>
+          <node id='#{node.id}' lon='0.0' lat='0.0' changeset='#{changeset_id}' version='1'/>
+          <node id='#{node.id}' lon='0.1' lat='0.0' changeset='#{changeset_id}' version='2'/>
+          <node id='#{node.id}' lon='0.1' lat='0.1' changeset='#{changeset_id}' version='3'/>
+          <node id='#{node.id}' lon='0.1' lat='0.2' changeset='#{changeset_id}' version='4'/>
+          <node id='#{node.id}' lon='0.2' lat='0.2' changeset='#{changeset_id}' version='5'/>
+          <node id='#{node.id}' lon='0.3' lat='0.2' changeset='#{changeset_id}' version='6'/>
+          <node id='#{node.id}' lon='0.3' lat='0.3' changeset='#{changeset_id}' version='7'/>
+          <node id='#{node.id}' lon='0.9' lat='0.9' changeset='#{changeset_id}' version='8'/>
          </modify>
         </osmChange>
       CHANGESET
@@ -1719,15 +2004,15 @@ module Api
       diff = <<~CHANGESET
         <osmChange>
          <delete>
-          <node id='#{node.id}' lon='0' lat='0' changeset='#{changeset_id}' version='1'/>
+          <node id='#{node.id}' lon='0.0' lat='0.0' changeset='#{changeset_id}' version='1'/>
          </delete>
          <create>
-          <node id='-1' lon='9' lat='9' changeset='#{changeset_id}' version='0'/>
-          <node id='-2' lon='8' lat='9' changeset='#{changeset_id}' version='0'/>
-          <node id='-3' lon='7' lat='9' changeset='#{changeset_id}' version='0'/>
+          <node id='-1' lon='0.9' lat='0.9' changeset='#{changeset_id}' version='0'/>
+          <node id='-2' lon='0.8' lat='0.9' changeset='#{changeset_id}' version='0'/>
+          <node id='-3' lon='0.7' lat='0.9' changeset='#{changeset_id}' version='0'/>
          </create>
          <modify>
-          <node id='#{node2.id}' lon='20' lat='15' changeset='#{changeset_id}' version='1'/>
+          <node id='#{node2.id}' lon='2.0' lat='1.5' changeset='#{changeset_id}' version='1'/>
           <way id='#{way.id}' changeset='#{changeset_id}' version='1'>
            <nd ref='#{node2.id}'/>
            <nd ref='-1'/>
@@ -1779,12 +2064,46 @@ module Api
       end
     end
 
+    test "sorts downloaded elements by timestamp" do
+      changeset = create(:changeset)
+      node1 = create(:old_node, :version => 2, :timestamp => "2020-02-01", :changeset => changeset)
+      node0 = create(:old_node, :version => 2, :timestamp => "2020-01-01", :changeset => changeset)
+
+      get changeset_download_path(changeset)
+      assert_response :success
+      assert_dom "modify", :count => 2 do |modify|
+        assert_dom modify[0], ">node", :count => 1 do |node|
+          assert_dom node, ">@id", node0.node_id.to_s
+        end
+        assert_dom modify[1], ">node", :count => 1 do |node|
+          assert_dom node, ">@id", node1.node_id.to_s
+        end
+      end
+    end
+
+    test "sorts downloaded elements by version" do
+      changeset = create(:changeset)
+      node1 = create(:old_node, :version => 3, :timestamp => "2020-01-01", :changeset => changeset)
+      node0 = create(:old_node, :version => 2, :timestamp => "2020-01-01", :changeset => changeset)
+
+      get changeset_download_path(changeset)
+      assert_response :success
+      assert_dom "modify", :count => 2 do |modify|
+        assert_dom modify[0], ">node", :count => 1 do |node|
+          assert_dom node, ">@id", node0.node_id.to_s
+        end
+        assert_dom modify[1], ">node", :count => 1 do |node|
+          assert_dom node, ">@id", node1.node_id.to_s
+        end
+      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))
+      create(:way_node, :way => way, :node => create(:node, :lat => 0.3, :lon => 0.3))
 
       auth_header = basic_authorization_header create(:user).email, "test"
 
@@ -1796,7 +2115,7 @@ module Api
 
       # add a single node to it
       with_controller(NodesController.new) do
-        xml = "<osm><node lon='1' lat='2' changeset='#{changeset_id}'/></osm>"
+        xml = "<osm><node lon='0.1' lat='0.2' changeset='#{changeset_id}'/></osm>"
         put node_create_path, :params => xml, :headers => auth_header
         assert_response :success, "Couldn't create node."
       end
@@ -1804,14 +2123,14 @@ module Api
       # get the bounding box back from the changeset
       get changeset_show_path(: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
+      assert_select "osm>changeset[min_lon='0.1000000']", 1
+      assert_select "osm>changeset[max_lon='0.1000000']", 1
+      assert_select "osm>changeset[min_lat='0.2000000']", 1
+      assert_select "osm>changeset[max_lat='0.2000000']", 1
 
       # add another node to it
       with_controller(NodesController.new) do
-        xml = "<osm><node lon='2' lat='1' changeset='#{changeset_id}'/></osm>"
+        xml = "<osm><node lon='0.2' lat='0.1' changeset='#{changeset_id}'/></osm>"
         put node_create_path, :params => xml, :headers => auth_header
         assert_response :success, "Couldn't create second node."
       end
@@ -1819,10 +2138,10 @@ module Api
       # get the bounding box back from the changeset
       get changeset_show_path(: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
+      assert_select "osm>changeset[min_lon='0.1000000']", 1
+      assert_select "osm>changeset[max_lon='0.2000000']", 1
+      assert_select "osm>changeset[min_lat='0.1000000']", 1
+      assert_select "osm>changeset[max_lat='0.2000000']", 1
 
       # add (delete) a way to it, which contains a point at (3,3)
       with_controller(WaysController.new) do
@@ -1834,10 +2153,10 @@ module Api
       # get the bounding box back from the changeset
       get changeset_show_path(: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
+      assert_select "osm>changeset[min_lon='0.1000000']", 1
+      assert_select "osm>changeset[max_lon='0.3000000']", 1
+      assert_select "osm>changeset[min_lat='0.1000000']", 1
+      assert_select "osm>changeset[max_lat='0.3000000']", 1
     end
 
     ##
@@ -1863,8 +2182,10 @@ module Api
       # not found when looking for changesets of non-existing users
       get changesets_path(:user => User.maximum(:id) + 1)
       assert_response :not_found
+      assert_equal "text/plain", @response.media_type
       get changesets_path(:display_name => " ")
       assert_response :not_found
+      assert_equal "text/plain", @response.media_type
 
       # can't get changesets of user 1 without authenticating
       get changesets_path(:user => private_user.id)
@@ -1950,23 +2271,105 @@ module Api
 
       get changesets_path
       assert_response :success
-      assert_changesets [changeset5, changeset4, changeset3, changeset2, changeset1]
+      assert_changesets_in_order [changeset5, changeset4, changeset3, changeset2, changeset1]
 
       get changesets_path(:limit => "3")
       assert_response :success
-      assert_changesets [changeset5, changeset4, changeset3]
+      assert_changesets_in_order [changeset5, changeset4, changeset3]
 
       get changesets_path(:limit => "0")
       assert_response :bad_request
 
       get changesets_path(:limit => Settings.max_changeset_query_limit)
       assert_response :success
-      assert_changesets [changeset5, changeset4, changeset3, changeset2, changeset1]
+      assert_changesets_in_order [changeset5, changeset4, changeset3, changeset2, changeset1]
 
       get changesets_path(:limit => Settings.max_changeset_query_limit + 1)
       assert_response :bad_request
     end
 
+    ##
+    # test the query functionality of sequential changesets with order and time parameters
+    def test_query_order
+      user = create(:user)
+      changeset1 = 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, :closed, :user => user, :created_at => Time.utc(2008, 2, 1, 0, 0, 0), :closed_at => Time.utc(2008, 2, 2, 0, 0, 0))
+      changeset3 = create(:changeset, :closed, :user => user, :created_at => Time.utc(2008, 3, 1, 0, 0, 0), :closed_at => Time.utc(2008, 3, 2, 0, 0, 0))
+      changeset4 = create(:changeset, :closed, :user => user, :created_at => Time.utc(2008, 4, 1, 0, 0, 0), :closed_at => Time.utc(2008, 4, 2, 0, 0, 0))
+      changeset5 = create(:changeset, :closed, :user => user, :created_at => Time.utc(2008, 5, 1, 0, 0, 0), :closed_at => Time.utc(2008, 5, 2, 0, 0, 0))
+      changeset6 = create(:changeset, :closed, :user => user, :created_at => Time.utc(2008, 6, 1, 0, 0, 0), :closed_at => Time.utc(2008, 6, 2, 0, 0, 0))
+
+      get changesets_path
+      assert_response :success
+      assert_changesets_in_order [changeset6, changeset5, changeset4, changeset3, changeset2, changeset1]
+
+      get changesets_path(:order => "oldest")
+      assert_response :success
+      assert_changesets_in_order [changeset1, changeset2, changeset3, changeset4, changeset5, changeset6]
+
+      [
+        # lower time bound at the opening time of a changeset
+        ["2008-02-01T00:00:00Z", "2008-05-15T00:00:00Z", [changeset5, changeset4, changeset3, changeset2], [changeset5, changeset4, changeset3, changeset2]],
+        # lower time bound in the middle of a changeset
+        ["2008-02-01T12:00:00Z", "2008-05-15T00:00:00Z", [changeset5, changeset4, changeset3, changeset2], [changeset5, changeset4, changeset3]],
+        # lower time bound at the closing time of a changeset
+        ["2008-02-02T00:00:00Z", "2008-05-15T00:00:00Z", [changeset5, changeset4, changeset3, changeset2], [changeset5, changeset4, changeset3]],
+        # lower time bound after the closing time of a changeset
+        ["2008-02-02T00:00:01Z", "2008-05-15T00:00:00Z", [changeset5, changeset4, changeset3], [changeset5, changeset4, changeset3]],
+        # upper time bound in the middle of a changeset
+        ["2007-09-09T12:00:00Z", "2008-04-01T12:00:00Z", [changeset4, changeset3, changeset2, changeset1], [changeset4, changeset3, changeset2, changeset1]],
+        # empty range
+        ["2009-02-02T00:00:01Z", "2018-05-15T00:00:00Z", [], []]
+      ].each do |from, to, interval_changesets, point_changesets|
+        get changesets_path(:time => "#{from},#{to}")
+        assert_response :success
+        assert_changesets_in_order interval_changesets
+
+        get changesets_path(:from => from, :to => to)
+        assert_response :success
+        assert_changesets_in_order point_changesets
+
+        get changesets_path(:from => from, :to => to, :order => "oldest")
+        assert_response :success
+        assert_changesets_in_order point_changesets.reverse
+      end
+    end
+
+    ##
+    # test the query functionality of overlapping changesets with order and time parameters
+    def test_query_order_overlapping
+      user = create(:user)
+      changeset1 = create(:changeset, :closed, :user => user, :created_at => Time.utc(2015, 6, 4, 17, 0, 0), :closed_at => Time.utc(2015, 6, 4, 17, 0, 0))
+      changeset2 = create(:changeset, :closed, :user => user, :created_at => Time.utc(2015, 6, 4, 16, 0, 0), :closed_at => Time.utc(2015, 6, 4, 18, 0, 0))
+      changeset3 = create(:changeset, :closed, :user => user, :created_at => Time.utc(2015, 6, 4, 14, 0, 0), :closed_at => Time.utc(2015, 6, 4, 20, 0, 0))
+      changeset4 = create(:changeset, :closed, :user => user, :created_at => Time.utc(2015, 6, 3, 23, 0, 0), :closed_at => Time.utc(2015, 6, 4, 23, 0, 0))
+      create(:changeset, :closed, :user => user, :created_at => Time.utc(2015, 6, 2, 23, 0, 0), :closed_at => Time.utc(2015, 6, 3, 23, 0, 0))
+
+      get changesets_path(:time => "2015-06-04T00:00:00Z")
+      assert_response :success
+      assert_changesets_in_order [changeset1, changeset2, changeset3, changeset4]
+
+      get changesets_path(:from => "2015-06-04T00:00:00Z")
+      assert_response :success
+      assert_changesets_in_order [changeset1, changeset2, changeset3]
+
+      get changesets_path(:from => "2015-06-04T00:00:00Z", :order => "oldest")
+      assert_response :success
+      assert_changesets_in_order [changeset3, changeset2, changeset1]
+
+      get changesets_path(:time => "2015-06-04T16:00:00Z,2015-06-04T17:30:00Z")
+      assert_response :success
+      assert_changesets_in_order [changeset1, changeset2, changeset3, changeset4]
+
+      get changesets_path(:from => "2015-06-04T16:00:00Z", :to => "2015-06-04T17:30:00Z")
+      assert_response :success
+      assert_changesets_in_order [changeset1, changeset2]
+
+      get changesets_path(:from => "2015-06-04T16:00:00Z", :to => "2015-06-04T17:30:00Z", :order => "oldest")
+      assert_response :success
+      assert_changesets_in_order [changeset2, changeset1]
+    end
+
     ##
     # check that errors are returned if garbage is inserted
     # into query strings
@@ -1994,6 +2397,9 @@ module Api
         get changesets_path(:user => uid)
         assert_response :bad_request, "'#{uid}' isn't a valid user ID"
       end
+
+      get changesets_path(:order => "oldest", :time => "2008-01-01T00:00Z,2018-01-01T00:00Z")
+      assert_response :bad_request, "cannot use order=oldest with time"
     end
 
     ##
@@ -2072,7 +2478,11 @@ module Api
     # 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
-      auth_header = basic_authorization_header create(:user).email, "test"
+      user = create(:user)
+      auth_header = basic_authorization_header user.email, "test"
+
+      # create an old changeset to ensure we have the maximum rate limit
+      create(:changeset, :user => user, :created_at => Time.now.utc - 28.days)
 
       # open a new changeset
       xml = "<osm><changeset/></osm>"
@@ -2095,7 +2505,7 @@ module Api
         assert_response :success, "can't create a new node"
         node_id = @response.body.to_i
 
-        get api_node_path(:id => node_id)
+        get api_node_path(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
@@ -2106,7 +2516,7 @@ module Api
           node_xml["lon"] = rand.to_s
           node_xml["version"] = (i + 1).to_s
 
-          put api_node_path(:id => node_id), :params => node_doc.to_s, :headers => auth_header
+          put api_node_path(node_id), :params => node_doc.to_s, :headers => auth_header
           assert_response :success, "attempt #{i} should have succeeded"
         end
 
@@ -2115,7 +2525,7 @@ module Api
         node_xml["lon"] = rand.to_s
         node_xml["version"] = offset.to_s
 
-        put api_node_path(:id => node_id), :params => node_doc.to_s, :headers => auth_header
+        put api_node_path(node_id), :params => node_doc.to_s, :headers => auth_header
         assert_response :conflict, "final attempt should have failed"
       end
 
@@ -2154,14 +2564,14 @@ module Api
       changeset = create(:changeset, :closed)
 
       assert_difference "changeset.subscribers.count", 1 do
-        post changeset_subscribe_path(changeset), :headers => auth_header
+        post api_changeset_subscribe_path(changeset), :headers => auth_header
       end
       assert_response :success
 
       # not closed changeset
       changeset = create(:changeset)
       assert_difference "changeset.subscribers.count", 1 do
-        post changeset_subscribe_path(changeset), :headers => auth_header
+        post api_changeset_subscribe_path(changeset), :headers => auth_header
       end
       assert_response :success
     end
@@ -2174,7 +2584,7 @@ module Api
       # unauthorized
       changeset = create(:changeset, :closed)
       assert_no_difference "changeset.subscribers.count" do
-        post changeset_subscribe_path(changeset)
+        post api_changeset_subscribe_path(changeset)
       end
       assert_response :unauthorized
 
@@ -2182,7 +2592,7 @@ module Api
 
       # bad changeset id
       assert_no_difference "changeset.subscribers.count" do
-        post changeset_subscribe_path(:id => 999111), :headers => auth_header
+        post api_changeset_subscribe_path(:id => 999111), :headers => auth_header
       end
       assert_response :not_found
 
@@ -2190,7 +2600,7 @@ module Api
       changeset = create(:changeset, :closed)
       changeset.subscribers.push(user)
       assert_no_difference "changeset.subscribers.count" do
-        post changeset_subscribe_path(changeset), :headers => auth_header
+        post api_changeset_subscribe_path(changeset), :headers => auth_header
       end
       assert_response :conflict
     end
@@ -2204,7 +2614,7 @@ module Api
       changeset.subscribers.push(user)
 
       assert_difference "changeset.subscribers.count", -1 do
-        post changeset_unsubscribe_path(changeset), :headers => auth_header
+        post api_changeset_unsubscribe_path(changeset), :headers => auth_header
       end
       assert_response :success
 
@@ -2213,7 +2623,7 @@ module Api
       changeset.subscribers.push(user)
 
       assert_difference "changeset.subscribers.count", -1 do
-        post changeset_unsubscribe_path(changeset), :headers => auth_header
+        post api_changeset_unsubscribe_path(changeset), :headers => auth_header
       end
       assert_response :success
     end
@@ -2224,7 +2634,7 @@ module Api
       # unauthorized
       changeset = create(:changeset, :closed)
       assert_no_difference "changeset.subscribers.count" do
-        post changeset_unsubscribe_path(changeset)
+        post api_changeset_unsubscribe_path(changeset)
       end
       assert_response :unauthorized
 
@@ -2232,14 +2642,14 @@ module Api
 
       # bad changeset id
       assert_no_difference "changeset.subscribers.count" do
-        post changeset_unsubscribe_path(:id => 999111), :headers => auth_header
+        post api_changeset_unsubscribe_path(:id => 999111), :headers => auth_header
       end
       assert_response :not_found
 
       # trying to unsubscribe when not subscribed
       changeset = create(:changeset, :closed)
       assert_no_difference "changeset.subscribers.count" do
-        post changeset_unsubscribe_path(changeset), :headers => auth_header
+        post api_changeset_unsubscribe_path(changeset), :headers => auth_header
       end
       assert_response :not_found
     end
@@ -2247,8 +2657,34 @@ module Api
     private
 
     ##
-    # boilerplate for checking that certain changesets exist in the
-    # output.
+    # check that the output consists of one specific changeset
+    def assert_single_changeset(changeset)
+      assert_select "osm>changeset", 1
+      assert_select "osm>changeset>@id", changeset.id.to_s
+      assert_select "osm>changeset>@created_at", changeset.created_at.xmlschema
+      if changeset.open?
+        assert_select "osm>changeset>@open", "true"
+        assert_select "osm>changeset>@closed_at", 0
+      else
+        assert_select "osm>changeset>@open", "false"
+        assert_select "osm>changeset>@closed_at", changeset.closed_at.xmlschema
+      end
+    end
+
+    def assert_single_changeset_json(changeset, js)
+      assert_equal changeset.id, js["changeset"]["id"]
+      assert_equal changeset.created_at.xmlschema, js["changeset"]["created_at"]
+      if changeset.open?
+        assert js["changeset"]["open"]
+        assert_nil js["changeset"]["closed_at"]
+      else
+        assert_not js["changeset"]["open"]
+        assert_equal changeset.closed_at.xmlschema, js["changeset"]["closed_at"]
+      end
+    end
+
+    ##
+    # check that certain changesets exist in the output
     def assert_changesets(changesets)
       assert_select "osm>changeset", changesets.size
       changesets.each do |changeset|
@@ -2256,6 +2692,15 @@ module Api
       end
     end
 
+    ##
+    # check that certain changesets exist in the output in the specified order
+    def assert_changesets_in_order(changesets)
+      assert_select "osm>changeset", changesets.size
+      changesets.each_with_index do |changeset, index|
+        assert_select "osm>changeset:nth-child(#{index + 1})[id='#{changeset.id}']", 1
+      end
+    end
+
     ##
     # update the changeset_id of a way element
     def update_changeset(xml, changeset_id)