blob: 11493a81a53c04d97074ab2fa4ae599ecda66bf2 [file] [log] [blame]
% Licensed under the Apache License, Version 2.0 (the "License"); you may not
% use this file except in compliance with the License. You may obtain a copy of
% the License at
%
% http://www.apache.org/licenses/LICENSE-2.0
%
% Unless required by applicable law or agreed to in writing, software
% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
% License for the specific language governing permissions and limitations under
% the License.
-module(couchdb_attachments_tests).
-include_lib("couch/include/couch_eunit.hrl").
-include_lib("couch/include/couch_db.hrl").
-define(COMPRESSION_LEVEL, 8).
-define(ATT_BIN_NAME, <<"logo.png">>).
-define(ATT_TXT_NAME, <<"file.erl">>).
-define(FIXTURE_PNG, filename:join([?FIXTURESDIR, "logo.png"])).
-define(FIXTURE_TXT, ?ABS_PATH(?FILE)).
-define(TIMEOUT, 1000).
-define(TIMEOUT_EUNIT, 10).
-define(TIMEWAIT, 100).
-define(i2l(I), integer_to_list(I)).
start() ->
Ctx = test_util:start_couch(),
% ensure in default compression settings for attachments_compression_tests
config:set("attachments", "compression_level",
?i2l(?COMPRESSION_LEVEL), false),
config:set("attachments", "compressible_types", "text/*", false),
Ctx.
setup() ->
DbName = ?tempdb(),
{ok, Db} = couch_db:create(DbName, []),
ok = couch_db:close(Db),
Addr = config:get("httpd", "bind_address", "127.0.0.1"),
Port = mochiweb_socket_server:get(couch_httpd, port),
Host = Addr ++ ":" ++ ?i2l(Port),
{Host, ?b2l(DbName)}.
setup({binary, standalone}) ->
{Host, DbName} = setup(),
setup_att(fun create_standalone_png_att/2, Host, DbName, ?FIXTURE_PNG);
setup({text, standalone}) ->
{Host, DbName} = setup(),
setup_att(fun create_standalone_text_att/2, Host, DbName, ?FIXTURE_TXT);
setup({binary, inline}) ->
{Host, DbName} = setup(),
setup_att(fun create_inline_png_att/2, Host, DbName, ?FIXTURE_PNG);
setup({text, inline}) ->
{Host, DbName} = setup(),
setup_att(fun create_inline_text_att/2, Host, DbName, ?FIXTURE_TXT);
setup(compressed) ->
{Host, DbName} = setup(),
setup_att(fun create_already_compressed_att/2, Host, DbName, ?FIXTURE_TXT).
setup_att(Fun, Host, DbName, File) ->
HttpHost = "http://" ++ Host,
AttUrl = Fun(HttpHost, DbName),
{ok, Data} = file:read_file(File),
DocUrl = string:join([HttpHost, DbName, "doc"], "/"),
Helpers = {DbName, DocUrl, AttUrl},
{Data, Helpers}.
teardown(_, {_, {DbName, _, _}}) ->
teardown(DbName).
teardown({_, DbName}) ->
teardown(DbName);
teardown(DbName) ->
ok = couch_server:delete(?l2b(DbName), []),
ok.
attachments_test_() ->
{
"Attachments tests",
{
setup,
fun start/0, fun test_util:stop_couch/1,
[
attachments_md5_tests(),
attachments_compression_tests()
]
}
}.
attachments_md5_tests() ->
{
"Attachments MD5 tests",
{
foreach,
fun setup/0, fun teardown/1,
[
fun should_upload_attachment_without_md5/1,
fun should_upload_attachment_by_chunks_without_md5/1,
fun should_upload_attachment_with_valid_md5_header/1,
fun should_upload_attachment_by_chunks_with_valid_md5_header/1,
fun should_upload_attachment_by_chunks_with_valid_md5_trailer/1,
fun should_reject_attachment_with_invalid_md5/1,
fun should_reject_chunked_attachment_with_invalid_md5/1,
fun should_reject_chunked_attachment_with_invalid_md5_trailer/1
]
}
}.
attachments_compression_tests() ->
Funs = [
fun should_get_att_without_accept_gzip_encoding/2,
fun should_get_att_with_accept_gzip_encoding/2,
fun should_get_att_with_accept_deflate_encoding/2,
fun should_return_406_response_on_unsupported_encoding/2,
fun should_get_doc_with_att_data/2,
fun should_get_doc_with_att_data_stub/2
],
{
"Attachments compression tests",
[
{
"Created via Attachments API",
created_attachments_compression_tests(standalone, Funs)
},
{
"Created inline via Document API",
created_attachments_compression_tests(inline, Funs)
},
{
"Created already been compressed via Attachments API",
{
foreachx,
fun setup/1, fun teardown/2,
[{compressed, Fun} || Fun <- Funs]
}
},
{
foreach,
fun setup/0, fun teardown/1,
[
fun should_not_create_compressed_att_with_deflate_encoding/1,
fun should_not_create_compressed_att_with_compress_encoding/1,
fun should_create_compressible_att_with_ctype_params/1
]
}
]
}.
created_attachments_compression_tests(Mod, Funs) ->
[
{
"Compressiable attachments",
{
foreachx,
fun setup/1, fun teardown/2,
[{{text, Mod}, Fun} || Fun <- Funs]
}
},
{
"Uncompressiable attachments",
{
foreachx,
fun setup/1, fun teardown/2,
[{{binary, Mod}, Fun} || Fun <- Funs]
}
}
].
should_upload_attachment_without_md5({Host, DbName}) ->
?_test(begin
AttUrl = string:join(["", DbName, ?docid(), "readme.txt"], "/"),
Body = "We all live in a yellow submarine!",
Headers = [
{"Content-Length", "34"},
{"Content-Type", "text/plain"},
{"Host", Host}
],
{ok, Code, Json} = request("PUT", AttUrl, Headers, Body),
?assertEqual(201, Code),
?assertEqual(true, get_json(Json, [<<"ok">>]))
end).
should_upload_attachment_by_chunks_without_md5({Host, DbName}) ->
?_test(begin
AttUrl = string:join(["", DbName, ?docid(), "readme.txt"], "/"),
AttData = <<"We all live in a yellow submarine!">>,
<<Part1:21/binary, Part2:13/binary>> = AttData,
Body = [chunked_body([Part1, Part2]), "\r\n"],
Headers = [
{"Content-Type", "text/plain"},
{"Transfer-Encoding", "chunked"},
{"Host", Host}
],
{ok, Code, Json} = request("PUT", AttUrl, Headers, Body),
?assertEqual(201, Code),
?assertEqual(true, get_json(Json, [<<"ok">>]))
end).
should_upload_attachment_with_valid_md5_header({Host, DbName}) ->
?_test(begin
AttUrl = string:join(["", DbName, ?docid(), "readme.txt"], "/"),
Body = "We all live in a yellow submarine!",
Headers = [
{"Content-Length", "34"},
{"Content-Type", "text/plain"},
{"Content-MD5", ?b2l(base64:encode(couch_crypto:hash(md5, Body)))},
{"Host", Host}
],
{ok, Code, Json} = request("PUT", AttUrl, Headers, Body),
?assertEqual(201, Code),
?assertEqual(true, get_json(Json, [<<"ok">>]))
end).
should_upload_attachment_by_chunks_with_valid_md5_header({Host, DbName}) ->
?_test(begin
AttUrl = string:join(["", DbName, ?docid(), "readme.txt"], "/"),
AttData = <<"We all live in a yellow submarine!">>,
<<Part1:21/binary, Part2:13/binary>> = AttData,
Body = [chunked_body([Part1, Part2]), "\r\n"],
Headers = [
{"Content-Type", "text/plain"},
{"Content-MD5", ?b2l(base64:encode(couch_crypto:hash(md5, AttData)))},
{"Host", Host},
{"Transfer-Encoding", "chunked"}
],
{ok, Code, Json} = request("PUT", AttUrl, Headers, Body),
?assertEqual(201, Code),
?assertEqual(true, get_json(Json, [<<"ok">>]))
end).
should_upload_attachment_by_chunks_with_valid_md5_trailer({Host, DbName}) ->
?_test(begin
AttUrl = string:join(["", DbName, ?docid(), "readme.txt"], "/"),
AttData = <<"We all live in a yellow submarine!">>,
<<Part1:21/binary, Part2:13/binary>> = AttData,
Body = [chunked_body([Part1, Part2]),
"Content-MD5: ", base64:encode(couch_crypto:hash(md5, AttData)),
"\r\n\r\n"],
Headers = [
{"Content-Type", "text/plain"},
{"Host", Host},
{"Trailer", "Content-MD5"},
{"Transfer-Encoding", "chunked"}
],
{ok, Code, Json} = request("PUT", AttUrl, Headers, Body),
?assertEqual(201, Code),
?assertEqual(true, get_json(Json, [<<"ok">>]))
end).
should_reject_attachment_with_invalid_md5({Host, DbName}) ->
?_test(begin
AttUrl = string:join(["", DbName, ?docid(), "readme.txt"], "/"),
Body = "We all live in a yellow submarine!",
Headers = [
{"Content-Length", "34"},
{"Content-Type", "text/plain"},
{"Content-MD5", ?b2l(base64:encode(<<"foobar!">>))},
{"Host", Host}
],
{ok, Code, Json} = request("PUT", AttUrl, Headers, Body),
?assertEqual(400, Code),
?assertEqual(<<"content_md5_mismatch">>,
get_json(Json, [<<"error">>]))
end).
should_reject_chunked_attachment_with_invalid_md5({Host, DbName}) ->
?_test(begin
AttUrl = string:join(["", DbName, ?docid(), "readme.txt"], "/"),
AttData = <<"We all live in a yellow submarine!">>,
<<Part1:21/binary, Part2:13/binary>> = AttData,
Body = [chunked_body([Part1, Part2]), "\r\n"],
Headers = [
{"Content-Type", "text/plain"},
{"Content-MD5", ?b2l(base64:encode(<<"foobar!">>))},
{"Host", Host},
{"Transfer-Encoding", "chunked"}
],
{ok, Code, Json} = request("PUT", AttUrl, Headers, Body),
?assertEqual(400, Code),
?assertEqual(<<"content_md5_mismatch">>,
get_json(Json, [<<"error">>]))
end).
should_reject_chunked_attachment_with_invalid_md5_trailer({Host, DbName}) ->
?_test(begin
AttUrl = string:join(["", DbName, ?docid(), "readme.txt"], "/"),
AttData = <<"We all live in a yellow submarine!">>,
<<Part1:21/binary, Part2:13/binary>> = AttData,
Body = [chunked_body([Part1, Part2]),
"Content-MD5: ", base64:encode(<<"foobar!">>),
"\r\n\r\n"],
Headers = [
{"Content-Type", "text/plain"},
{"Host", Host},
{"Trailer", "Content-MD5"},
{"Transfer-Encoding", "chunked"}
],
{ok, Code, Json} = request("PUT", AttUrl, Headers, Body),
?assertEqual(400, Code),
?assertEqual(<<"content_md5_mismatch">>, get_json(Json, [<<"error">>]))
end).
should_get_att_without_accept_gzip_encoding(_, {Data, {_, _, AttUrl}}) ->
?_test(begin
{ok, Code, Headers, Body} = test_request:get(AttUrl),
?assertEqual(200, Code),
?assertNot(lists:member({"Content-Encoding", "gzip"}, Headers)),
?assertEqual(Data, iolist_to_binary(Body))
end).
should_get_att_with_accept_gzip_encoding(compressed, {Data, {_, _, AttUrl}}) ->
?_test(begin
{ok, Code, Headers, Body} = test_request:get(
AttUrl, [{"Accept-Encoding", "gzip"}]),
?assertEqual(200, Code),
?assert(lists:member({"Content-Encoding", "gzip"}, Headers)),
?assertEqual(Data, zlib:gunzip(iolist_to_binary(Body)))
end);
should_get_att_with_accept_gzip_encoding({text, _}, {Data, {_, _, AttUrl}}) ->
?_test(begin
{ok, Code, Headers, Body} = test_request:get(
AttUrl, [{"Accept-Encoding", "gzip"}]),
?assertEqual(200, Code),
?assert(lists:member({"Content-Encoding", "gzip"}, Headers)),
?assertEqual(Data, zlib:gunzip(iolist_to_binary(Body)))
end);
should_get_att_with_accept_gzip_encoding({binary, _}, {Data, {_, _, AttUrl}}) ->
?_test(begin
{ok, Code, Headers, Body} = test_request:get(
AttUrl, [{"Accept-Encoding", "gzip"}]),
?assertEqual(200, Code),
?assertEqual(undefined,
couch_util:get_value("Content-Encoding", Headers)),
?assertEqual(Data, iolist_to_binary(Body))
end).
should_get_att_with_accept_deflate_encoding(_, {Data, {_, _, AttUrl}}) ->
?_test(begin
{ok, Code, Headers, Body} = test_request:get(
AttUrl, [{"Accept-Encoding", "deflate"}]),
?assertEqual(200, Code),
?assertEqual(undefined,
couch_util:get_value("Content-Encoding", Headers)),
?assertEqual(Data, iolist_to_binary(Body))
end).
should_return_406_response_on_unsupported_encoding(_, {_, {_, _, AttUrl}}) ->
?_assertEqual(406,
begin
{ok, Code, _, _} = test_request:get(
AttUrl, [{"Accept-Encoding", "deflate, *;q=0"}]),
Code
end).
should_get_doc_with_att_data(compressed, {Data, {_, DocUrl, _}}) ->
?_test(begin
Url = DocUrl ++ "?attachments=true",
{ok, Code, _, Body} = test_request:get(
Url, [{"Accept", "application/json"}]),
?assertEqual(200, Code),
Json = jiffy:decode(Body),
AttJson = couch_util:get_nested_json_value(
Json, [<<"_attachments">>, ?ATT_TXT_NAME]),
AttData = couch_util:get_nested_json_value(
AttJson, [<<"data">>]),
?assertEqual(
<<"text/plain">>,
couch_util:get_nested_json_value(AttJson,[<<"content_type">>])),
?assertEqual(Data, base64:decode(AttData))
end);
should_get_doc_with_att_data({text, _}, {Data, {_, DocUrl, _}}) ->
?_test(begin
Url = DocUrl ++ "?attachments=true",
{ok, Code, _, Body} = test_request:get(
Url, [{"Accept", "application/json"}]),
?assertEqual(200, Code),
Json = jiffy:decode(Body),
AttJson = couch_util:get_nested_json_value(
Json, [<<"_attachments">>, ?ATT_TXT_NAME]),
AttData = couch_util:get_nested_json_value(
AttJson, [<<"data">>]),
?assertEqual(
<<"text/plain">>,
couch_util:get_nested_json_value(AttJson,[<<"content_type">>])),
?assertEqual(Data, base64:decode(AttData))
end);
should_get_doc_with_att_data({binary, _}, {Data, {_, DocUrl, _}}) ->
?_test(begin
Url = DocUrl ++ "?attachments=true",
{ok, Code, _, Body} = test_request:get(
Url, [{"Accept", "application/json"}]),
?assertEqual(200, Code),
Json = jiffy:decode(Body),
AttJson = couch_util:get_nested_json_value(
Json, [<<"_attachments">>, ?ATT_BIN_NAME]),
AttData = couch_util:get_nested_json_value(
AttJson, [<<"data">>]),
?assertEqual(
<<"image/png">>,
couch_util:get_nested_json_value(AttJson,[<<"content_type">>])),
?assertEqual(Data, base64:decode(AttData))
end).
should_get_doc_with_att_data_stub(compressed, {Data, {_, DocUrl, _}}) ->
?_test(begin
Url = DocUrl ++ "?att_encoding_info=true",
{ok, Code, _, Body} = test_request:get(
Url, [{"Accept", "application/json"}]),
?assertEqual(200, Code),
Json = jiffy:decode(Body),
{AttJson} = couch_util:get_nested_json_value(
Json, [<<"_attachments">>, ?ATT_TXT_NAME]),
?assertEqual(<<"gzip">>,
couch_util:get_value(<<"encoding">>, AttJson)),
AttLength = couch_util:get_value(<<"length">>, AttJson),
EncLength = couch_util:get_value(<<"encoded_length">>, AttJson),
?assertEqual(AttLength, EncLength),
?assertEqual(iolist_size(zlib:gzip(Data)), AttLength)
end);
should_get_doc_with_att_data_stub({text, _}, {Data, {_, DocUrl, _}}) ->
?_test(begin
Url = DocUrl ++ "?att_encoding_info=true",
{ok, Code, _, Body} = test_request:get(
Url, [{"Accept", "application/json"}]),
?assertEqual(200, Code),
Json = jiffy:decode(Body),
{AttJson} = couch_util:get_nested_json_value(
Json, [<<"_attachments">>, ?ATT_TXT_NAME]),
?assertEqual(<<"gzip">>,
couch_util:get_value(<<"encoding">>, AttJson)),
AttEncLength = iolist_size(gzip(Data)),
?assertEqual(AttEncLength,
couch_util:get_value(<<"encoded_length">>, AttJson)),
?assertEqual(byte_size(Data),
couch_util:get_value(<<"length">>, AttJson))
end);
should_get_doc_with_att_data_stub({binary, _}, {Data, {_, DocUrl, _}}) ->
?_test(begin
Url = DocUrl ++ "?att_encoding_info=true",
{ok, Code, _, Body} = test_request:get(
Url, [{"Accept", "application/json"}]),
?assertEqual(200, Code),
Json = jiffy:decode(Body),
{AttJson} = couch_util:get_nested_json_value(
Json, [<<"_attachments">>, ?ATT_BIN_NAME]),
?assertEqual(undefined,
couch_util:get_value(<<"encoding">>, AttJson)),
?assertEqual(undefined,
couch_util:get_value(<<"encoded_length">>, AttJson)),
?assertEqual(byte_size(Data),
couch_util:get_value(<<"length">>, AttJson))
end).
should_not_create_compressed_att_with_deflate_encoding({Host, DbName}) ->
?_assertEqual(415,
begin
HttpHost = "http://" ++ Host,
AttUrl = string:join([HttpHost, DbName, ?docid(), "file.txt"], "/"),
{ok, Data} = file:read_file(?FIXTURE_TXT),
Body = zlib:compress(Data),
Headers = [
{"Content-Encoding", "deflate"},
{"Content-Type", "text/plain"}
],
{ok, Code, _, _} = test_request:put(AttUrl, Headers, Body),
Code
end).
should_not_create_compressed_att_with_compress_encoding({Host, DbName}) ->
% Note: As of OTP R13B04, it seems there's no LZW compression
% (i.e. UNIX compress utility implementation) lib in OTP.
% However there's a simple working Erlang implementation at:
% http://scienceblogs.com/goodmath/2008/01/simple_lempelziv_compression_i.php
?_assertEqual(415,
begin
HttpHost = "http://" ++ Host,
AttUrl = string:join([HttpHost, DbName, ?docid(), "file.txt"], "/"),
{ok, Data} = file:read_file(?FIXTURE_TXT),
Headers = [
{"Content-Encoding", "compress"},
{"Content-Type", "text/plain"}
],
{ok, Code, _, _} = test_request:put(AttUrl, Headers, Data),
Code
end).
should_create_compressible_att_with_ctype_params({Host, DbName}) ->
{timeout, ?TIMEOUT_EUNIT, ?_test(begin
HttpHost = "http://" ++ Host,
DocUrl = string:join([HttpHost, DbName, ?docid()], "/"),
AttUrl = string:join([DocUrl, ?b2l(?ATT_TXT_NAME)], "/"),
{ok, Data} = file:read_file(?FIXTURE_TXT),
Headers = [{"Content-Type", "text/plain; charset=UTF-8"}],
{ok, Code0, _, _} = test_request:put(AttUrl, Headers, Data),
?assertEqual(201, Code0),
{ok, Code1, _, Body} = test_request:get(
DocUrl ++ "?att_encoding_info=true"),
?assertEqual(200, Code1),
Json = jiffy:decode(Body),
{AttJson} = couch_util:get_nested_json_value(
Json, [<<"_attachments">>, ?ATT_TXT_NAME]),
?assertEqual(<<"gzip">>,
couch_util:get_value(<<"encoding">>, AttJson)),
AttEncLength = iolist_size(gzip(Data)),
?assertEqual(AttEncLength,
couch_util:get_value(<<"encoded_length">>, AttJson)),
?assertEqual(byte_size(Data),
couch_util:get_value(<<"length">>, AttJson))
end)}.
get_json(Json, Path) ->
couch_util:get_nested_json_value(Json, Path).
to_hex(Val) ->
to_hex(Val, []).
to_hex(0, Acc) ->
Acc;
to_hex(Val, Acc) ->
to_hex(Val div 16, [hex_char(Val rem 16) | Acc]).
hex_char(V) when V < 10 -> $0 + V;
hex_char(V) -> $A + V - 10.
chunked_body(Chunks) ->
chunked_body(Chunks, []).
chunked_body([], Acc) ->
iolist_to_binary(lists:reverse(Acc, "0\r\n"));
chunked_body([Chunk | Rest], Acc) ->
Size = to_hex(size(Chunk)),
chunked_body(Rest, ["\r\n", Chunk, "\r\n", Size | Acc]).
get_socket() ->
Options = [binary, {packet, 0}, {active, false}],
Port = mochiweb_socket_server:get(couch_httpd, port),
{ok, Sock} = gen_tcp:connect(bind_address(), Port, Options),
Sock.
bind_address() ->
case config:get("httpd", "bind_address") of
undefined -> any;
Address -> Address
end.
request(Method, Url, Headers, Body) ->
RequestHead = [Method, " ", Url, " HTTP/1.1"],
RequestHeaders = [[string:join([Key, Value], ": "), "\r\n"]
|| {Key, Value} <- Headers],
Request = [RequestHead, "\r\n", RequestHeaders, "\r\n", Body],
Sock = get_socket(),
gen_tcp:send(Sock, list_to_binary(lists:flatten(Request))),
timer:sleep(?TIMEWAIT), % must wait to receive complete response
{ok, R} = gen_tcp:recv(Sock, 0),
gen_tcp:close(Sock),
[Header, Body1] = re:split(R, "\r\n\r\n", [{return, binary}]),
{ok, {http_response, _, Code, _}, _} =
erlang:decode_packet(http, Header, []),
Json = jiffy:decode(Body1),
{ok, Code, Json}.
create_standalone_text_att(Host, DbName) ->
{ok, Data} = file:read_file(?FIXTURE_TXT),
Url = string:join([Host, DbName, "doc", ?b2l(?ATT_TXT_NAME)], "/"),
{ok, Code, _Headers, _Body} = test_request:put(
Url, [{"Content-Type", "text/plain"}], Data),
?assertEqual(201, Code),
Url.
create_standalone_png_att(Host, DbName) ->
{ok, Data} = file:read_file(?FIXTURE_PNG),
Url = string:join([Host, DbName, "doc", ?b2l(?ATT_BIN_NAME)], "/"),
{ok, Code, _Headers, _Body} = test_request:put(
Url, [{"Content-Type", "image/png"}], Data),
?assertEqual(201, Code),
Url.
create_inline_text_att(Host, DbName) ->
{ok, Data} = file:read_file(?FIXTURE_TXT),
Url = string:join([Host, DbName, "doc"], "/"),
Doc = {[
{<<"_attachments">>, {[
{?ATT_TXT_NAME, {[
{<<"content_type">>, <<"text/plain">>},
{<<"data">>, base64:encode(Data)}
]}
}]}}
]},
{ok, Code, _Headers, _Body} = test_request:put(
Url, [{"Content-Type", "application/json"}], jiffy:encode(Doc)),
?assertEqual(201, Code),
string:join([Url, ?b2l(?ATT_TXT_NAME)], "/").
create_inline_png_att(Host, DbName) ->
{ok, Data} = file:read_file(?FIXTURE_PNG),
Url = string:join([Host, DbName, "doc"], "/"),
Doc = {[
{<<"_attachments">>, {[
{?ATT_BIN_NAME, {[
{<<"content_type">>, <<"image/png">>},
{<<"data">>, base64:encode(Data)}
]}
}]}}
]},
{ok, Code, _Headers, _Body} = test_request:put(
Url, [{"Content-Type", "application/json"}], jiffy:encode(Doc)),
?assertEqual(201, Code),
string:join([Url, ?b2l(?ATT_BIN_NAME)], "/").
create_already_compressed_att(Host, DbName) ->
{ok, Data} = file:read_file(?FIXTURE_TXT),
Url = string:join([Host, DbName, "doc", ?b2l(?ATT_TXT_NAME)], "/"),
{ok, Code, _Headers, _Body} = test_request:put(
Url, [{"Content-Type", "text/plain"}, {"Content-Encoding", "gzip"}],
zlib:gzip(Data)),
?assertEqual(201, Code),
Url.
gzip(Data) ->
Z = zlib:open(),
ok = zlib:deflateInit(Z, ?COMPRESSION_LEVEL, deflated, 16 + 15, 8, default),
zlib:deflate(Z, Data),
Last = zlib:deflate(Z, [], finish),
ok = zlib:deflateEnd(Z),
ok = zlib:close(Z),
Last.