blob: c2b81c476c30f6953c48ebe2b45efda935bb5f84 [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(couch_doc_json_tests).
-include_lib("couch/include/couch_eunit.hrl").
-include_lib("couch/include/couch_db.hrl").
setup() ->
mock(couch_log),
mock(config),
mock(fabric2_db_plugin),
ok.
teardown(_) ->
meck:unload(couch_log),
meck:unload(config),
meck:unload(fabric2_db_plugin),
ok.
mock(fabric2_db_plugin) ->
ok = meck:new(fabric2_db_plugin, [passthrough]),
ok = meck:expect(fabric2_db_plugin, validate_docid, fun(_) -> false end),
ok;
mock(couch_log) ->
ok = meck:new(couch_log, [passthrough]),
ok = meck:expect(couch_log, debug, fun(_, _) -> ok end),
ok;
mock(config) ->
meck:new(config, [passthrough]),
meck:expect(
config,
get_integer,
fun
("couchdb", "max_document_id_length", _) -> 512;
("couchdb", "max_document_size", _) -> 1024;
("couchdb", "max_attachment_size", _) -> 1024
end
),
meck:expect(config, get, fun(_, _) -> undefined end),
meck:expect(config, get, fun(_, _, Default) -> Default end),
ok.
json_doc_test_() ->
{
setup,
fun setup/0,
fun teardown/1,
fun(_) ->
[
{"Document from JSON", [
from_json_with_dbname_error_cases(),
from_json_success_cases(),
from_json_error_cases()
]},
{"Document to JSON", [
to_json_success_cases()
]}
]
end
}.
from_json_success_cases() ->
Cases = [
{
{[]},
#doc{},
"Return an empty document for an empty JSON object."
},
{
{[{<<"_id">>, <<"zing!">>}]},
#doc{id = <<"zing!">>},
"Parses document ids."
},
{
{[{<<"_id">>, <<"_design/foo">>}]},
#doc{id = <<"_design/foo">>},
"_design/document ids."
},
{
{[{<<"_id">>, <<"_local/bam">>}]},
#doc{id = <<"_local/bam">>},
"_local/document ids."
},
{
{[{<<"_rev">>, <<"4-230234">>}]},
#doc{revs = {4, [<<"230234">>]}},
"_rev stored in revs."
},
{
{[{<<"soap">>, 35}]},
#doc{body = {[{<<"soap">>, 35}]}},
"Non underscore prefixed fields stored in body."
},
{
{[
{<<"_attachments">>,
{[
{<<"my_attachment.fu">>,
{[
{<<"stub">>, true},
{<<"content_type">>, <<"application/awesome">>},
{<<"length">>, 45}
]}},
{<<"noahs_private_key.gpg">>,
{[
{<<"data">>, <<"SSBoYXZlIGEgcGV0IGZpc2gh">>},
{<<"content_type">>, <<"application/pgp-signature">>}
]}}
]}}
]},
#doc{
atts = [
couch_att:new([
{name, <<"my_attachment.fu">>},
{data, stub},
{type, <<"application/awesome">>},
{att_len, 45},
{disk_len, 45},
{revpos, undefined},
{encoding, identity},
{md5, <<>>}
]),
couch_att:new([
{name, <<"noahs_private_key.gpg">>},
{data, <<"I have a pet fish!">>},
{type, <<"application/pgp-signature">>},
{att_len, 18},
{disk_len, 18},
{revpos, 0},
{encoding, undefined},
{md5, undefined}
])
]
},
"Attachments are parsed correctly."
},
{
{[{<<"_deleted">>, true}]},
#doc{deleted = true},
"_deleted controls the deleted field."
},
{
{[{<<"_deleted">>, false}]},
#doc{},
"{\"_deleted\": false} is ok."
},
{
{[
{<<"_revisions">>,
{[
{<<"start">>, 4},
{<<"ids">>, [<<"foo1">>, <<"phi3">>, <<"omega">>]}
]}},
{<<"_rev">>, <<"6-something">>}
]},
#doc{revs = {4, [<<"foo1">>, <<"phi3">>, <<"omega">>]}},
"_revisions attribute are preferred to _rev."
},
{
{[{<<"_revs_info">>, dropping}]},
#doc{},
"Drops _revs_info."
},
{
{[{<<"_local_seq">>, dropping}]},
#doc{},
"Drops _local_seq."
},
{
{[{<<"_conflicts">>, dropping}]},
#doc{},
"Drops _conflicts."
},
{
{[{<<"_deleted_conflicts">>, dropping}]},
#doc{},
"Drops _deleted_conflicts."
}
],
lists:map(
fun({EJson, Expect, Msg}) ->
{Msg, ?_assertMatch(Expect, couch_doc:from_json_obj_validate(EJson))}
end,
Cases
).
from_json_error_cases() ->
Cases = [
{
[],
{bad_request, "Document must be a JSON object"},
"arrays are invalid"
},
{
4,
{bad_request, "Document must be a JSON object"},
"integers are invalid"
},
{
true,
{bad_request, "Document must be a JSON object"},
"literals are invalid"
},
{
{[{<<"_id">>, {[{<<"foo">>, 5}]}}]},
{illegal_docid, <<"Document id must be a string">>},
"Document id must be a string."
},
{
{[{<<"_id">>, <<"_random">>}]},
{illegal_docid, <<"Only reserved document ids may start with underscore.">>},
"Disallow arbitrary underscore prefixed docids."
},
{
{[{<<"_rev">>, 5}]},
{bad_request, <<"Invalid rev format">>},
"_rev must be a string"
},
{
{[{<<"_rev">>, "foobar"}]},
{bad_request, <<"Invalid rev format">>},
"_rev must be %d-%s"
},
{
{[{<<"_rev">>, "foo-bar"}]},
"Error if _rev's integer exception is broken."
},
{
{[{<<"_revisions">>, {[{<<"start">>, true}]}}]},
{doc_validation, "_revisions.start isn't an integer."},
"_revisions.start must be an integer."
},
{
{[{<<"_revisions">>, {[{<<"start">>, 0}, {<<"ids">>, 5}]}}]},
{doc_validation, "_revisions.ids isn't a array."},
"_revions.ids must be a list."
},
{
{[{<<"_revisions">>, {[{<<"start">>, 0}, {<<"ids">>, [5]}]}}]},
{doc_validation, "RevId isn't a string"},
"Revision ids must be strings."
},
{
{[
{<<"_revisions">>,
{[
{<<"start">>, 0},
{<<"ids">>, [<<"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx">>]}
]}}
]},
{doc_validation, "RevId isn't a valid hexadecimal"},
"Revision ids must be a valid hex."
},
{
{[{<<"_something">>, 5}]},
{doc_validation, <<"Bad special document member: _something">>},
"Underscore prefix fields are reserved."
},
{
fun() ->
{[
{<<"_id">>, <<"large_doc">>},
{<<"x">>, <<<<"x">> || _ <- lists:seq(1, 1025)>>}
]}
end,
{request_entity_too_large, <<"large_doc">>},
"Document too large."
}
],
lists:map(
fun
({Fun, Expect, Msg}) when is_function(Fun, 0) ->
{Msg, ?_assertThrow(Expect, couch_doc:from_json_obj_validate(Fun()))};
({EJson, Expect, Msg}) ->
{Msg, ?_assertThrow(Expect, couch_doc:from_json_obj_validate(EJson))};
({EJson, Msg}) ->
{Msg, ?_assertThrow(_, couch_doc:from_json_obj_validate(EJson))}
end,
Cases
).
from_json_with_dbname_error_cases() ->
Cases = [
{
{[{<<"_id">>, <<"_random">>}]},
<<"foobar">>,
{illegal_docid, <<"Only reserved document ids may start with underscore.">>},
"Disallow arbitrary underscore prefixed docids in regular database."
},
{
{[{<<"_id">>, <<"_users">>}]},
<<"foobar">>,
{illegal_docid, <<"Only reserved document ids may start with underscore.">>},
"Disallow system-DB docid _users in regular database."
}
],
lists:map(
fun({EJson, DbName, Expect, Msg}) ->
Error = (catch couch_doc:from_json_obj_validate(EJson, DbName)),
{Msg, ?_assertMatch(Expect, Error)}
end,
Cases
).
to_json_success_cases() ->
Cases = [
{
#doc{},
{[{<<"_id">>, <<"">>}]},
"Empty docs are {\"_id\": \"\"}"
},
{
#doc{id = <<"foo">>},
{[{<<"_id">>, <<"foo">>}]},
"_id is added."
},
{
#doc{revs = {5, ["foo"]}},
{[{<<"_id">>, <<>>}, {<<"_rev">>, <<"5-foo">>}]},
"_rev is added."
},
{
[revs],
#doc{revs = {5, [<<"first">>, <<"second">>]}},
{[
{<<"_id">>, <<>>},
{<<"_rev">>, <<"5-first">>},
{<<"_revisions">>,
{[
{<<"start">>, 5},
{<<"ids">>, [<<"first">>, <<"second">>]}
]}}
]},
"_revisions include with revs option"
},
{
#doc{body = {[{<<"foo">>, <<"bar">>}]}},
{[{<<"_id">>, <<>>}, {<<"foo">>, <<"bar">>}]},
"Arbitrary fields are added."
},
{
#doc{deleted = true, body = {[{<<"foo">>, <<"bar">>}]}},
{[{<<"_id">>, <<>>}, {<<"foo">>, <<"bar">>}, {<<"_deleted">>, true}]},
"Deleted docs no longer drop body members."
},
{
#doc{
meta = [
{revs_info, 4, [{<<"fin">>, deleted}, {<<"zim">>, missing}]}
]
},
{[
{<<"_id">>, <<>>},
{<<"_revs_info">>, [
{[{<<"rev">>, <<"4-fin">>}, {<<"status">>, <<"deleted">>}]},
{[{<<"rev">>, <<"3-zim">>}, {<<"status">>, <<"missing">>}]}
]}
]},
"_revs_info field is added correctly."
},
{
#doc{meta = [{local_seq, 5}]},
{[{<<"_id">>, <<>>}, {<<"_local_seq">>, 5}]},
"_local_seq is added as an integer."
},
{
#doc{meta = [{conflicts, [{3, <<"yep">>}, {1, <<"snow">>}]}]},
{[
{<<"_id">>, <<>>},
{<<"_conflicts">>, [<<"3-yep">>, <<"1-snow">>]}
]},
"_conflicts is added as an array of strings."
},
{
#doc{meta = [{deleted_conflicts, [{10923, <<"big_cowboy_hat">>}]}]},
{[
{<<"_id">>, <<>>},
{<<"_deleted_conflicts">>, [<<"10923-big_cowboy_hat">>]}
]},
"_deleted_conflicsts is added as an array of strings."
},
{
#doc{
atts = [
couch_att:new([
{name, <<"big.xml">>},
{type, <<"xml/sucks">>},
{data, fun() -> ok end},
{revpos, 1},
{att_len, 400},
{disk_len, 400},
{md5, <<>>},
{encoding, identity}
]),
couch_att:new([
{name, <<"fast.json">>},
{type, <<"json/ftw">>},
{data, <<"{\"so\": \"there!\"}">>},
{revpos, 1},
{att_len, 16},
{disk_len, 16},
{md5, <<>>},
{encoding, identity}
])
]
},
{[
{<<"_id">>, <<>>},
{<<"_attachments">>,
{[
{<<"big.xml">>,
{[
{<<"content_type">>, <<"xml/sucks">>},
{<<"revpos">>, 1},
{<<"length">>, 400},
{<<"stub">>, true}
]}},
{<<"fast.json">>,
{[
{<<"content_type">>, <<"json/ftw">>},
{<<"revpos">>, 1},
{<<"length">>, 16},
{<<"stub">>, true}
]}}
]}}
]},
"Attachments attached as stubs only include a length."
},
{
[attachments],
#doc{
atts = [
couch_att:new([
{name, <<"stuff.txt">>},
{type, <<"text/plain">>},
{data, fun() -> <<"diet pepsi">> end},
{revpos, 1},
{att_len, 10},
{disk_len, 10},
{md5, <<>>},
{encoding, identity}
]),
couch_att:new([
{name, <<"food.now">>},
{type, <<"application/food">>},
{revpos, 1},
{data, <<"sammich">>},
{md5, <<>>},
{encoding, identity}
]),
couch_att:new([
{name, <<"animals.json">>},
{type, <<"application/json">>},
{revpos, 1},
{data, <<"leon">>},
{md5, undefined},
{encoding, identity}
])
]
},
{[
{<<"_id">>, <<>>},
{<<"_attachments">>,
{[
{<<"stuff.txt">>,
{[
{<<"content_type">>, <<"text/plain">>},
{<<"revpos">>, 1},
{<<"data">>, <<"ZGlldCBwZXBzaQ==">>}
]}},
{<<"food.now">>,
{[
{<<"content_type">>, <<"application/food">>},
{<<"revpos">>, 1},
{<<"data">>, <<"c2FtbWljaA==">>}
]}},
{<<"animals.json">>,
{[
{<<"content_type">>, <<"application/json">>},
{<<"revpos">>, 1},
{<<"data">>, <<"bGVvbg==">>}
]}}
]}}
]},
"Attachments included inline with attachments option."
}
],
lists:map(
fun
({Doc, EJson, Msg}) ->
{Msg, ?_assertMatch(EJson, couch_doc:to_json_obj(Doc, []))};
({Options, Doc, EJson, Msg}) ->
{Msg, ?_assertMatch(EJson, couch_doc:to_json_obj(Doc, Options))}
end,
Cases
).