blob: cbfe7cca6ed524dec4589cd95065ab6074c7e6e8 [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_db_misc_tests).
% Used in events_listener test
-export([
event_listener_callback/3
]).
-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").
misc_test_() ->
{
"Test database miscellaney",
{
setup,
fun setup/0,
fun cleanup/1,
with([
?TDEF(empty_db_info),
?TDEF(accessors),
?TDEF(set_revs_limit),
?TDEF(set_security),
?TDEF(get_security_cached),
?TDEF(is_system_db),
?TDEF(validate_dbname),
?TDEF(validate_doc_ids),
?TDEF(get_doc_info),
?TDEF(get_doc_info_not_found),
?TDEF(get_full_doc_info),
?TDEF(get_full_doc_info_not_found),
?TDEF(get_full_doc_infos),
?TDEF(ensure_full_commit),
?TDEF(metadata_bump),
?TDEF(db_version_bump),
?TDEF(db_cache_doesnt_evict_newer_handles),
?TDEF(events_listener)
])
}
}.
setup() ->
Ctx = test_util:start_couch([fabric]),
DbName = ?tempdb(),
{ok, Db} = fabric2_db:create(DbName, [{user_ctx, ?ADMIN_USER}]),
{DbName, Db, Ctx}.
cleanup({_DbName, Db, Ctx}) ->
meck:unload(),
ok = fabric2_db:delete(fabric2_db:name(Db), []),
test_util:stop_couch(Ctx).
empty_db_info({DbName, Db, _}) ->
{ok, Info} = fabric2_db:get_db_info(Db),
?assertEqual(DbName, fabric2_util:get_value(db_name, Info)),
?assertEqual(0, fabric2_util:get_value(doc_count, Info)),
?assertEqual(0, fabric2_util:get_value(doc_del_count, Info)),
?assert(is_binary(fabric2_util:get_value(update_seq, Info))),
InfoUUID = fabric2_util:get_value(uuid, Info),
UUID = fabric2_db:get_uuid(Db),
?assertEqual(UUID, InfoUUID).
accessors({DbName, Db, _}) ->
SeqZero = fabric2_fdb:vs_to_seq(fabric2_util:seq_zero_vs()),
?assertEqual(DbName, fabric2_db:name(Db)),
?assertEqual(0, fabric2_db:get_instance_start_time(Db)),
?assertEqual(nil, fabric2_db:get_pid(Db)),
?assertEqual(undefined, fabric2_db:get_before_doc_update_fun(Db)),
?assertEqual(undefined, fabric2_db:get_after_doc_read_fun(Db)),
?assertEqual(SeqZero, fabric2_db:get_committed_update_seq(Db)),
?assertEqual(SeqZero, fabric2_db:get_compacted_seq(Db)),
?assertEqual(SeqZero, fabric2_db:get_update_seq(Db)),
?assertEqual(nil, fabric2_db:get_compactor_pid(Db)),
?assertEqual(1000, fabric2_db:get_revs_limit(Db)),
?assertMatch(<<_:32/binary>>, fabric2_db:get_uuid(Db)),
?assertEqual(true, fabric2_db:is_db(Db)),
?assertEqual(false, fabric2_db:is_db(#{})),
?assertEqual(false, fabric2_db:is_partitioned(Db)),
?assertEqual(false, fabric2_db:is_clustered(Db)).
set_revs_limit({DbName, Db, _}) ->
?assertEqual(ok, fabric2_db:set_revs_limit(Db, 500)),
{ok, Db2} = fabric2_db:open(DbName, []),
?assertEqual(500, fabric2_db:get_revs_limit(Db2)).
set_security({DbName, Db, _}) ->
SecObj =
{[
{<<"admins">>,
{[
{<<"names">>, []},
{<<"roles">>, []}
]}}
]},
?assertEqual(ok, fabric2_db:set_security(Db, SecObj)),
{ok, Db2} = fabric2_db:open(DbName, []),
?assertEqual(SecObj, fabric2_db:get_security(Db2)).
get_security_cached({DbName, Db, _}) ->
OldSecObj = fabric2_db:get_security(Db),
SecObj =
{[
{<<"admins">>,
{[
{<<"names">>, [<<"foo1">>]},
{<<"roles">>, []}
]}}
]},
% Set directly so we don't auto-update the local cache
{ok, Db1} = fabric2_db:open(DbName, [?ADMIN_CTX]),
?assertMatch(
{ok, #{}},
fabric2_fdb:transactional(Db1, fun(TxDb) ->
fabric2_fdb:set_config(TxDb, security_doc, SecObj)
end)
),
{ok, Db2} = fabric2_db:open(DbName, [?ADMIN_CTX]),
?assertEqual(OldSecObj, fabric2_db:get_security(Db2, [{max_age, 1000}])),
timer:sleep(100),
?assertEqual(SecObj, fabric2_db:get_security(Db2, [{max_age, 50}])),
?assertEqual(ok, fabric2_db:set_security(Db2, OldSecObj)).
is_system_db({DbName, Db, _}) ->
?assertEqual(false, fabric2_db:is_system_db(Db)),
?assertEqual(false, fabric2_db:is_system_db_name("foo")),
?assertEqual(false, fabric2_db:is_system_db_name(DbName)),
?assertEqual(true, fabric2_db:is_system_db_name(<<"_replicator">>)),
?assertEqual(true, fabric2_db:is_system_db_name("_replicator")),
?assertEqual(true, fabric2_db:is_system_db_name(<<"foo/_replicator">>)),
?assertEqual(false, fabric2_db:is_system_db_name(<<"f.o/_replicator">>)),
?assertEqual(false, fabric2_db:is_system_db_name(<<"foo/bar">>)).
validate_dbname(_) ->
Tests = [
{ok, <<"foo">>},
{ok, "foo"},
{ok, <<"_replicator">>},
{error, illegal_database_name, <<"Foo">>},
{error, illegal_database_name, <<"foo|bar">>},
{error, illegal_database_name, <<"Foo">>},
{error, database_name_too_long, <<
"0123456789012345678901234567890123456789"
"0123456789012345678901234567890123456789"
"0123456789012345678901234567890123456789"
"0123456789012345678901234567890123456789"
"0123456789012345678901234567890123456789"
"0123456789012345678901234567890123456789"
>>}
],
CheckFun = fun
({ok, DbName}) ->
?assertEqual(ok, fabric2_db:validate_dbname(DbName));
({error, Reason, DbName}) ->
Expect = {error, {Reason, DbName}},
?assertEqual(Expect, fabric2_db:validate_dbname(DbName))
end,
try
% Don't allow epi plugins to interfere with test results
meck:new(couch_epi, [passthrough]),
meck:expect(couch_epi, decide, 5, no_decision),
lists:foreach(CheckFun, Tests)
after
% Unload within the test to minimize interference with other tests
meck:unload()
end.
validate_doc_ids(_) ->
% Basic test with default max infinity length
?assertEqual(ok, fabric2_db:validate_docid(<<"foo">>)),
Tests = [
{ok, <<"_local/foo">>},
{ok, <<"_design/foo">>},
{ok, generate_long_doc_id(16)},
{ok, generate_long_doc_id(512)},
{illegal_docid, <<"">>},
{illegal_docid, <<"_design/">>},
{illegal_docid, <<"_local/">>},
{illegal_docid, generate_long_doc_id(513)},
{illegal_docid, <<16#FF>>},
{illegal_docid, <<"_bad">>},
{illegal_docid, null}
],
CheckFun = fun
({ok, DocId}) ->
?assertEqual(ok, fabric2_db:validate_docid(DocId));
({illegal_docid, DocId}) ->
?assertThrow({illegal_docid, _}, fabric2_db:validate_docid(DocId))
end,
try
meck:new(config, [passthrough]),
meck:expect(
config,
get,
["couchdb", "max_document_id_length", "512"],
"16"
),
lists:foreach(CheckFun, Tests),
% Check that fabric2_db_plugin can't allow for
% underscore prefixed dbs
meck:new(fabric2_db_plugin, [passthrough]),
meck:expect(fabric2_db_plugin, validate_docid, ['_'], true),
?assertEqual(ok, fabric2_db:validate_docid(<<"_wheee">>))
after
% Unloading within the test as the config mock
% interferes with the db version bump test.
meck:unload()
end.
generate_long_doc_id(Size) ->
list_to_binary(string:copies("x", Size)).
get_doc_info({_, Db, _}) ->
DocId = couch_uuids:random(),
InsertDoc = #doc{
id = DocId,
body = {[{<<"foo">>, true}]}
},
{ok, {Pos, Rev}} = fabric2_db:update_doc(Db, InsertDoc, []),
DI = fabric2_db:get_doc_info(Db, DocId),
?assert(is_record(DI, doc_info)),
#doc_info{
id = DIDocId,
high_seq = HighSeq,
revs = Revs
} = DI,
?assertEqual(DocId, DIDocId),
?assert(is_binary(HighSeq)),
?assertMatch([#rev_info{}], Revs),
[
#rev_info{
rev = DIRev,
seq = Seq,
deleted = Deleted,
body_sp = BodySp
}
] = Revs,
?assertEqual({Pos, Rev}, DIRev),
?assert(is_binary(Seq)),
?assert(not Deleted),
?assertMatch(undefined, BodySp).
get_doc_info_not_found({_, Db, _}) ->
DocId = couch_uuids:random(),
?assertEqual(not_found, fabric2_db:get_doc_info(Db, DocId)).
get_full_doc_info({_, Db, _}) ->
DocId = couch_uuids:random(),
InsertDoc = #doc{
id = DocId,
body = {[{<<"foo">>, true}]}
},
{ok, {Pos, Rev}} = fabric2_db:update_doc(Db, InsertDoc, []),
FDI = fabric2_db:get_full_doc_info(Db, DocId),
?assert(is_record(FDI, full_doc_info)),
#full_doc_info{
id = FDIDocId,
update_seq = UpdateSeq,
deleted = Deleted,
rev_tree = RevTree,
sizes = SizeInfo
} = FDI,
?assertEqual(DocId, FDIDocId),
?assert(is_binary(UpdateSeq)),
?assert(not Deleted),
?assertMatch([{Pos, {Rev, _, []}}], RevTree),
?assertEqual(#size_info{}, SizeInfo).
get_full_doc_info_not_found({_, Db, _}) ->
DocId = couch_uuids:random(),
?assertEqual(not_found, fabric2_db:get_full_doc_info(Db, DocId)).
get_full_doc_infos({_, Db, _}) ->
DocIds = lists:map(
fun(_) ->
DocId = couch_uuids:random(),
Doc = #doc{id = DocId},
{ok, _} = fabric2_db:update_doc(Db, Doc, []),
DocId
end,
lists:seq(1, 5)
),
FDIs = fabric2_db:get_full_doc_infos(Db, DocIds),
lists:zipwith(
fun(DocId, FDI) ->
?assertEqual(DocId, FDI#full_doc_info.id)
end,
DocIds,
FDIs
).
ensure_full_commit({_, Db, _}) ->
?assertEqual({ok, 0}, fabric2_db:ensure_full_commit(Db)),
?assertEqual({ok, 0}, fabric2_db:ensure_full_commit(Db, 5)).
metadata_bump({DbName, _, _}) ->
% Call open again here to make sure we have a version in the cache
% as we'll be checking if that version gets its metadata bumped
{ok, Db} = fabric2_db:open(DbName, [{user_ctx, ?ADMIN_USER}]),
% Emulate a remote client bumping the metadataversion
{ok, Fdb} = application:get_env(fabric, db),
erlfdb:transactional(Fdb, fun(Tx) ->
erlfdb:set_versionstamped_value(Tx, ?METADATA_VERSION_KEY, <<0:112>>)
end),
NewMDVersion = erlfdb:transactional(Fdb, fun(Tx) ->
erlfdb:wait(erlfdb:get(Tx, ?METADATA_VERSION_KEY))
end),
% Save timestamp before ensure_current/1 is called
TsBeforeEnsureCurrent = erlang:monotonic_time(millisecond),
% Perform a random operation which calls ensure_current
{ok, _} = fabric2_db:get_db_info(Db),
% Check that db handle in the cache got the new metadata version
% and that check_current_ts was updated
CachedDb = fabric2_server:fetch(DbName, undefined),
?assertMatch(
#{
md_version := NewMDVersion,
check_current_ts := Ts
} when Ts >= TsBeforeEnsureCurrent,
CachedDb
).
db_version_bump({DbName, _, _}) ->
% Call open again here to make sure we have a version in the cache
% as we'll be checking if that version gets its metadata bumped
{ok, Db} = fabric2_db:open(DbName, [{user_ctx, ?ADMIN_USER}]),
% Emulate a remote client bumping db version. We don't go through the
% regular db open + update security doc or something like that to make sure
% we don't touch the local cache
#{db_prefix := DbPrefix} = Db,
DbVersionKey = erlfdb_tuple:pack({?DB_VERSION}, DbPrefix),
{ok, Fdb} = application:get_env(fabric, db),
NewDbVersion = fabric2_util:uuid(),
erlfdb:transactional(Fdb, fun(Tx) ->
erlfdb:set(Tx, DbVersionKey, NewDbVersion),
erlfdb:set_versionstamped_value(Tx, ?METADATA_VERSION_KEY, <<0:112>>)
end),
% Perform a random operation which calls ensure_current
{ok, _} = fabric2_db:get_db_info(Db),
% After previous operation, the cache should have been cleared
?assertMatch(undefined, fabric2_server:fetch(DbName, undefined)),
% Call open again and check that we have the latest db version
{ok, Db2} = fabric2_db:open(DbName, [{user_ctx, ?ADMIN_USER}]),
% Check that db handle in the cache got the new metadata version
?assertMatch(#{db_version := NewDbVersion}, Db2).
db_cache_doesnt_evict_newer_handles({DbName, _, _}) ->
{ok, Db} = fabric2_db:open(DbName, [{user_ctx, ?ADMIN_USER}]),
CachedDb = fabric2_server:fetch(DbName, undefined),
StaleDb = Db#{md_version := <<0>>},
ok = fabric2_server:store(StaleDb),
?assertEqual(CachedDb, fabric2_server:fetch(DbName, undefined)),
?assert(not fabric2_server:maybe_update(StaleDb)),
?assertEqual(CachedDb, fabric2_server:fetch(DbName, undefined)),
?assert(not fabric2_server:maybe_remove(StaleDb)),
?assertEqual(CachedDb, fabric2_server:fetch(DbName, undefined)).
events_listener({DbName, Db, _}) ->
Opts = [
{dbname, DbName},
{uuid, fabric2_db:get_uuid(Db)},
{timeout, 100}
],
Fun = event_listener_callback,
{ok, Pid} = fabric2_events:link_listener(?MODULE, Fun, self(), Opts),
unlink(Pid),
Ref = monitor(process, Pid),
NextEvent = fun(Timeout) ->
receive
{Pid, Evt} when is_pid(Pid) -> Evt;
{'DOWN', Ref, _, _, normal} -> exited_normal
after Timeout ->
timeout
end
end,
Doc1 = #doc{id = couch_uuids:random()},
{ok, _} = fabric2_db:update_doc(Db, Doc1, []),
?assertEqual(updated, NextEvent(1000)),
% Just one update, then expect a timeout
?assertEqual(timeout, NextEvent(500)),
Doc2 = #doc{id = couch_uuids:random()},
{ok, _} = fabric2_db:update_doc(Db, Doc2, []),
?assertEqual(updated, NextEvent(1000)),
% Process is still alive
?assert(is_process_alive(Pid)),
% Recreate db
ok = fabric2_db:delete(DbName, [?ADMIN_CTX]),
{ok, _} = fabric2_db:create(DbName, [?ADMIN_CTX]),
?assertEqual(deleted, NextEvent(1000)),
% After db is deleted or re-created listener should die
?assertEqual(exited_normal, NextEvent(1000)).
% Callback for event_listener function
event_listener_callback(_DbName, Event, TestPid) ->
TestPid ! {self(), Event},
{ok, TestPid}.