blob: 5d28b6da0c3210f0b336fa89d7bb543758788e6e [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(fabric2_doc_att_tests).
-include_lib("couch/include/couch_db.hrl").
-include_lib("couch/include/couch_eunit.hrl").
-include_lib("eunit/include/eunit.hrl").
-include("fabric2.hrl").
-include("fabric2_test.hrl").
doc_crud_test_() ->
{
"Test document CRUD operations",
{
setup,
fun setup/0,
fun cleanup/1,
with([
?TDEF(create_att),
?TDEF(create_att_already_compressed),
?TDEF(delete_att),
?TDEF(multiple_atts),
?TDEF(delete_one_att),
?TDEF(large_att),
?TDEF(att_on_conflict_isolation)
])
}
}.
setup() ->
Ctx = test_util:start_couch([fabric]),
{ok, Db} = fabric2_db:create(?tempdb(), [{user_ctx, ?ADMIN_USER}]),
{Db, Ctx}.
cleanup({Db, Ctx}) ->
ok = fabric2_db:delete(fabric2_db:name(Db), []),
test_util:stop_couch(Ctx).
create_att({Db, _}) ->
DocId = fabric2_util:uuid(),
Att1 = couch_att:new([
{name, <<"foo.txt">>},
{type, <<"application/octet-stream">>},
{att_len, 6},
{data, <<"foobar">>},
{encoding, identity},
{md5, <<>>}
]),
Doc1 = #doc{
id = DocId,
atts = [Att1]
},
{ok, _} = fabric2_db:update_doc(Db, Doc1),
{ok, Doc2} = fabric2_db:open_doc(Db, DocId),
#doc{
atts = [Att2]
} = Doc2,
{loc, _Db, DocId, AttId} = couch_att:fetch(data, Att2),
AttData = fabric2_db:read_attachment(Db, DocId, AttId),
?assertEqual(<<"foobar">>, AttData),
% Check that the raw keys exist
#{
db_prefix := DbPrefix
} = Db,
IdKey = erlfdb_tuple:pack({?DB_ATT_NAMES, DocId, AttId}, DbPrefix),
AttKey = erlfdb_tuple:pack({?DB_ATTS, DocId, AttId}, DbPrefix),
fabric2_fdb:transactional(fun(Tx) ->
IdVal = erlfdb:wait(erlfdb:get(Tx, IdKey)),
AttVals = erlfdb:wait(erlfdb:get_range_startswith(Tx, AttKey)),
?assertEqual(erlfdb_tuple:pack({0, true}), IdVal),
Opts = [{minor_version, 1}, {compressed, 6}],
Expect = term_to_binary(<<"foobar">>, Opts),
?assertMatch([{_, Expect}], AttVals)
end).
create_att_already_compressed({Db, _}) ->
DocId = fabric2_util:uuid(),
Att1 = couch_att:new([
{name, <<"foo.txt">>},
{type, <<"application/octet-stream">>},
{att_len, 6},
{data, <<"foobar">>},
{encoding, gzip},
{md5, <<>>}
]),
Doc1 = #doc{
id = DocId,
atts = [Att1]
},
{ok, _} = fabric2_db:update_doc(Db, Doc1),
{ok, Doc2} = fabric2_db:open_doc(Db, DocId),
#doc{
atts = [Att2]
} = Doc2,
{loc, _Db, DocId, AttId} = couch_att:fetch(data, Att2),
AttData = fabric2_db:read_attachment(Db, DocId, AttId),
?assertEqual(<<"foobar">>, AttData),
% Check that the raw keys exist
#{
db_prefix := DbPrefix
} = Db,
IdKey = erlfdb_tuple:pack({?DB_ATT_NAMES, DocId, AttId}, DbPrefix),
AttKey = erlfdb_tuple:pack({?DB_ATTS, DocId, AttId}, DbPrefix),
fabric2_fdb:transactional(fun(Tx) ->
IdVal = erlfdb:wait(erlfdb:get(Tx, IdKey)),
AttVals = erlfdb:wait(erlfdb:get_range_startswith(Tx, AttKey)),
?assertEqual(erlfdb_tuple:pack({0, false}), IdVal),
?assertMatch([{_, <<"foobar">>}], AttVals)
end).
delete_att({Db, _}) ->
DocId = fabric2_util:uuid(),
Att1 = couch_att:new([
{name, <<"foo.txt">>},
{type, <<"application/octet-stream">>},
{att_len, 6},
{data, <<"foobar">>},
{encoding, identity},
{md5, <<>>}
]),
Doc1 = #doc{
id = DocId,
atts = [Att1]
},
{ok, _} = fabric2_db:update_doc(Db, Doc1),
{ok, Doc2} = fabric2_db:open_doc(Db, DocId),
#doc{
atts = [Att2]
} = Doc2,
{loc, _Db, DocId, AttId} = couch_att:fetch(data, Att2),
Doc3 = Doc2#doc{atts = []},
{ok, _} = fabric2_db:update_doc(Db, Doc3),
{ok, Doc4} = fabric2_db:open_doc(Db, DocId),
?assertEqual([], Doc4#doc.atts),
% Check that the raw keys were removed
#{
db_prefix := DbPrefix
} = Db,
IdKey = erlfdb_tuple:pack({?DB_ATT_NAMES, DocId, AttId}, DbPrefix),
AttKey = erlfdb_tuple:pack({?DB_ATTS, DocId, AttId}, DbPrefix),
fabric2_fdb:transactional(fun(Tx) ->
IdVal = erlfdb:wait(erlfdb:get(Tx, IdKey)),
AttVals = erlfdb:wait(erlfdb:get_range_startswith(Tx, AttKey)),
?assertEqual(not_found, IdVal),
?assertMatch([], AttVals)
end).
multiple_atts({Db, _}) ->
DocId = fabric2_util:uuid(),
Atts = [
mk_att(<<"foo.txt">>, <<"foobar">>),
mk_att(<<"bar.txt">>, <<"barfoo">>),
mk_att(<<"baz.png">>, <<"blargh">>)
],
{ok, _} = create_doc(Db, DocId, Atts),
?assertEqual(
#{
<<"foo.txt">> => <<"foobar">>,
<<"bar.txt">> => <<"barfoo">>,
<<"baz.png">> => <<"blargh">>
},
read_atts(Db, DocId)
).
delete_one_att({Db, _}) ->
DocId = fabric2_util:uuid(),
Atts1 = [
mk_att(<<"foo.txt">>, <<"foobar">>),
mk_att(<<"bar.txt">>, <<"barfoo">>),
mk_att(<<"baz.png">>, <<"blargh">>)
],
{ok, RevId} = create_doc(Db, DocId, Atts1),
Atts2 = tl(Atts1),
{ok, _} = update_doc(Db, DocId, RevId, stubify(RevId, Atts2)),
?assertEqual(
#{
<<"bar.txt">> => <<"barfoo">>,
<<"baz.png">> => <<"blargh">>
},
read_atts(Db, DocId)
).
large_att({Db, _}) ->
DocId = fabric2_util:uuid(),
% Total size ~360,000 bytes
AttData = iolist_to_binary([
<<"foobar">> || _ <- lists:seq(1, 60000)
]),
Att1 = mk_att(<<"long.txt">>, AttData, gzip),
{ok, _} = create_doc(Db, DocId, [Att1]),
?assertEqual(#{<<"long.txt">> => AttData}, read_atts(Db, DocId)),
{ok, Doc} = fabric2_db:open_doc(Db, DocId),
#doc{atts = [Att2]} = Doc,
{loc, _Db, DocId, AttId} = couch_att:fetch(data, Att2),
#{db_prefix := DbPrefix} = Db,
AttKey = erlfdb_tuple:pack({?DB_ATTS, DocId, AttId}, DbPrefix),
fabric2_fdb:transactional(fun(Tx) ->
AttVals = erlfdb:wait(erlfdb:get_range_startswith(Tx, AttKey)),
?assertEqual(4, length(AttVals))
end).
att_on_conflict_isolation({Db, _}) ->
DocId = fabric2_util:uuid(),
[PosRevA1, PosRevB1] = create_conflicts(Db, DocId, []),
Att = mk_att(<<"happy_goat.tiff">>, <<":D>">>),
{ok, PosRevA2} = update_doc(Db, DocId, PosRevA1, [Att]),
?assertEqual(
#{<<"happy_goat.tiff">> => <<":D>">>},
read_atts(Db, DocId, PosRevA2)
),
?assertEqual(#{}, read_atts(Db, DocId, PosRevB1)).
mk_att(Name, Data) ->
mk_att(Name, Data, identity).
mk_att(Name, Data, Encoding) ->
couch_att:new([
{name, Name},
{type, <<"application/octet-stream">>},
{att_len, size(Data)},
{data, Data},
{encoding, Encoding},
{md5, <<>>}
]).
stubify(RevId, Atts) when is_list(Atts) ->
lists:map(fun(Att) ->
stubify(RevId, Att)
end, Atts);
stubify({Pos, _Rev}, Att) ->
couch_att:store([
{data, stub},
{revpos, Pos}
], Att).
create_doc(Db, DocId, Atts) ->
Doc = #doc{
id = DocId,
atts = Atts
},
fabric2_db:update_doc(Db, Doc).
update_doc(Db, DocId, {Pos, Rev}, Atts) ->
Doc = #doc{
id = DocId,
revs = {Pos, [Rev]},
atts = Atts
},
fabric2_db:update_doc(Db, Doc).
create_conflicts(Db, DocId, Atts) ->
Base = #doc{
id = DocId,
atts = Atts
},
{ok, {_, Rev1} = PosRev} = fabric2_db:update_doc(Db, Base),
<<Rev2:16/binary, Rev3:16/binary>> = fabric2_util:uuid(),
Doc1 = #doc{
id = DocId,
revs = {2, [Rev2, Rev1]},
atts = stubify(PosRev, Atts)
},
Doc2 = #doc{
id = DocId,
revs = {2, [Rev3, Rev1]},
atts = stubify(PosRev, Atts)
},
{ok, _} = fabric2_db:update_doc(Db, Doc1, [replicated_changes]),
{ok, _} = fabric2_db:update_doc(Db, Doc2, [replicated_changes]),
lists:reverse(lists:sort([{2, Rev2}, {2, Rev3}])).
read_atts(Db, DocId) ->
{ok, #doc{atts = Atts}} = fabric2_db:open_doc(Db, DocId),
atts_to_map(Db, DocId, Atts).
read_atts(Db, DocId, PosRev) ->
{ok, Docs} = fabric2_db:open_doc_revs(Db, DocId, [PosRev], []),
[{ok, #doc{atts = Atts}}] = Docs,
atts_to_map(Db, DocId, Atts).
atts_to_map(Db, DocId, Atts) ->
lists:foldl(fun(Att, Acc) ->
[Name, Data] = couch_att:fetch([name, data], Att),
{loc, _Db, DocId, AttId} = Data,
AttBin = fabric2_db:read_attachment(Db, DocId, AttId),
maps:put(Name, AttBin, Acc)
end, #{}, Atts).