%%%------------------------------------------------------------------- %%% @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]).