%%%------------------------------------------------------------------- %%% @doc Тесты административного API для управления бан-словами. %%% %%% Покрывает эндпоинты: %%% GET /v1/admin/banned-words %%% POST /v1/admin/banned-words %%% PUT /v1/admin/banned-words/:word %%% DELETE /v1/admin/banned-words/:word %%% POST /v1/admin/banned-words/batch %%% %%% Проверяет: %%% - получение списка слов %%% - добавление нового слова %%% - редактирование существующего слова %%% - удаление слова %%% - массовую загрузку списком %%% - **фильтрацию** по слову (q) %%% - **сортировку** по word, added_at %%% - **комбинации фильтрации, сортировки и пагинации** %%% - **пагинацию** (limit/offset, x-total-count) %%% @end %%%------------------------------------------------------------------- -module(admin_banned_words_tests). -include_lib("eunit/include/eunit.hrl"). -export([test/0]). %%%=================================================================== %%% Главная тестовая функция %%%=================================================================== -spec test() -> ok. test() -> ct:pal("=== Admin Banned Words Tests ==="), Token = api_test_runner:get_admin_token(), % Добавляем несколько слов для тестирования Unique = integer_to_binary(erlang:system_time()), Word1 = <<"alpha_", Unique/binary>>, Word2 = <<"beta_", Unique/binary>>, Word3 = <<"gamma_", Unique/binary>>, api_test_runner:admin_post(<<"/v1/admin/banned-words">>, Token, #{<<"word">> => Word1}), api_test_runner:admin_post(<<"/v1/admin/banned-words">>, Token, #{<<"word">> => Word2}), api_test_runner:admin_post(<<"/v1/admin/banned-words">>, Token, #{<<"word">> => Word3}), % ── Основные тесты ── test_list_words(Token), test_add_word(Token), test_update_word(Token, Word1), test_batch_add_words(Token), test_update_word_conflict(Token), test_batch_add_invalid(Token), test_batch_add_duplicates(Token), % ── Пагинация ── test_words_pagination(Token), test_pagination_headers(Token), % ── Фильтрация ── test_filter_by_word(Token, Word3), % ── Сортировка ── test_sort_by_word_asc(Token), test_sort_by_added_at_desc(Token), % ── Комбинации ── test_filter_with_sort(Token, Unique), test_filter_sort_pagination(Token, Unique), % Удаление beta только после всех тестов фильтрации/сортировки test_delete_word(Token, Word2), ct:pal("=== All admin banned words tests passed ==="), ok. %%%=================================================================== %%% Тестовые функции %%%=================================================================== %% @doc GET /v1/admin/banned-words – список всех слов. test_list_words(Token) -> ct:pal(" TEST: List all banned words"), Words = api_test_runner:admin_get(<<"/v1/admin/banned-words">>, Token), ?assert(is_list(Words)), ?assert(length(Words) >= 3), ct:pal(" OK: ~p words", [length(Words)]). %% @doc POST /v1/admin/banned-words – добавление нового слова. test_add_word(Token) -> ct:pal(" TEST: Add a new banned word"), Unique = integer_to_binary(erlang:system_time()), NewWord = <<"newbadword", Unique/binary>>, Result = api_test_runner:admin_post(<<"/v1/admin/banned-words">>, Token, #{<<"word">> => NewWord}), ?assertEqual(<<"added">>, maps:get(<<"status">>, Result)), % Проверяем, что слово найдено через поиск по точному совпадению Found = api_test_runner:admin_get(<<"/v1/admin/banned-words?q=", NewWord/binary>>, Token), ?assertEqual(1, length(Found)), ?assertEqual(NewWord, maps:get(<<"word">>, hd(Found))), ct:pal(" OK"). %% @doc PUT /v1/admin/banned-words/:word – редактирование слова. test_update_word(Token, Word) -> ct:pal(" TEST: Update a banned word"), NewWord = <>, Path = <<"/v1/admin/banned-words/", Word/binary>>, Result = api_test_runner:admin_put(Path, Token, #{<<"word">> => NewWord}), ?assertEqual(<<"updated">>, maps:get(<<"status">>, Result)), % Проверяем, что новое слово существует (поиск возвращает ровно 1 запись) NewSearch = api_test_runner:admin_get(<<"/v1/admin/banned-words?q=", NewWord/binary>>, Token), ?assertEqual(1, length(NewSearch)), ?assertEqual(NewWord, maps:get(<<"word">>, hd(NewSearch))), % Проверяем, что старое слово отсутствует: все найденные слова не равны старому OldSearch = api_test_runner:admin_get(<<"/v1/admin/banned-words?q=", Word/binary>>, Token), ?assert(lists:all(fun(W) -> maps:get(<<"word">>, W) =/= Word end, OldSearch)), ct:pal(" OK"). %% @doc POST /v1/admin/banned-words/batch – массовая загрузка. test_batch_add_words(Token) -> ct:pal(" TEST: Batch add banned words"), Unique = integer_to_binary(erlang:system_time()), Words = [<<"batch1_", Unique/binary>>, <<"batch2_", Unique/binary>>, <<"batch3_", Unique/binary>>], Body = jsx:encode(#{<<"words">> => Words}), {ok, 200, _, RespBody} = api_test_runner:admin_request(post, <<"/v1/admin/banned-words/batch">>, Token, Body), Result = jsx:decode(list_to_binary(RespBody), [return_maps]), ?assertEqual(3, maps:get(<<"added">>, Result)), ?assertEqual(0, maps:get(<<"skipped">>, Result)), AllWords = api_test_runner:admin_get(<<"/v1/admin/banned-words">>, Token), [?assert(lists:any(fun(W) -> maps:get(<<"word">>, W) =:= Word end, AllWords)) || Word <- Words], ct:pal(" OK"). %% @doc DELETE /v1/admin/banned-words/:word – удаление слова. test_delete_word(Token, Word) -> ct:pal(" TEST: Delete a banned word"), Path = <<"/v1/admin/banned-words/", Word/binary>>, Result = api_test_runner:admin_delete(Path, Token), ?assertEqual(<<"deleted">>, maps:get(<<"status">>, Result)), Words = api_test_runner:admin_get(<<"/v1/admin/banned-words">>, Token), ?assertNot(lists:any(fun(W) -> maps:get(<<"word">>, W) =:= Word end, Words)), ct:pal(" OK"). %% @doc PUT /v1/admin/banned-words/:word – попытка переименовать в уже существующее слово (409). test_update_word_conflict(Token) -> ct:pal(" TEST: Update banned word to existing word (conflict)"), Unique = integer_to_binary(erlang:system_time()), WordA = <<"conflictA_", Unique/binary>>, WordB = <<"conflictB_", Unique/binary>>, api_test_runner:admin_post(<<"/v1/admin/banned-words">>, Token, #{<<"word">> => WordA}), api_test_runner:admin_post(<<"/v1/admin/banned-words">>, Token, #{<<"word">> => WordB}), Path = <<"/v1/admin/banned-words/", WordA/binary>>, Resp = api_test_runner:admin_request(put, Path, Token, jsx:encode(#{<<"word">> => WordB})), ?assertMatch({ok, 409, _, _}, Resp), ct:pal(" OK: got 409 conflict"). %% @doc POST /v1/admin/banned-words/batch – с невалидными элементами. test_batch_add_invalid(Token) -> ct:pal(" TEST: Batch add with invalid items"), Unique = integer_to_binary(erlang:system_time()), Words = [ <<"valid_", Unique/binary>>, null, 12345, <<"valid2_", Unique/binary>> ], Body = jsx:encode(#{<<"words">> => Words}), {ok, 200, _, RespBody} = api_test_runner:admin_request(post, <<"/v1/admin/banned-words/batch">>, Token, Body), Result = jsx:decode(list_to_binary(RespBody), [return_maps]), ?assertEqual(2, maps:get(<<"added">>, Result)), ?assertEqual(0, maps:get(<<"skipped">>, Result)), ?assertEqual(2, maps:get(<<"invalid_count">>, Result)), ct:pal(" OK: invalid items filtered"). %% @doc POST /v1/admin/banned-words/batch – с дубликатами. test_batch_add_duplicates(Token) -> ct:pal(" TEST: Batch add with duplicates"), Unique = integer_to_binary(erlang:system_time()), Word1 = <<"dup1_", Unique/binary>>, Word2 = <<"dup2_", Unique/binary>>, Words = [Word1, Word2, Word1, Word2], Body = jsx:encode(#{<<"words">> => Words}), {ok, 200, _, RespBody} = api_test_runner:admin_request(post, <<"/v1/admin/banned-words/batch">>, Token, Body), Result = jsx:decode(list_to_binary(RespBody), [return_maps]), ?assertEqual(2, maps:get(<<"added">>, Result)), ?assertEqual(2, maps:get(<<"skipped">>, Result)), ct:pal(" OK: duplicates handled"). %% ── Пагинация ── %% @doc GET /v1/admin/banned-words?limit=...&offset=... – пагинация. test_words_pagination(Token) -> ct:pal(" TEST: Banned words pagination"), Page1 = api_test_runner:admin_get(<<"/v1/admin/banned-words?limit=1&offset=0">>, Token), ?assert(length(Page1) >= 1), Page2 = api_test_runner:admin_get(<<"/v1/admin/banned-words?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/banned-words?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 Фильтрация по слову (q). test_filter_by_word(Token, Word) -> ct:pal(" TEST: Filter banned words by word (q)"), Words = api_test_runner:admin_get(<<"/v1/admin/banned-words?q=", Word/binary>>, Token), ?assert(is_list(Words)), ?assert(length(Words) >= 1), [?assertEqual(Word, maps:get(<<"word">>, W)) || W <- Words], ct:pal(" OK: ~p words match", [length(Words)]). %% ── Сортировка ── %% @doc Сортировка по word по возрастанию. test_sort_by_word_asc(Token) -> ct:pal(" TEST: Sort banned words by word asc"), Words = api_test_runner:admin_get(<<"/v1/admin/banned-words?sort=word&order=asc">>, Token), WordList = [maps:get(<<"word">>, W) || W <- Words], ?assertEqual(lists:sort(WordList), WordList), ct:pal(" OK: sorted by word asc"). %% @doc Сортировка по added_at по убыванию. test_sort_by_added_at_desc(Token) -> ct:pal(" TEST: Sort banned words by added_at desc"), Words = api_test_runner:admin_get(<<"/v1/admin/banned-words?sort=added_at&order=desc">>, Token), Dates = [maps:get(<<"added_at">>, W) || W <- Words], Sorted = lists:reverse(lists:sort(Dates)), ?assertEqual(Sorted, Dates), ct:pal(" OK: sorted by added_at desc"). %% ── Комбинации ── %% @doc Фильтр + сортировка: q=alpha, сортировка по added_at asc. test_filter_with_sort(Token, Unique) -> ct:pal(" TEST: Filter + sort (q=alpha, sort=added_at, order=asc)"), Q = <<"alpha_", Unique/binary>>, Words = api_test_runner:admin_get( <<"/v1/admin/banned-words?q=", Q/binary, "&sort=added_at&order=asc">>, Token), ?assert(length(Words) >= 1), % Проверяем, что все слова содержат "alpha" [?assert(binary:match(maps:get(<<"word">>, W), <<"alpha">>) =/= nomatch) || W <- Words], Dates = [maps:get(<<"added_at">>, W) || W <- Words], ?assertEqual(lists:sort(Dates), Dates), ct:pal(" OK: ~p words", [length(Words)]). %% @doc Фильтр + сортировка + пагинация: q=beta, sort=word desc, limit=1. test_filter_sort_pagination(Token, Unique) -> ct:pal(" TEST: Filter + sort + pagination (q=beta, sort=word desc, limit=1)"), Q = <<"beta_", Unique/binary>>, {ok, 200, Headers, Body} = api_test_runner:admin_request(get, <<"/v1/admin/banned-words?q=", Q/binary, "&sort=word&order=desc&limit=1&offset=0">>, Token), Words = jsx:decode(list_to_binary(Body), [return_maps]), ?assert(length(Words) =< 1), case Words of [W | _] -> ?assert(binary:match(maps:get(<<"word">>, W), <<"beta">>) =/= nomatch); _ -> ok end, Total = list_to_integer( proplists:get_value("x-total-count", Headers, "0")), ?assert(Total >= 1), ct:pal(" OK: got ~p words (total ~p)", [length(Words), Total]).