1+ // Copyright 2025 Glyn Matthews.
2+ // Distributed under the Boost Software License, Version 1.0.
3+ // (See accompanying file LICENSE_1_0.txt or copy at
4+ // http://www.boost.org/LICENSE_1_0.txt)
5+ //
6+ // WPT (Web Platform Tests) Conformance Tests
7+ // These tests are based on failures from the official WPT URL test suite.
8+ // They focus on high-value conformance issues worth fixing.
9+
10+ #include < exception>
11+ #include < catch2/catch_all.hpp>
12+ #include < skyr/url.hpp>
13+
14+ TEST_CASE (" wpt_whitespace_handling" , " [url][wpt][whitespace]" ) {
15+ using namespace std ::string_literals;
16+
17+ SECTION (" tab_in_hostname_should_be_stripped" ) {
18+ // WPT: "foo://ho\tst/" should parse as "foo://host/"
19+ auto url = skyr::make_url (" foo://ho\t st/" );
20+ REQUIRE (url);
21+ CHECK (url->hostname () == " host" );
22+ CHECK (url->host () == " host" );
23+ CHECK (url->href () == " foo://host/" );
24+ }
25+
26+ SECTION (" newline_in_hostname_should_be_stripped" ) {
27+ // WPT: "foo://ho\nst/" should parse as "foo://host/"
28+ auto url = skyr::make_url (" foo://ho\n st/" );
29+ REQUIRE (url);
30+ CHECK (url->hostname () == " host" );
31+ CHECK (url->host () == " host" );
32+ CHECK (url->href () == " foo://host/" );
33+ }
34+
35+ SECTION (" carriage_return_in_hostname_should_be_stripped" ) {
36+ // WPT: "foo://host/" (with embedded \r) should parse as "foo://host/"
37+ auto url = skyr::make_url (" foo://ho\r st/" );
38+ REQUIRE (url);
39+ CHECK (url->hostname () == " host" );
40+ CHECK (url->host () == " host" );
41+ CHECK (url->href () == " foo://host/" );
42+ }
43+
44+ SECTION (" http_tab_in_hostname_should_be_stripped" ) {
45+ // WPT: "http://ho\tst/" should parse as "http://host/"
46+ auto url = skyr::make_url (" http://ho\t st/" );
47+ REQUIRE (url);
48+ CHECK (url->hostname () == " host" );
49+ CHECK (url->host () == " host" );
50+ CHECK (url->href () == " http://host/" );
51+ }
52+
53+ SECTION (" http_newline_in_hostname_should_be_stripped" ) {
54+ // WPT: "http://ho\nst/" should parse as "http://host/"
55+ auto url = skyr::make_url (" http://ho\n st/" );
56+ REQUIRE (url);
57+ CHECK (url->hostname () == " host" );
58+ CHECK (url->host () == " host" );
59+ CHECK (url->href () == " http://host/" );
60+ }
61+
62+ SECTION (" http_carriage_return_in_hostname_should_be_stripped" ) {
63+ // WPT: "http://host/" (with embedded \r) should parse as "http://host/"
64+ auto url = skyr::make_url (" http://ho\r st/" );
65+ REQUIRE (url);
66+ CHECK (url->hostname () == " host" );
67+ CHECK (url->host () == " host" );
68+ CHECK (url->href () == " http://host/" );
69+ }
70+
71+ SECTION (" tab_and_newline_in_hostname_base_url" ) {
72+ // WPT: "http://example\t.\norg" with base "http://example.org/foo/bar"
73+ // Should parse as "http://example.org/"
74+ auto base = skyr::url (" http://example.org/foo/bar" );
75+ auto url = skyr::make_url (" http://example\t .\n org" , base);
76+ REQUIRE (url);
77+ CHECK (url->hostname () == " example.org" );
78+ CHECK (url->host () == " example.org" );
79+ CHECK (url->href () == " http://example.org/" );
80+ }
81+
82+ SECTION (" newline_in_authority_should_parse" ) {
83+ // WPT: "http://f:\n/c" with base should parse successfully
84+ auto base = skyr::url (" http://example.org/foo/bar" );
85+ auto url = skyr::make_url (" http://f:\n /c" , base);
86+ CHECK (url); // Should not fail to parse
87+ }
88+
89+ SECTION (" tabs_in_non_special_opaque_path" ) {
90+ // WPT: "non-special:opaque \t\t \t#hi" should collapse tabs/spaces
91+ // Expected pathname: "opaque %20" (tabs converted to spaces, then one encoded)
92+ auto url = skyr::make_url (" non-special:opaque \t\t \t #hi" );
93+ REQUIRE (url);
94+ CHECK (url->pathname () == " opaque %20" );
95+ CHECK (url->href () == " non-special:opaque %20#hi" );
96+ }
97+
98+ SECTION (" tabs_in_entire_url" ) {
99+ // WPT: Complex case with tabs throughout
100+ // "h\tt\ntp://h\to\nst:9\t0\n00/p\ta\nth?q\tu\nery#f\tr\nag"
101+ // Should parse as "http://host:9000/path?query#frag"
102+ auto url = skyr::make_url (" h\t t\n tp://h\t o\n st:9\t 0\n 00/p\t a\n th?q\t u\n ery#f\t r\n ag" );
103+ REQUIRE (url);
104+ CHECK (url->protocol () == " http:" );
105+ CHECK (url->hostname () == " host" );
106+ CHECK (url->host () == " host:9000" );
107+ CHECK (url->port () == " 9000" );
108+ CHECK (url->pathname () == " /path" );
109+ CHECK (url->search () == " ?query" );
110+ CHECK (url->hash () == " #frag" );
111+ CHECK (url->href () == " http://host:9000/path?query#frag" );
112+ }
113+
114+ SECTION (" newline_in_file_url_path" ) {
115+ // WPT: "C|\n/" with base "file://host/dir/file"
116+ // Expected: "file://host/C:/" with pathname "/C:/"
117+ auto base = skyr::url (" file://host/dir/file" );
118+ auto url = skyr::make_url (" C|\n /" , base);
119+ REQUIRE (url);
120+ CHECK (url->pathname () == " /C:/" );
121+ CHECK (url->href () == " file://host/C:/" );
122+ }
123+ }
124+
125+ TEST_CASE (" wpt_percent_encoding" , " [url][wpt][percent-encoding]" ) {
126+ using namespace std ::string_literals;
127+
128+ SECTION (" spaces_in_non_special_path_before_query" ) {
129+ // WPT: "non-special:opaque ?hi"
130+ // Expected pathname: "opaque %20" (trailing spaces should be percent-encoded)
131+ auto url = skyr::make_url (" non-special:opaque ?hi" );
132+ REQUIRE (url);
133+ CHECK (url->pathname () == " opaque %20" );
134+ CHECK (url->href () == " non-special:opaque %20?hi" );
135+ }
136+
137+ SECTION (" spaces_in_non_special_path_before_fragment" ) {
138+ // WPT: "non-special:opaque #hi"
139+ // Expected pathname: "opaque %20" (trailing spaces should be percent-encoded)
140+ auto url = skyr::make_url (" non-special:opaque #hi" );
141+ REQUIRE (url);
142+ CHECK (url->pathname () == " opaque %20" );
143+ CHECK (url->href () == " non-special:opaque %20#hi" );
144+ }
145+
146+ SECTION (" password_with_special_chars_at_sign_colon" ) {
147+ // WPT: "http://::@c@d:2" with base "http://example.org/foo/bar"
148+ // Username should be empty, password should be "%3A%40c"
149+ auto base = skyr::url (" http://example.org/foo/bar" );
150+ auto url = skyr::make_url (" http://::@c@d:2" , base);
151+ REQUIRE (url);
152+ CHECK (url->username () == " " );
153+ CHECK (url->password () == " %3A%40c" );
154+ CHECK (url->hostname () == " d" );
155+ CHECK (url->port () == " 2" );
156+ CHECK (url->href () == " http://:%3A%40c@d:2/" );
157+ }
158+
159+ SECTION (" password_encoding_special_characters_non_special_scheme" ) {
160+ // WPT: "foo://joe: !\"$%&'()*+,-.:;<=>@[\\]^_`{|}~@host/"
161+ // Username: "joe", Password should encode special chars including @ within password
162+ auto url = skyr::make_url (" foo://joe: !\" $%&'()*+,-.:;<=>@[\\ ]^_`{|}~@host/" );
163+ REQUIRE (url);
164+ CHECK (url->username () == " joe" );
165+ CHECK (url->password () == " %20!%22$%&'()*+,-.%3A%3B%3C%3D%3E%40%5B%5C%5D%5E_%60%7B%7C%7D~" );
166+ CHECK (url->hostname () == " host" );
167+ CHECK (url->href () == " foo://joe:%20!%22$%&'()*+,-.%3A%3B%3C%3D%3E%40%5B%5C%5D%5E_%60%7B%7C%7D~@host/" );
168+ }
169+
170+ SECTION (" password_encoding_special_characters_wss_scheme" ) {
171+ // WPT: "wss://joe: !\"$%&'()*+,-.:;<=>@[]^_`{|}~@host/"
172+ // Similar to above but for wss:// (special scheme)
173+ auto url = skyr::make_url (" wss://joe: !\" $%&'()*+,-.:;<=>@[]^_`{|}~@host/" );
174+ REQUIRE (url);
175+ CHECK (url->username () == " joe" );
176+ CHECK (url->password () == " %20!%22$%&'()*+,-.%3A%3B%3C%3D%3E%40%5B%5D%5E_%60%7B%7C%7D~" );
177+ CHECK (url->hostname () == " host" );
178+ CHECK (url->href () == " wss://joe:%20!%22$%&'()*+,-.%3A%3B%3C%3D%3E%40%5B%5D%5E_%60%7B%7C%7D~@host/" );
179+ }
180+
181+ SECTION (" caret_encoding_in_non_special_path" ) {
182+ // WPT: "foo://host/ !\"$%&'()*+,-./:;<=>@[\\]^_`{|}~"
183+ // Caret (^) should be encoded as %5E in path
184+ auto url = skyr::make_url (" foo://host/ !\" $%&'()*+,-./:;<=>@[\\ ]^_`{|}~" );
185+ REQUIRE (url);
186+ CHECK (url->pathname () == " /%20!%22$%&'()*+,-./:;%3C=%3E@[\\ ]%5E_%60%7B|%7D~" );
187+ CHECK (url->href () == " foo://host/%20!%22$%&'()*+,-./:;%3C=%3E@[\\ ]%5E_%60%7B|%7D~" );
188+ }
189+
190+ SECTION (" caret_encoding_in_wss_path" ) {
191+ // WPT: "wss://host/ !\"$%&'()*+,-./:;<=>@[\\]^_`{|}~"
192+ // Caret (^) should be encoded as %5E in path
193+ auto url = skyr::make_url (" wss://host/ !\" $%&'()*+,-./:;<=>@[\\ ]^_`{|}~" );
194+ REQUIRE (url);
195+ CHECK (url->pathname () == " /%20!%22$%&'()*+,-./:;%3C=%3E@[/]%5E_%60%7B|%7D~" );
196+ CHECK (url->href () == " wss://host/%20!%22$%&'()*+,-./:;%3C=%3E@[/]%5E_%60%7B|%7D~" );
197+ }
198+ }
199+
200+ TEST_CASE (" wpt_invalid_characters" , " [url][wpt][validation]" ) {
201+ using namespace std ::string_literals;
202+
203+ SECTION (" pipe_in_non_special_hostname_should_fail" ) {
204+ // WPT: "sc://a|b/" should fail to parse (pipe character invalid in host)
205+ auto url = skyr::make_url (" sc://a|b/" );
206+ CHECK_FALSE (url);
207+ }
208+
209+ // Note: Many of the "Invalid Character Accepted" failures involve
210+ // invisible/control characters that are hard to represent in source code.
211+ // These would need to be constructed programmatically or read from test data files.
212+ // The WPT runner already tests these comprehensively.
213+ }
0 commit comments