%%%------------------------------------------------------------------- %%% @doc Тесты административного API для управления подписками. %%% %%% Покрывает эндпоинты: %%% GET /v1/admin/subscriptions %%% GET /v1/admin/subscriptions/:id %%% PUT /v1/admin/subscriptions/:id %%% DELETE /v1/admin/subscriptions/:id %%% %%% Проверяет: %%% - получение списка подписок %%% - получение подписки по ID %%% - обновление подписки (изменение плана, статуса) %%% - удаление подписки %%% - **фильтрацию** по статусу и плану %%% - **комбинации фильтров** (status + plan) %%% - **пагинацию** (limit/offset, x‑total‑count) %%% - **сортировку** по created_at и expires_at %%% - **комбинации сортировки с фильтрацией и пагинацией** %%% @end %%%------------------------------------------------------------------- -module(admin_subscriptions_tests). -include_lib("eunit/include/eunit.hrl"). -export([test/0]). %%%=================================================================== %%% Главная тестовая функция %%%=================================================================== -spec test() -> ok. test() -> ct:pal("=== Admin Subscriptions Tests ==="), Token = api_test_runner:get_admin_token(), UserToken = api_test_runner:get_user_token(), % Создаём две подписки для разных проверок #{<<"id">> := Sub1Id} = api_test_runner:client_post(<<"/v1/subscription">>, UserToken, #{<<"action">> => <<"start_trial">>}), % Для второй подписки создаём нового пользователя User2Email = api_test_runner:unique_email(<<"sub2">>), User2Token = api_test_runner:register_and_login(User2Email, <<"pass2">>), #{<<"id">> := Sub2Id} = api_test_runner:client_post(<<"/v1/subscription">>, User2Token, #{<<"action">> => <<"start_trial">>}), % Основные проверки test_list_subscriptions(Token, Sub1Id), test_get_subscription(Token, Sub1Id), test_update_subscription(Token, Sub1Id), % Фильтрация test_filter_subscriptions(Token), % Комбинации фильтров test_filter_combined_status_plan(Token), test_filter_combined_plan_status(Token), % Сортировка test_sort_by_created_at_desc(Token), test_sort_by_created_at_asc(Token), test_sort_by_expires_at_asc(Token), % Фильтр + сортировка test_filter_status_sort_created_at(Token), test_filter_plan_sort_expires_at(Token), % Пагинация test_subscription_pagination(Token), test_pagination_headers(Token), % Фильтр + пагинация test_filter_status_with_pagination(Token), % Фильтр + сортировка + пагинация test_filter_sort_pagination_combined(Token), % Удаляем вторую подписку, чтобы проверить delete test_delete_subscription(Token, Sub2Id), ct:pal("=== All admin subscriptions tests passed ==="), ok. %%%=================================================================== %%% Тестовые функции %%%=================================================================== %% @doc GET /v1/admin/subscriptions – список подписок. -spec test_list_subscriptions(binary(), binary()) -> ok. test_list_subscriptions(Token, SubId) -> ct:pal(" TEST: List all subscriptions"), Subs = api_test_runner:admin_get(<<"/v1/admin/subscriptions">>, Token), ?assert(is_list(Subs)), ?assert(length(Subs) >= 2), ?assert(lists:any(fun(S) -> maps:get(<<"id">>, S) =:= SubId end, Subs)), ct:pal(" OK: ~p subscriptions", [length(Subs)]). %% @doc GET /v1/admin/subscriptions/:id – получение подписки по ID. -spec test_get_subscription(binary(), binary()) -> ok. test_get_subscription(Token, SubId) -> ct:pal(" TEST: Get subscription by ID"), Path = <<"/v1/admin/subscriptions/", SubId/binary>>, Sub = api_test_runner:admin_get(Path, Token), ?assertEqual(SubId, maps:get(<<"id">>, Sub)), ?assert(maps:is_key(<<"plan">>, Sub)), ct:pal(" OK"). %% @doc PUT /v1/admin/subscriptions/:id – обновление подписки (план + статус). -spec test_update_subscription(binary(), binary()) -> ok. test_update_subscription(Token, SubId) -> ct:pal(" TEST: Update subscription (change plan)"), Path = <<"/v1/admin/subscriptions/", SubId/binary>>, Updated = api_test_runner:admin_put(Path, Token, #{ <<"plan">> => <<"biannual">>, <<"status">> => <<"active">> }), ?assertEqual(<<"biannual">>, maps:get(<<"plan">>, Updated)), ?assertEqual(<<"active">>, maps:get(<<"status">>, Updated)), ct:pal(" OK"). %% @doc DELETE /v1/admin/subscriptions/:id – удаление подписки. -spec test_delete_subscription(binary(), binary()) -> ok. test_delete_subscription(Token, SubId) -> ct:pal(" TEST: Delete subscription"), Path = <<"/v1/admin/subscriptions/", SubId/binary>>, Deleted = api_test_runner:admin_delete(Path, Token), ?assertEqual(<<"deleted">>, maps:get(<<"status">>, Deleted)), ct:pal(" OK"). %% ── Фильтрация ── %% @doc GET /v1/admin/subscriptions?status=...&plan=... – фильтрация. -spec test_filter_subscriptions(binary()) -> ok. test_filter_subscriptions(Token) -> ct:pal(" TEST: Filter subscriptions by status=active"), Subs = api_test_runner:admin_get(<<"/v1/admin/subscriptions?status=active">>, Token), ?assert(is_list(Subs)), ?assert(length(Subs) >= 1), [?assertEqual(<<"active">>, maps:get(<<"status">>, S)) || S <- Subs], ct:pal(" OK: ~p active subscriptions", [length(Subs)]), ct:pal(" TEST: Filter subscriptions by plan=biannual"), Subs2 = api_test_runner:admin_get(<<"/v1/admin/subscriptions?plan=biannual">>, Token), ?assert(is_list(Subs2)), ?assert(length(Subs2) >= 1), [?assertEqual(<<"biannual">>, maps:get(<<"plan">>, S)) || S <- Subs2], ct:pal(" OK: ~p biannual subscriptions", [length(Subs2)]). %% ── Комбинации фильтров ── %% @doc Комбинация фильтров: status=active&plan=biannual -spec test_filter_combined_status_plan(binary()) -> ok. test_filter_combined_status_plan(Token) -> ct:pal(" TEST: Combined filter (status=active & plan=biannual)"), Subs = api_test_runner:admin_get( <<"/v1/admin/subscriptions?status=active&plan=biannual">>, Token), ?assert(is_list(Subs)), [?assertEqual(<<"active">>, maps:get(<<"status">>, S)) || S <- Subs], [?assertEqual(<<"biannual">>, maps:get(<<"plan">>, S)) || S <- Subs], ct:pal(" OK: ~p subscriptions match both criteria", [length(Subs)]). %% @doc Комбинация фильтров: plan=biannual&status=active (обратный порядок) -spec test_filter_combined_plan_status(binary()) -> ok. test_filter_combined_plan_status(Token) -> ct:pal(" TEST: Combined filter (plan=biannual & status=active)"), Subs = api_test_runner:admin_get( <<"/v1/admin/subscriptions?plan=biannual&status=active">>, Token), ?assert(is_list(Subs)), [?assertEqual(<<"active">>, maps:get(<<"status">>, S)) || S <- Subs], [?assertEqual(<<"biannual">>, maps:get(<<"plan">>, S)) || S <- Subs], ct:pal(" OK: ~p subscriptions match both criteria", [length(Subs)]). %% ── Сортировка ── %% @doc Сортировка по created_at по убыванию. -spec test_sort_by_created_at_desc(binary()) -> ok. test_sort_by_created_at_desc(Token) -> ct:pal(" TEST: Sort subscriptions by created_at desc"), {ok, 200, _, Body} = api_test_runner:admin_request(get, <<"/v1/admin/subscriptions?sort=created_at&order=desc">>, Token, <<"">>), Subs = jsx:decode(list_to_binary(Body), [return_maps]), Dates = [maps:get(<<"created_at">>, S) || S <- Subs], Sorted = lists:reverse(lists:sort(Dates)), ?assertEqual(Sorted, Dates), ct:pal(" OK: sorted by created_at desc"). %% @doc Сортировка по created_at по возрастанию. -spec test_sort_by_created_at_asc(binary()) -> ok. test_sort_by_created_at_asc(Token) -> ct:pal(" TEST: Sort subscriptions by created_at asc"), {ok, 200, _, Body} = api_test_runner:admin_request(get, <<"/v1/admin/subscriptions?sort=created_at&order=asc">>, Token, <<"">>), Subs = jsx:decode(list_to_binary(Body), [return_maps]), Dates = [maps:get(<<"created_at">>, S) || S <- Subs], ?assertEqual(lists:sort(Dates), Dates), ct:pal(" OK: sorted by created_at asc"). %% @doc Сортировка по expires_at по возрастанию. -spec test_sort_by_expires_at_asc(binary()) -> ok. test_sort_by_expires_at_asc(Token) -> ct:pal(" TEST: Sort subscriptions by expires_at asc"), {ok, 200, _, Body} = api_test_runner:admin_request(get, <<"/v1/admin/subscriptions?sort=expires_at&order=asc">>, Token, <<"">>), Subs = jsx:decode(list_to_binary(Body), [return_maps]), Dates = [maps:get(<<"expires_at">>, S) || S <- Subs], ?assertEqual(lists:sort(Dates), Dates), ct:pal(" OK: sorted by expires_at asc"). %% ── Фильтр + сортировка ── %% @doc Фильтр status=active + сортировка по created_at desc. -spec test_filter_status_sort_created_at(binary()) -> ok. test_filter_status_sort_created_at(Token) -> ct:pal(" TEST: Filter status=active + sort by created_at desc"), {ok, 200, _, Body} = api_test_runner:admin_request(get, <<"/v1/admin/subscriptions?status=active&sort=created_at&order=desc">>, Token, <<"">>), Subs = jsx:decode(list_to_binary(Body), [return_maps]), [?assertEqual(<<"active">>, maps:get(<<"status">>, S)) || S <- Subs], Dates = [maps:get(<<"created_at">>, S) || S <- Subs], Sorted = lists:reverse(lists:sort(Dates)), ?assertEqual(Sorted, Dates), ct:pal(" OK: ~p subscriptions sorted", [length(Subs)]). %% @doc Фильтр plan=biannual + сортировка по expires_at asc. -spec test_filter_plan_sort_expires_at(binary()) -> ok. test_filter_plan_sort_expires_at(Token) -> ct:pal(" TEST: Filter plan=biannual + sort by expires_at asc"), {ok, 200, _, Body} = api_test_runner:admin_request(get, <<"/v1/admin/subscriptions?plan=biannual&sort=expires_at&order=asc">>, Token, <<"">>), Subs = jsx:decode(list_to_binary(Body), [return_maps]), [?assertEqual(<<"biannual">>, maps:get(<<"plan">>, S)) || S <- Subs], Dates = [maps:get(<<"expires_at">>, S) || S <- Subs], ?assertEqual(lists:sort(Dates), Dates), ct:pal(" OK: ~p subscriptions sorted", [length(Subs)]). %% ── Пагинация ── %% @doc GET /v1/admin/subscriptions?limit=...&offset=... – пагинация. -spec test_subscription_pagination(binary()) -> ok. test_subscription_pagination(Token) -> ct:pal(" TEST: Subscription pagination"), Page1 = api_test_runner:admin_get(<<"/v1/admin/subscriptions?limit=1&offset=0">>, Token), ?assert(length(Page1) >= 1), Page2 = api_test_runner:admin_get(<<"/v1/admin/subscriptions?limit=1&offset=1">>, Token), ?assert(length(Page2) >= 1), Id1 = maps:get(<<"id">>, hd(Page1)), Id2 = maps:get(<<"id">>, hd(Page2)), ?assertNotEqual(Id1, Id2), ct:pal(" OK"). %% @doc Проверка заголовков пагинации (x-total-count). -spec test_pagination_headers(binary()) -> ok. test_pagination_headers(Token) -> ct:pal(" TEST: Pagination headers (x-total-count)"), {ok, 200, Headers, _} = api_test_runner:admin_request(get, <<"/v1/admin/subscriptions?limit=2&offset=0">>, Token, <<"">>), Total = list_to_integer( proplists:get_value("x-total-count", Headers, "0")), ?assert(Total >= 1), ct:pal(" OK: total count = ~p", [Total]). %% ── Фильтр + пагинация ── %% @doc Фильтр status=active + пагинация limit=1. -spec test_filter_status_with_pagination(binary()) -> ok. test_filter_status_with_pagination(Token) -> ct:pal(" TEST: Filter status=active + pagination (limit=1)"), {ok, 200, Headers, Body} = api_test_runner:admin_request(get, <<"/v1/admin/subscriptions?status=active&limit=1&offset=0">>, Token, <<"">>), Subs = jsx:decode(list_to_binary(Body), [return_maps]), ?assert(length(Subs) =< 1), [?assertEqual(<<"active">>, maps:get(<<"status">>, S)) || S <- Subs], Total = list_to_integer( proplists:get_value("x-total-count", Headers, "0")), ?assert(Total >= 1), ct:pal(" OK: total count = ~p", [Total]). %% ── Фильтр + сортировка + пагинация ── %% @doc Фильтр status=active + сортировка по created_at desc + пагинация limit=1. -spec test_filter_sort_pagination_combined(binary()) -> ok. test_filter_sort_pagination_combined(Token) -> ct:pal(" TEST: Filter + sort + pagination (status=active, sort=created_at desc, limit=1)"), {ok, 200, Headers, Body} = api_test_runner:admin_request(get, <<"/v1/admin/subscriptions?status=active&sort=created_at&order=desc&limit=1&offset=0">>, Token, <<"">>), Subs = jsx:decode(list_to_binary(Body), [return_maps]), ?assert(length(Subs) =< 1), [?assertEqual(<<"active">>, maps:get(<<"status">>, S)) || S <- Subs], Total = list_to_integer( proplists:get_value("x-total-count", Headers, "0")), ?assert(Total >= 1), ct:pal(" OK: total count = ~p", [Total]).