270 lines
13 KiB
Erlang
270 lines
13 KiB
Erlang
%%%-------------------------------------------------------------------
|
||
%%% @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]). |