1Code.require_file("test_helper.exs", __DIR__)
2
3defmodule URITest do
4  use ExUnit.Case, async: true
5
6  doctest URI
7
8  test "encode/1,2" do
9    assert URI.encode("4_test.is-s~") == "4_test.is-s~"
10
11    assert URI.encode("\r\n&<%>\" ゆ", &URI.char_unreserved?/1) ==
12             "%0D%0A%26%3C%25%3E%22%20%E3%82%86"
13  end
14
15  test "encode_www_form/1" do
16    assert URI.encode_www_form("4test ~1.x") == "4test+~1.x"
17    assert URI.encode_www_form("poll:146%") == "poll%3A146%25"
18    assert URI.encode_www_form("/\n+/ゆ") == "%2F%0A%2B%2F%E3%82%86"
19  end
20
21  test "encode_query/1,2" do
22    assert URI.encode_query([{:foo, :bar}, {:baz, :quux}]) == "foo=bar&baz=quux"
23    assert URI.encode_query([{"foo", "bar"}, {"baz", "quux"}]) == "foo=bar&baz=quux"
24
25    assert URI.encode_query([{"foo z", :bar}]) == "foo+z=bar"
26    assert URI.encode_query([{"foo z", :bar}], :rfc3986) == "foo%20z=bar"
27    assert URI.encode_query([{"foo z", :bar}], :www_form) == "foo+z=bar"
28
29    assert URI.encode_query([{"foo[]", "+=/?&# Ñ"}]) ==
30             "foo%5B%5D=%2B%3D%2F%3F%26%23+%C3%91"
31
32    assert URI.encode_query([{"foo[]", "+=/?&# Ñ"}], :rfc3986) ==
33             "foo%5B%5D=%2B%3D%2F%3F%26%23%20%C3%91"
34
35    assert URI.encode_query([{"foo[]", "+=/?&# Ñ"}], :www_form) ==
36             "foo%5B%5D=%2B%3D%2F%3F%26%23+%C3%91"
37
38    assert_raise ArgumentError, fn ->
39      URI.encode_query([{"foo", 'bar'}])
40    end
41
42    assert_raise ArgumentError, fn ->
43      URI.encode_query([{'foo', "bar"}])
44    end
45  end
46
47  test "decode_query/1,2,3" do
48    assert URI.decode_query("", %{}) == %{}
49
50    assert URI.decode_query("safe=off", %{"cookie" => "foo"}) ==
51             %{"safe" => "off", "cookie" => "foo"}
52
53    assert URI.decode_query("q=search%20query&cookie=ab%26cd&block+buster=") ==
54             %{"block buster" => "", "cookie" => "ab&cd", "q" => "search query"}
55
56    assert URI.decode_query("q=search%20query&cookie=ab%26cd&block+buster=", %{}, :rfc3986) ==
57             %{"block+buster" => "", "cookie" => "ab&cd", "q" => "search query"}
58
59    assert URI.decode_query("something=weird%3Dhappening") == %{"something" => "weird=happening"}
60
61    assert URI.decode_query("=") == %{"" => ""}
62    assert URI.decode_query("key") == %{"key" => ""}
63    assert URI.decode_query("key=") == %{"key" => ""}
64    assert URI.decode_query("=value") == %{"" => "value"}
65    assert URI.decode_query("something=weird=happening") == %{"something" => "weird=happening"}
66  end
67
68  test "query_decoder/1,2" do
69    decoder = URI.query_decoder("q=search%20query&cookie=ab%26cd&block+buster=")
70    expected = [{"q", "search query"}, {"cookie", "ab&cd"}, {"block buster", ""}]
71    assert Enum.map(decoder, & &1) == expected
72
73    decoder = URI.query_decoder("q=search%20query&cookie=ab%26cd&block+buster=", :rfc3986)
74    expected = [{"q", "search query"}, {"cookie", "ab&cd"}, {"block+buster", ""}]
75    assert Enum.map(decoder, & &1) == expected
76  end
77
78  test "decode/1" do
79    assert URI.decode("%0D%0A%26%3C%25%3E%22%20%E3%82%86") == "\r\n&<%>\" ゆ"
80    assert URI.decode("%2f%41%4a%55") == "/AJU"
81    assert URI.decode("4_t+st.is-s~") == "4_t+st.is-s~"
82    assert URI.decode("% invalid") == "% invalid"
83    assert URI.decode("invalid %") == "invalid %"
84    assert URI.decode("%%") == "%%"
85  end
86
87  test "decode_www_form/1" do
88    assert URI.decode_www_form("%3Eval+ue%2B") == ">val ue+"
89    assert URI.decode_www_form("%E3%82%86+") == "ゆ "
90    assert URI.decode_www_form("% invalid") == "% invalid"
91    assert URI.decode_www_form("invalid %") == "invalid %"
92    assert URI.decode_www_form("%%") == "%%"
93  end
94
95  describe "new/1" do
96    test "empty" do
97      assert URI.new("") == {:ok, %URI{}}
98    end
99
100    test "errors on bad URIs" do
101      assert URI.new("/>") == {:error, ">"}
102      assert URI.new(":https") == {:error, ":"}
103      assert URI.new("ht\0tps://foo.com") == {:error, "\0"}
104    end
105  end
106
107  describe "new!/1" do
108    test "returns the given URI if a %URI{} struct is given" do
109      assert URI.new!(uri = %URI{scheme: "http", host: "foo.com"}) == uri
110    end
111
112    test "works with HTTP scheme" do
113      expected_uri = %URI{
114        scheme: "http",
115        host: "foo.com",
116        path: "/path/to/something",
117        query: "foo=bar&bar=foo",
118        fragment: "fragment",
119        port: 80,
120        userinfo: nil
121      }
122
123      assert URI.new!("http://foo.com/path/to/something?foo=bar&bar=foo#fragment") ==
124               expected_uri
125    end
126
127    test "works with HTTPS scheme" do
128      expected_uri = %URI{
129        scheme: "https",
130        host: "foo.com",
131        query: nil,
132        fragment: nil,
133        port: 443,
134        path: nil,
135        userinfo: nil
136      }
137
138      assert URI.new!("https://foo.com") == expected_uri
139    end
140
141    test "works with file scheme" do
142      expected_uri = %URI{
143        scheme: "file",
144        host: "",
145        path: "/foo/bar/baz",
146        userinfo: nil,
147        query: nil,
148        fragment: nil,
149        port: nil
150      }
151
152      assert URI.new!("file:///foo/bar/baz") == expected_uri
153    end
154
155    test "works with FTP scheme" do
156      expected_uri = %URI{
157        scheme: "ftp",
158        host: "private.ftp-server.example.com",
159        userinfo: "user001:password",
160        path: "/my_directory/my_file.txt",
161        query: nil,
162        fragment: nil,
163        port: 21
164      }
165
166      ftp = "ftp://user001:password@private.ftp-server.example.com/my_directory/my_file.txt"
167      assert URI.new!(ftp) == expected_uri
168    end
169
170    test "works with SFTP scheme" do
171      expected_uri = %URI{
172        scheme: "sftp",
173        host: "private.ftp-server.example.com",
174        userinfo: "user001:password",
175        path: "/my_directory/my_file.txt",
176        query: nil,
177        fragment: nil,
178        port: 22
179      }
180
181      sftp = "sftp://user001:password@private.ftp-server.example.com/my_directory/my_file.txt"
182      assert URI.new!(sftp) == expected_uri
183    end
184
185    test "works with TFTP scheme" do
186      expected_uri = %URI{
187        scheme: "tftp",
188        host: "private.ftp-server.example.com",
189        userinfo: "user001:password",
190        path: "/my_directory/my_file.txt",
191        query: nil,
192        fragment: nil,
193        port: 69
194      }
195
196      tftp = "tftp://user001:password@private.ftp-server.example.com/my_directory/my_file.txt"
197      assert URI.new!(tftp) == expected_uri
198    end
199
200    test "works with LDAP scheme" do
201      expected_uri = %URI{
202        scheme: "ldap",
203        host: "",
204        userinfo: nil,
205        path: "/dc=example,dc=com",
206        query: "?sub?(givenName=John)",
207        fragment: nil,
208        port: 389
209      }
210
211      assert URI.new!("ldap:///dc=example,dc=com??sub?(givenName=John)") == expected_uri
212
213      expected_uri = %URI{
214        scheme: "ldap",
215        host: "ldap.example.com",
216        userinfo: nil,
217        path: "/cn=John%20Doe,dc=foo,dc=com",
218        fragment: nil,
219        port: 389,
220        query: nil
221      }
222
223      assert URI.new!("ldap://ldap.example.com/cn=John%20Doe,dc=foo,dc=com") == expected_uri
224    end
225
226    test "can parse IPv6 addresses" do
227      addresses = [
228        # undefined
229        "::",
230        # loopback
231        "::1",
232        # unicast
233        "1080::8:800:200C:417A",
234        # multicast
235        "FF01::101",
236        # link-local
237        "fe80::",
238        # abbreviated
239        "2607:f3f0:2:0:216:3cff:fef0:174a",
240        # mixed hex case
241        "2607:f3F0:2:0:216:3cFf:Fef0:174A",
242        # complete
243        "2051:0db8:2d5a:3521:8313:ffad:1242:8e2e",
244        # embedded IPv4
245        "::00:192.168.10.184"
246      ]
247
248      Enum.each(addresses, fn addr ->
249        simple_uri = URI.new!("http://[#{addr}]/")
250        assert simple_uri.host == addr
251
252        userinfo_uri = URI.new!("http://user:pass@[#{addr}]/")
253        assert userinfo_uri.host == addr
254        assert userinfo_uri.userinfo == "user:pass"
255
256        port_uri = URI.new!("http://[#{addr}]:2222/")
257        assert port_uri.host == addr
258        assert port_uri.port == 2222
259
260        userinfo_port_uri = URI.new!("http://user:pass@[#{addr}]:2222/")
261        assert userinfo_port_uri.host == addr
262        assert userinfo_port_uri.userinfo == "user:pass"
263        assert userinfo_port_uri.port == 2222
264      end)
265    end
266
267    test "downcases the scheme" do
268      assert URI.new!("hTtP://google.com").scheme == "http"
269    end
270
271    test "preserves empty fragments" do
272      assert URI.new!("http://example.com#").fragment == ""
273      assert URI.new!("http://example.com/#").fragment == ""
274      assert URI.new!("http://example.com/test#").fragment == ""
275    end
276
277    test "preserves an empty query" do
278      assert URI.new!("http://foo.com/?").query == ""
279    end
280  end
281
282  test "default_port/1,2" do
283    assert URI.default_port("http") == 80
284
285    try do
286      URI.default_port("http", 8000)
287      assert URI.default_port("http") == 8000
288    after
289      URI.default_port("http", 80)
290    end
291
292    assert URI.default_port("unknown") == nil
293    URI.default_port("unknown", 13)
294    assert URI.default_port("unknown") == 13
295  end
296
297  test "to_string/1 and Kernel.to_string/1" do
298    assert to_string(URI.new!("http://google.com")) == "http://google.com"
299    assert to_string(URI.new!("http://google.com:443")) == "http://google.com:443"
300    assert to_string(URI.new!("https://google.com:443")) == "https://google.com"
301    assert to_string(URI.new!("file:/path")) == "file:/path"
302    assert to_string(URI.new!("file:///path")) == "file:///path"
303    assert to_string(URI.new!("file://///path")) == "file://///path"
304    assert to_string(URI.new!("http://lol:wut@google.com")) == "http://lol:wut@google.com"
305    assert to_string(URI.new!("http://google.com/elixir")) == "http://google.com/elixir"
306    assert to_string(URI.new!("http://google.com?q=lol")) == "http://google.com?q=lol"
307    assert to_string(URI.new!("http://google.com?q=lol#omg")) == "http://google.com?q=lol#omg"
308    assert to_string(URI.new!("//google.com/elixir")) == "//google.com/elixir"
309    assert to_string(URI.new!("//google.com:8080/elixir")) == "//google.com:8080/elixir"
310    assert to_string(URI.new!("//user:password@google.com/")) == "//user:password@google.com/"
311    assert to_string(URI.new!("http://[2001:db8::]:8080")) == "http://[2001:db8::]:8080"
312    assert to_string(URI.new!("http://[2001:db8::]")) == "http://[2001:db8::]"
313
314    assert URI.to_string(URI.new!("http://google.com")) == "http://google.com"
315    assert URI.to_string(URI.new!("gid:hello/123")) == "gid:hello/123"
316
317    assert URI.to_string(URI.new!("//user:password@google.com/")) ==
318             "//user:password@google.com/"
319
320    assert_raise ArgumentError,
321                 ~r":path in URI must be empty or an absolute path if URL has a :host",
322                 fn -> %URI{host: "foo.com", path: "hello/123"} |> URI.to_string() end
323  end
324
325  test "merge/2" do
326    assert_raise ArgumentError, "you must merge onto an absolute URI", fn ->
327      URI.merge("/relative", "")
328    end
329
330    assert URI.merge("http://google.com/foo", "http://example.com/baz")
331           |> to_string == "http://example.com/baz"
332
333    assert URI.merge("http://google.com/foo", "http://example.com/.././bar/../../baz")
334           |> to_string == "http://example.com/baz"
335
336    assert URI.merge("http://google.com/foo", "//example.com/baz")
337           |> to_string == "http://example.com/baz"
338
339    assert URI.merge("http://google.com/foo", "//example.com/.././bar/../../../baz")
340           |> to_string == "http://example.com/baz"
341
342    assert URI.merge("http://example.com", URI.new!("/foo"))
343           |> to_string == "http://example.com/foo"
344
345    assert URI.merge("http://example.com", URI.new!("/.././bar/../../../baz"))
346           |> to_string == "http://example.com/baz"
347
348    base = URI.new!("http://example.com/foo/bar")
349    assert URI.merge(base, "") |> to_string == "http://example.com/foo/bar"
350    assert URI.merge(base, "#fragment") |> to_string == "http://example.com/foo/bar#fragment"
351    assert URI.merge(base, "?query") |> to_string == "http://example.com/foo/bar?query"
352    assert URI.merge(base, %URI{}) |> to_string == "http://example.com/foo/bar"
353
354    assert URI.merge(base, %URI{fragment: "fragment"})
355           |> to_string == "http://example.com/foo/bar#fragment"
356
357    base = URI.new!("http://example.com")
358    assert URI.merge(base, "/foo") |> to_string == "http://example.com/foo"
359    assert URI.merge(base, "foo") |> to_string == "http://example.com/foo"
360
361    base = URI.new!("http://example.com/foo/bar")
362    assert URI.merge(base, "/baz") |> to_string == "http://example.com/baz"
363    assert URI.merge(base, "baz") |> to_string == "http://example.com/foo/baz"
364    assert URI.merge(base, "../baz") |> to_string == "http://example.com/baz"
365    assert URI.merge(base, ".././baz") |> to_string == "http://example.com/baz"
366    assert URI.merge(base, "./baz") |> to_string == "http://example.com/foo/baz"
367    assert URI.merge(base, "bar/./baz") |> to_string == "http://example.com/foo/bar/baz"
368
369    base = URI.new!("http://example.com/foo/bar/")
370    assert URI.merge(base, "/baz") |> to_string == "http://example.com/baz"
371    assert URI.merge(base, "baz") |> to_string == "http://example.com/foo/bar/baz"
372    assert URI.merge(base, "../baz") |> to_string == "http://example.com/foo/baz"
373    assert URI.merge(base, ".././baz") |> to_string == "http://example.com/foo/baz"
374    assert URI.merge(base, "./baz") |> to_string == "http://example.com/foo/bar/baz"
375    assert URI.merge(base, "bar/./baz") |> to_string == "http://example.com/foo/bar/bar/baz"
376
377    base = URI.new!("http://example.com/foo/bar/baz")
378    assert URI.merge(base, "../../foobar") |> to_string == "http://example.com/foobar"
379    assert URI.merge(base, "../../../foobar") |> to_string == "http://example.com/foobar"
380    assert URI.merge(base, "../../../../../../foobar") |> to_string == "http://example.com/foobar"
381
382    base = URI.new!("http://example.com/foo/../bar")
383    assert URI.merge(base, "baz") |> to_string == "http://example.com/baz"
384
385    base = URI.new!("http://example.com/foo/./bar")
386    assert URI.merge(base, "baz") |> to_string == "http://example.com/foo/baz"
387
388    base = URI.new!("http://example.com/foo?query1")
389    assert URI.merge(base, "?query2") |> to_string == "http://example.com/foo?query2"
390    assert URI.merge(base, "") |> to_string == "http://example.com/foo?query1"
391
392    base = URI.new!("http://example.com/foo#fragment1")
393    assert URI.merge(base, "#fragment2") |> to_string == "http://example.com/foo#fragment2"
394    assert URI.merge(base, "") |> to_string == "http://example.com/foo"
395
396    page_url = "https://example.com/guide/"
397    image_url = "https://images.example.com/t/1600x/https://images.example.com/foo.jpg"
398
399    assert URI.merge(URI.new!(page_url), URI.new!(image_url)) |> to_string ==
400             "https://images.example.com/t/1600x/https://images.example.com/foo.jpg"
401  end
402
403  ## Deprecate API
404
405  describe "authority" do
406    test "to_string" do
407      assert URI.to_string(%URI{authority: "foo@example.com:80"}) ==
408               "//foo@example.com:80"
409
410      assert URI.to_string(%URI{userinfo: "bar", host: "example.org", port: 81}) ==
411               "//bar@example.org:81"
412
413      assert URI.to_string(%URI{
414               authority: "foo@example.com:80",
415               userinfo: "bar",
416               host: "example.org",
417               port: 81
418             }) ==
419               "//bar@example.org:81"
420    end
421  end
422
423  describe "parse/1" do
424    test "returns the given URI if a %URI{} struct is given" do
425      assert URI.parse(uri = %URI{scheme: "http", host: "foo.com"}) == uri
426    end
427
428    test "works with HTTP scheme" do
429      expected_uri = %URI{
430        scheme: "http",
431        host: "foo.com",
432        path: "/path/to/something",
433        query: "foo=bar&bar=foo",
434        fragment: "fragment",
435        port: 80,
436        authority: "foo.com",
437        userinfo: nil
438      }
439
440      assert URI.parse("http://foo.com/path/to/something?foo=bar&bar=foo#fragment") ==
441               expected_uri
442    end
443
444    test "works with HTTPS scheme" do
445      expected_uri = %URI{
446        scheme: "https",
447        host: "foo.com",
448        authority: "foo.com",
449        query: nil,
450        fragment: nil,
451        port: 443,
452        path: nil,
453        userinfo: nil
454      }
455
456      assert URI.parse("https://foo.com") == expected_uri
457    end
458
459    test "works with \"file\" scheme" do
460      expected_uri = %URI{
461        scheme: "file",
462        host: "",
463        path: "/foo/bar/baz",
464        userinfo: nil,
465        query: nil,
466        fragment: nil,
467        port: nil,
468        authority: ""
469      }
470
471      assert URI.parse("file:///foo/bar/baz") == expected_uri
472    end
473
474    test "works with FTP scheme" do
475      expected_uri = %URI{
476        scheme: "ftp",
477        host: "private.ftp-server.example.com",
478        userinfo: "user001:password",
479        authority: "user001:password@private.ftp-server.example.com",
480        path: "/my_directory/my_file.txt",
481        query: nil,
482        fragment: nil,
483        port: 21
484      }
485
486      ftp = "ftp://user001:password@private.ftp-server.example.com/my_directory/my_file.txt"
487      assert URI.parse(ftp) == expected_uri
488    end
489
490    test "works with SFTP scheme" do
491      expected_uri = %URI{
492        scheme: "sftp",
493        host: "private.ftp-server.example.com",
494        userinfo: "user001:password",
495        authority: "user001:password@private.ftp-server.example.com",
496        path: "/my_directory/my_file.txt",
497        query: nil,
498        fragment: nil,
499        port: 22
500      }
501
502      sftp = "sftp://user001:password@private.ftp-server.example.com/my_directory/my_file.txt"
503      assert URI.parse(sftp) == expected_uri
504    end
505
506    test "works with TFTP scheme" do
507      expected_uri = %URI{
508        scheme: "tftp",
509        host: "private.ftp-server.example.com",
510        userinfo: "user001:password",
511        authority: "user001:password@private.ftp-server.example.com",
512        path: "/my_directory/my_file.txt",
513        query: nil,
514        fragment: nil,
515        port: 69
516      }
517
518      tftp = "tftp://user001:password@private.ftp-server.example.com/my_directory/my_file.txt"
519      assert URI.parse(tftp) == expected_uri
520    end
521
522    test "works with LDAP scheme" do
523      expected_uri = %URI{
524        scheme: "ldap",
525        host: "",
526        authority: "",
527        userinfo: nil,
528        path: "/dc=example,dc=com",
529        query: "?sub?(givenName=John)",
530        fragment: nil,
531        port: 389
532      }
533
534      assert URI.parse("ldap:///dc=example,dc=com??sub?(givenName=John)") == expected_uri
535
536      expected_uri = %URI{
537        scheme: "ldap",
538        host: "ldap.example.com",
539        authority: "ldap.example.com",
540        userinfo: nil,
541        path: "/cn=John%20Doe,dc=foo,dc=com",
542        fragment: nil,
543        port: 389,
544        query: nil
545      }
546
547      assert URI.parse("ldap://ldap.example.com/cn=John%20Doe,dc=foo,dc=com") == expected_uri
548    end
549
550    test "splits authority" do
551      expected_uri = %URI{
552        scheme: "http",
553        host: "foo.com",
554        path: nil,
555        query: nil,
556        fragment: nil,
557        port: 4444,
558        authority: "foo:bar@foo.com:4444",
559        userinfo: "foo:bar"
560      }
561
562      assert URI.parse("http://foo:bar@foo.com:4444") == expected_uri
563
564      expected_uri = %URI{
565        scheme: "https",
566        host: "foo.com",
567        path: nil,
568        query: nil,
569        fragment: nil,
570        port: 443,
571        authority: "foo:bar@foo.com",
572        userinfo: "foo:bar"
573      }
574
575      assert URI.parse("https://foo:bar@foo.com") == expected_uri
576
577      expected_uri = %URI{
578        scheme: "http",
579        host: "foo.com",
580        path: nil,
581        query: nil,
582        fragment: nil,
583        port: 4444,
584        authority: "foo.com:4444",
585        userinfo: nil
586      }
587
588      assert URI.parse("http://foo.com:4444") == expected_uri
589    end
590
591    test "can parse bad URIs" do
592      assert URI.parse("")
593      assert URI.parse("https:??@?F?@#>F//23/")
594
595      assert URI.parse(":https").path == ":https"
596      assert URI.parse("https").path == "https"
597      assert URI.parse("ht\0tps://foo.com").path == "ht\0tps://foo.com"
598    end
599
600    test "can parse IPv6 addresses" do
601      addresses = [
602        # undefined
603        "::",
604        # loopback
605        "::1",
606        # unicast
607        "1080::8:800:200C:417A",
608        # multicast
609        "FF01::101",
610        # link-local
611        "fe80::",
612        # abbreviated
613        "2607:f3f0:2:0:216:3cff:fef0:174a",
614        # mixed hex case
615        "2607:f3F0:2:0:216:3cFf:Fef0:174A",
616        # complete
617        "2051:0db8:2d5a:3521:8313:ffad:1242:8e2e",
618        # embedded IPv4
619        "::00:192.168.10.184"
620      ]
621
622      Enum.each(addresses, fn addr ->
623        simple_uri = URI.parse("http://[#{addr}]/")
624        assert simple_uri.authority == "[#{addr}]"
625        assert simple_uri.host == addr
626
627        userinfo_uri = URI.parse("http://user:pass@[#{addr}]/")
628        assert userinfo_uri.authority == "user:pass@[#{addr}]"
629        assert userinfo_uri.host == addr
630        assert userinfo_uri.userinfo == "user:pass"
631
632        port_uri = URI.parse("http://[#{addr}]:2222/")
633        assert port_uri.authority == "[#{addr}]:2222"
634        assert port_uri.host == addr
635        assert port_uri.port == 2222
636
637        userinfo_port_uri = URI.parse("http://user:pass@[#{addr}]:2222/")
638        assert userinfo_port_uri.authority == "user:pass@[#{addr}]:2222"
639        assert userinfo_port_uri.host == addr
640        assert userinfo_port_uri.userinfo == "user:pass"
641        assert userinfo_port_uri.port == 2222
642      end)
643    end
644
645    test "downcases the scheme" do
646      assert URI.parse("hTtP://google.com").scheme == "http"
647    end
648
649    test "preserves empty fragments" do
650      assert URI.parse("http://example.com#").fragment == ""
651      assert URI.parse("http://example.com/#").fragment == ""
652      assert URI.parse("http://example.com/test#").fragment == ""
653    end
654
655    test "preserves an empty query" do
656      assert URI.parse("http://foo.com/?").query == ""
657    end
658
659    test "merges empty path" do
660      base = URI.parse("http://example.com")
661      assert URI.merge(base, "/foo") |> to_string == "http://example.com/foo"
662      assert URI.merge(base, "foo") |> to_string == "http://example.com/foo"
663    end
664  end
665end
666