diff --git a/test/ip/ipv6_classify_test.cc b/test/ip/ipv6_classify_test.cc index 822e9629e..b5ffa29a6 100644 --- a/test/ip/ipv6_classify_test.cc +++ b/test/ip/ipv6_classify_test.cc @@ -241,3 +241,13 @@ TEST(invalid_empty) { TEST(invalid_ipv4) { EXPECT_FALSE(sourcemeta::core::ipv6_classify("127.0.0.1").has_value()); } + +TEST(anycast_block_with_nonzero_interface_is_reserved) { + const auto result{sourcemeta::core::ipv6_classify("2001:1:0:0:0:0:100:1")}; + EXPECT_EQ(result.value(), sourcemeta::core::IPAddressClass::Reserved); +} + +TEST(leading_zero_but_not_mapped_form) { + const auto result{sourcemeta::core::ipv6_classify("::100:0:0")}; + EXPECT_EQ(result.value(), sourcemeta::core::IPAddressClass::Reserved); +} diff --git a/test/ip/ipv6_test.cc b/test/ip/ipv6_test.cc index dcbcb9768..99b549130 100644 --- a/test/ip/ipv6_test.cc +++ b/test/ip/ipv6_test.cc @@ -355,3 +355,15 @@ TEST(valid_two_digit_hex_groups_with_leading_zeros) { TEST(valid_three_digit_hex_groups_with_leading_zeros) { EXPECT_TRUE(sourcemeta::core::is_ipv6("001:002:003:004:005:006:007:008")); } + +TEST(rejects_trailing_bracket) { + EXPECT_FALSE(sourcemeta::core::is_ipv6("1::]")); +} + +TEST(rejects_leading_single_colon_with_double) { + EXPECT_FALSE(sourcemeta::core::is_ipv6(":1::2")); +} + +TEST(rejects_trailing_single_colon_with_double) { + EXPECT_FALSE(sourcemeta::core::is_ipv6("1::2:")); +} diff --git a/test/json/json_auto_test.cc b/test/json/json_auto_test.cc index 091cd8808..576730af6 100644 --- a/test/json/json_auto_test.cc +++ b/test/json/json_auto_test.cc @@ -1189,3 +1189,100 @@ TEST(to_json_string_view_subview) { EXPECT_TRUE(result.is_string()); EXPECT_EQ(result.to_string(), "hello"); } + +TEST(from_json_optional_null_returns_nullopt_inner) { + const auto result{sourcemeta::core::from_json>( + sourcemeta::core::parse_json("null"))}; + EXPECT_TRUE(result.has_value()); + EXPECT_FALSE(result.value().has_value()); +} + +TEST(from_json_vector_element_mismatch_fails) { + const auto result{sourcemeta::core::from_json>( + sourcemeta::core::parse_json("[1]"))}; + EXPECT_FALSE(result.has_value()); +} + +TEST(from_json_vector_non_array_fails) { + const auto result{sourcemeta::core::from_json>( + sourcemeta::core::parse_json("5"))}; + EXPECT_FALSE(result.has_value()); +} + +TEST(from_json_map_value_mismatch_fails) { + const auto result{sourcemeta::core::from_json>( + sourcemeta::core::parse_json(R"({ "a": 1 })"))}; + EXPECT_FALSE(result.has_value()); +} + +TEST(from_json_map_non_object_fails) { + const auto result{sourcemeta::core::from_json>( + sourcemeta::core::parse_json("5"))}; + EXPECT_FALSE(result.has_value()); +} + +TEST(from_json_pair_non_array_fails) { + const auto result{sourcemeta::core::from_json>( + sourcemeta::core::parse_json("5"))}; + EXPECT_FALSE(result.has_value()); +} + +TEST(from_json_pair_wrong_size_fails) { + const auto result{sourcemeta::core::from_json>( + sourcemeta::core::parse_json("[1]"))}; + EXPECT_FALSE(result.has_value()); +} + +TEST(from_json_pair_first_mismatch_fails) { + const auto result{sourcemeta::core::from_json>( + sourcemeta::core::parse_json("[1, 2]"))}; + EXPECT_FALSE(result.has_value()); +} + +TEST(from_json_single_tuple_non_array_fails) { + const auto result{sourcemeta::core::from_json>( + sourcemeta::core::parse_json("5"))}; + EXPECT_FALSE(result.has_value()); +} + +TEST(from_json_single_tuple_wrong_size_fails) { + const auto result{sourcemeta::core::from_json>( + sourcemeta::core::parse_json("[1, 2]"))}; + EXPECT_FALSE(result.has_value()); +} + +TEST(from_json_tuple_non_array_fails) { + const auto result{sourcemeta::core::from_json>( + sourcemeta::core::parse_json("5"))}; + EXPECT_FALSE(result.has_value()); +} + +TEST(from_json_tuple_wrong_size_fails) { + const auto result{sourcemeta::core::from_json>( + sourcemeta::core::parse_json("[1]"))}; + EXPECT_FALSE(result.has_value()); +} + +TEST(from_json_variant_non_array_fails) { + const auto result{sourcemeta::core::from_json>( + sourcemeta::core::parse_json("5"))}; + EXPECT_FALSE(result.has_value()); +} + +TEST(from_json_variant_wrong_size_fails) { + const auto result{sourcemeta::core::from_json>( + sourcemeta::core::parse_json("[0]"))}; + EXPECT_FALSE(result.has_value()); +} + +TEST(from_json_variant_non_integer_index_fails) { + const auto result{sourcemeta::core::from_json>( + sourcemeta::core::parse_json(R"([ "x", 1 ])"))}; + EXPECT_FALSE(result.has_value()); +} + +TEST(from_json_variant_index_out_of_range_fails) { + const auto result{sourcemeta::core::from_json>( + sourcemeta::core::parse_json("[ 5, 0 ]"))}; + EXPECT_FALSE(result.has_value()); +} diff --git a/test/json/json_value_test.cc b/test/json/json_value_test.cc index 99ed5df7a..bcde80a37 100644 --- a/test/json/json_value_test.cc +++ b/test/json/json_value_test.cc @@ -4,6 +4,8 @@ #include // std::size_t #include // std::int64_t #include // std::reference_wrapper +#include // std::numeric_limits +#include // std::out_of_range #include // std::string #include // std::is_default_constructible, etc #include // std::unordered_map @@ -841,3 +843,83 @@ TEST(deep_copy_of_a_nested_object) { EXPECT_EQ(copy, document); EXPECT_EQ(copy.at(0).at("a").at("b").at(1).to_integer(), 2); } + +TEST(add_integer_overflow_promotes_to_decimal) { + const sourcemeta::core::JSON left{std::numeric_limits::min()}; + const sourcemeta::core::JSON right{-1}; + // The integer sum overflows int64, so the operator promotes to Decimal. The + // exact value would be -9223372036854775809, but the Decimal add currently + // rounds to the working precision (tracked in the decimal precision bug + // report). Pin the exact current output so a change in either direction is + // caught + const auto result{left + right}; + EXPECT_TRUE(result.is_decimal()); + EXPECT_EQ(result.to_decimal().to_string(), "-9.223372036854776e+18"); +} + +TEST(subtract_integer_overflow_promotes_to_decimal) { + const sourcemeta::core::JSON left{std::numeric_limits::max()}; + const sourcemeta::core::JSON right{-1}; + // See the note on the addition overflow test above. Exact value would be + // 9223372036854775808; pin the current rounded output + const auto result{left - right}; + EXPECT_TRUE(result.is_decimal()); + EXPECT_EQ(result.to_decimal().to_string(), "9.223372036854776e+18"); +} + +TEST(copy_self_assignment) { + sourcemeta::core::JSON value{ + sourcemeta::core::parse_json(R"JSON({ "a": [ 1, 2 ] })JSON")}; + const sourcemeta::core::JSON &alias{value}; + value = alias; + EXPECT_EQ(value, + sourcemeta::core::parse_json(R"JSON({ "a": [ 1, 2 ] })JSON")); +} + +TEST(move_self_assignment) { + sourcemeta::core::JSON value{ + sourcemeta::core::parse_json(R"JSON({ "a": [ 1, 2 ] })JSON")}; + // Route through a reference so the compiler cannot statically flag the + // self-move, while the runtime self-assignment branch is still exercised + sourcemeta::core::JSON &alias{value}; + value = std::move(alias); + EXPECT_EQ(value, + sourcemeta::core::parse_json(R"JSON({ "a": [ 1, 2 ] })JSON")); +} + +TEST(null_less_than_null_is_false) { + EXPECT_FALSE(sourcemeta::core::JSON{nullptr} < + sourcemeta::core::JSON{nullptr}); +} + +TEST(real_below_int64_range_throws_on_as_integer) { + const sourcemeta::core::JSON value{-1e300}; + try { + static_cast(value.as_integer()); + FAIL(); + } catch (const std::out_of_range &) { + } +} + +TEST(empty_on_string_returns_false) { + const sourcemeta::core::JSON value{"abc"}; + EXPECT_FALSE(value.empty()); +} + +TEST(fast_hash_of_real_below_int64_range) { + const sourcemeta::core::JSON value{-1e300}; + // A real outside the int64 range falls back to the real type hash + EXPECT_EQ(value.fast_hash(), 5); +} + +TEST(fast_hash_of_real_above_int64_range) { + const sourcemeta::core::JSON value{1e300}; + EXPECT_EQ(value.fast_hash(), 5); +} + +TEST(merge_object_key_over_non_object_source) { + auto target{sourcemeta::core::parse_json(R"JSON({ "k": { "x": 1 } })JSON")}; + const auto source{sourcemeta::core::parse_json(R"JSON({ "k": 2 })JSON")}; + target.merge(source.as_object()); + EXPECT_EQ(target.at("k").to_integer(), 2); +} diff --git a/test/jsonld/jsonld_expand_test.cc b/test/jsonld/jsonld_expand_test.cc index 763b67cba..d2d7b6d0a 100644 --- a/test/jsonld/jsonld_expand_test.cc +++ b/test/jsonld/jsonld_expand_test.cc @@ -562,3 +562,101 @@ TEST(self_referential_compact_term_via_prefix) { ])")}; EXPECT_EQ(result, expected); } + +TEST(type_array_with_non_string_item_is_rejected) { + const auto input = sourcemeta::core::parse_json(R"({ "@type": [ 123 ] })"); + try { + const auto result{sourcemeta::core::jsonld_expand(input)}; + FAIL(); + } catch (const sourcemeta::core::JSONLDError &error) { + EXPECT_STREQ(error.what(), "Invalid type value"); + } +} + +TEST(top_level_non_string_direction_is_rejected) { + const auto input = sourcemeta::core::parse_json(R"({ "@direction": 123 })"); + try { + const auto result{sourcemeta::core::jsonld_expand(input)}; + FAIL(); + } catch (const sourcemeta::core::JSONLDError &error) { + EXPECT_STREQ(error.what(), "Invalid base direction"); + } +} + +TEST(nest_array_item_not_an_object_is_rejected) { + const auto input = sourcemeta::core::parse_json(R"({ + "@context": { "@vocab": "http://example.com/" }, + "@nest": [ "x" ] + })"); + try { + const auto result{sourcemeta::core::jsonld_expand(input)}; + FAIL(); + } catch (const sourcemeta::core::JSONLDError &error) { + EXPECT_STREQ(error.what(), "Invalid @nest value"); + } +} + +TEST(nest_array_item_value_object_is_rejected) { + const auto input = sourcemeta::core::parse_json(R"({ + "@context": { "@vocab": "http://example.com/" }, + "@nest": [ { "@value": "x" } ] + })"); + try { + const auto result{sourcemeta::core::jsonld_expand(input)}; + FAIL(); + } catch (const sourcemeta::core::JSONLDError &error) { + EXPECT_STREQ(error.what(), "Invalid @nest value"); + } +} + +TEST(term_container_array_with_non_string_is_rejected) { + const auto input = sourcemeta::core::parse_json(R"({ + "@context": { "t": { "@id": "http://ex/t", "@container": [ 123 ] } }, + "t": "x" + })"); + try { + const auto result{sourcemeta::core::jsonld_expand(input)}; + FAIL(); + } catch (const sourcemeta::core::JSONLDError &error) { + EXPECT_STREQ(error.what(), "Invalid container mapping"); + } +} + +TEST(term_container_array_with_invalid_container_is_rejected) { + const auto input = sourcemeta::core::parse_json(R"({ + "@context": { "t": { "@id": "http://ex/t", "@container": [ "@bogus" ] } }, + "t": "x" + })"); + try { + const auto result{sourcemeta::core::jsonld_expand(input)}; + FAIL(); + } catch (const sourcemeta::core::JSONLDError &error) { + EXPECT_STREQ(error.what(), "Invalid container mapping"); + } +} + +TEST(term_direction_non_string_is_rejected) { + const auto input = sourcemeta::core::parse_json(R"({ + "@context": { "t": { "@id": "http://ex/t", "@direction": 123 } }, + "t": "x" + })"); + try { + const auto result{sourcemeta::core::jsonld_expand(input)}; + FAIL(); + } catch (const sourcemeta::core::JSONLDError &error) { + EXPECT_STREQ(error.what(), "Invalid base direction"); + } +} + +TEST(term_nest_non_string_is_rejected) { + const auto input = sourcemeta::core::parse_json(R"({ + "@context": { "t": { "@id": "http://ex/t", "@nest": 123 } }, + "t": "x" + })"); + try { + const auto result{sourcemeta::core::jsonld_expand(input)}; + FAIL(); + } catch (const sourcemeta::core::JSONLDError &error) { + EXPECT_STREQ(error.what(), "Invalid @nest value"); + } +} diff --git a/test/jsonld/jsonld_is_expanded_test.cc b/test/jsonld/jsonld_is_expanded_test.cc index b3c6961f8..91ec2f3e0 100644 --- a/test/jsonld/jsonld_is_expanded_test.cc +++ b/test/jsonld/jsonld_is_expanded_test.cc @@ -345,3 +345,233 @@ TEST(reverse_value_not_array) { ])"); EXPECT_FALSE(sourcemeta::core::jsonld_is_expanded(document)); } + +TEST(value_object_with_type_and_direction_no_language) { + const auto document = sourcemeta::core::parse_json(R"([ + { + "http://example.com/p": [ + { + "@value": "x", + "@type": "http://example.com/t", + "@direction": "ltr" + } + ] + } + ])"); + EXPECT_FALSE(sourcemeta::core::jsonld_is_expanded(document)); +} + +TEST(value_object_with_non_string_type) { + const auto document = sourcemeta::core::parse_json(R"([ + { + "http://example.com/p": [ + { + "@value": "x", + "@type": 123 + } + ] + } + ])"); + EXPECT_FALSE(sourcemeta::core::jsonld_is_expanded(document)); +} + +TEST(value_object_with_non_iri_type) { + const auto document = sourcemeta::core::parse_json(R"([ + { + "http://example.com/p": [ + { + "@value": "x", + "@type": "notaniri" + } + ] + } + ])"); + EXPECT_FALSE(sourcemeta::core::jsonld_is_expanded(document)); +} + +TEST(value_object_with_object_contents_and_type) { + const auto document = sourcemeta::core::parse_json(R"([ + { + "http://example.com/p": [ + { + "@value": { + "a": 1 + }, + "@type": "http://example.com/t" + } + ] + } + ])"); + EXPECT_FALSE(sourcemeta::core::jsonld_is_expanded(document)); +} + +TEST(value_object_with_array_contents_and_type) { + const auto document = sourcemeta::core::parse_json(R"([ + { + "http://example.com/p": [ + { + "@value": [ 1, 2 ], + "@type": "http://example.com/t" + } + ] + } + ])"); + EXPECT_FALSE(sourcemeta::core::jsonld_is_expanded(document)); +} + +TEST(value_object_with_array_contents_no_type) { + const auto document = sourcemeta::core::parse_json(R"([ + { + "http://example.com/p": [ + { + "@value": [ 1, 2 ] + } + ] + } + ])"); + EXPECT_FALSE(sourcemeta::core::jsonld_is_expanded(document)); +} + +TEST(value_object_with_non_string_language) { + const auto document = sourcemeta::core::parse_json(R"([ + { + "http://example.com/p": [ + { + "@value": "x", + "@language": 123 + } + ] + } + ])"); + EXPECT_FALSE(sourcemeta::core::jsonld_is_expanded(document)); +} + +TEST(value_object_with_language_but_non_string_value) { + const auto document = sourcemeta::core::parse_json(R"([ + { + "http://example.com/p": [ + { + "@value": 123, + "@language": "en" + } + ] + } + ])"); + EXPECT_FALSE(sourcemeta::core::jsonld_is_expanded(document)); +} + +TEST(value_object_with_non_string_direction) { + const auto document = sourcemeta::core::parse_json(R"([ + { + "http://example.com/p": [ + { + "@value": "x", + "@direction": 123 + } + ] + } + ])"); + EXPECT_FALSE(sourcemeta::core::jsonld_is_expanded(document)); +} + +TEST(value_object_with_direction_but_non_string_value) { + const auto document = sourcemeta::core::parse_json(R"([ + { + "http://example.com/p": [ + { + "@value": 123, + "@direction": "ltr" + } + ] + } + ])"); + EXPECT_FALSE(sourcemeta::core::jsonld_is_expanded(document)); +} + +TEST(value_object_with_non_string_index) { + const auto document = sourcemeta::core::parse_json(R"([ + { + "http://example.com/p": [ + { + "@value": "x", + "@index": 123 + } + ] + } + ])"); + EXPECT_FALSE(sourcemeta::core::jsonld_is_expanded(document)); +} + +TEST(node_with_non_string_index) { + const auto document = sourcemeta::core::parse_json(R"([ + { + "@index": 123 + } + ])"); + EXPECT_FALSE(sourcemeta::core::jsonld_is_expanded(document)); +} + +TEST(node_type_array_with_invalid_reference) { + const auto document = sourcemeta::core::parse_json(R"([ + { + "@type": [ "bad ref with spaces" ] + } + ])"); + EXPECT_FALSE(sourcemeta::core::jsonld_is_expanded(document)); +} + +TEST(node_with_non_array_graph) { + const auto document = sourcemeta::core::parse_json(R"([ + { + "@graph": "x" + } + ])"); + EXPECT_FALSE(sourcemeta::core::jsonld_is_expanded(document)); +} + +TEST(node_with_non_object_reverse) { + const auto document = sourcemeta::core::parse_json(R"([ + { + "@reverse": "x" + } + ])"); + EXPECT_FALSE(sourcemeta::core::jsonld_is_expanded(document)); +} + +TEST(node_reverse_with_non_iri_key) { + const auto document = sourcemeta::core::parse_json(R"([ + { + "@reverse": { + "notaterm": [] + } + } + ])"); + EXPECT_FALSE(sourcemeta::core::jsonld_is_expanded(document)); +} + +TEST(list_object_with_non_array_list) { + const auto document = sourcemeta::core::parse_json(R"([ + { + "http://example.com/p": [ + { + "@list": "x" + } + ] + } + ])"); + EXPECT_FALSE(sourcemeta::core::jsonld_is_expanded(document)); +} + +TEST(list_object_with_non_string_index) { + const auto document = sourcemeta::core::parse_json(R"([ + { + "http://example.com/p": [ + { + "@list": [], + "@index": 123 + } + ] + } + ])"); + EXPECT_FALSE(sourcemeta::core::jsonld_is_expanded(document)); +} diff --git a/test/jsonpath/jsonpath_filter_test.cc b/test/jsonpath/jsonpath_filter_test.cc index 8ab9f7c0a..a7f849eca 100644 --- a/test/jsonpath/jsonpath_filter_test.cc +++ b/test/jsonpath/jsonpath_filter_test.cc @@ -580,3 +580,39 @@ TEST(jsonpath_filter_negated_search) { EXPECT_EQ(nodes.size(), 1); EXPECT_EQ(nodes.at(0).value->at("a").to_string(), "xyz"); } + +TEST(jsonpath_filter_singular_index_comparison) { + const auto document{ + sourcemeta::core::parse_json(R"JSON([ [ 1, 9 ], [ 5, 9 ] ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?@[0]==1]"}; + const auto nodes{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 1); + EXPECT_EQ(nodes.at(0).value->at(0).to_integer(), 1); +} + +TEST(jsonpath_filter_singular_negative_index_comparison) { + const auto document{ + sourcemeta::core::parse_json(R"JSON([ [ 1, 9 ], [ 5, 3 ] ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?@[-1]==9]"}; + const auto nodes{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 1); + EXPECT_EQ(nodes.at(0).value->at(1).to_integer(), 9); +} + +TEST(jsonpath_filter_match_over_non_string_subject) { + const auto document{ + sourcemeta::core::parse_json(R"JSON([ { "a": 5 }, { "a": "ab" } ])JSON")}; + const sourcemeta::core::JSONPath path{R"($[?match(@.a, "ab")])"}; + const auto nodes{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 1); + EXPECT_EQ(nodes.at(0).value->at("a").to_string(), "ab"); +} + +TEST(jsonpath_filter_search_over_non_string_subject) { + const auto document{sourcemeta::core::parse_json( + R"JSON([ { "a": 5 }, { "a": "xby" } ])JSON")}; + const sourcemeta::core::JSONPath path{R"($[?search(@.a, "b")])"}; + const auto nodes{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 1); + EXPECT_EQ(nodes.at(0).value->at("a").to_string(), "xby"); +} diff --git a/test/jsonpath/jsonpath_parse_test.cc b/test/jsonpath/jsonpath_parse_test.cc index 6dbe36ade..3455bfce9 100644 --- a/test/jsonpath/jsonpath_parse_test.cc +++ b/test/jsonpath/jsonpath_parse_test.cc @@ -437,6 +437,24 @@ TEST(jsonpath_parse_error_value_function_call_as_test){ TEST(jsonpath_parse_error_negated_value_function_as_test){ EXPECT_JSONPATH_PARSE_ERROR("$[?!length(@.a)]", 16)} -TEST(jsonpath_parse_error_negated_literal) { - EXPECT_JSONPATH_PARSE_ERROR("$[?!1]", 5) +TEST(jsonpath_parse_error_negated_literal){ + EXPECT_JSONPATH_PARSE_ERROR("$[?!1]", 5)} + +TEST(jsonpath_parse_error_lone_high_surrogate_at_string_end){ + EXPECT_JSONPATH_PARSE_ERROR("$[\"\\uD800\"]", 10)} + +TEST(jsonpath_parse_error_high_surrogate_then_non_low){ + EXPECT_JSONPATH_PARSE_ERROR("$[\"\\uD800\\u0041\"]", 16)} + +TEST(jsonpath_parse_error_fraction_without_digit){ + EXPECT_JSONPATH_PARSE_ERROR("$[?@.a==1.]", 11)} + +TEST(jsonpath_parse_error_exponent_without_digit){ + EXPECT_JSONPATH_PARSE_ERROR("$[?@.a==1e]", 11)} + +TEST(jsonpath_parse_error_integer_above_maximum){ + EXPECT_JSONPATH_PARSE_ERROR("$[?@.a==999999999999999999]", 27)} + +TEST(jsonpath_parse_error_slice_step_non_digit) { + EXPECT_JSONPATH_PARSE_ERROR("$[1:2:x]", 7) } diff --git a/test/jsonpointer/jsonpointer_pointer_test.cc b/test/jsonpointer/jsonpointer_pointer_test.cc index 04dece8fb..d37a40ace 100644 --- a/test/jsonpointer/jsonpointer_pointer_test.cc +++ b/test/jsonpointer/jsonpointer_pointer_test.cc @@ -347,3 +347,29 @@ TEST(to_pointer_from_json_string) { const auto pointer{sourcemeta::core::to_pointer(document)}; EXPECT_EQ(pointer, sourcemeta::core::to_pointer("/foo/0")); } + +TEST(push_back_empty_pointer_is_noop) { + sourcemeta::core::Pointer target{"foo"}; + const sourcemeta::core::Pointer other; + target.push_back(other); + EXPECT_EQ(target, sourcemeta::core::Pointer{"foo"}); +} + +TEST(push_back_single_token_pointer) { + sourcemeta::core::Pointer target{"foo"}; + const sourcemeta::core::Pointer other{"bar"}; + target.push_back(other); + EXPECT_EQ(target, (sourcemeta::core::Pointer{"foo", "bar"})); +} + +TEST(push_back_empty_rvalue_pointer_is_noop) { + sourcemeta::core::Pointer target{"foo"}; + target.push_back(sourcemeta::core::Pointer{}); + EXPECT_EQ(target, sourcemeta::core::Pointer{"foo"}); +} + +TEST(push_back_single_token_rvalue_pointer) { + sourcemeta::core::Pointer target{"foo"}; + target.push_back(sourcemeta::core::Pointer{"bar"}); + EXPECT_EQ(target, (sourcemeta::core::Pointer{"foo", "bar"})); +} diff --git a/test/oauth/oauth_dpop_test.cc b/test/oauth/oauth_dpop_test.cc index 91a2aad72..25e4a9ae8 100644 --- a/test/oauth/oauth_dpop_test.cc +++ b/test/oauth/oauth_dpop_test.cc @@ -1134,3 +1134,13 @@ TEST(replay_store_saturates_a_window_beyond_the_clock) { EXPECT_FALSE(store.check_and_insert("id", "https://server.example.com/token", FIXED_TIME, window)); } + +TEST(proof_thumbprint_rejects_non_object_jwk_header) { + const std::string proof{ + "eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjoibm90LWFuLW9iamVjdCJ9" + ".eyJodHUiOiJodHRwczovL3JzLmV4YW1wbGUuY29tL3giLCJodG0iOiJQT1NUIiwianRpIjo" + "i" + "YWJjIiwiaWF0IjoxNzAwMDAwMDAwfQ.c2ln"}; + EXPECT_FALSE( + sourcemeta::core::oauth_dpop_proof_thumbprint(proof).has_value()); +} diff --git a/test/oauth/oauth_metadata_test.cc b/test/oauth/oauth_metadata_test.cc index 694630242..03e3bd5a3 100644 --- a/test/oauth/oauth_metadata_test.cc +++ b/test/oauth/oauth_metadata_test.cc @@ -2661,3 +2661,45 @@ TEST(server_metadata_accepts_a_protected_resource_entry_with_a_query) { EXPECT_FALSE(metadata.value().supports_protected_resource( "https://nonexistent.example.invalid")); } + +TEST(server_metadata_rejects_non_string_issuer) { + EXPECT_FALSE(sourcemeta::core::OAuthServerMetadata::from( + sourcemeta::core::parse_json(R"JSON({ "issuer": 123 })JSON"), + "https://as.example.com") + .has_value()); +} + +TEST(server_metadata_rejects_non_array_response_types) { + EXPECT_FALSE(sourcemeta::core::OAuthServerMetadata::from( + sourcemeta::core::parse_json(R"JSON({ + "issuer": "https://as.example.com", + "response_types_supported": 123 + })JSON"), + "https://as.example.com") + .has_value()); +} + +TEST(server_metadata_explicit_false_pushed_authorization) { + const auto metadata{sourcemeta::core::OAuthServerMetadata::from( + sourcemeta::core::parse_json(R"JSON({ + "issuer": "https://as.example.com", + "response_types_supported": [ "code" ], + "require_pushed_authorization_requests": false + })JSON"), + "https://as.example.com")}; + EXPECT_TRUE(metadata.has_value()); + EXPECT_FALSE(metadata.value().require_pushed_authorization_requests()); +} + +TEST(server_metadata_explicit_false_iss_parameter) { + const auto metadata{sourcemeta::core::OAuthServerMetadata::from( + sourcemeta::core::parse_json(R"JSON({ + "issuer": "https://as.example.com", + "response_types_supported": [ "code" ], + "authorization_response_iss_parameter_supported": false + })JSON"), + "https://as.example.com")}; + EXPECT_TRUE(metadata.has_value()); + EXPECT_FALSE( + metadata.value().authorization_response_iss_parameter_supported()); +} diff --git a/test/oauth/oauth_resource_metadata_test.cc b/test/oauth/oauth_resource_metadata_test.cc index e93c4fdff..3aef778bc 100644 --- a/test/oauth/oauth_resource_metadata_test.cc +++ b/test/oauth/oauth_resource_metadata_test.cc @@ -2327,3 +2327,25 @@ TEST(make_resource_metadata_accepts_a_cleartext_tos_uri) { })JSON")}; EXPECT_EQ(document.value(), expected); } + +TEST(resource_metadata_explicit_false_dpop_bound) { + const auto metadata{sourcemeta::core::OAuthResourceMetadata::from( + sourcemeta::core::parse_json(R"JSON({ + "resource": "https://rs.example.com", + "dpop_bound_access_tokens_required": false + })JSON"), + "https://rs.example.com")}; + EXPECT_TRUE(metadata.has_value()); + EXPECT_FALSE(metadata.value().dpop_bound_access_tokens_required()); +} + +TEST(resource_metadata_explicit_false_tls_client_cert) { + const auto metadata{sourcemeta::core::OAuthResourceMetadata::from( + sourcemeta::core::parse_json(R"JSON({ + "resource": "https://rs.example.com", + "tls_client_certificate_bound_access_tokens": false + })JSON"), + "https://rs.example.com")}; + EXPECT_TRUE(metadata.has_value()); + EXPECT_FALSE(metadata.value().tls_client_certificate_bound_access_tokens()); +} diff --git a/test/oidc/oidc_authentication_test.cc b/test/oidc/oidc_authentication_test.cc index 8e8d7e0a9..47b046f6f 100644 --- a/test/oidc/oidc_authentication_test.cc +++ b/test/oidc/oidc_authentication_test.cc @@ -610,3 +610,42 @@ TEST(parse_rejects_a_duplicated_parameter) { EXPECT_FALSE(sourcemeta::core::oidc_parse_authentication_request( "client_id=a&client_id=b&scope=openid", storage, request)); } + +TEST(parse_populates_all_optional_openid_parameters) { + std::string storage; + sourcemeta::core::OIDCAuthenticationRequest request; + EXPECT_TRUE(sourcemeta::core::oidc_parse_authentication_request( + "client_id=x&redirect_uri=https%3A%2F%2Fcb.example.com&scope=openid&" + "response_type=code&display=page&ui_locales=en-US&id_token_hint=abc&" + "login_hint=user&claims=%7B%7D&request=jwt&response_mode=query", + storage, request, sourcemeta::core::OIDCProfile::Legacy)); + EXPECT_EQ(request.display, "page"); + EXPECT_EQ(request.ui_locales, "en-US"); + EXPECT_EQ(request.id_token_hint, "abc"); + EXPECT_EQ(request.login_hint, "user"); + EXPECT_EQ(request.claims, "{}"); + EXPECT_EQ(request.request, "jwt"); + EXPECT_EQ(request.response_mode, "query"); +} + +TEST(parse_accepts_code_challenge_with_underscore) { + std::string storage; + sourcemeta::core::OIDCAuthenticationRequest request; + EXPECT_TRUE(sourcemeta::core::oidc_parse_authentication_request( + "client_id=x&redirect_uri=https%3A%2F%2Fcb.example.com&scope=openid&" + "response_type=code&code_challenge_method=S256&" + "code_challenge=abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOP_", + storage, request, sourcemeta::core::OIDCProfile::Strict)); + EXPECT_EQ(request.code_challenge, + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOP_"); +} + +TEST(parse_drops_offline_access_scope_with_prompt_none) { + std::string storage; + sourcemeta::core::OIDCAuthenticationRequest request; + EXPECT_TRUE(sourcemeta::core::oidc_parse_authentication_request( + "client_id=x&redirect_uri=https%3A%2F%2Fcb.example.com&" + "scope=openid%20offline_access&response_type=code&prompt=none", + storage, request, sourcemeta::core::OIDCProfile::Legacy)); + EXPECT_EQ(request.scope, "openid"); +} diff --git a/test/oidc/oidc_discovery_test.cc b/test/oidc/oidc_discovery_test.cc index 68a342aa6..c32f1cf00 100644 --- a/test/oidc/oidc_discovery_test.cc +++ b/test/oidc/oidc_discovery_test.cc @@ -272,3 +272,75 @@ TEST(webfinger_issuer_skips_an_invalid_matching_link) { EXPECT_TRUE(issuer.has_value()); EXPECT_EQ(issuer.value(), "https://example.com"); } + +TEST(webfinger_request_rejects_empty_scheme_candidate) { + EXPECT_FALSE(sourcemeta::core::oidc_webfinger_request(":8080").has_value()); +} + +TEST(webfinger_request_rejects_scheme_first_char_non_alpha) { + EXPECT_FALSE(sourcemeta::core::oidc_webfinger_request("1:foo").has_value()); +} + +TEST(webfinger_request_rejects_invalid_scheme_character) { + EXPECT_FALSE(sourcemeta::core::oidc_webfinger_request("a b:x").has_value()); +} + +TEST(webfinger_request_rejects_acct_without_at) { + EXPECT_FALSE( + sourcemeta::core::oidc_webfinger_request("acct:foo").has_value()); +} + +TEST(webfinger_request_rejects_acct_with_empty_host) { + EXPECT_FALSE(sourcemeta::core::oidc_webfinger_request("foo@").has_value()); +} + +TEST(webfinger_issuer_rejects_non_object) { + EXPECT_FALSE( + sourcemeta::core::oidc_webfinger_issuer(sourcemeta::core::JSON{"string"}) + .has_value()); +} + +TEST(webfinger_issuer_rejects_non_array_links) { + const auto descriptor{ + sourcemeta::core::parse_json(R"JSON({ "links": "x" })JSON")}; + EXPECT_FALSE(sourcemeta::core::oidc_webfinger_issuer(descriptor).has_value()); +} + +TEST(webfinger_issuer_rejects_non_object_link) { + const auto descriptor{ + sourcemeta::core::parse_json(R"JSON({ "links": [ 123 ] })JSON")}; + EXPECT_FALSE(sourcemeta::core::oidc_webfinger_issuer(descriptor).has_value()); +} + +TEST(webfinger_issuer_skips_link_without_rel) { + const auto descriptor{sourcemeta::core::parse_json(R"JSON({ + "links": [ { "href": "https://issuer.example.com" } ] + })JSON")}; + EXPECT_FALSE(sourcemeta::core::oidc_webfinger_issuer(descriptor).has_value()); +} + +TEST(webfinger_issuer_skips_link_with_non_string_rel) { + const auto descriptor{sourcemeta::core::parse_json(R"JSON({ + "links": [ { "rel": 123, "href": "https://issuer.example.com" } ] + })JSON")}; + EXPECT_FALSE(sourcemeta::core::oidc_webfinger_issuer(descriptor).has_value()); +} + +TEST(webfinger_issuer_skips_link_with_other_rel) { + const auto descriptor{sourcemeta::core::parse_json(R"JSON({ + "links": [ { "rel": "other", "href": "https://issuer.example.com" } ] + })JSON")}; + EXPECT_FALSE(sourcemeta::core::oidc_webfinger_issuer(descriptor).has_value()); +} + +TEST(webfinger_issuer_rejects_matching_rel_with_non_string_href) { + const auto descriptor{sourcemeta::core::parse_json(R"JSON({ + "links": [ + { + "rel": "http://openid.net/specs/connect/1.0/issuer", + "href": 123 + } + ] + })JSON")}; + EXPECT_FALSE(sourcemeta::core::oidc_webfinger_issuer(descriptor).has_value()); +} diff --git a/test/oidc/oidc_registration_test.cc b/test/oidc/oidc_registration_test.cc index d2c973c83..91acc12ba 100644 --- a/test/oidc/oidc_registration_test.cc +++ b/test/oidc/oidc_registration_test.cc @@ -448,3 +448,92 @@ TEST(encrypted_and_userinfo_algorithm_accessors) { EXPECT_TRUE(metadata.value().userinfo_signed_response_alg().has_value()); EXPECT_EQ(metadata.value().userinfo_signed_response_alg().value(), "ES256"); } + +TEST(from_rejects_non_array_redirect_uris) { + auto document{ + sourcemeta::core::parse_json(R"JSON({ "redirect_uris": "x" })JSON")}; + EXPECT_FALSE(sourcemeta::core::OIDCClientMetadata::from(std::move(document)) + .has_value()); +} + +TEST(from_rejects_non_string_redirect_uri_element) { + auto document{ + sourcemeta::core::parse_json(R"JSON({ "redirect_uris": [ 123 ] })JSON")}; + EXPECT_FALSE(sourcemeta::core::OIDCClientMetadata::from(std::move(document)) + .has_value()); +} + +TEST(from_rejects_non_string_subject_type) { + auto document{sourcemeta::core::parse_json(R"JSON({ + "redirect_uris": [ "https://cb.example.com" ], + "subject_type": 123 + })JSON")}; + EXPECT_FALSE(sourcemeta::core::OIDCClientMetadata::from(std::move(document)) + .has_value()); +} + +TEST(from_rejects_non_string_id_token_signed_alg) { + auto document{sourcemeta::core::parse_json(R"JSON({ + "redirect_uris": [ "https://cb.example.com" ], + "id_token_signed_response_alg": 123 + })JSON")}; + EXPECT_FALSE(sourcemeta::core::OIDCClientMetadata::from(std::move(document)) + .has_value()); +} + +TEST(from_rejects_non_string_id_token_encrypted_alg) { + auto document{sourcemeta::core::parse_json(R"JSON({ + "redirect_uris": [ "https://cb.example.com" ], + "id_token_encrypted_response_alg": 123 + })JSON")}; + EXPECT_FALSE(sourcemeta::core::OIDCClientMetadata::from(std::move(document)) + .has_value()); +} + +TEST(from_rejects_non_string_userinfo_signed_alg) { + auto document{sourcemeta::core::parse_json(R"JSON({ + "redirect_uris": [ "https://cb.example.com" ], + "userinfo_signed_response_alg": 123 + })JSON")}; + EXPECT_FALSE(sourcemeta::core::OIDCClientMetadata::from(std::move(document)) + .has_value()); +} + +TEST(from_rejects_non_string_sector_identifier_uri) { + auto document{sourcemeta::core::parse_json(R"JSON({ + "redirect_uris": [ "https://cb.example.com" ], + "sector_identifier_uri": 123 + })JSON")}; + EXPECT_FALSE(sourcemeta::core::OIDCClientMetadata::from(std::move(document)) + .has_value()); +} + +TEST(from_rejects_non_string_initiate_login_uri) { + auto document{sourcemeta::core::parse_json(R"JSON({ + "redirect_uris": [ "https://cb.example.com" ], + "initiate_login_uri": 123 + })JSON")}; + EXPECT_FALSE(sourcemeta::core::OIDCClientMetadata::from(std::move(document)) + .has_value()); +} + +TEST(from_accepts_valid_initiate_login_uri) { + auto document{sourcemeta::core::parse_json(R"JSON({ + "redirect_uris": [ "https://cb.example.com" ], + "initiate_login_uri": "https://login.example.com" + })JSON")}; + const auto metadata{ + sourcemeta::core::OIDCClientMetadata::from(std::move(document))}; + EXPECT_TRUE(metadata.has_value()); +} + +TEST(require_auth_time_false_accessor) { + auto document{sourcemeta::core::parse_json(R"JSON({ + "redirect_uris": [ "https://cb.example.com" ], + "require_auth_time": false + })JSON")}; + const auto metadata{ + sourcemeta::core::OIDCClientMetadata::from(std::move(document))}; + EXPECT_TRUE(metadata.has_value()); + EXPECT_FALSE(metadata.value().require_auth_time()); +} diff --git a/test/regex/regex_rfc9485_matches_test.cc b/test/regex/regex_rfc9485_matches_test.cc index 5565dd4de..20c30643d 100644 --- a/test/regex/regex_rfc9485_matches_test.cc +++ b/test/regex/regex_rfc9485_matches_test.cc @@ -1127,3 +1127,76 @@ TEST(rfc9485_matches_class_double_dash) { EXPECT_TRUE(sourcemeta::core::matches(regex.value(), "-")); EXPECT_FALSE(sourcemeta::core::matches(regex.value(), "a")); } + +TEST(iregexp_mark_category) { + EXPECT_TRUE(sourcemeta::core::to_regex( + "\\p{M}", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(iregexp_punctuation_category) { + EXPECT_TRUE(sourcemeta::core::to_regex( + "\\p{P}", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(iregexp_separator_category) { + EXPECT_TRUE(sourcemeta::core::to_regex( + "\\p{Z}", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(iregexp_symbol_category) { + EXPECT_TRUE(sourcemeta::core::to_regex( + "\\p{S}", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(iregexp_number_category_single_letter) { + EXPECT_TRUE(sourcemeta::core::to_regex( + "\\p{N}", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(iregexp_other_category_single_letter) { + EXPECT_TRUE(sourcemeta::core::to_regex( + "\\p{C}", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(iregexp_letter_subcategory_invalid) { + EXPECT_FALSE(sourcemeta::core::to_regex( + "\\p{Lx}", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(iregexp_property_at_end_incomplete) { + EXPECT_FALSE( + sourcemeta::core::to_regex("\\p", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(iregexp_malformed_utf8_pattern) { + EXPECT_FALSE( + sourcemeta::core::to_regex(std::string_view{"\xC3\x28", 2}, + sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(iregexp_unclosed_nested_class) { + EXPECT_FALSE( + sourcemeta::core::to_regex("[a[", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(iregexp_quantifier_max_overflow) { + EXPECT_FALSE(sourcemeta::core::to_regex( + "a{0,9999999999}", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} + +TEST(iregexp_quantifier_junk_after_digits) { + EXPECT_FALSE(sourcemeta::core::to_regex( + "a{1x}", sourcemeta::core::RegexDialect::IRegexp) + .has_value()); +} diff --git a/test/regex/regex_to_regex_test.cc b/test/regex/regex_to_regex_test.cc index 593a69732..7f909238b 100644 --- a/test/regex/regex_to_regex_test.cc +++ b/test/regex/regex_to_regex_test.cc @@ -91,3 +91,15 @@ TEST(oversized_unicode_brace_escape_does_not_overflow) { const auto regex{sourcemeta::core::to_regex("\\u{FFFFFFFFFFFFFFFFFFFFFFFF}")}; EXPECT_FALSE(regex.has_value()); } + +TEST(permissive_named_group_uppercase_start) { + EXPECT_TRUE(sourcemeta::core::to_regex("(?x)").has_value()); +} + +TEST(permissive_named_group_underscore_start) { + EXPECT_TRUE(sourcemeta::core::to_regex("(?<_a>x)").has_value()); +} + +TEST(permissive_negated_unicode_property) { + EXPECT_TRUE(sourcemeta::core::to_regex("\\P{Letter}").has_value()); +} diff --git a/test/time/imf_fixdate_test.cc b/test/time/imf_fixdate_test.cc index 78786fcb5..06d368d08 100644 --- a/test/time/imf_fixdate_test.cc +++ b/test/time/imf_fixdate_test.cc @@ -297,3 +297,123 @@ TEST(format_year_below_1000_pads_to_four_digits) { EXPECT_EQ(sourcemeta::core::from_imf_fixdate(formatted), point); } } + +TEST(rejects_separator_at_3) { + EXPECT_FALSE( + sourcemeta::core::from_imf_fixdate("Sun. 06 Nov 1994 08:49:37 GMT") + .has_value()); +} + +TEST(rejects_separator_at_4) { + EXPECT_FALSE( + sourcemeta::core::from_imf_fixdate("Sun,_06 Nov 1994 08:49:37 GMT") + .has_value()); +} + +TEST(rejects_separator_at_7) { + EXPECT_FALSE( + sourcemeta::core::from_imf_fixdate("Sun, 06_Nov 1994 08:49:37 GMT") + .has_value()); +} + +TEST(rejects_separator_at_11) { + EXPECT_FALSE( + sourcemeta::core::from_imf_fixdate("Sun, 06 Nov_1994 08:49:37 GMT") + .has_value()); +} + +TEST(rejects_separator_at_16) { + EXPECT_FALSE( + sourcemeta::core::from_imf_fixdate("Sun, 06 Nov 1994_08:49:37 GMT") + .has_value()); +} + +TEST(rejects_separator_at_19) { + EXPECT_FALSE( + sourcemeta::core::from_imf_fixdate("Sun, 06 Nov 1994 08.49:37 GMT") + .has_value()); +} + +TEST(rejects_separator_at_22) { + EXPECT_FALSE( + sourcemeta::core::from_imf_fixdate("Sun, 06 Nov 1994 08:49.37 GMT") + .has_value()); +} + +TEST(rejects_separator_at_25) { + EXPECT_FALSE( + sourcemeta::core::from_imf_fixdate("Sun, 06 Nov 1994 08:49:37_GMT") + .has_value()); +} + +TEST(rejects_non_digit_at_5) { + EXPECT_FALSE( + sourcemeta::core::from_imf_fixdate("Sun, X6 Nov 1994 08:49:37 GMT") + .has_value()); +} + +TEST(rejects_non_digit_at_6) { + EXPECT_FALSE( + sourcemeta::core::from_imf_fixdate("Sun, 0X Nov 1994 08:49:37 GMT") + .has_value()); +} + +TEST(rejects_non_digit_at_12) { + EXPECT_FALSE( + sourcemeta::core::from_imf_fixdate("Sun, 06 Nov X994 08:49:37 GMT") + .has_value()); +} + +TEST(rejects_non_digit_at_13) { + EXPECT_FALSE( + sourcemeta::core::from_imf_fixdate("Sun, 06 Nov 1X94 08:49:37 GMT") + .has_value()); +} + +TEST(rejects_non_digit_at_14) { + EXPECT_FALSE( + sourcemeta::core::from_imf_fixdate("Sun, 06 Nov 19X4 08:49:37 GMT") + .has_value()); +} + +TEST(rejects_non_digit_at_15) { + EXPECT_FALSE( + sourcemeta::core::from_imf_fixdate("Sun, 06 Nov 199X 08:49:37 GMT") + .has_value()); +} + +TEST(rejects_non_digit_at_17) { + EXPECT_FALSE( + sourcemeta::core::from_imf_fixdate("Sun, 06 Nov 1994 X8:49:37 GMT") + .has_value()); +} + +TEST(rejects_non_digit_at_18) { + EXPECT_FALSE( + sourcemeta::core::from_imf_fixdate("Sun, 06 Nov 1994 0X:49:37 GMT") + .has_value()); +} + +TEST(rejects_non_digit_at_20) { + EXPECT_FALSE( + sourcemeta::core::from_imf_fixdate("Sun, 06 Nov 1994 08:X9:37 GMT") + .has_value()); +} + +TEST(rejects_non_digit_at_21) { + EXPECT_FALSE( + sourcemeta::core::from_imf_fixdate("Sun, 06 Nov 1994 08:4X:37 GMT") + .has_value()); +} + +TEST(rejects_non_digit_at_23) { + EXPECT_FALSE( + sourcemeta::core::from_imf_fixdate("Sun, 06 Nov 1994 08:49:X7 GMT") + .has_value()); +} + +TEST(rejects_non_digit_at_24) { + EXPECT_FALSE( + sourcemeta::core::from_imf_fixdate("Sun, 06 Nov 1994 08:49:3X GMT") + .has_value()); +} diff --git a/test/uri/uri_parse_test.cc b/test/uri/uri_parse_test.cc index 8de6b2934..eee98b2f9 100644 --- a/test/uri/uri_parse_test.cc +++ b/test/uri/uri_parse_test.cc @@ -947,3 +947,25 @@ TEST(uri_constructor_rejects_non_ascii) { EXPECT_EQ(error.column(), 24); } } + +TEST(ipvfuture_uppercase_marker) { + const sourcemeta::core::URI uri{"http://[V1.a]/"}; + EXPECT_TRUE(uri.host().has_value()); + EXPECT_EQ(uri.host().value(), "V1.a"); +} + +TEST(ipvfuture_missing_version_hexdigit) { + EXPECT_FALSE(sourcemeta::core::URI::is_uri("http://[v.a]/")); +} + +TEST(ipvfuture_missing_dot_separator) { + EXPECT_FALSE(sourcemeta::core::URI::is_uri("http://[v1x]/")); +} + +TEST(ipvfuture_missing_content_after_dot) { + EXPECT_FALSE(sourcemeta::core::URI::is_uri("http://[v1.]/")); +} + +TEST(ipvfuture_invalid_content_character) { + EXPECT_FALSE(sourcemeta::core::URI::is_uri("http://[v1.a%]/")); +} diff --git a/test/uri/uri_path_test.cc b/test/uri/uri_path_test.cc index 7e84bcac1..2b7c3b282 100644 --- a/test/uri/uri_path_test.cc +++ b/test/uri/uri_path_test.cc @@ -990,3 +990,82 @@ TEST(append_path_that_normalizes_away) { uri.append_path(".."); EXPECT_EQ(uri.recompose(), ""); } + +TEST(set_path_second_percent_digit_invalid) { + sourcemeta::core::URI uri{"http://example.com"}; + try { + uri.path("/%Ag"); + FAIL(); + } catch (const sourcemeta::core::URIError &error) { + EXPECT_STREQ( + error.what(), + "You cannot set a path with an invalid percent-encoded sequence"); + } +} + +TEST(set_path_with_sub_delim) { + sourcemeta::core::URI uri{"http://example.com"}; + uri.path("/a!b"); + EXPECT_EQ(uri.path().value(), "/a!b"); +} + +TEST(set_path_with_colon) { + sourcemeta::core::URI uri{"http://example.com"}; + uri.path("/a:b"); + EXPECT_EQ(uri.path().value(), "/a:b"); +} + +TEST(set_path_with_at_sign) { + sourcemeta::core::URI uri{"http://example.com"}; + uri.path("/a@b"); + EXPECT_EQ(uri.path().value(), "/a@b"); +} + +TEST(set_rvalue_path_with_host_no_scheme) { + sourcemeta::core::URI uri{"//example.com"}; + uri.path(std::string{"foo"}); + EXPECT_EQ(uri.recompose(), "//example.com/foo"); +} + +TEST(set_const_path_with_host_no_scheme) { + sourcemeta::core::URI uri{"//example.com"}; + const std::string value{"foo"}; + uri.path(value); + EXPECT_EQ(uri.recompose(), "//example.com/foo"); +} + +TEST(append_path_view_with_host_no_scheme) { + sourcemeta::core::URI uri{"//example.com"}; + uri.append_path("foo"); + EXPECT_EQ(uri.recompose(), "//example.com/foo"); +} + +TEST(append_path_reference_with_host_no_scheme) { + sourcemeta::core::URI uri{"//example.com"}; + uri.append_path(sourcemeta::core::URI{"rel"}); + EXPECT_EQ(uri.recompose(), "//example.com/rel"); +} + +TEST(append_path_rejects_reference_with_userinfo) { + sourcemeta::core::URI uri{"http://x"}; + try { + uri.append_path(sourcemeta::core::URI{"//user@host"}); + FAIL(); + } catch (const sourcemeta::core::URIError &error) { + EXPECT_STREQ( + error.what(), + "Cannot append a URI as a path that contains a scheme or authority"); + } +} + +TEST(append_path_rejects_reference_with_fragment) { + sourcemeta::core::URI uri{"http://x"}; + try { + uri.append_path(sourcemeta::core::URI{"#frag"}); + FAIL(); + } catch (const sourcemeta::core::URIError &error) { + EXPECT_STREQ( + error.what(), + "Cannot append a URI as a path that contains a query or fragment"); + } +} diff --git a/test/uri/uri_relative_to_test.cc b/test/uri/uri_relative_to_test.cc index 49b8119e6..4b1ebf998 100644 --- a/test/uri/uri_relative_to_test.cc +++ b/test/uri/uri_relative_to_test.cc @@ -370,3 +370,15 @@ TEST(authority_no_path_base_query_target_none) { uri.relative_to(base); EXPECT_EQ(uri.recompose(), "https://www.example.com"); } + +TEST(relative_to_same_path_with_query_in_base) { + sourcemeta::core::URI target{"schema:foo?bar=1"}; + target.relative_to(sourcemeta::core::URI{"schema:foo"}); + EXPECT_EQ(target.recompose(), "?bar=1"); +} + +TEST(relative_to_slashless_base_path) { + sourcemeta::core::URI target{"schema:bar"}; + target.relative_to(sourcemeta::core::URI{"schema:foo"}); + EXPECT_EQ(target.recompose(), "schema:bar"); +} diff --git a/test/uritemplate/uritemplate_parse_error_test.cc b/test/uritemplate/uritemplate_parse_error_test.cc index caf4fb288..5659175a1 100644 --- a/test/uritemplate/uritemplate_parse_error_test.cc +++ b/test/uritemplate/uritemplate_parse_error_test.cc @@ -199,3 +199,7 @@ TEST(modifier_without_varname) { EXPECT_URITEMPLATE_PARSE_ERROR("{:1}", 2); } TEST(two_prefix_modifiers) { EXPECT_URITEMPLATE_PARSE_ERROR("{a:1:2}", 5); } TEST(varname_after_explode) { EXPECT_URITEMPLATE_PARSE_ERROR("{a*b}", 4); } + +TEST(prefix_colon_not_followed_by_positive_digit) { + EXPECT_URITEMPLATE_PARSE_ERROR("{a:x}", 4); +} diff --git a/test/yaml/yaml_parse_test.cc b/test/yaml/yaml_parse_test.cc index 02070461b..0e671d2cd 100644 --- a/test/yaml/yaml_parse_test.cc +++ b/test/yaml/yaml_parse_test.cc @@ -3,8 +3,9 @@ #include #include -#include // std::istringstream -#include // std::string +#include // std::cerr +#include // std::istringstream +#include // std::string TEST(deeply_nested_flow_is_rejected) { const std::string input{std::string(2000, '[') + std::string(2000, ']')}; @@ -1739,3 +1740,290 @@ TEST(alias_quoted_keyword_key_is_preserved) { R"JSON([ "null", { "null": "value" } ])JSON")}; EXPECT_EQ(result, expected); } + +TEST(escape_vertical_tab) { + const std::string input{"\"\\v\""}; + const auto result{sourcemeta::core::parse_yaml(input)}; + EXPECT_TRUE(result.is_string()); + EXPECT_EQ(result.to_string(), std::string("\x0b", 1)); +} + +TEST(escape_form_feed) { + const std::string input{"\"\\f\""}; + const auto result{sourcemeta::core::parse_yaml(input)}; + EXPECT_TRUE(result.is_string()); + EXPECT_EQ(result.to_string(), std::string("\x0c", 1)); +} + +TEST(escape_line_separator) { + const std::string input{"\"\\L\""}; + const auto result{sourcemeta::core::parse_yaml(input)}; + EXPECT_TRUE(result.is_string()); + EXPECT_EQ(result.to_string(), std::string("\xe2\x80\xa8", 3)); +} + +TEST(escape_paragraph_separator) { + const std::string input{"\"\\P\""}; + const auto result{sourcemeta::core::parse_yaml(input)}; + EXPECT_TRUE(result.is_string()); + EXPECT_EQ(result.to_string(), std::string("\xe2\x80\xa9", 3)); +} + +TEST(escape_incomplete_hex) { + const std::string input{"\"\\xZ\""}; + try { + const auto result{sourcemeta::core::parse_yaml(input)}; + FAIL(); + } catch (const sourcemeta::core::YAMLParseError &error) { + EXPECT_STREQ(error.what(), "Invalid hex escape sequence"); + } +} + +TEST(unterminated_single_quote) { + const std::string input{"'abc"}; + try { + const auto result{sourcemeta::core::parse_yaml(input)}; + FAIL(); + } catch (const sourcemeta::core::YAMLParseError &error) { + EXPECT_STREQ(error.what(), "Missing closing quote in single-quoted scalar"); + } +} + +TEST(single_quoted_cr_folds_to_space) { + const std::string input{"'a\rb'"}; + const auto result{sourcemeta::core::parse_yaml(input)}; + EXPECT_EQ(result.to_string(), "a b"); +} + +TEST(single_quoted_crlf_folds_to_space) { + const std::string input{"'a\r\nb'"}; + const auto result{sourcemeta::core::parse_yaml(input)}; + EXPECT_EQ(result.to_string(), "a b"); +} + +TEST(double_quoted_cr_folds_to_space) { + const std::string input{"\"a\rb\""}; + const auto result{sourcemeta::core::parse_yaml(input)}; + EXPECT_EQ(result.to_string(), "a b"); +} + +TEST(double_quoted_escaped_cr_line_continuation) { + const std::string input{"\"a\\\rb\""}; + const auto result{sourcemeta::core::parse_yaml(input)}; + EXPECT_EQ(result.to_string(), "ab"); +} + +TEST(double_quoted_escaped_crlf_line_continuation) { + const std::string input{"\"a\\\r\nb\""}; + const auto result{sourcemeta::core::parse_yaml(input)}; + EXPECT_EQ(result.to_string(), "ab"); +} + +TEST(block_scalar_with_cr_line_breaks) { + const std::string input{"|\r a\r b\r"}; + const auto result{sourcemeta::core::parse_yaml(input)}; + EXPECT_EQ(result.to_string(), "a\nb\n"); +} + +TEST(block_scalar_with_crlf_line_breaks) { + const std::string input{"|\r\n a\r\n b\r\n"}; + const auto result{sourcemeta::core::parse_yaml(input)}; + EXPECT_EQ(result.to_string(), "a\nb\n"); +} + +TEST(flow_mapping_bare_key_at_eof_is_rejected) { + try { + const auto result{sourcemeta::core::parse_yaml("{a")}; + FAIL(); + } catch (const sourcemeta::core::YAMLParseError &error) { + EXPECT_STREQ(error.what(), "Expected ':' after mapping key"); + } +} + +TEST(flow_mapping_colon_at_eof_is_rejected) { + try { + const auto result{sourcemeta::core::parse_yaml("{a:")}; + FAIL(); + } catch (const sourcemeta::core::YAMLParseError &error) { + EXPECT_STREQ(error.what(), "Unexpected token"); + } +} + +TEST(flow_mapping_anchor_key_at_eof_is_rejected) { + try { + const auto result{sourcemeta::core::parse_yaml("{&a")}; + FAIL(); + } catch (const sourcemeta::core::YAMLParseError &error) { + EXPECT_STREQ(error.what(), "Expected scalar key in mapping"); + } +} + +TEST(explicit_bool_true_capitalized) { + const auto result{sourcemeta::core::parse_yaml("!!bool True")}; + EXPECT_TRUE(result.is_boolean()); + EXPECT_TRUE(result.to_boolean()); +} + +TEST(explicit_bool_true_uppercase) { + const auto result{sourcemeta::core::parse_yaml("!!bool TRUE")}; + EXPECT_TRUE(result.is_boolean()); + EXPECT_TRUE(result.to_boolean()); +} + +TEST(sign_only_scalar_is_a_string) { + const auto result{sourcemeta::core::parse_yaml("+")}; + EXPECT_TRUE(result.is_string()); + EXPECT_EQ(result.to_string(), "+"); +} + +TEST(dot_after_exponent_is_a_string) { + const auto result{sourcemeta::core::parse_yaml("1e1.5")}; + EXPECT_TRUE(result.is_string()); + EXPECT_EQ(result.to_string(), "1e1.5"); +} + +TEST(double_exponent_is_a_string) { + const auto result{sourcemeta::core::parse_yaml("1e2e3")}; + EXPECT_TRUE(result.is_string()); + EXPECT_EQ(result.to_string(), "1e2e3"); +} + +TEST(line_starting_with_double_dash_non_marker) { + const auto result{sourcemeta::core::parse_yaml("--x")}; + EXPECT_TRUE(result.is_string()); + EXPECT_EQ(result.to_string(), "--x"); +} + +TEST(flow_mapping_colon_before_flow_indicator) { + const auto result{sourcemeta::core::parse_yaml("{a:}")}; + EXPECT_TRUE(result.is_object()); + EXPECT_TRUE(result.defines("a")); +} + +TEST(colon_as_final_byte_no_newline) { + const auto result{sourcemeta::core::parse_yaml("a:")}; + EXPECT_TRUE(result.is_object()); + EXPECT_TRUE(result.defines("a")); +} + +TEST(flow_sequence_dash_at_eof_is_rejected) { + try { + const auto result{sourcemeta::core::parse_yaml("[-")}; + FAIL(); + } catch (const sourcemeta::core::YAMLParseError &error) { + EXPECT_STREQ(error.what(), "Invalid plain scalar start in flow context"); + } +} + +TEST(anchor_with_empty_value_before_document_end) { + const auto result{sourcemeta::core::parse_yaml("&a\n...")}; + EXPECT_TRUE(result.is_null()); +} + +TEST(tag_then_flow_mapping_end) { + const auto result{sourcemeta::core::parse_yaml("{ a: !!str }")}; + EXPECT_TRUE(result.is_object()); + EXPECT_TRUE(result.defines("a")); +} + +TEST(tag_then_flow_sequence_end) { + const auto result{sourcemeta::core::parse_yaml("[ !!str ]")}; + EXPECT_TRUE(result.is_array()); + EXPECT_EQ(result.size(), 1); +} + +TEST(alias_key_referencing_unknown_anchor_is_rejected) { + try { + const auto result{sourcemeta::core::parse_yaml("*a: b")}; + FAIL(); + } catch (const sourcemeta::core::YAMLParseError &error) { + EXPECT_STREQ(error.what(), "YAML alias references undefined anchor"); + } +} + +TEST(flow_explicit_key_at_eof_is_rejected) { + try { + const auto result{sourcemeta::core::parse_yaml("[?")}; + FAIL(); + } catch (const sourcemeta::core::YAMLParseError &error) { + EXPECT_STREQ(error.what(), "Unexpected token"); + } +} + +TEST(flow_collection_indented_at_parent_block_level_is_rejected) { + try { + const auto result{sourcemeta::core::parse_yaml("a: [\n1]")}; + FAIL(); + } catch (const sourcemeta::core::YAMLParseError &error) { + EXPECT_STREQ(error.what(), "Insufficient indentation in flow collection"); + } +} + +TEST(explicit_key_block_mapping_duplicate_is_rejected) { + try { + const auto result{sourcemeta::core::parse_yaml("? a\n: 1\n? a\n: 2")}; + FAIL(); + } catch (const sourcemeta::core::YAMLParseError &error) { + EXPECT_STREQ(error.what(), "Duplicate key in YAML mapping"); + } +} + +TEST(explicit_key_value_at_document_end) { + const auto result{sourcemeta::core::parse_yaml("? a\n:\n...")}; + EXPECT_TRUE(result.is_object()); + EXPECT_TRUE(result.defines("a")); +} + +TEST(block_mapping_second_key_value_at_document_end) { + const auto result{sourcemeta::core::parse_yaml("a: 1\nb:\n...")}; + EXPECT_TRUE(result.is_object()); + EXPECT_EQ(result.at("a").to_integer(), 1); + EXPECT_TRUE(result.at("b").is_null()); +} + +TEST(leading_colon_value_indicator_mapping) { + const auto result{sourcemeta::core::parse_yaml(": v")}; + EXPECT_TRUE(result.is_object()); + EXPECT_EQ(result.size(), 1); +} + +TEST(anchor_empty_value_before_document_start) { + const auto result{sourcemeta::core::parse_yaml("&a\n---\nb")}; + EXPECT_TRUE(result.is_null()); +} + +TEST(mapping_key_that_is_a_collection_is_rejected) { + try { + const auto result{sourcemeta::core::parse_yaml("[? {a: 1}: b]")}; + FAIL(); + } catch (const sourcemeta::core::YAMLParseError &error) { + EXPECT_STREQ(error.what(), "Mapping key cannot be a collection"); + } +} + +TEST(tab_trailing_after_quoted_scalar_is_rejected) { + try { + const auto result{sourcemeta::core::parse_yaml("'x'\tbad")}; + FAIL(); + } catch (const sourcemeta::core::YAMLParseError &error) { + EXPECT_STREQ(error.what(), "Invalid trailing content"); + } +} + +TEST(cr_trailing_after_quoted_scalar) { + const auto result{sourcemeta::core::parse_yaml("'x'\r")}; + EXPECT_TRUE(result.is_string()); + EXPECT_EQ(result.to_string(), "x"); +} + +TEST(block_scalar_blank_lines_with_cr) { + const auto result{sourcemeta::core::parse_yaml("|\r a\r\r b")}; + EXPECT_TRUE(result.is_string()); + EXPECT_EQ(result.to_string(), "a\n\nb"); +} + +TEST(tab_after_escaped_line_break) { + const auto result{sourcemeta::core::parse_yaml("\"a\\\n\tb\"")}; + EXPECT_TRUE(result.is_string()); + EXPECT_EQ(result.to_string(), "ab"); +} diff --git a/test/yaml/yaml_stringify_test.cc b/test/yaml/yaml_stringify_test.cc index 964ddd7bb..df44482f0 100644 --- a/test/yaml/yaml_stringify_test.cc +++ b/test/yaml/yaml_stringify_test.cc @@ -913,3 +913,66 @@ TEST(string_with_quote_backslash_and_newline) { sourcemeta::core::stringify_yaml(document, stream); EXPECT_EQ(stream.str(), "\"a\\\"b\\\\c\\nd\"\n"); } + +TEST(quotes_uppercase_hex_lookalike) { + const sourcemeta::core::JSON document{"0X1"}; + std::ostringstream stream; + sourcemeta::core::stringify_yaml(document, stream); + EXPECT_EQ(stream.str(), "\"0X1\"\n"); +} + +TEST(quotes_uppercase_exponent_lookalike) { + const sourcemeta::core::JSON document{"1E5"}; + std::ostringstream stream; + sourcemeta::core::stringify_yaml(document, stream); + EXPECT_EQ(stream.str(), "\"1E5\"\n"); +} + +TEST(quotes_positive_exponent_lookalike) { + const sourcemeta::core::JSON document{"1e+5"}; + std::ostringstream stream; + sourcemeta::core::stringify_yaml(document, stream); + EXPECT_EQ(stream.str(), "\"1e+5\"\n"); +} + +TEST(quotes_negative_exponent_lookalike) { + const sourcemeta::core::JSON document{"1e-5"}; + std::ostringstream stream; + sourcemeta::core::stringify_yaml(document, stream); + EXPECT_EQ(stream.str(), "\"1e-5\"\n"); +} + +TEST(quotes_uppercase_positive_infinity) { + const sourcemeta::core::JSON document{".INF"}; + std::ostringstream stream; + sourcemeta::core::stringify_yaml(document, stream); + EXPECT_EQ(stream.str(), "\".INF\"\n"); +} + +TEST(quotes_signed_positive_infinity) { + const sourcemeta::core::JSON document{"+.inf"}; + std::ostringstream stream; + sourcemeta::core::stringify_yaml(document, stream); + EXPECT_EQ(stream.str(), "\"+.inf\"\n"); +} + +TEST(quotes_signed_negative_infinity) { + const sourcemeta::core::JSON document{"-.Inf"}; + std::ostringstream stream; + sourcemeta::core::stringify_yaml(document, stream); + EXPECT_EQ(stream.str(), "\"-.Inf\"\n"); +} + +TEST(quotes_uppercase_nan) { + const sourcemeta::core::JSON document{".NAN"}; + std::ostringstream stream; + sourcemeta::core::stringify_yaml(document, stream); + EXPECT_EQ(stream.str(), "\".NAN\"\n"); +} + +TEST(quotes_document_marker_with_tab) { + const sourcemeta::core::JSON document{"---\t"}; + std::ostringstream stream; + sourcemeta::core::stringify_yaml(document, stream); + EXPECT_EQ(stream.str(), "\"---\\t\"\n"); +}