Files
EventHubBack/test/api/admins/admin_reviews_tests.erl
T

270 lines
13 KiB
Erlang
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
%%%-------------------------------------------------------------------
%%% @doc Тесты административного API для управления отзывами.
%%%
%%% Покрывает эндпоинты:
%%% 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]).
%%%===================================================================
%%% Главная тестовая функция
%%%===================================================================
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, #{
title => <<"Event for review testing">>,
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..."),
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 первого пользователя для теста фильтрации по 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_bulk_update_missing_reason(Token, Review1Id),
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.
%%%===================================================================
%%% Тестовые функции
%%%===================================================================
%% @doc GET /v1/admin/reviews список отзывов.
test_list_reviews(Token) ->
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)]).
%% @doc GET /v1/admin/reviews?target_type=event&target_id=... фильтрация по цели.
test_filter_reviews(Token, EventId) ->
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)]).
%% @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"),
BodyMap = #{
<<"operations">> => [
#{<<"id">> => Review1Id, <<"status">> => <<"visible">>},
#{<<"id">> => Review2Id, <<"status">> => <<"hidden">>}
],
<<"reason">> => <<"Moderation action">>
},
Body = jsx:encode(BodyMap),
{ok, 200, _, RespBody} = api_test_runner:admin_request(patch, <<"/v1/admin/reviews">>, Token, Body),
Result = jsx:decode(list_to_binary(RespBody), [return_maps]),
?assertEqual(2, maps:get(<<"updated_count">>, Result)),
ct:pal(" OK: updated ~p reviews", [maps:get(<<"updated_count">>, Result)]).
%% @doc PATCH /v1/admin/reviews отсутствует reason → 400.
test_bulk_update_missing_reason(Token, ReviewId) ->
ct:pal(" TEST: Bulk update missing reason (400)"),
BodyMap = #{
<<"operations">> => [#{<<"id">> => ReviewId, <<"status">> => <<"hidden">>}]
% reason отсутствует
},
Body = jsx:encode(BodyMap),
{ok, 400, _, _} = api_test_runner:admin_request(patch, <<"/v1/admin/reviews">>, Token, Body),
ct:pal(" OK: got 400").
%% @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"),
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)]).
%% @doc PUT /v1/admin/reviews/:id обновление отзыва.
test_update_review(Token, ReviewId) ->
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").
%% ── Сортировка ──
%% @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"),
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),
?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).
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]).