Merge 14059:14394 from trunk.
[rails.git] / test / functional / amf_controller_test.rb
1 require File.dirname(__FILE__) + '/../test_helper'
2 require 'stringio'
3 include Potlatch
4
5 class AmfControllerTest < ActionController::TestCase
6   api_fixtures
7
8   # this should be what AMF controller returns when the bbox of a request
9   # is invalid or too large.
10   BOUNDARY_ERROR = [-2,"Sorry - I can't get the map for that area."]
11
12   def test_getway
13     # check a visible way
14     id = current_ways(:visible_way).id
15     amf_content "getway", "/1", [id]
16     post :amf_read
17     assert_response :success
18     amf_parse_response                                         
19     assert_equal amf_result("/1")[0], id
20   end
21
22   def test_getway_invisible
23     # check an invisible way
24     id = current_ways(:invisible_way).id
25     amf_content "getway", "/1", [id]
26     post :amf_read
27     assert_response :success
28     amf_parse_response
29     way = amf_result("/1")
30     assert_equal way[0], id
31     assert way[1].empty? and way[2].empty?
32   end
33
34   def test_getway_nonexistent
35     # check chat a non-existent way is not returned
36     amf_content "getway", "/1", [0]
37     post :amf_read
38     assert_response :success
39     amf_parse_response
40     way = amf_result("/1")
41     assert_equal way[0], 0
42     assert way[1].empty? and way[2].empty?
43   end
44
45   def test_whichways
46     node = current_nodes(:used_node_1)
47     minlon = node.lon-0.1
48     minlat = node.lat-0.1
49     maxlon = node.lon+0.1
50     maxlat = node.lat+0.1
51     amf_content "whichways", "/1", [minlon, minlat, maxlon, maxlat]
52     post :amf_read
53     assert_response :success
54     amf_parse_response 
55
56     # check contents of message
57     map = amf_result "/1"
58     assert_equal 0, map[0], 'map error code should be 0'
59
60     # check the formatting of the message
61     assert_equal 4, map.length, 'map should have length 4'
62     assert_equal Array, map[1].class, 'map "ways" element should be an array'
63     assert_equal Array, map[2].class, 'map "nodes" element should be an array'
64     assert_equal Array, map[3].class, 'map "relations" element should be an array'
65     map[1].each do |w|
66       assert_equal 2, w.length, 'way should be (id, version) pair'
67       assert w[0] == w[0].floor, 'way ID should be an integer'
68       assert w[1] == w[1].floor, 'way version should be an integer'
69     end
70
71     map[2].each do |n|
72       assert_equal 5, w.length, 'node should be (id, lat, lon, [tags], version) tuple'
73       assert n[0] == n[0].floor, 'node ID should be an integer'
74       assert n[1] >= minlat - 0.01, 'node lat should be greater than min'
75       assert n[1] <= maxlat - 0.01, 'node lat should be less than max'
76       assert n[2] >= minlon - 0.01, 'node lon should be greater than min'
77       assert n[2] <= maxlon - 0.01, 'node lon should be less than max'
78       assert_equal Array, a[3].class, 'node tags should be array'
79       assert n[4] == n[4].floor, 'node version should be an integer'
80     end
81
82     map[3].each do |r|
83       assert_equal 2, r.length, 'relation should be (id, version) pair'
84       assert r[0] == r[0].floor, 'relation ID should be an integer'
85       assert r[1] == r[1].floor, 'relation version should be an integer'
86     end
87
88     # TODO: looks like amf_controller changed since this test was written
89     # so someone who knows what they're doing should check this!
90     ways = map[1].collect { |x| x[0] }
91     assert ways.include?(current_ways(:used_way).id),
92       "map should include used way"
93     assert !ways.include?(current_ways(:invisible_way).id),
94       'map should not include deleted way'
95   end
96
97   ##
98   # checks that too-large a bounding box will not be served.
99   def test_whichways_toobig
100     bbox = [-0.1,-0.1,1.1,1.1]
101     check_bboxes_are_bad [bbox] do |map,bbox|
102       assert_equal BOUNDARY_ERROR, map, "AMF controller should have returned an error."
103     end
104   end
105
106   ##
107   # checks that an invalid bounding box will not be served. in this case
108   # one with max < min latitudes.
109   #
110   # NOTE: the controller expands the bbox by 0.01 in each direction!
111   def test_whichways_badlat
112     bboxes = [[0,0.1,0.1,0], [-0.1,80,0.1,70], [0.24,54.35,0.25,54.33]]
113     check_bboxes_are_bad bboxes do |map, bbox|
114       assert_equal BOUNDARY_ERROR, map, "AMF controller should have returned an error #{bbox.inspect}."
115     end
116   end
117
118   ##
119   # same as test_whichways_badlat, but for longitudes
120   #
121   # NOTE: the controller expands the bbox by 0.01 in each direction!
122   def test_whichways_badlon
123     bboxes = [[80,-0.1,70,0.1], [54.35,0.24,54.33,0.25]]
124     check_bboxes_are_bad bboxes do |map, bbox|
125       assert_equal BOUNDARY_ERROR, map, "AMF controller should have returned an error #{bbox.inspect}."
126     end
127   end
128
129   def test_whichways_deleted
130     node = current_nodes(:used_node_1)
131     minlon = node.lon-0.1
132     minlat = node.lat-0.1
133     maxlon = node.lon+0.1
134     maxlat = node.lat+0.1
135     amf_content "whichways_deleted", "/1", [minlon, minlat, maxlon, maxlat]
136     post :amf_read
137     assert_response :success
138     amf_parse_response
139
140     # check contents of message
141     map = amf_result "/1"
142     assert_equal 0, map[0], 'first map element should be 0'
143     assert_equal Array, map[1].class, 'second map element should be an array'
144     # TODO: looks like amf_controller changed since this test was written
145     # so someone who knows what they're doing should check this!
146     assert !map[1].include?(current_ways(:used_way).id),
147       "map should not include used way"
148     assert map[1].include?(current_ways(:invisible_way).id),
149       'map should include deleted way'
150   end
151
152   def test_whichways_deleted_toobig
153     bbox = [-0.1,-0.1,1.1,1.1]
154     amf_content "whichways_deleted", "/1", bbox
155     post :amf_read
156     assert_response :success
157     amf_parse_response 
158
159     map = amf_result "/1"
160     assert_equal BOUNDARY_ERROR, map, "AMF controller should have returned an error."
161   end
162
163   def test_getrelation
164     id = current_relations(:visible_relation).id
165     amf_content "getrelation", "/1", [id]
166     post :amf_read
167     assert_response :success
168     amf_parse_response
169     assert_equal amf_result("/1")[0], id
170   end
171
172   def test_getrelation_invisible
173     id = current_relations(:invisible_relation).id
174     amf_content "getrelation", "/1", [id]
175     post :amf_read
176     assert_response :success
177     amf_parse_response
178     rel = amf_result("/1")
179     assert_equal rel[0], id
180     assert rel[1].empty? and rel[2].empty?
181   end
182
183   def test_getrelation_nonexistent
184     id = 0
185     amf_content "getrelation", "/1", [id]
186     post :amf_read
187     assert_response :success
188     amf_parse_response
189     rel = amf_result("/1")
190     assert_equal rel[0], id
191     assert rel[1].empty? and rel[2].empty?
192   end
193
194   def test_getway_old
195     # try to get the last visible version (specified by <0) (should be current version)
196     latest = current_ways(:way_with_versions)
197     # NOTE: looks from the API changes that this now expects a timestamp
198     # instead of a version number...
199     # try to get version 1
200     v1 = ways(:way_with_versions_v1)
201     { latest => '', 
202       v1 => v1.timestamp.strftime("%d %b %Y, %H:%M:%S")
203     }.each do |way, t|
204       amf_content "getway_old", "/1", [way.id, t]
205       post :amf_read      
206       assert_response :success
207       amf_parse_response
208       returned_way = amf_result("/1")
209       assert_equal way.id, returned_way[1]
210       # API returns the *latest* version, even for old ways...
211       assert_equal latest.version, returned_way[4]
212     end
213   end
214   
215   ##
216   # test that the server doesn't fall over when rubbish is passed
217   # into the method args.
218   def test_getway_old_invalid
219     way_id = current_ways(:way_with_versions).id
220     { "foo"  => "bar",
221       way_id => "not a date",
222       way_id => "2009-03-25 00:00:00", # <- wrong format
223       way_id => "0 Jan 2009 00:00:00", # <- invalid date
224       -1     => "1 Jan 2009 00:00:00"  # <- invalid ID
225     }.each do |id, t|
226       amf_content "getway_old", "/1", [id, t]
227       post :amf_read
228       assert_response :success
229       amf_parse_response
230       returned_way = amf_result("/1")
231       assert returned_way[2].empty?
232       assert returned_way[3].empty?
233       assert returned_way[4] < 0
234     end
235   end
236
237   def test_getway_old_nonexistent
238     # try to get the last version+10 (shoudn't exist)
239     v1 = ways(:way_with_versions_v1)
240     # try to get last visible version of non-existent way
241     # try to get specific version of non-existent way
242     [[nil, ''], 
243      [nil, '1 Jan 1970, 00:00:00'], 
244      [v1, (v1.timestamp - 10).strftime("%d %b %Y, %H:%M:%S")]
245     ].each do |way, t|
246       amf_content "getway_old", "/1", [way.nil? ? 0 : way.id, t]
247       post :amf_read
248       assert_response :success
249       amf_parse_response
250       returned_way = amf_result("/1")
251       assert returned_way[2].empty?
252       assert returned_way[3].empty?
253       assert returned_way[4] < 0
254     end
255   end
256
257   def test_getway_history
258     latest = current_ways(:way_with_versions)
259     oldest = ways(:way_with_versions_v1)
260
261     amf_content "getway_history", "/1", [latest.id]
262     post :amf_read
263     assert_response :success
264     amf_parse_response
265     history = amf_result("/1")
266
267     # ['way',wayid,history]
268     assert_equal 'way', history[0]
269     assert_equal latest.id, history[1] 
270     # for some reason undocumented, the potlatch API now prefers dates
271     # over version numbers. presumably no-one edits concurrently any more?
272     assert_equal latest.timestamp.strftime("%d %b %Y, %H:%M:%S"), history[2].first[0]
273     assert_equal oldest.timestamp.strftime("%d %b %Y, %H:%M:%S"), history[2].last[0]
274   end
275
276   def test_getway_history_nonexistent
277     amf_content "getway_history", "/1", [0]
278     post :amf_read
279     assert_response :success
280     amf_parse_response
281     history = amf_result("/1")
282
283     # ['way',wayid,history]
284     assert_equal history[0], 'way'
285     assert_equal history[1], 0
286     assert history[2].empty?
287   end
288
289   def test_getnode_history
290     latest = current_nodes(:node_with_versions)
291     amf_content "getnode_history", "/1", [latest.id]
292     post :amf_read
293     assert_response :success
294     amf_parse_response
295     history = amf_result("/1")
296
297     # ['node',nodeid,history]
298     assert_equal history[0], 'node', 
299       'first element should be "node"'
300     assert_equal history[1], latest.id,
301       'second element should be the input node ID'
302     # NOTE: changed this test to match what amf_controller actually 
303     # outputs - which may or may not be what potlatch is expecting.
304     # someone who knows potlatch (i.e: richard f) should review this.
305     # NOTE2: wow - this is the second time this has changed in the
306     # API and the tests are being patched up. 
307     assert_equal history[2].first[0], 
308       latest.timestamp.strftime("%d %b %Y, %H:%M:%S"),
309       'first part of third element should be the latest version'
310     assert_equal history[2].last[0], 
311       nodes(:node_with_versions_v1).timestamp.strftime("%d %b %Y, %H:%M:%S"),
312       'second part of third element should be the initial version'
313   end
314
315   def test_getnode_history_nonexistent
316     amf_content "getnode_history", "/1", [0]
317     post :amf_read
318     assert_response :success
319     amf_parse_response
320     history = amf_result("/1")
321
322     # ['node',nodeid,history]
323     assert_equal history[0], 'node'
324     assert_equal history[1], 0
325     assert history[2].empty?
326   end
327
328   # ************************************************************
329   # AMF Write tests
330   def test_putpoi_update_valid
331     nd = current_nodes(:visible_node)
332     amf_content "putpoi", "/1", ["test@openstreetmap.org:test", nd.changeset_id, nd.version, nd.id, nd.lon, nd.lat, nd.tags, nd.visible]
333     post :amf_write
334     assert_response :success
335     amf_parse_response
336     result = amf_result("/1")
337     
338     assert_equal 0, result[0]
339     assert_equal nd.id, result[1]
340     assert_equal nd.id, result[2]
341     assert_equal nd.version+1, result[3]
342     
343     # Now try to update again, with a different lat/lon, using the updated version number
344     lat = nd.lat+0.1
345     lon = nd.lon-0.1
346     amf_content "putpoi", "/2", ["test@openstreetmap.org:test", nd.changeset_id, nd.version+1, nd.id, lon, lat, nd.tags, nd.visible]
347     post :amf_write
348     assert_response :success
349     amf_parse_response
350     result = amf_result("/2")
351     
352     assert_equal 0, result[0]
353     assert_equal nd.id, result[1]
354     assert_equal nd.id, result[2]
355     assert_equal nd.version+2, result[3]
356   end
357   
358   # Check that we can create a no valid poi
359   # Using similar method for the node controller test
360   def test_putpoi_create_valid
361     # This node has no tags
362     nd = Node.new
363     # create a node with random lat/lon
364     lat = rand(100)-50 + rand
365     lon = rand(100)-50 + rand
366     # normal user has a changeset open
367     changeset = changesets(:normal_user_first_change)
368     
369     amf_content "putpoi", "/1", ["test@openstreetmap.org:test", changeset.id, nil, nil, lon, lat, {}, nil]
370     post :amf_write
371     assert_response :success
372     amf_parse_response
373     result = amf_result("/1")
374     
375     # check the array returned by the amf
376     assert_equal 4, result.size
377     assert_equal 0, result[0], "expected to get the status ok from the amf"
378     assert_equal 0, result[1], "The old id should be 0"
379     assert result[2] > 0, "The new id should be greater than 0"
380     assert_equal 1, result[3], "The new version should be 1"
381     
382     # Finally check that the node that was saved has saved the data correctly 
383     # in both the current and history tables
384     # First check the current table
385     current_node = Node.find(result[2])
386     assert_in_delta lat, current_node.lat, 0.00001, "The latitude was not retreieved correctly"
387     assert_in_delta lon, current_node.lon, 0.00001, "The longitude was not retreived correctly"
388     assert_equal 0, current_node.tags.size, "There seems to be a tag that has been added to the node"
389     assert_equal result[3], current_node.version, "The version returned, is different to the one returned by the amf"
390     # Now check the history table
391     historic_nodes = Node.find(:all, :conditions => { :id => result[2] })
392     assert_equal 1, historic_nodes.size, "There should only be one historic node created"
393     first_historic_node = historic_nodes.first
394     assert_in_delta lat, first_historic_node.lat, 0.00001, "The latitude was not retreived correctly"
395     assert_in_delta lon, first_historic_node.lon, 0.00001, "The longitude was not retreuved correctly"
396     assert_equal 0, first_historic_node.tags.size, "There seems to be a tag that have been attached to this node"
397     assert_equal result[3], first_historic_node.version, "The version returned, is different to the one returned by the amf"
398     
399     ####
400     # This node has some tags
401     tnd = Node.new
402     # create a node with random lat/lon
403     lat = rand(100)-50 + rand
404     lon = rand(100)-50 + rand
405     # normal user has a changeset open
406     changeset = changesets(:normal_user_first_change)
407     
408     amf_content "putpoi", "/2", ["test@openstreetmap.org:test", changeset.id, nil, nil, lon, lat, { "key" => "value", "ping" => "pong" }, nil]
409     post :amf_write
410     assert_response :success
411     amf_parse_response
412     result = amf_result("/2")
413
414     # check the array returned by the amf
415     assert_equal 4, result.size
416     assert_equal 0, result[0], "Expected to get the status ok in the amf"
417     assert_equal 0, result[1], "The old id should be 0"
418     assert result[2] > 0, "The new id should be greater than 0"
419     assert_equal 1, result[3], "The new version should be 1"
420     
421     # Finally check that the node that was saved has saved the data correctly 
422     # in both the current and history tables
423     # First check the current table
424     current_node = Node.find(result[2])
425     assert_in_delta lat, current_node.lat, 0.00001, "The latitude was not retreieved correctly"
426     assert_in_delta lon, current_node.lon, 0.00001, "The longitude was not retreived correctly"
427     assert_equal 2, current_node.tags.size, "There seems to be a tag that has been added to the node"
428     assert_equal({ "key" => "value", "ping" => "pong" }, current_node.tags, "tags are different")
429     assert_equal result[3], current_node.version, "The version returned, is different to the one returned by the amf"
430     # Now check the history table
431     historic_nodes = Node.find(:all, :conditions => { :id => result[2] })
432     assert_equal 1, historic_nodes.size, "There should only be one historic node created"
433     first_historic_node = historic_nodes.first
434     assert_in_delta lat, first_historic_node.lat, 0.00001, "The latitude was not retreived correctly"
435     assert_in_delta lon, first_historic_node.lon, 0.00001, "The longitude was not retreuved correctly"
436     assert_equal 2, first_historic_node.tags.size, "There seems to be a tag that have been attached to this node"
437     assert_equal({ "key" => "value", "ping" => "pong" }, first_historic_node.tags, "tags are different")
438     assert_equal result[3], first_historic_node.version, "The version returned, is different to the one returned by the amf"
439   end
440   
441   def test_putpoi_delete_valid
442     
443   end
444   
445   def test_putpoi_delete_already_deleted
446     
447   end
448   
449   def test_putpoi_delete_not_found
450     
451   end
452   
453   def test_putpoi_invalid_latlon
454     
455   end
456
457   # ************************************************************
458   # AMF Helper functions
459
460   # Get the result record for the specified ID
461   # It's an assertion FAIL if the record does not exist
462   def amf_result ref
463     assert @amf_result.has_key?("#{ref}/onResult")
464     @amf_result["#{ref}/onResult"]
465   end
466
467   # Encode the AMF message to invoke "target" with parameters as
468   # the passed data. The ref is used to retrieve the results.
469   def amf_content(target, ref, data)
470     a,b=1.divmod(256)
471     c = StringIO.new()
472     c.write 0.chr+0.chr   # version 0
473     c.write 0.chr+0.chr   # n headers
474     c.write a.chr+b.chr   # n bodies
475     c.write AMF.encodestring(target)
476     c.write AMF.encodestring(ref)
477     c.write [-1].pack("N")
478     c.write AMF.encodevalue(data)
479
480     @request.env["RAW_POST_DATA"] = c.string
481   end
482
483   # Parses the @response object as an AMF messsage.
484   # The result is a hash of message_ref => data.
485   # The attribute @amf_result is initialised to this hash.
486   def amf_parse_response
487     if @response.body.class.to_s == 'Proc'
488       res = StringIO.new()
489       @response.body.call @response, res
490       req = StringIO.new(res.string)
491     else
492       req = StringIO.new(@response.body)
493     end
494     req.read(2)   # version
495
496     # parse through any headers
497         headers=AMF.getint(req)                                 # Read number of headers
498         headers.times do                                                # Read each header
499           name=AMF.getstring(req)                               #  |
500           req.getc                                                              #  | skip boolean
501           value=AMF.getvalue(req)                               #  |
502         end
503
504     # parse through responses
505     results = {}
506     bodies=AMF.getint(req)                                      # Read number of bodies
507         bodies.times do                                                 # Read each body
508           message=AMF.getstring(req)                    #  | get message name
509           index=AMF.getstring(req)                              #  | get index in response sequence
510           bytes=AMF.getlong(req)                                #  | get total size in bytes
511           args=AMF.getvalue(req)                                #  | get response (probably an array)
512       results[message] = args
513     end
514     @amf_result = results
515     results
516   end
517
518   ##
519   # given an array of bounding boxes (each an array of 4 floats), call the
520   # AMF "whichways" controller for each and pass the result back to the
521   # caller's block for assertion testing.
522   def check_bboxes_are_bad(bboxes)
523     bboxes.each do |bbox|
524       amf_content "whichways", "/1", bbox
525       post :amf_read
526       assert_response :success
527       amf_parse_response
528
529       # pass the response back to the caller's block to be tested
530       # against what the caller expected.
531       map = amf_result "/1"
532       yield map, bbox
533     end
534   end
535 end