dominant/test/dmt_api_tests_SUITE.erl
Aleksey Kashapov f95b1607fb
FIN-31: Bumps valitydev/damsel@ab292d9, adds support for commit creation timestamp (#37)
* FIN-31: Bumps valitydev/damsel@ab292d9, adds support for commit creation timestamp

* Bumps workflow, prometheus and dmt-client

* Fixes formatting

* Refactors commits history to respect events occurrence timestamps
2024-05-15 16:40:12 +03:00

431 lines
14 KiB
Erlang

-module(dmt_api_tests_SUITE).
-include_lib("stdlib/include/assert.hrl").
-export([all/0]).
-export([groups/0]).
-export([init_per_suite/1]).
-export([end_per_suite/1]).
-export([init_per_group/2]).
-export([end_per_group/2]).
-export([init_per_testcase/2]).
-export([end_per_testcase/2]).
-export([pull_commit/1]).
-export([retry_commit/1]).
-export([insert/1]).
-export([update/1]).
-export([delete/1]).
-export([missing_version/1]).
-export([obsolete/1]).
-export([conflict_notfound/1]).
-export([conflict_exists/1]).
-export([conflict_mismatch/1]).
-export([nonexistent/1]).
-export([reference_cycles/1]).
-export([checkout_object/1]).
-include_lib("damsel/include/dmsl_domain_conf_thrift.hrl").
-include_lib("damsel/include/dmsl_domain_thrift.hrl").
%% tests descriptions
-type config() :: [{atom(), term()}].
-define(config(Key, C), (element(2, lists:keyfind(Key, 1, C)))).
% to emulate unlimited polling
-define(DEFAULT_LIMIT, 9001).
-type test_case_name() :: atom().
-type group_name() :: atom().
-spec all() -> [{group, group_name()}].
all() ->
[
{group, basic_lifecycle_v5},
{group, cacheless_lifecycle},
{group, repository_client}
].
-spec groups() -> [{group_name(), list(), [test_case_name()]}].
groups() ->
[
{basic_lifecycle_v5, [sequence], [
pull_commit,
{group, basic_lifecycle},
{group, error_mapping},
retry_commit
]},
{cacheless_lifecycle, [sequence], [
pull_commit,
{group, basic_lifecycle},
retry_commit
]},
{basic_lifecycle, [sequence, {repeat, 10}, shuffle], [
insert,
update,
delete
]},
{error_mapping, [], [
missing_version,
obsolete,
conflict_notfound,
conflict_exists,
conflict_mismatch,
nonexistent,
reference_cycles
]},
{repository_client, [parallel], [
checkout_object
]}
].
%%
%% starting/stopping
-spec init_per_suite(config()) -> config().
init_per_suite(C) ->
Apps = genlib_app:start_application_with(scoper, [
{storage, scoper_storage_logger}
]),
[{suite_apps, Apps} | C].
-spec end_per_suite(config()) -> term().
end_per_suite(C) ->
genlib_app:stop_unload_applications(?config(suite_apps, C)).
-spec init_per_group(group_name(), config()) -> config().
init_per_group(basic_lifecycle_v5, C) ->
Apps = start_dmt_api([{repository, dmt_api_repository_v5}]),
[{group_apps, Apps ++ start_client()} | C];
init_per_group(cacheless_lifecycle, C) ->
Apps = start_dmt_api([
{repository, dmt_api_repository_v5},
{max_cache_size, 0}
]),
[{group_apps, Apps ++ start_client()} | C];
init_per_group(repository_client, C) ->
Apps = start_dmt_api([{repository, dmt_api_repository_v5}]),
[{group_apps, Apps ++ start_client()} | C];
init_per_group(_, C) ->
C.
start_dmt_api(Overrides) ->
genlib_app:start_application_with(
dmt_api,
[
{services, #{
automaton => #{
url => "http://machinegun:8022/v1/automaton"
}
}},
% 100Kb
{max_cache_size, 102400}
] ++ Overrides
).
start_client() ->
genlib_app:start_application_with(dmt_client, [
% milliseconds
{cache_update_interval, 5000},
{cache_update_pull_limit, ?DEFAULT_LIMIT},
{max_cache_size, #{
elements => 20,
% 50Mb
memory => 52428800
}},
{service_urls, #{
'Repository' => <<"http://dominant:8022/v1/domain/repository">>,
'RepositoryClient' => <<"http://dominant:8022/v1/domain/repository_client">>
}}
]).
-spec end_per_group(group_name(), config()) -> term().
end_per_group(GroupName, C) when
GroupName == basic_lifecycle_v5;
GroupName == cacheless_lifecycle;
GroupName == repository_client
->
genlib_app:stop_unload_applications(?config(group_apps, C));
end_per_group(_, _C) ->
ok.
-spec init_per_testcase(test_case_name(), config()) -> config().
init_per_testcase(_, C) ->
%% added because dmt_client:checkout(latest)
%% could return old version from cache otherwise
{ok, _Version} = dmt_client_cache:update(),
C.
-spec end_per_testcase(test_case_name(), config()) -> term().
end_per_testcase(_, _) ->
ok.
%%
%% tests
-spec insert(term()) -> term().
insert(_C) ->
ID = next_id(),
Object = fixture_domain_object(ID, <<"InsertFixture">>),
Ref = fixture_object_ref(ID),
#domain_conf_ObjectNotFound{} = (catch dmt_client:checkout_object(Ref)),
#domain_conf_Snapshot{version = Version1, created_at = _} = dmt_client:checkout(latest),
Version2 = dmt_client:insert(Version1, Object),
_ = dmt_client_cache:update(),
Object = dmt_client:checkout_object(Ref),
#domain_conf_ObjectNotFound{} = (catch dmt_client:checkout_object(Version1, Ref)),
Object = dmt_client:checkout_object(Version2, Ref).
-spec update(term()) -> term().
update(_C) ->
ID = next_id(),
Object1 = fixture_domain_object(ID, <<"UpdateFixture1">>),
Object2 = fixture_domain_object(ID, <<"UpdateFixture2">>),
Ref = fixture_object_ref(ID),
#domain_conf_Snapshot{version = Version0} = dmt_client:checkout(latest),
Version1 = dmt_client:commit(Version0, dmt_ct_helper:mk_insert_commit(Object1)),
Version2 = dmt_client:commit(Version1, dmt_ct_helper:mk_update_commit(Object1, Object2)),
_ = dmt_client_cache:update(),
Object1 = dmt_client:checkout_object(Version1, Ref),
Object2 = dmt_client:checkout_object(Version2, Ref),
#domain_conf_Snapshot{version = Version2} = dmt_client:checkout(latest).
-spec delete(term()) -> term().
delete(_C) ->
ID = next_id(),
Object = fixture_domain_object(ID, <<"DeleteFixture">>),
Ref = fixture_object_ref(ID),
#domain_conf_Snapshot{version = Version0} = dmt_client:checkout(latest),
Version1 = dmt_client:commit(Version0, dmt_ct_helper:mk_insert_commit(Object)),
Version2 = dmt_client:commit(Version1, dmt_ct_helper:mk_remove_commit(Object)),
Object = dmt_client:checkout_object(Version1, Ref),
#domain_conf_ObjectNotFound{} = (catch dmt_client:checkout_object(Version2, Ref)),
#domain_conf_Snapshot{version = Version2} = dmt_client:checkout(latest).
-spec pull_commit(term()) -> term().
pull_commit(_C) ->
ID = next_id(),
History1 = #{} = dmt_client:pull_range(0, ?DEFAULT_LIMIT),
Version1 = lists:max([0 | maps:keys(History1)]),
Object = fixture_domain_object(ID, <<"PullFixture">>),
Timestamp = <<"2024-05-14T10:00:00+03:00">>,
Commit = (dmt_ct_helper:mk_insert_commit(Object))#domain_conf_Commit{created_at = Timestamp},
#domain_conf_Commit{ops = CommitOps} = Commit,
Version2 = dmt_client:commit(Version1, Commit),
PulledCommits = dmt_client:pull_range(Version1, ?DEFAULT_LIMIT),
%% Commit matches ops but not given timestamp
?assertMatch(
#{Version2 := #domain_conf_Commit{ops = CommitOps, created_at = CreatedAt}} when CreatedAt =/= Timestamp,
PulledCommits
),
%% All pulled commits must have historical timestamps
_ = [
?assertMatch(#domain_conf_Commit{created_at = CreatedAt} when is_binary(CreatedAt), C)
|| C <- maps:values(PulledCommits)
].
-spec retry_commit(term()) -> term().
retry_commit(_C) ->
Commit1 = dmt_ct_helper:mk_insert_commit(
fixture_domain_object(next_id(), <<"RetryCommitFixture">>)
),
#domain_conf_Snapshot{version = Version1} = dmt_client:checkout(latest),
Version2 = dmt_client:commit(Version1, Commit1),
Version2 = Version1 + 1,
Version2 = dmt_client:commit(Version1, Commit1),
#domain_conf_Snapshot{version = Version2} = dmt_client:checkout(latest),
Commit2 = dmt_ct_helper:mk_insert_commit(
fixture_domain_object(next_id(), <<"RetryCommitFixture">>)
),
Version3 = dmt_client:commit(Version2, Commit2),
Version3 = Version2 + 1,
Version2 = dmt_client:commit(Version1, Commit1),
#domain_conf_Snapshot{version = Version3} = dmt_client:checkout(latest).
-spec missing_version(term()) -> term().
missing_version(_C) ->
#domain_conf_Snapshot{version = Version1} = dmt_client:checkout(latest),
_ = ?assertThrow(
#domain_conf_VersionNotFound{},
dmt_client:insert(
Version1 + 42,
fixture_domain_object(next_id(), <<"MissingVersionFixture">>)
)
).
-spec obsolete(term()) -> term().
obsolete(_C) ->
Commit1 = dmt_ct_helper:mk_insert_commit(
fixture_domain_object(next_id(), <<"InitialFixture">>)
),
Commit2 = dmt_ct_helper:mk_insert_commit(
fixture_domain_object(next_id(), <<"ObsoleteFixture">>)
),
#domain_conf_Snapshot{version = Version1} = dmt_client:checkout(latest),
_Version2 = dmt_client:commit(Version1, Commit1),
_ = ?assertThrow(
#domain_conf_ObsoleteCommitVersion{},
dmt_client:commit(Version1, Commit2)
).
-spec conflict_notfound(term()) -> term().
conflict_notfound(_C) ->
#domain_conf_Snapshot{version = Version1} = dmt_client:checkout(latest),
_ = ?assertThrow(
#domain_conf_OperationConflict{
conflict =
{object_not_found, #domain_conf_ObjectNotFoundConflict{
object_ref = {criterion, #domain_CriterionRef{id = 42}}
}}
},
dmt_client:commit(
Version1,
dmt_ct_helper:mk_update_commit(
criterion_w_refs(42, []),
criterion_w_refs(42, [43, 44, 45])
)
)
).
-spec conflict_exists(term()) -> term().
conflict_exists(_C) ->
ID = next_id(),
Ref = fixture_object_ref(ID),
Commit = dmt_ct_helper:mk_insert_commit(
fixture_domain_object(ID, <<"ExistingObjectFixture">>)
),
#domain_conf_Snapshot{version = Version1} = dmt_client:checkout(latest),
Version2 = dmt_client:commit(Version1, Commit),
_ = ?assertThrow(
#domain_conf_OperationConflict{
conflict = {object_already_exists, #domain_conf_ObjectAlreadyExistsConflict{object_ref = Ref}}
},
dmt_client:commit(Version2, Commit)
).
-spec conflict_mismatch(term()) -> term().
conflict_mismatch(_C) ->
ID1 = next_id(),
ID2 = next_id(),
Object1 = fixture_domain_object(ID1, <<"Original">>),
Ref2 = fixture_object_ref(ID2),
#domain_conf_Snapshot{version = Version1} = dmt_client:checkout(latest),
Version2 = dmt_client:commit(Version1, dmt_ct_helper:mk_insert_commit(Object1)),
_ = ?assertThrow(
#domain_conf_OperationConflict{
conflict = {object_reference_mismatch, #domain_conf_ObjectReferenceMismatchConflict{object_ref = Ref2}}
},
dmt_client:commit(
Version2,
dmt_ct_helper:mk_update_commit(
Object1,
fixture_domain_object(ID2, <<"Mismatch">>)
)
)
).
-spec nonexistent(term()) -> term().
nonexistent(_C) ->
#domain_conf_Snapshot{version = Version1} = dmt_client:checkout(latest),
_ = ?assertThrow(
#domain_conf_OperationInvalid{
errors = [
{object_not_exists, #domain_conf_NonexistantObject{
object_ref = {criterion, #domain_CriterionRef{}},
referenced_by = [{criterion, #domain_CriterionRef{id = 42}}]
}}
| _
]
},
dmt_client:insert(Version1, criterion_w_refs(42, [43, 44, 45]))
).
-spec reference_cycles(term()) -> term().
reference_cycles(_C) ->
#domain_conf_Snapshot{version = Version1} = dmt_client:checkout(latest),
_ = ?assertThrow(
#domain_conf_OperationInvalid{
errors = [
%% we expect 3 cycles to be found
{object_reference_cycle, #domain_conf_ObjectReferenceCycle{
cycle = [{criterion, #domain_CriterionRef{}} | _]
}},
{object_reference_cycle, #domain_conf_ObjectReferenceCycle{
cycle = [{criterion, #domain_CriterionRef{}} | _]
}},
{object_reference_cycle, #domain_conf_ObjectReferenceCycle{
cycle = [{criterion, #domain_CriterionRef{}} | _]
}}
]
},
dmt_client:insert(
Version1,
[
criterion_w_refs(1, [2]),
criterion_w_refs(2, [3]),
criterion_w_refs(3, [4, 1]),
criterion_w_refs(4, [1, 2])
]
)
).
-spec checkout_object(term()) -> term().
checkout_object(_C) ->
ID = next_id(),
Object = fixture_domain_object(ID, <<"InsertFixture">>),
Ref = fixture_object_ref(ID),
#domain_conf_Snapshot{version = Version1} = dmt_client:checkout(latest),
Version2 = dmt_client:insert(Version1, Object),
?assertEqual(
{ok, #domain_conf_VersionedObject{version = Version2, object = Object}},
call_checkout_object({head, #domain_conf_Head{}}, Ref)
),
?assertEqual(
{exception, #domain_conf_ObjectNotFound{}},
call_checkout_object({version, Version1}, Ref)
),
?assertEqual(
{ok, #domain_conf_VersionedObject{version = Version2, object = Object}},
call_checkout_object({version, Version2}, Ref)
),
?assertEqual(
{exception, #domain_conf_VersionNotFound{}},
call_checkout_object({version, Version2 + 1}, Ref)
).
next_id() ->
dmt_ct_helper:next_id().
fixture_domain_object(Ref, Data) ->
{category, #domain_CategoryObject{
ref = #domain_CategoryRef{id = Ref},
data = #domain_Category{name = Data, description = Data}
}}.
fixture_object_ref(Ref) ->
{category, #domain_CategoryRef{id = Ref}}.
criterion_w_refs(ID, Refs) ->
{criterion, #domain_CriterionObject{
ref = #domain_CriterionRef{id = ID},
data = #domain_Criterion{
name = genlib:format(ID),
predicate = {any_of, ordsets:from_list([{criterion, #domain_CriterionRef{id = Ref}} || Ref <- Refs])}
}
}}.
%%
call_checkout_object(Version, ObjectReference) ->
call('RepositoryClient', 'checkoutObject', {Version, ObjectReference}).
call(ServiceName, Function, Args) ->
Url = <<"http://dominant:8022/v1/domain/repository_client">>,
Call = {{dmsl_domain_conf_thrift, ServiceName}, Function, Args},
CallOpts = #{
url => Url,
event_handler => [scoper_woody_event_handler]
},
woody_client:call(Call, CallOpts, woody_context:new()).