%%%------------------------------------------------------------------- %%% @doc API-тесты для административного управления календарями. %%% Покрывает эндпоинты: %%% GET /v1/admin/calendars %%% GET /v1/admin/calendars/:id %%% PUT /v1/admin/calendars/:id %%% DELETE /v1/admin/calendars/:id %%% Проверяет пагинацию, фильтрацию по статусу/типу, поиск и сортировку. %%% @end %%%------------------------------------------------------------------- -module(admin_calendars_tests). -include_lib("eunit/include/eunit.hrl"). -export([test/0]). %%%=================================================================== %%% Главная тестовая функция %%%=================================================================== -spec test() -> ok. test() -> ct:pal("=== Admin Calendars Tests ==="), {AdminToken, UserToken, CalIds} = setup(), [CalId1, CalId2, _CalId3] = CalIds, try %% Основные CRUD test_get_calendar(AdminToken, CalId1), test_get_calendar_not_found(AdminToken), test_update_calendar(AdminToken, CalId1, UserToken), test_update_calendar_not_found(AdminToken), test_update_calendar_invalid_body(AdminToken, CalId1), test_delete_calendar(AdminToken, CalId2), test_delete_calendar_not_found(AdminToken), %% Пагинация test_pagination(AdminToken), %% Фильтрация test_filter_by_status(AdminToken), test_filter_by_type(AdminToken), test_search(AdminToken), %% Сортировка test_sort_by_title(AdminToken), test_sort_by_created_at(AdminToken) after cleanup(AdminToken, CalIds) end, ct:pal("=== All admin calendars tests passed ==="), ok. %%%=================================================================== %%% Подготовка и очистка %%%=================================================================== -spec setup() -> {binary(), binary(), [binary()]}. setup() -> ct:pal(" Setup: register user and create calendars"), AdminToken = api_test_runner:get_admin_token(), Email = api_test_runner:unique_email(<<"caladmin">>), Password = <<"testpass">>, UserToken = api_test_runner:register_and_login(Email, Password), CalId1 = create_calendar(UserToken, <<"Personal Active">>, <<"Desc1">>), CalId2 = create_calendar(UserToken, <<"Commercial">>, <<"Desc2">>), CalId3 = create_calendar(UserToken, <<"Frozen">>, <<"Desc3">>), update_calendar_admin(AdminToken, CalId2, #{<<"type">> => <<"commercial">>}), update_calendar_admin(AdminToken, CalId3, #{<<"status">> => <<"frozen">>}), ct:pal(" Created calendars: ~p", [[CalId1, CalId2, CalId3]]), {AdminToken, UserToken, [CalId1, CalId2, CalId3]}. -spec create_calendar(binary(), binary(), binary()) -> binary(). create_calendar(UserToken, Title, Description) -> Body = jsx:encode(#{title => Title, description => Description}), {ok, 201, _, Resp} = api_test_runner:client_request(post, <<"/v1/calendars">>, UserToken, Body), #{<<"id">> := Id} = jsx:decode(list_to_binary(Resp), [return_maps]), Id. -spec update_calendar_admin(binary(), binary(), map()) -> ok. update_calendar_admin(AdminToken, CalId, Updates) -> % Получаем текущий календарь {ok, 200, _, RespBody} = api_test_runner:admin_request(get, <<"/v1/admin/calendars/", CalId/binary>>, AdminToken, <<"">>), Cal = jsx:decode(list_to_binary(RespBody), [return_maps]), % Сливаем изменения UpdatedCal = maps:merge(Cal, Updates), Body = jsx:encode(UpdatedCal), {ok, 200, _, _} = api_test_runner:admin_request(put, <<"/v1/admin/calendars/", CalId/binary>>, AdminToken, Body), ok. -spec cleanup(binary(), [binary()]) -> ok. cleanup(_AdminToken, []) -> ok; cleanup(AdminToken, [Id | Rest]) -> catch api_test_runner:admin_request(delete, <<"/v1/admin/calendars/", Id/binary>>, AdminToken, <<"">>), cleanup(AdminToken, Rest). %%%=================================================================== %%% CRUD тесты %%%=================================================================== test_get_calendar(AdminToken, CalId) -> ct:pal(" TEST: Get calendar by ID (admin)"), {ok, 200, _, RespBody} = api_test_runner:admin_request(get, <<"/v1/admin/calendars/", CalId/binary>>, AdminToken, <<"">>), Cal = jsx:decode(list_to_binary(RespBody), [return_maps]), ?assertEqual(CalId, maps:get(<<"id">>, Cal)), ct:pal(" OK"). test_get_calendar_not_found(AdminToken) -> ct:pal(" TEST: Get non-existent calendar"), {ok, 404, _, _} = api_test_runner:admin_request(get, <<"/v1/admin/calendars/nonexistent">>, AdminToken, <<"">>), ct:pal(" OK"). test_update_calendar(AdminToken, CalId, UserToken) -> ct:pal(" TEST: Update calendar (admin)"), NewTitle = <<"Updated by admin">>, update_calendar_admin(AdminToken, CalId, #{<<"title">> => NewTitle}), % Проверяем через пользовательский API {ok, 200, _, UserResp} = api_test_runner:client_request(get, <<"/v1/calendars/", CalId/binary>>, UserToken, <<"">>), UserCal = jsx:decode(list_to_binary(UserResp), [return_maps]), ?assertEqual(NewTitle, maps:get(<<"title">>, UserCal)), ct:pal(" OK"). test_update_calendar_not_found(AdminToken) -> ct:pal(" TEST: Update non-existent calendar"), {ok, 404, _, _} = api_test_runner:admin_request(put, <<"/v1/admin/calendars/nonexistent">>, AdminToken, jsx:encode(#{title => <<"x">>})), ct:pal(" OK"). test_update_calendar_invalid_body(AdminToken, CalId) -> ct:pal(" TEST: Update calendar with invalid JSON"), {ok, 400, _, _} = api_test_runner:admin_request(put, <<"/v1/admin/calendars/", CalId/binary>>, AdminToken, <<"not json">>), ct:pal(" OK"). test_delete_calendar(AdminToken, CalId) -> ct:pal(" TEST: Delete calendar (admin)"), {ok, 200, _, _} = api_test_runner:admin_request(delete, <<"/v1/admin/calendars/", CalId/binary>>, AdminToken, <<"">>), % Проверяем, что календарь не удалён физически, а помечен как deleted (soft delete) {ok, 200, _, RespBody} = api_test_runner:admin_request(get, <<"/v1/admin/calendars/", CalId/binary>>, AdminToken, <<"">>), Cal = jsx:decode(list_to_binary(RespBody), [return_maps]), ?assertEqual(<<"deleted">>, maps:get(<<"status">>, Cal)), ct:pal(" OK: calendar soft deleted"). test_delete_calendar_not_found(AdminToken) -> ct:pal(" TEST: Delete non-existent calendar"), {ok, 404, _, _} = api_test_runner:admin_request(delete, <<"/v1/admin/calendars/nonexistent">>, AdminToken, <<"">>), ct:pal(" OK"). %%%=================================================================== %%% Пагинация %%%=================================================================== test_pagination(AdminToken) -> ct:pal(" TEST: Pagination"), {ok, 200, Headers1, Body1} = api_test_runner:admin_request(get, <<"/v1/admin/calendars?limit=2&offset=0">>, AdminToken, <<"">>), List1 = jsx:decode(list_to_binary(Body1), [return_maps]), ?assertEqual(2, length(List1)), TotalCount = list_to_integer( proplists:get_value("x-total-count", Headers1, "0") ), ?assert(TotalCount >= 3), {ok, 200, _, Body2} = api_test_runner:admin_request(get, <<"/v1/admin/calendars?limit=2&offset=2">>, AdminToken, <<"">>), List2 = jsx:decode(list_to_binary(Body2), [return_maps]), ?assert(length(List2) >= 1), ct:pal(" OK: got pages"). %%%=================================================================== %%% Фильтрация %%%=================================================================== test_filter_by_status(AdminToken) -> ct:pal(" TEST: Filter by status"), {ok, 200, _, BodyActive} = api_test_runner:admin_request(get, <<"/v1/admin/calendars?status=active">>, AdminToken, <<"">>), ActiveList = jsx:decode(list_to_binary(BodyActive), [return_maps]), [?assertEqual(<<"active">>, maps:get(<<"status">>, C)) || C <- ActiveList], {ok, 200, _, BodyFrozen} = api_test_runner:admin_request(get, <<"/v1/admin/calendars?status=frozen">>, AdminToken, <<"">>), FrozenList = jsx:decode(list_to_binary(BodyFrozen), [return_maps]), [?assertEqual(<<"frozen">>, maps:get(<<"status">>, C)) || C <- FrozenList], ?assert(length(FrozenList) >= 1), ct:pal(" OK: status filter works"). test_filter_by_type(AdminToken) -> ct:pal(" TEST: Filter by type"), {ok, 200, _, Body} = api_test_runner:admin_request(get, <<"/v1/admin/calendars?type=commercial">>, AdminToken, <<"">>), CommercialList = jsx:decode(list_to_binary(Body), [return_maps]), [?assertEqual(<<"commercial">>, maps:get(<<"type">>, C)) || C <- CommercialList], ?assert(length(CommercialList) >= 1), ct:pal(" OK: type filter works"). test_search(AdminToken) -> ct:pal(" TEST: Search (q parameter)"), {ok, 200, _, Body} = api_test_runner:admin_request(get, <<"/v1/admin/calendars?q=commercial">>, AdminToken, <<"">>), Results = jsx:decode(list_to_binary(Body), [return_maps]), ?assert(length(Results) >= 1), HasMatch = lists:any(fun(C) -> Title = string:lowercase(binary_to_list(maps:get(<<"title">>, C, <<>>))), Desc = string:lowercase(binary_to_list(maps:get(<<"description">>, C, <<>>))), string:str(Title, "commercial") > 0 orelse string:str(Desc, "commercial") > 0 end, Results), ?assert(HasMatch), ct:pal(" OK: search works"). %%%=================================================================== %%% Сортировка %%%=================================================================== test_sort_by_title(AdminToken) -> ct:pal(" TEST: Sort by title asc"), {ok, 200, _, Body} = api_test_runner:admin_request(get, <<"/v1/admin/calendars?sort_by=title&sort_order=asc">>, AdminToken, <<"">>), List = jsx:decode(list_to_binary(Body), [return_maps]), Titles = [maps:get(<<"title">>, C) || C <- List], ?assertEqual(lists:sort(Titles), Titles), ct:pal(" OK: sorted by title"). test_sort_by_created_at(AdminToken) -> ct:pal(" TEST: Sort by created_at desc"), {ok, 200, _, Body} = api_test_runner:admin_request(get, <<"/v1/admin/calendars?sort_by=created_at&sort_order=desc">>, AdminToken, <<"">>), List = jsx:decode(list_to_binary(Body), [return_maps]), Dates = [maps:get(<<"created_at">>, C) || C <- List], Sorted = lists:reverse(lists:sort(Dates)), ?assertEqual(Sorted, Dates), ct:pal(" OK: sorted by created_at desc").