|
3 | 3 | -include_lib("proper/include/proper.hrl"). |
4 | 4 | -include_lib("stdlib/include/assert.hrl"). |
5 | 5 |
|
| 6 | +%% ------------------------------------------------------------------- |
| 7 | +%% Chunked encoding: every chunk has at least 2 CRLFs |
| 8 | +%% ------------------------------------------------------------------- |
| 9 | + |
6 | 10 | prop_encoded_chunk_always_contains_at_least_two_crlfs() -> |
7 | 11 | ?FORALL(Chunk, iodata(), begin |
8 | 12 | Encoded = gen_http_parser_h1:encode_chunk(Chunk), |
9 | | - %% Flatten iolist to binary to count CRLFs |
10 | 13 | Binary = iolist_to_binary(Encoded), |
11 | | - %% Count occurrences of "\r\n" |
12 | 14 | CrlfCount = count_crlf(Binary), |
13 | | - %% HTTP chunked encoding always has at least 2 CRLFs: |
14 | | - %% one after the size, one after the chunk data |
15 | 15 | ?assert(CrlfCount >= 2), |
16 | 16 | true |
17 | 17 | end). |
18 | 18 |
|
| 19 | +%% ------------------------------------------------------------------- |
| 20 | +%% Chunk size prefix matches actual data length |
| 21 | +%% ------------------------------------------------------------------- |
| 22 | + |
| 23 | +prop_chunk_size_matches_data_length() -> |
| 24 | + ?FORALL( |
| 25 | + Data, |
| 26 | + binary(), |
| 27 | + begin |
| 28 | + Encoded = iolist_to_binary(gen_http_parser_h1:encode_chunk(Data)), |
| 29 | + %% Format: "<hex-size>\r\n<data>\r\n" |
| 30 | + [SizeHex | _] = binary:split(Encoded, <<"\r\n">>), |
| 31 | + ParsedSize = binary_to_integer(SizeHex, 16), |
| 32 | + ?assertEqual(byte_size(Data), ParsedSize), |
| 33 | + true |
| 34 | + end |
| 35 | + ). |
| 36 | + |
| 37 | +%% ------------------------------------------------------------------- |
| 38 | +%% Request encoding roundtrip: encode then parse back |
| 39 | +%% ------------------------------------------------------------------- |
| 40 | + |
| 41 | +prop_encode_request_parses_back() -> |
| 42 | + ?FORALL( |
| 43 | + {Method, Path, Headers}, |
| 44 | + {http_method(), http_path(), list(http_header())}, |
| 45 | + begin |
| 46 | + {ok, IoData} = gen_http_parser_h1:encode_request(Method, Path, Headers, undefined), |
| 47 | + Bin = iolist_to_binary(IoData), |
| 48 | + %% Parse status line (we encoded a request, but let's at least |
| 49 | + %% verify the wire format contains the method and path) |
| 50 | + ?assert(binary:match(Bin, [Method]) =/= nomatch), |
| 51 | + ?assert(binary:match(Bin, [Path]) =/= nomatch), |
| 52 | + ?assert(binary:match(Bin, [<<"HTTP/1.1">>]) =/= nomatch), |
| 53 | + %% Each header name should appear in the output |
| 54 | + lists:all( |
| 55 | + fun({Name, _Value}) -> |
| 56 | + binary:match(Bin, [Name]) =/= nomatch |
| 57 | + end, |
| 58 | + Headers |
| 59 | + ) |
| 60 | + end |
| 61 | + ). |
| 62 | + |
| 63 | +%% ------------------------------------------------------------------- |
| 64 | +%% Valid header names survive encoding |
| 65 | +%% ------------------------------------------------------------------- |
| 66 | + |
| 67 | +prop_valid_headers_encode_ok() -> |
| 68 | + ?FORALL( |
| 69 | + Headers, |
| 70 | + non_empty(list(http_header())), |
| 71 | + begin |
| 72 | + case gen_http_parser_h1:encode_request(<<"GET">>, <<"/">>, Headers, undefined) of |
| 73 | + {ok, _IoData} -> true; |
| 74 | + {error, _} -> false |
| 75 | + end |
| 76 | + end |
| 77 | + ). |
| 78 | + |
| 79 | +%% ------------------------------------------------------------------- |
| 80 | +%% Invalid header names are rejected |
| 81 | +%% ------------------------------------------------------------------- |
| 82 | + |
| 83 | +prop_invalid_header_names_rejected() -> |
| 84 | + ?FORALL( |
| 85 | + BadName, |
| 86 | + bad_header_name(), |
| 87 | + begin |
| 88 | + case gen_http_parser_h1:encode_request(<<"GET">>, <<"/">>, [{BadName, <<"v">>}], undefined) of |
| 89 | + {error, {invalid_header_name, _}} -> true; |
| 90 | + {ok, _} -> false |
| 91 | + end |
| 92 | + end |
| 93 | + ). |
| 94 | + |
| 95 | +%% ------------------------------------------------------------------- |
| 96 | +%% Valid targets survive encoding |
| 97 | +%% ------------------------------------------------------------------- |
| 98 | + |
| 99 | +prop_valid_targets_encode_ok() -> |
| 100 | + ?FORALL( |
| 101 | + Target, |
| 102 | + http_path(), |
| 103 | + begin |
| 104 | + case gen_http_parser_h1:encode_request(<<"GET">>, Target, [], undefined) of |
| 105 | + {ok, _} -> true; |
| 106 | + {error, _} -> false |
| 107 | + end |
| 108 | + end |
| 109 | + ). |
| 110 | + |
| 111 | +%% ------------------------------------------------------------------- |
| 112 | +%% Chunk EOF produces the terminator |
| 113 | +%% ------------------------------------------------------------------- |
| 114 | + |
| 115 | +prop_eof_chunk_is_terminator() -> |
| 116 | + Encoded = iolist_to_binary(gen_http_parser_h1:encode_chunk(eof)), |
| 117 | + ?assertEqual(<<"0\r\n\r\n">>, Encoded), |
| 118 | + true. |
| 119 | + |
| 120 | +%% ------------------------------------------------------------------- |
| 121 | +%% Encode request with body includes body bytes |
| 122 | +%% ------------------------------------------------------------------- |
| 123 | + |
| 124 | +prop_encode_request_with_body() -> |
| 125 | + ?FORALL( |
| 126 | + Body, |
| 127 | + non_empty(binary()), |
| 128 | + begin |
| 129 | + {ok, IoData} = gen_http_parser_h1:encode_request(<<"POST">>, <<"/">>, [], Body), |
| 130 | + Bin = iolist_to_binary(IoData), |
| 131 | + %% Body should appear after the header block |
| 132 | + binary:match(Bin, [Body]) =/= nomatch |
| 133 | + end |
| 134 | + ). |
| 135 | + |
| 136 | +%% ------------------------------------------------------------------- |
| 137 | +%% Generators |
| 138 | +%% ------------------------------------------------------------------- |
| 139 | + |
| 140 | +http_method() -> |
| 141 | + oneof([<<"GET">>, <<"POST">>, <<"PUT">>, <<"DELETE">>, <<"PATCH">>, <<"HEAD">>, <<"OPTIONS">>]). |
| 142 | + |
| 143 | +http_path() -> |
| 144 | + ?LET( |
| 145 | + Segments, |
| 146 | + list(path_segment()), |
| 147 | + iolist_to_binary([<<"/">> | lists:join(<<"/">>, Segments)]) |
| 148 | + ). |
| 149 | + |
| 150 | +path_segment() -> |
| 151 | + ?LET( |
| 152 | + Chars, |
| 153 | + non_empty(list(oneof(lists:seq($a, $z) ++ lists:seq($0, $9) ++ [$-, $_, $.]))), |
| 154 | + list_to_binary(Chars) |
| 155 | + ). |
| 156 | + |
| 157 | +http_header() -> |
| 158 | + {header_name(), header_value()}. |
| 159 | + |
| 160 | +header_name() -> |
| 161 | + ?LET( |
| 162 | + Chars, |
| 163 | + non_empty(list(oneof(lists:seq($a, $z) ++ lists:seq($0, $9) ++ [$-, $_]))), |
| 164 | + list_to_binary(Chars) |
| 165 | + ). |
| 166 | + |
| 167 | +header_value() -> |
| 168 | + ?LET( |
| 169 | + Chars, |
| 170 | + list(oneof(lists:seq(33, 126) ++ [$\s, $\t])), |
| 171 | + list_to_binary(Chars) |
| 172 | + ). |
| 173 | + |
| 174 | +bad_header_name() -> |
| 175 | + ?LET( |
| 176 | + {Prefix, Suffix}, |
| 177 | + {non_empty(list(oneof(lists:seq($a, $z)))), oneof([<<" ">>, <<"\t">>, <<"\r">>, <<"\n">>])}, |
| 178 | + <<(list_to_binary(Prefix))/binary, Suffix/binary>> |
| 179 | + ). |
| 180 | + |
| 181 | +%% ------------------------------------------------------------------- |
| 182 | +%% Helpers |
| 183 | +%% ------------------------------------------------------------------- |
| 184 | + |
19 | 185 | -spec count_crlf(binary()) -> non_neg_integer(). |
20 | 186 | count_crlf(Binary) -> |
21 | 187 | count_crlf(Binary, 0). |
|
0 commit comments