capnproto

FORK: Cap'n Proto serialization/RPC system - core tools and C++ library
git clone https://git.neptards.moe/neptards/capnproto.git
Log | Files | Refs | README | LICENSE

url-test.c++ (21701B)


      1 // Copyright (c) 2017 Cloudflare, Inc. and contributors
      2 // Licensed under the MIT License:
      3 //
      4 // Permission is hereby granted, free of charge, to any person obtaining a copy
      5 // of this software and associated documentation files (the "Software"), to deal
      6 // in the Software without restriction, including without limitation the rights
      7 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
      8 // copies of the Software, and to permit persons to whom the Software is
      9 // furnished to do so, subject to the following conditions:
     10 //
     11 // The above copyright notice and this permission notice shall be included in
     12 // all copies or substantial portions of the Software.
     13 //
     14 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
     15 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
     16 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
     17 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
     18 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
     19 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
     20 // THE SOFTWARE.
     21 
     22 #include "url.h"
     23 #include <kj/debug.h>
     24 #include <kj/test.h>
     25 
     26 namespace kj {
     27 namespace {
     28 
     29 Url parseAndCheck(kj::StringPtr originalText, kj::StringPtr expectedRestringified = nullptr,
     30                   Url::Options options = {}) {
     31   if (expectedRestringified == nullptr) expectedRestringified = originalText;
     32   auto url = Url::parse(originalText, Url::REMOTE_HREF, options);
     33   KJ_EXPECT(kj::str(url) == expectedRestringified, url, originalText, expectedRestringified);
     34   // Make sure clones also restringify to the expected string.
     35   auto clone = url.clone();
     36   KJ_EXPECT(kj::str(clone) == expectedRestringified, clone, originalText, expectedRestringified);
     37   return url;
     38 }
     39 
     40 static constexpr Url::Options NO_DECODE {
     41   false,  // percentDecode
     42   false,  // allowEmpty
     43 };
     44 
     45 static constexpr Url::Options ALLOW_EMPTY {
     46   true,    // percentDecode
     47   true,    // allowEmpty
     48 };
     49 
     50 KJ_TEST("parse / stringify URL") {
     51   {
     52     auto url = parseAndCheck("https://capnproto.org");
     53     KJ_EXPECT(url.scheme == "https");
     54     KJ_EXPECT(url.userInfo == nullptr);
     55     KJ_EXPECT(url.host == "capnproto.org");
     56     KJ_EXPECT(url.path == nullptr);
     57     KJ_EXPECT(!url.hasTrailingSlash);
     58     KJ_EXPECT(url.query == nullptr);
     59     KJ_EXPECT(url.fragment == nullptr);
     60   }
     61 
     62   {
     63     auto url = parseAndCheck("https://capnproto.org:80");
     64     KJ_EXPECT(url.scheme == "https");
     65     KJ_EXPECT(url.userInfo == nullptr);
     66     KJ_EXPECT(url.host == "capnproto.org:80");
     67     KJ_EXPECT(url.path == nullptr);
     68     KJ_EXPECT(!url.hasTrailingSlash);
     69     KJ_EXPECT(url.query == nullptr);
     70     KJ_EXPECT(url.fragment == nullptr);
     71   }
     72 
     73   {
     74     auto url = parseAndCheck("https://capnproto.org/");
     75     KJ_EXPECT(url.scheme == "https");
     76     KJ_EXPECT(url.userInfo == nullptr);
     77     KJ_EXPECT(url.host == "capnproto.org");
     78     KJ_EXPECT(url.path == nullptr);
     79     KJ_EXPECT(url.hasTrailingSlash);
     80     KJ_EXPECT(url.query == nullptr);
     81     KJ_EXPECT(url.fragment == nullptr);
     82   }
     83 
     84   {
     85     auto url = parseAndCheck("https://capnproto.org/foo/bar");
     86     KJ_EXPECT(url.scheme == "https");
     87     KJ_EXPECT(url.userInfo == nullptr);
     88     KJ_EXPECT(url.host == "capnproto.org");
     89     KJ_EXPECT(url.path.asPtr() == kj::ArrayPtr<const StringPtr>({"foo", "bar"}));
     90     KJ_EXPECT(!url.hasTrailingSlash);
     91     KJ_EXPECT(url.query == nullptr);
     92     KJ_EXPECT(url.fragment == nullptr);
     93   }
     94 
     95   {
     96     auto url = parseAndCheck("https://capnproto.org/foo/bar/");
     97     KJ_EXPECT(url.scheme == "https");
     98     KJ_EXPECT(url.userInfo == nullptr);
     99     KJ_EXPECT(url.host == "capnproto.org");
    100     KJ_EXPECT(url.path.asPtr() == kj::ArrayPtr<const StringPtr>({"foo", "bar"}));
    101     KJ_EXPECT(url.hasTrailingSlash);
    102     KJ_EXPECT(url.query == nullptr);
    103     KJ_EXPECT(url.fragment == nullptr);
    104   }
    105 
    106   {
    107     auto url = parseAndCheck("https://capnproto.org/foo/bar?baz=qux&corge#garply");
    108     KJ_EXPECT(url.scheme == "https");
    109     KJ_EXPECT(url.userInfo == nullptr);
    110     KJ_EXPECT(url.host == "capnproto.org");
    111     KJ_EXPECT(url.path.asPtr() == kj::ArrayPtr<const StringPtr>({"foo", "bar"}));
    112     KJ_EXPECT(!url.hasTrailingSlash);
    113     KJ_ASSERT(url.query.size() == 2);
    114     KJ_EXPECT(url.query[0].name == "baz");
    115     KJ_EXPECT(url.query[0].value == "qux");
    116     KJ_EXPECT(url.query[1].name == "corge");
    117     KJ_EXPECT(url.query[1].value == nullptr);
    118     KJ_EXPECT(KJ_ASSERT_NONNULL(url.fragment) == "garply");
    119   }
    120 
    121   {
    122     auto url = parseAndCheck("https://capnproto.org/foo/bar?baz=qux&corge=#garply");
    123     KJ_EXPECT(url.scheme == "https");
    124     KJ_EXPECT(url.userInfo == nullptr);
    125     KJ_EXPECT(url.host == "capnproto.org");
    126     KJ_EXPECT(url.path.asPtr() == kj::ArrayPtr<const StringPtr>({"foo", "bar"}));
    127     KJ_EXPECT(!url.hasTrailingSlash);
    128     KJ_ASSERT(url.query.size() == 2);
    129     KJ_EXPECT(url.query[0].name == "baz");
    130     KJ_EXPECT(url.query[0].value == "qux");
    131     KJ_EXPECT(url.query[1].name == "corge");
    132     KJ_EXPECT(url.query[1].value == nullptr);
    133     KJ_EXPECT(KJ_ASSERT_NONNULL(url.fragment) == "garply");
    134   }
    135 
    136   {
    137     auto url = parseAndCheck("https://capnproto.org/foo/bar?baz=&corge=grault#garply");
    138     KJ_EXPECT(url.scheme == "https");
    139     KJ_EXPECT(url.userInfo == nullptr);
    140     KJ_EXPECT(url.host == "capnproto.org");
    141     KJ_EXPECT(url.path.asPtr() == kj::ArrayPtr<const StringPtr>({"foo", "bar"}));
    142     KJ_EXPECT(!url.hasTrailingSlash);
    143     KJ_ASSERT(url.query.size() == 2);
    144     KJ_EXPECT(url.query[0].name == "baz");
    145     KJ_EXPECT(url.query[0].value == "");
    146     KJ_EXPECT(url.query[1].name == "corge");
    147     KJ_EXPECT(url.query[1].value == "grault");
    148     KJ_EXPECT(KJ_ASSERT_NONNULL(url.fragment) == "garply");
    149   }
    150 
    151   {
    152     auto url = parseAndCheck("https://capnproto.org/foo/bar/?baz=qux&corge=grault#garply");
    153     KJ_EXPECT(url.scheme == "https");
    154     KJ_EXPECT(url.userInfo == nullptr);
    155     KJ_EXPECT(url.host == "capnproto.org");
    156     KJ_EXPECT(url.path.asPtr() == kj::ArrayPtr<const StringPtr>({"foo", "bar"}));
    157     KJ_EXPECT(url.hasTrailingSlash);
    158     KJ_ASSERT(url.query.size() == 2);
    159     KJ_EXPECT(url.query[0].name == "baz");
    160     KJ_EXPECT(url.query[0].value == "qux");
    161     KJ_EXPECT(url.query[1].name == "corge");
    162     KJ_EXPECT(url.query[1].value == "grault");
    163     KJ_EXPECT(KJ_ASSERT_NONNULL(url.fragment) == "garply");
    164   }
    165 
    166   {
    167     auto url = parseAndCheck("https://capnproto.org/foo/bar?baz=qux#garply");
    168     KJ_EXPECT(url.scheme == "https");
    169     KJ_EXPECT(url.userInfo == nullptr);
    170     KJ_EXPECT(url.host == "capnproto.org");
    171     KJ_EXPECT(url.path.asPtr() == kj::ArrayPtr<const StringPtr>({"foo", "bar"}));
    172     KJ_EXPECT(!url.hasTrailingSlash);
    173     KJ_ASSERT(url.query.size() == 1);
    174     KJ_EXPECT(url.query[0].name == "baz");
    175     KJ_EXPECT(url.query[0].value == "qux");
    176     KJ_EXPECT(KJ_ASSERT_NONNULL(url.fragment) == "garply");
    177   }
    178 
    179   {
    180     auto url = parseAndCheck("https://capnproto.org/foo?bar%20baz=qux+quux",
    181                              "https://capnproto.org/foo?bar+baz=qux+quux");
    182     KJ_ASSERT(url.query.size() == 1);
    183     KJ_EXPECT(url.query[0].name == "bar baz");
    184     KJ_EXPECT(url.query[0].value == "qux quux");
    185   }
    186 
    187   {
    188     auto url = parseAndCheck("https://capnproto.org/foo/bar#garply");
    189     KJ_EXPECT(url.scheme == "https");
    190     KJ_EXPECT(url.userInfo == nullptr);
    191     KJ_EXPECT(url.host == "capnproto.org");
    192     KJ_EXPECT(url.path.asPtr() == kj::ArrayPtr<const StringPtr>({"foo", "bar"}));
    193     KJ_EXPECT(!url.hasTrailingSlash);
    194     KJ_EXPECT(url.query == nullptr);
    195     KJ_EXPECT(KJ_ASSERT_NONNULL(url.fragment) == "garply");
    196   }
    197 
    198   {
    199     auto url = parseAndCheck("https://capnproto.org/foo/bar/#garply");
    200     KJ_EXPECT(url.scheme == "https");
    201     KJ_EXPECT(url.userInfo == nullptr);
    202     KJ_EXPECT(url.host == "capnproto.org");
    203     KJ_EXPECT(url.path.asPtr() == kj::ArrayPtr<const StringPtr>({"foo", "bar"}));
    204     KJ_EXPECT(url.hasTrailingSlash);
    205     KJ_EXPECT(url.query == nullptr);
    206     KJ_EXPECT(KJ_ASSERT_NONNULL(url.fragment) == "garply");
    207   }
    208 
    209   {
    210     auto url = parseAndCheck("https://capnproto.org#garply");
    211     KJ_EXPECT(url.scheme == "https");
    212     KJ_EXPECT(url.userInfo == nullptr);
    213     KJ_EXPECT(url.host == "capnproto.org");
    214     KJ_EXPECT(url.path == nullptr);
    215     KJ_EXPECT(!url.hasTrailingSlash);
    216     KJ_EXPECT(url.query == nullptr);
    217     KJ_EXPECT(KJ_ASSERT_NONNULL(url.fragment) == "garply");
    218   }
    219 
    220   {
    221     auto url = parseAndCheck("https://capnproto.org/#garply");
    222     KJ_EXPECT(url.scheme == "https");
    223     KJ_EXPECT(url.userInfo == nullptr);
    224     KJ_EXPECT(url.host == "capnproto.org");
    225     KJ_EXPECT(url.path == nullptr);
    226     KJ_EXPECT(url.hasTrailingSlash);
    227     KJ_EXPECT(url.query == nullptr);
    228     KJ_EXPECT(KJ_ASSERT_NONNULL(url.fragment) == "garply");
    229   }
    230 
    231   {
    232     auto url = parseAndCheck("https://foo@capnproto.org");
    233     KJ_EXPECT(url.scheme == "https");
    234     auto& user = KJ_ASSERT_NONNULL(url.userInfo);
    235     KJ_EXPECT(user.username == "foo");
    236     KJ_EXPECT(user.password == nullptr);
    237     KJ_EXPECT(url.host == "capnproto.org");
    238     KJ_EXPECT(url.path == nullptr);
    239     KJ_EXPECT(!url.hasTrailingSlash);
    240     KJ_EXPECT(url.query == nullptr);
    241     KJ_EXPECT(url.fragment == nullptr);
    242   }
    243 
    244   {
    245     auto url = parseAndCheck("https://$foo&:12+,34@capnproto.org");
    246     KJ_EXPECT(url.scheme == "https");
    247     auto& user = KJ_ASSERT_NONNULL(url.userInfo);
    248     KJ_EXPECT(user.username == "$foo&");
    249     KJ_EXPECT(KJ_ASSERT_NONNULL(user.password) == "12+,34");
    250     KJ_EXPECT(url.host == "capnproto.org");
    251     KJ_EXPECT(url.path == nullptr);
    252     KJ_EXPECT(!url.hasTrailingSlash);
    253     KJ_EXPECT(url.query == nullptr);
    254     KJ_EXPECT(url.fragment == nullptr);
    255   }
    256 
    257   {
    258     auto url = parseAndCheck("https://[2001:db8::1234]:80/foo");
    259     KJ_EXPECT(url.scheme == "https");
    260     KJ_EXPECT(url.userInfo == nullptr);
    261     KJ_EXPECT(url.host == "[2001:db8::1234]:80");
    262     KJ_EXPECT(url.path.asPtr() == kj::ArrayPtr<const StringPtr>({"foo"}));
    263     KJ_EXPECT(!url.hasTrailingSlash);
    264     KJ_EXPECT(url.query == nullptr);
    265     KJ_EXPECT(url.fragment == nullptr);
    266   }
    267 
    268   {
    269     auto url = parseAndCheck("https://capnproto.org/foo%2Fbar/baz");
    270     KJ_EXPECT(url.path.asPtr() == kj::ArrayPtr<const StringPtr>({"foo/bar", "baz"}));
    271   }
    272 
    273   parseAndCheck("https://capnproto.org/foo/bar?", "https://capnproto.org/foo/bar");
    274   parseAndCheck("https://capnproto.org/foo/bar?#", "https://capnproto.org/foo/bar#");
    275   parseAndCheck("https://capnproto.org/foo/bar#");
    276 
    277   // Scheme and host are forced to lower-case.
    278   parseAndCheck("hTtP://capNprotO.org/fOo/bAr", "http://capnproto.org/fOo/bAr");
    279 
    280   // URLs with underscores in their hostnames are allowed, but you probably shouldn't use them. They
    281   // are not valid domain names.
    282   parseAndCheck("https://bad_domain.capnproto.org/");
    283 
    284   // Make sure URLs with %-encoded '%' signs in their userinfo, path, query, and fragment components
    285   // get correctly re-encoded.
    286   parseAndCheck("https://foo%25bar:baz%25qux@capnproto.org/");
    287   parseAndCheck("https://capnproto.org/foo%25bar");
    288   parseAndCheck("https://capnproto.org/?foo%25bar=baz%25qux");
    289   parseAndCheck("https://capnproto.org/#foo%25bar");
    290 
    291   // Make sure redundant /'s and &'s aren't collapsed when options.removeEmpty is false.
    292   parseAndCheck("https://capnproto.org/foo//bar///test//?foo=bar&&baz=qux&", nullptr, ALLOW_EMPTY);
    293 
    294   // "." and ".." are still processed, though.
    295   parseAndCheck("https://capnproto.org/foo//../bar/.",
    296                 "https://capnproto.org/foo/bar/", ALLOW_EMPTY);
    297 
    298   {
    299     auto url = parseAndCheck("https://foo/", nullptr, ALLOW_EMPTY);
    300     KJ_EXPECT(url.path.size() == 0);
    301     KJ_EXPECT(url.hasTrailingSlash);
    302   }
    303 
    304   {
    305     auto url = parseAndCheck("https://foo/bar/", nullptr, ALLOW_EMPTY);
    306     KJ_EXPECT(url.path.size() == 1);
    307     KJ_EXPECT(url.hasTrailingSlash);
    308   }
    309 }
    310 
    311 KJ_TEST("URL percent encoding") {
    312   parseAndCheck(
    313       "https://b%6fb:%61bcd@capnpr%6fto.org/f%6fo?b%61r=b%61z#q%75x",
    314       "https://bob:abcd@capnproto.org/foo?bar=baz#qux");
    315 
    316   parseAndCheck(
    317       "https://b\001b:\001bcd@capnproto.org/f\001o?b\001r=b\001z#q\001x",
    318       "https://b%01b:%01bcd@capnproto.org/f%01o?b%01r=b%01z#q%01x");
    319 
    320   parseAndCheck(
    321       "https://b b: bcd@capnproto.org/f o?b r=b z#q x",
    322       "https://b%20b:%20bcd@capnproto.org/f%20o?b+r=b+z#q%20x");
    323 
    324   parseAndCheck(
    325       "https://capnproto.org/foo?bar=baz#@?#^[\\]{|}",
    326       "https://capnproto.org/foo?bar=baz#@?#^[\\]{|}");
    327 
    328   // All permissible non-alphanumeric, non-separator path characters.
    329   parseAndCheck(
    330       "https://capnproto.org/!$&'()*+,-.:;=@[]^_|~",
    331       "https://capnproto.org/!$&'()*+,-.:;=@[]^_|~");
    332 }
    333 
    334 KJ_TEST("parse / stringify URL w/o decoding") {
    335   {
    336     auto url = parseAndCheck("https://capnproto.org/foo%2Fbar/baz", nullptr, NO_DECODE);
    337     KJ_EXPECT(url.path.asPtr() == kj::ArrayPtr<const StringPtr>({"foo%2Fbar", "baz"}));
    338   }
    339 
    340   {
    341     // This case would throw an exception without NO_DECODE.
    342     Url url = parseAndCheck("https://capnproto.org/R%20%26%20S?%foo=%QQ", nullptr, NO_DECODE);
    343     KJ_EXPECT(url.scheme == "https");
    344     KJ_EXPECT(url.host == "capnproto.org");
    345     KJ_EXPECT(url.path.asPtr() == kj::ArrayPtr<const StringPtr>({"R%20%26%20S"}));
    346     KJ_EXPECT(!url.hasTrailingSlash);
    347     KJ_ASSERT(url.query.size() == 1);
    348     KJ_EXPECT(url.query[0].name == "%foo");
    349     KJ_EXPECT(url.query[0].value == "%QQ");
    350   }
    351 }
    352 
    353 KJ_TEST("URL relative paths") {
    354   parseAndCheck(
    355       "https://capnproto.org/foo//bar",
    356       "https://capnproto.org/foo/bar");
    357 
    358   parseAndCheck(
    359       "https://capnproto.org/foo/./bar",
    360       "https://capnproto.org/foo/bar");
    361 
    362   parseAndCheck(
    363       "https://capnproto.org/foo/bar//",
    364       "https://capnproto.org/foo/bar/");
    365 
    366   parseAndCheck(
    367       "https://capnproto.org/foo/bar/.",
    368       "https://capnproto.org/foo/bar/");
    369 
    370   parseAndCheck(
    371       "https://capnproto.org/foo/baz/../bar",
    372       "https://capnproto.org/foo/bar");
    373 
    374   parseAndCheck(
    375       "https://capnproto.org/foo/bar/baz/..",
    376       "https://capnproto.org/foo/bar/");
    377 
    378   parseAndCheck(
    379       "https://capnproto.org/..",
    380       "https://capnproto.org/");
    381 
    382   parseAndCheck(
    383       "https://capnproto.org/foo/../..",
    384       "https://capnproto.org/");
    385 }
    386 
    387 KJ_TEST("URL for HTTP request") {
    388   {
    389     Url url = Url::parse("https://bob:1234@capnproto.org/foo/bar?baz=qux#corge");
    390     KJ_EXPECT(url.toString(Url::REMOTE_HREF) ==
    391         "https://bob:1234@capnproto.org/foo/bar?baz=qux#corge");
    392     KJ_EXPECT(url.toString(Url::HTTP_PROXY_REQUEST) == "https://capnproto.org/foo/bar?baz=qux");
    393     KJ_EXPECT(url.toString(Url::HTTP_REQUEST) == "/foo/bar?baz=qux");
    394   }
    395 
    396   {
    397     Url url = Url::parse("https://capnproto.org");
    398     KJ_EXPECT(url.toString(Url::REMOTE_HREF) == "https://capnproto.org");
    399     KJ_EXPECT(url.toString(Url::HTTP_PROXY_REQUEST) == "https://capnproto.org");
    400     KJ_EXPECT(url.toString(Url::HTTP_REQUEST) == "/");
    401   }
    402 
    403   {
    404     Url url = Url::parse("/foo/bar?baz=qux&corge", Url::HTTP_REQUEST);
    405     KJ_EXPECT(url.scheme == nullptr);
    406     KJ_EXPECT(url.host == nullptr);
    407     KJ_EXPECT(url.path.asPtr() == kj::ArrayPtr<const StringPtr>({"foo", "bar"}));
    408     KJ_EXPECT(!url.hasTrailingSlash);
    409     KJ_ASSERT(url.query.size() == 2);
    410     KJ_EXPECT(url.query[0].name == "baz");
    411     KJ_EXPECT(url.query[0].value == "qux");
    412     KJ_EXPECT(url.query[1].name == "corge");
    413     KJ_EXPECT(url.query[1].value == nullptr);
    414   }
    415 
    416   {
    417     Url url = Url::parse("https://capnproto.org/foo/bar?baz=qux&corge", Url::HTTP_PROXY_REQUEST);
    418     KJ_EXPECT(url.scheme == "https");
    419     KJ_EXPECT(url.host == "capnproto.org");
    420     KJ_EXPECT(url.path.asPtr() == kj::ArrayPtr<const StringPtr>({"foo", "bar"}));
    421     KJ_EXPECT(!url.hasTrailingSlash);
    422     KJ_ASSERT(url.query.size() == 2);
    423     KJ_EXPECT(url.query[0].name == "baz");
    424     KJ_EXPECT(url.query[0].value == "qux");
    425     KJ_EXPECT(url.query[1].name == "corge");
    426     KJ_EXPECT(url.query[1].value == nullptr);
    427   }
    428 
    429   {
    430     // '#' is allowed in path components in the HTTP_REQUEST context.
    431     Url url = Url::parse("/foo#bar", Url::HTTP_REQUEST);
    432     KJ_EXPECT(url.toString(Url::HTTP_REQUEST) == "/foo%23bar");
    433     KJ_EXPECT(url.scheme == nullptr);
    434     KJ_EXPECT(url.host == nullptr);
    435     KJ_EXPECT(url.path.asPtr() == kj::ArrayPtr<const StringPtr>{"foo#bar"});
    436     KJ_EXPECT(!url.hasTrailingSlash);
    437     KJ_EXPECT(url.query == nullptr);
    438     KJ_EXPECT(url.fragment == nullptr);
    439   }
    440 
    441   {
    442     // '#' is allowed in path components in the HTTP_PROXY_REQUEST context.
    443     Url url = Url::parse("https://capnproto.org/foo#bar", Url::HTTP_PROXY_REQUEST);
    444     KJ_EXPECT(url.toString(Url::HTTP_PROXY_REQUEST) == "https://capnproto.org/foo%23bar");
    445     KJ_EXPECT(url.scheme == "https");
    446     KJ_EXPECT(url.host == "capnproto.org");
    447     KJ_EXPECT(url.path.asPtr() == kj::ArrayPtr<const StringPtr>{"foo#bar"});
    448     KJ_EXPECT(!url.hasTrailingSlash);
    449     KJ_EXPECT(url.query == nullptr);
    450     KJ_EXPECT(url.fragment == nullptr);
    451   }
    452 
    453   {
    454     // '#' is allowed in query components in the HTTP_REQUEST context.
    455     Url url = Url::parse("/?foo=bar#123", Url::HTTP_REQUEST);
    456     KJ_EXPECT(url.toString(Url::HTTP_REQUEST) == "/?foo=bar%23123");
    457     KJ_EXPECT(url.scheme == nullptr);
    458     KJ_EXPECT(url.host == nullptr);
    459     KJ_EXPECT(url.path == nullptr);
    460     KJ_EXPECT(url.hasTrailingSlash);
    461     KJ_ASSERT(url.query.size() == 1);
    462     KJ_EXPECT(url.query[0].name == "foo");
    463     KJ_EXPECT(url.query[0].value == "bar#123");
    464     KJ_EXPECT(url.fragment == nullptr);
    465   }
    466 
    467   {
    468     // '#' is allowed in query components in the HTTP_PROXY_REQUEST context.
    469     Url url = Url::parse("https://capnproto.org/?foo=bar#123", Url::HTTP_PROXY_REQUEST);
    470     KJ_EXPECT(url.toString(Url::HTTP_PROXY_REQUEST) == "https://capnproto.org/?foo=bar%23123");
    471     KJ_EXPECT(url.scheme == "https");
    472     KJ_EXPECT(url.host == "capnproto.org");
    473     KJ_EXPECT(url.path == nullptr);
    474     KJ_EXPECT(url.hasTrailingSlash);
    475     KJ_ASSERT(url.query.size() == 1);
    476     KJ_EXPECT(url.query[0].name == "foo");
    477     KJ_EXPECT(url.query[0].value == "bar#123");
    478     KJ_EXPECT(url.fragment == nullptr);
    479   }
    480 }
    481 
    482 KJ_TEST("parse URL failure") {
    483   KJ_EXPECT(Url::tryParse("ht/tps://capnproto.org") == nullptr);
    484   KJ_EXPECT(Url::tryParse("capnproto.org") == nullptr);
    485   KJ_EXPECT(Url::tryParse("https:foo") == nullptr);
    486 
    487   // percent-decode errors
    488   KJ_EXPECT(Url::tryParse("https://capnproto.org/f%nno") == nullptr);
    489   KJ_EXPECT(Url::tryParse("https://capnproto.org/foo?b%nnr=baz") == nullptr);
    490 
    491   // components not valid in context
    492   KJ_EXPECT(Url::tryParse("https://capnproto.org/foo", Url::HTTP_REQUEST) == nullptr);
    493   KJ_EXPECT(Url::tryParse("https://bob:123@capnproto.org/foo", Url::HTTP_PROXY_REQUEST) == nullptr);
    494   KJ_EXPECT(Url::tryParse("https://capnproto.org#/foo", Url::HTTP_PROXY_REQUEST) == nullptr);
    495 }
    496 
    497 void parseAndCheckRelative(kj::StringPtr base, kj::StringPtr relative, kj::StringPtr expected,
    498                            Url::Options options = {}) {
    499   auto parsed = Url::parse(base, Url::REMOTE_HREF, options).parseRelative(relative);
    500   KJ_EXPECT(kj::str(parsed) == expected, parsed, expected);
    501   auto clone = parsed.clone();
    502   KJ_EXPECT(kj::str(clone) == expected, clone, expected);
    503 }
    504 
    505 KJ_TEST("parse relative URL") {
    506   parseAndCheckRelative("https://capnproto.org/foo/bar?baz=qux#corge",
    507                         "#grault",
    508                         "https://capnproto.org/foo/bar?baz=qux#grault");
    509   parseAndCheckRelative("https://capnproto.org/foo/bar?baz#corge",
    510                         "#grault",
    511                         "https://capnproto.org/foo/bar?baz#grault");
    512   parseAndCheckRelative("https://capnproto.org/foo/bar?baz=#corge",
    513                         "#grault",
    514                         "https://capnproto.org/foo/bar?baz=#grault");
    515   parseAndCheckRelative("https://capnproto.org/foo/bar?baz=qux#corge",
    516                         "?grault",
    517                         "https://capnproto.org/foo/bar?grault");
    518   parseAndCheckRelative("https://capnproto.org/foo/bar?baz=qux#corge",
    519                         "?grault=",
    520                         "https://capnproto.org/foo/bar?grault=");
    521   parseAndCheckRelative("https://capnproto.org/foo/bar?baz=qux#corge",
    522                         "?grault+garply=waldo",
    523                         "https://capnproto.org/foo/bar?grault+garply=waldo");
    524   parseAndCheckRelative("https://capnproto.org/foo/bar?baz=qux#corge",
    525                         "grault",
    526                         "https://capnproto.org/foo/grault");
    527   parseAndCheckRelative("https://capnproto.org/foo/bar?baz=qux#corge",
    528                         "/grault",
    529                         "https://capnproto.org/grault");
    530   parseAndCheckRelative("https://capnproto.org/foo/bar?baz=qux#corge",
    531                         "//grault",
    532                         "https://grault");
    533   parseAndCheckRelative("https://capnproto.org/foo/bar?baz=qux#corge",
    534                         "//grault/garply",
    535                         "https://grault/garply");
    536   parseAndCheckRelative("https://capnproto.org/foo/bar?baz=qux#corge",
    537                         "http:/grault",
    538                         "http://capnproto.org/grault");
    539   parseAndCheckRelative("https://capnproto.org/foo/bar?baz=qux#corge",
    540                         "/http:/grault",
    541                         "https://capnproto.org/http:/grault");
    542   parseAndCheckRelative("https://capnproto.org/",
    543                         "/foo/../bar",
    544                         "https://capnproto.org/bar");
    545 }
    546 
    547 KJ_TEST("parse relative URL w/o decoding") {
    548   // This case would throw an exception without NO_DECODE.
    549   parseAndCheckRelative("https://capnproto.org/R%20%26%20S?%foo=%QQ",
    550                         "%ANOTH%ERBAD%URL",
    551                         "https://capnproto.org/%ANOTH%ERBAD%URL", NO_DECODE);
    552 }
    553 
    554 KJ_TEST("parse relative URL failure") {
    555   auto base = Url::parse("https://example.com/");
    556   KJ_EXPECT(base.tryParseRelative("https://[not a host]") == nullptr);
    557 }
    558 
    559 }  // namespace
    560 }  // namespace kj