Рефакторинг админских обработчиков - пагинация, сортировка, фильтрация. Финал

This commit is contained in:
2026-05-26 21:50:12 +03:00
parent ed5b275efb
commit 19b3d0dba0
21 changed files with 1833 additions and 421 deletions
+188 -29
View File
@@ -2,29 +2,30 @@
%%% @doc Тесты административного API для управления подписками.
%%%
%%% Покрывает эндпоинты:
%%% GET /v1/admin/subscriptions
%%% GET /v1/admin/subscriptions/:id
%%% PUT /v1/admin/subscriptions/:id
%%% DELETE /v1/admin/subscriptions/:id
%%% GET /v1/admin/subscriptions
%%% GET /v1/admin/subscriptions/:id
%%% PUT /v1/admin/subscriptions/:id
%%% DELETE /v1/admin/subscriptions/:id
%%%
%%% Проверяет:
%%% - получение списка подписок
%%% - получение подписки по ID
%%% - обновление подписки (изменение плана, статуса)
%%% - удаление подписки
%%% - фильтрацию по статусу и плану
%%% - пагинацию
%%% - получение списка подписок
%%% - получение подписки по ID
%%% - обновление подписки (изменение плана, статуса)
%%% - удаление подписки
%%% - **фильтрацию** по статусу и плану
%%% - **комбинации фильтров** (status + plan)
%%% - **пагинацию** (limit/offset, xtotalcount)
%%% - **сортировку** по 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 ==="),
@@ -34,20 +35,43 @@ test() ->
% Создаём две подписки для разных проверок
#{<<"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),
% После обновления Sub1Id имеет план biannual и статус active
% Фильтрация
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),
@@ -61,27 +85,27 @@ test() ->
%% @doc GET /v1/admin/subscriptions список подписок.
-spec test_list_subscriptions(binary(), binary()) -> ok.
test_list_subscriptions(Token, SubId) ->
ct:pal(" TEST: List all subscriptions"),
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)]).
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"),
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").
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)"),
ct:pal(" TEST: Update subscription (change plan)"),
Path = <<"/v1/admin/subscriptions/", SubId/binary>>,
Updated = api_test_runner:admin_put(Path, Token, #{
<<"plan">> => <<"biannual">>,
@@ -89,38 +113,129 @@ test_update_subscription(Token, SubId) ->
}),
?assertEqual(<<"biannual">>, maps:get(<<"plan">>, Updated)),
?assertEqual(<<"active">>, maps:get(<<"status">>, Updated)),
ct:pal(" OK").
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"),
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").
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"),
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(" OK: ~p active subscriptions", [length(Subs)]),
ct:pal(" TEST: Filter subscriptions by plan=biannual"),
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)]).
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"),
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),
@@ -128,4 +243,48 @@ test_subscription_pagination(Token) ->
Id1 = maps:get(<<"id">>, hd(Page1)),
Id2 = maps:get(<<"id">>, hd(Page2)),
?assertNotEqual(Id1, Id2),
ct:pal(" OK").
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]).