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

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
+156 -38
View File
@@ -1,26 +1,33 @@
%%%-------------------------------------------------------------------
%%% @doc Тесты административного API для управления отзывами.
%%%
%%% Покрывает эндпоинты:
%%% GET /v1/admin/reviews
%%% PATCH /v1/admin/reviews
%%% GET /v1/admin/reviews/:id
%%% PUT /v1/admin/reviews/:id
%%% GET /v1/admin/reviews
%%% PATCH /v1/admin/reviews
%%% GET /v1/admin/reviews/:id
%%% PUT /v1/admin/reviews/:id
%%%
%%% Проверяет:
%%% - получение списка отзывов
%%% - фильтрацию по цели, статусу, пользователю
%%% - комбинации фильтров
%%% - сортировку по created_at, rating
%%% - комбинации фильтрации с сортировкой
%%% - пагинацию
%%% - комбинации фильтрации, сортировки и пагинации
%%% @end
%%%-------------------------------------------------------------------
-module(admin_reviews_tests).
-include_lib("eunit/include/eunit.hrl").
-export([test/0]).
%%%===================================================================
%%% EUnit test generator
%%% Главная тестовая функция
%%%===================================================================
test() ->
ct:pal("=== Admin Reviews Tests ==="),
Token = api_test_runner:get_admin_token(),
UserToken = api_test_runner:get_user_token(),
%% Создаём тестовые данные: календарь, событие
CalId = api_test_runner:create_calendar(UserToken, #{title => <<"ReviewsTestCal">>}),
EventId = api_test_runner:create_event(UserToken, CalId, #{
@@ -28,46 +35,54 @@ test() ->
start_time => api_test_runner:future_date(),
duration => 60
}),
%% Создаём двух пользователей для отзывов
User1Email = api_test_runner:unique_email(<<"rev1">>),
User1Token = api_test_runner:register_and_login(User1Email, <<"pass1">>),
User2Email = api_test_runner:unique_email(<<"rev2">>),
User2Token = api_test_runner:register_and_login(User2Email, <<"pass2">>),
%% Записываем пользователей на событие и подтверждаем бронирования
#{<<"id">> := Booking1Id} = api_test_runner:client_post(
<<"/v1/events/", EventId/binary, "/bookings">>, User1Token, #{}),
#{<<"id">> := Booking2Id} = api_test_runner:client_post(
<<"/v1/events/", EventId/binary, "/bookings">>, User2Token, #{}),
ct:pal(" Confirming bookings as calendar owner..."),
ct:pal(" Confirming bookings as calendar owner..."),
api_test_runner:client_put(<<"/v1/bookings/", Booking1Id/binary>>, UserToken,
#{action => <<"confirm">>}),
api_test_runner:client_put(<<"/v1/bookings/", Booking2Id/binary>>, UserToken,
#{action => <<"confirm">>}),
%% Оставляем отзывы от имени участников
#{<<"id">> := Review1Id} = api_test_runner:client_post(<<"/v1/reviews">>, User1Token, #{
target_type => <<"event">>,
target_id => EventId,
rating => 5,
comment => <<"Great!">>
}),
#{<<"id">> := Review2Id} = api_test_runner:client_post(<<"/v1/reviews">>, User2Token, #{
target_type => <<"event">>,
target_id => EventId,
rating => 4,
comment => <<"Good">>
}),
#{<<"id">> := Review1Id} = api_test_runner:client_post(<<"/v1/reviews">>, User1Token,
#{ target_type => <<"event">>, target_id => EventId, rating => 5,
comment => <<"Great!">> }),
#{<<"id">> := Review2Id} = api_test_runner:client_post(<<"/v1/reviews">>, User2Token,
#{ target_type => <<"event">>, target_id => EventId, rating => 4,
comment => <<"Good">> }),
%% Получаем ID первого пользователя для теста фильтрации по user_id
#{<<"id">> := User1Id} = api_test_runner:client_get(<<"/v1/user/me">>, User1Token),
%% Запускаем тесты
test_list_reviews(Token),
test_filter_reviews(Token, EventId),
test_filter_by_user(Token, User1Id),
% Сначала меняем статусы, чтобы потом проверить фильтрацию по статусам
test_bulk_update_status(Token, Review1Id, Review2Id),
test_filter_by_status(Token),
test_filter_combined_status_target(Token, EventId),
test_get_review(Token, Review1Id),
test_update_review(Token, Review1Id),
test_sort_by_created_at_desc(Token),
test_sort_by_created_at_asc(Token),
test_sort_by_rating_asc(Token),
test_filter_status_sort_created_at(Token),
test_filter_sort_pagination_combined(Token, EventId),
test_review_pagination(Token),
test_pagination_headers(Token),
ct:pal("=== All admin reviews tests passed ==="),
ok.
@@ -78,54 +93,147 @@ test() ->
%% @doc GET /v1/admin/reviews список отзывов.
test_list_reviews(Token) ->
ct:pal(" TEST: List all reviews"),
ct:pal(" TEST: List all reviews"),
Reviews = api_test_runner:admin_get(<<"/v1/admin/reviews">>, Token),
?assert(is_list(Reviews)),
?assert(length(Reviews) >= 2),
ct:pal(" OK: ~p reviews", [length(Reviews)]).
ct:pal(" OK: ~p reviews", [length(Reviews)]).
%% @doc GET /v1/admin/reviews?target_type=event&target_id=... фильтрация по цели.
test_filter_reviews(Token, EventId) ->
ct:pal(" TEST: Filter reviews by target"),
ct:pal(" TEST: Filter reviews by target"),
Path = <<"/v1/admin/reviews?target_type=event&target_id=", EventId/binary>>,
Reviews = api_test_runner:admin_get(Path, Token),
?assert(is_list(Reviews)),
?assert(length(Reviews) >= 2),
[?assertEqual(EventId, maps:get(<<"target_id">>, R)) || R <- Reviews],
ct:pal(" OK: ~p reviews for event", [length(Reviews)]).
ct:pal(" OK: ~p reviews for event", [length(Reviews)]).
%% @doc GET /v1/admin/reviews?user_id=... фильтрация по пользователю.
test_filter_by_user(Token, UserId) ->
ct:pal(" TEST: Filter reviews by user_id"),
Path = <<"/v1/admin/reviews?user_id=", UserId/binary>>,
Reviews = api_test_runner:admin_get(Path, Token),
?assert(is_list(Reviews)),
[?assertEqual(UserId, maps:get(<<"user_id">>, R)) || R <- Reviews],
ct:pal(" OK: ~p reviews by user", [length(Reviews)]).
%% @doc PATCH /v1/admin/reviews – массовое обновление статусов.
test_bulk_update_status(Token, Review1Id, Review2Id) ->
ct:pal(" TEST: Bulk update review statuses"),
ct:pal(" TEST: Bulk update review statuses"),
Body = [
#{<<"id">> => Review1Id, <<"status">> => <<"visible">>},
#{<<"id">> => Review2Id, <<"status">> => <<"hidden">>}
],
#{<<"updated_count">> := Count} = api_test_runner:admin_patch(<<"/v1/admin/reviews">>, Token, Body),
#{<<"updated_count">> := Count} = api_test_runner:admin_patch(
<<"/v1/admin/reviews">>, Token, Body),
?assertEqual(2, Count),
ct:pal(" OK: updated ~p reviews", [Count]).
ct:pal(" OK: updated ~p reviews", [Count]).
%% @doc GET /v1/admin/reviews?status=visible фильтрация по статусу.
test_filter_by_status(Token) ->
ct:pal(" TEST: Filter reviews by status=visible"),
Reviews = api_test_runner:admin_get(<<"/v1/admin/reviews?status=visible">>, Token),
?assert(is_list(Reviews)),
[?assertEqual(<<"visible">>, maps:get(<<"status">>, R)) || R <- Reviews],
ct:pal(" OK: ~p visible reviews", [length(Reviews)]).
%% @doc GET /v1/admin/reviews?status=hidden&target_type=event комбинация фильтров.
test_filter_combined_status_target(Token, EventId) ->
ct:pal(" TEST: Combined filter (status=hidden & target_type=event)"),
Path = <<"/v1/admin/reviews?status=hidden&target_type=event&target_id=", EventId/binary>>,
Reviews = api_test_runner:admin_get(Path, Token),
?assert(is_list(Reviews)),
[?assertEqual(<<"hidden">>, maps:get(<<"status">>, R)) || R <- Reviews],
[?assertEqual(EventId, maps:get(<<"target_id">>, R)) || R <- Reviews],
ct:pal(" OK: ~p reviews match both criteria", [length(Reviews)]).
%% @doc GET /v1/admin/reviews/:id получение отзыва по ID.
test_get_review(Token, ReviewId) ->
ct:pal(" TEST: Get review by ID"),
ct:pal(" TEST: Get review by ID"),
Path = <<"/v1/admin/reviews/", ReviewId/binary>>,
Review = api_test_runner:admin_get(Path, Token),
?assertEqual(ReviewId, maps:get(<<"id">>, Review)),
?assertEqual(<<"visible">>, maps:get(<<"status">>, Review)),
ct:pal(" OK: ~s", [maps:get(<<"comment">>, Review)]).
ct:pal(" OK: ~s", [maps:get(<<"comment">>, Review)]).
%% @doc PUT /v1/admin/reviews/:id обновление отзыва.
test_update_review(Token, ReviewId) ->
ct:pal(" TEST: Update review"),
ct:pal(" TEST: Update review"),
Path = <<"/v1/admin/reviews/", ReviewId/binary>>,
Updates = #{<<"comment">> => <<"Updated by admin">>},
Updated = api_test_runner:admin_put(Path, Token, Updates),
?assertEqual(<<"Updated by admin">>, maps:get(<<"comment">>, Updated)),
ct:pal(" OK").
ct:pal(" OK").
%% ── Сортировка ──
%% @doc Сортировка по created_at по убыванию.
test_sort_by_created_at_desc(Token) ->
ct:pal(" TEST: Sort by created_at desc"),
{ok, 200, _, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/reviews?sort=created_at&order=desc">>, Token, <<"">>),
Reviews = jsx:decode(list_to_binary(Body), [return_maps]),
Dates = [maps:get(<<"created_at">>, R) || R <- Reviews],
Sorted = lists:reverse(lists:sort(Dates)),
?assertEqual(Sorted, Dates),
ct:pal(" OK: sorted by created_at desc").
%% @doc Сортировка по created_at по возрастанию.
test_sort_by_created_at_asc(Token) ->
ct:pal(" TEST: Sort by created_at asc"),
{ok, 200, _, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/reviews?sort=created_at&order=asc">>, Token, <<"">>),
Reviews = jsx:decode(list_to_binary(Body), [return_maps]),
Dates = [maps:get(<<"created_at">>, R) || R <- Reviews],
?assertEqual(lists:sort(Dates), Dates),
ct:pal(" OK: sorted by created_at asc").
%% @doc Сортировка по rating по возрастанию.
test_sort_by_rating_asc(Token) ->
ct:pal(" TEST: Sort by rating asc"),
{ok, 200, _, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/reviews?sort=rating&order=asc">>, Token, <<"">>),
Reviews = jsx:decode(list_to_binary(Body), [return_maps]),
Ratings = [maps:get(<<"rating">>, R) || R <- Reviews],
?assertEqual(lists:sort(Ratings), Ratings),
ct:pal(" OK: sorted by rating asc").
%% ── Фильтр + сортировка ──
%% @doc Фильтр status=visible + сортировка по created_at desc.
test_filter_status_sort_created_at(Token) ->
ct:pal(" TEST: Filter status=visible + sort by created_at desc"),
{ok, 200, _, Body} = api_test_runner:admin_request(get,
<<"/v1/admin/reviews?status=visible&sort=created_at&order=desc">>, Token, <<"">>),
Reviews = jsx:decode(list_to_binary(Body), [return_maps]),
[?assertEqual(<<"visible">>, maps:get(<<"status">>, R)) || R <- Reviews],
Dates = [maps:get(<<"created_at">>, R) || R <- Reviews],
Sorted = lists:reverse(lists:sort(Dates)),
?assertEqual(Sorted, Dates),
ct:pal(" OK: ~p reviews sorted", [length(Reviews)]).
%% ── Фильтр + сортировка + пагинация ──
%% @doc Фильтр target_type=event + сортировка по rating desc + пагинация limit=1.
test_filter_sort_pagination_combined(Token, EventId) ->
ct:pal(" TEST: Filter + sort + pagination (target_type=event, sort=rating desc, limit=1)"),
Path = <<"/v1/admin/reviews?target_type=event&target_id=", EventId/binary,
"&sort=rating&order=desc&limit=1&offset=0">>,
{ok, 200, Headers, Body} = api_test_runner:admin_request(get, Path, Token, <<"">>),
Reviews = jsx:decode(list_to_binary(Body), [return_maps]),
?assert(length(Reviews) =< 1),
[?assertEqual(EventId, maps:get(<<"target_id">>, R)) || R <- Reviews],
Total = list_to_integer(
proplists:get_value("x-total-count", Headers, "0")),
?assert(Total >= 1),
ct:pal(" OK: total count = ~p", [Total]).
%% ── Пагинация ──
%% @doc GET /v1/admin/reviews?limit=...&offset=... пагинация.
test_review_pagination(Token) ->
ct:pal(" TEST: Review pagination"),
ct:pal(" TEST: Review pagination"),
Page1 = api_test_runner:admin_get(<<"/v1/admin/reviews?limit=1&offset=0">>, Token),
?assertEqual(1, length(Page1)),
Page2 = api_test_runner:admin_get(<<"/v1/admin/reviews?limit=1&offset=1">>, Token),
@@ -133,4 +241,14 @@ test_review_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).
test_pagination_headers(Token) ->
ct:pal(" TEST: Pagination headers (x-total-count)"),
{ok, 200, Headers, _} = api_test_runner:admin_request(get,
<<"/v1/admin/reviews?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]).