Рефакторинг обработчиков. Часть 3 #21
This commit is contained in:
115
test/api/users/user_bookings_tests.erl
Normal file
115
test/api/users/user_bookings_tests.erl
Normal file
@@ -0,0 +1,115 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Тесты клиентского API для бронирований.
|
||||
%%%
|
||||
%%% Покрывает эндпоинты:
|
||||
%%% POST /v1/events/:id/bookings
|
||||
%%% PUT /v1/bookings/:id
|
||||
%%% DELETE /v1/bookings/:id
|
||||
%%%
|
||||
%%% Проверяет:
|
||||
%%% - создание бронирования участником
|
||||
%%% - подтверждение бронирования владельцем календаря
|
||||
%%% - отмену бронирования участником
|
||||
%%% - ошибку при повторном бронировании
|
||||
%%% - ошибку 401 без токена
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(user_bookings_tests).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-export([test/0]).
|
||||
|
||||
%%%===================================================================
|
||||
%%% Главная тестовая функция
|
||||
%%%===================================================================
|
||||
|
||||
-spec test() -> ok.
|
||||
test() ->
|
||||
ct:pal("=== User Bookings Tests ==="),
|
||||
OwnerToken = api_test_runner:get_user_token(),
|
||||
ParticipantEmail = api_test_runner:unique_email(<<"participant">>),
|
||||
ParticipantToken = api_test_runner:register_and_login(ParticipantEmail, <<"pass">>),
|
||||
|
||||
% Создаём календарь и событие
|
||||
CalId = api_test_runner:create_calendar(OwnerToken, #{title => <<"BookingTest">>}),
|
||||
#{<<"id">> := EventId} = api_test_runner:client_post(
|
||||
<<"/v1/calendars/", CalId/binary, "/events">>, OwnerToken,
|
||||
#{title => <<"Event to book">>,
|
||||
start_time => <<"2026-06-01T10:00:00Z">>,
|
||||
duration => 60}),
|
||||
|
||||
test_create_booking(ParticipantToken, EventId),
|
||||
test_confirm_booking(OwnerToken, EventId),
|
||||
test_cancel_booking(ParticipantToken, EventId),
|
||||
test_duplicate_booking(ParticipantToken, EventId),
|
||||
test_booking_unauthorized(EventId),
|
||||
|
||||
ct:pal("=== All user bookings tests passed ==="),
|
||||
ok.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Тестовые функции
|
||||
%%%===================================================================
|
||||
|
||||
%% @doc Успешное создание бронирования: 201 Created.
|
||||
-spec test_create_booking(binary(), binary()) -> ok.
|
||||
test_create_booking(Token, EventId) ->
|
||||
ct:pal(" TEST: Create booking"),
|
||||
Path = <<"/v1/events/", EventId/binary, "/bookings">>,
|
||||
Resp = api_test_runner:client_request(post, Path, Token, <<"{}">>),
|
||||
{ok, 201, _, Body} = Resp,
|
||||
#{<<"id">> := BookingId, <<"status">> := Status} = jsx:decode(list_to_binary(Body), [return_maps]),
|
||||
?assert(is_binary(BookingId)),
|
||||
?assertEqual(<<"pending">>, Status),
|
||||
ct:pal(" OK: booking ~s created", [BookingId]).
|
||||
|
||||
%% @doc Подтверждение бронирования владельцем: 200 OK.
|
||||
-spec test_confirm_booking(binary(), binary()) -> ok.
|
||||
test_confirm_booking(OwnerToken, EventId) ->
|
||||
ct:pal(" TEST: Confirm booking as owner"),
|
||||
% Создаём новое бронирование, которое ещё не подтверждено
|
||||
Participant2 = api_test_runner:register_and_login(
|
||||
api_test_runner:unique_email(<<"part2">>), <<"pass">>),
|
||||
Path = <<"/v1/events/", EventId/binary, "/bookings">>,
|
||||
#{<<"id">> := BookingId} = api_test_runner:client_post(Path, Participant2, #{}),
|
||||
% Подтверждаем
|
||||
ConfirmPath = <<"/v1/bookings/", BookingId/binary>>,
|
||||
Updated = api_test_runner:client_put(ConfirmPath, OwnerToken,
|
||||
#{action => <<"confirm">>}),
|
||||
?assertEqual(<<"confirmed">>, maps:get(<<"status">>, Updated)),
|
||||
ct:pal(" OK: booking ~s confirmed", [BookingId]).
|
||||
|
||||
%% @doc Отмена бронирования участником: 200 OK.
|
||||
-spec test_cancel_booking(binary(), binary()) -> ok.
|
||||
test_cancel_booking(OwnerToken, EventId) ->
|
||||
ct:pal(" TEST: Cancel booking as participant"),
|
||||
% Создаём нового участника, у которого нет бронирований
|
||||
CancelUserEmail = api_test_runner:unique_email(<<"canceluser">>),
|
||||
CancelUserToken = api_test_runner:register_and_login(CancelUserEmail, <<"pass">>),
|
||||
Path = <<"/v1/events/", EventId/binary, "/bookings">>,
|
||||
#{<<"id">> := BookingId} = api_test_runner:client_post(Path, CancelUserToken, #{}),
|
||||
CancelPath = <<"/v1/bookings/", BookingId/binary>>,
|
||||
Resp = api_test_runner:client_request(delete, CancelPath, CancelUserToken),
|
||||
?assertMatch({ok, 200, _, _}, Resp),
|
||||
ct:pal(" OK: booking ~s cancelled", [BookingId]).
|
||||
|
||||
%% @doc Повторное бронирование того же события: 409 Conflict.
|
||||
-spec test_duplicate_booking(binary(), binary()) -> ok.
|
||||
test_duplicate_booking(ParticipantToken, EventId) ->
|
||||
ct:pal(" TEST: Duplicate booking"),
|
||||
Path = <<"/v1/events/", EventId/binary, "/bookings">>,
|
||||
% Первый раз должно быть 201 (или 200, если уже есть pending)
|
||||
_ = api_test_runner:client_request(post, Path, ParticipantToken, <<"{}">>),
|
||||
% Второй раз – уже booked
|
||||
Resp2 = api_test_runner:client_request(post, Path, ParticipantToken, <<"{}">>),
|
||||
{ok, 409, _, _} = Resp2,
|
||||
ct:pal(" OK: got 409 conflict").
|
||||
|
||||
%% @doc Запрос без токена: 401 Unauthorized.
|
||||
-spec test_booking_unauthorized(binary()) -> ok.
|
||||
test_booking_unauthorized(EventId) ->
|
||||
ct:pal(" TEST: Booking without token"),
|
||||
Path = <<"/v1/events/", EventId/binary, "/bookings">>,
|
||||
Resp = api_test_runner:client_request(post, Path, <<>>, <<"{}">>),
|
||||
?assertMatch({ok, 401, _, _}, Resp),
|
||||
ct:pal(" OK: got 401").
|
||||
87
test/api/users/user_calendar_by_id_tests.erl
Normal file
87
test/api/users/user_calendar_by_id_tests.erl
Normal file
@@ -0,0 +1,87 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Тесты клиентского API для работы с конкретным календарём.
|
||||
%%% Покрывает GET, PUT, DELETE /v1/calendars/:id.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(user_calendar_by_id_tests).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-export([test/0]).
|
||||
|
||||
-spec test() -> ok.
|
||||
test() ->
|
||||
ct:pal("=== User Calendar By ID Tests ==="),
|
||||
Token = api_test_runner:get_user_token(),
|
||||
OtherToken = api_test_runner:register_and_login(
|
||||
api_test_runner:unique_email(<<"other">>), <<"pass">>),
|
||||
|
||||
% Создаём календарь для тестов
|
||||
#{<<"id">> := CalId} = api_test_runner:client_post(<<"/v1/calendars">>, Token,
|
||||
#{title => <<"TestCal">>, type => <<"personal">>}),
|
||||
|
||||
test_get_calendar(Token, CalId),
|
||||
test_get_calendar_unauthorized(CalId),
|
||||
test_get_calendar_not_found(Token),
|
||||
test_update_calendar(Token, CalId),
|
||||
test_update_calendar_forbidden(OtherToken, CalId),
|
||||
test_delete_calendar(Token, CalId),
|
||||
test_delete_calendar_forbidden(OtherToken, CalId),
|
||||
|
||||
ct:pal("=== All user calendar by id tests passed ==="),
|
||||
ok.
|
||||
|
||||
test_get_calendar(Token, CalId) ->
|
||||
ct:pal(" TEST: Get calendar by ID"),
|
||||
Path = <<"/v1/calendars/", CalId/binary>>,
|
||||
Cal = api_test_runner:client_get(Path, Token),
|
||||
?assertEqual(CalId, maps:get(<<"id">>, Cal)),
|
||||
?assert(maps:is_key(<<"title">>, Cal)),
|
||||
ct:pal(" OK: ~s", [maps:get(<<"title">>, Cal)]).
|
||||
|
||||
test_get_calendar_unauthorized(CalId) ->
|
||||
ct:pal(" TEST: Get calendar without token (401)"),
|
||||
Path = <<"/v1/calendars/", CalId/binary>>,
|
||||
Resp = api_test_runner:client_request(get, Path, <<>>),
|
||||
?assertMatch({ok, 401, _, _}, Resp),
|
||||
ct:pal(" OK: got 401").
|
||||
|
||||
test_get_calendar_not_found(Token) ->
|
||||
ct:pal(" TEST: Get non-existent calendar (404)"),
|
||||
Resp = api_test_runner:client_request(get, <<"/v1/calendars/fakeid">>, Token),
|
||||
?assertMatch({ok, 404, _, _}, Resp),
|
||||
ct:pal(" OK: got 404").
|
||||
|
||||
test_update_calendar(Token, CalId) ->
|
||||
ct:pal(" TEST: Update calendar"),
|
||||
Path = <<"/v1/calendars/", CalId/binary>>,
|
||||
Updated = api_test_runner:client_put(Path, Token,
|
||||
#{title => <<"Updated">>, description => <<"New desc">>}),
|
||||
?assertEqual(<<"Updated">>, maps:get(<<"title">>, Updated)),
|
||||
?assertEqual(<<"New desc">>, maps:get(<<"description">>, Updated)),
|
||||
ct:pal(" OK").
|
||||
|
||||
test_update_calendar_forbidden(OtherToken, CalId) ->
|
||||
ct:pal(" TEST: Update calendar by non-owner (403)"),
|
||||
Path = <<"/v1/calendars/", CalId/binary>>,
|
||||
Resp = api_test_runner:client_request(put, Path, OtherToken,
|
||||
jsx:encode(#{title => <<"fail">>})),
|
||||
?assertMatch({ok, 403, _, _}, Resp),
|
||||
ct:pal(" OK: got 403").
|
||||
|
||||
test_delete_calendar(Token, CalId) ->
|
||||
ct:pal(" TEST: Delete calendar (soft-delete)"),
|
||||
Path = <<"/v1/calendars/", CalId/binary>>,
|
||||
Resp = api_test_runner:client_request(delete, Path, Token),
|
||||
?assertMatch({ok, 200, _, _}, Resp),
|
||||
ct:pal(" OK: deleted").
|
||||
|
||||
test_delete_calendar_forbidden(OtherToken, CalId) ->
|
||||
% Первый раз мы уже удалили, но проверим на другом календаре
|
||||
ct:pal(" TEST: Delete calendar by non-owner (403)"),
|
||||
% Создадим новый календарь владельцем Token, попробуем удалить OtherToken
|
||||
#{<<"id">> := NewCalId} = api_test_runner:client_post(<<"/v1/calendars">>, api_test_runner:get_user_token(),
|
||||
#{title => <<"ForbiddenDel">>, type => <<"personal">>}),
|
||||
Path = <<"/v1/calendars/", NewCalId/binary>>,
|
||||
Resp = api_test_runner:client_request(delete, Path, OtherToken),
|
||||
?assertMatch({ok, 403, _, _}, Resp),
|
||||
ct:pal(" OK: got 403").
|
||||
61
test/api/users/user_calendar_view_tests.erl
Normal file
61
test/api/users/user_calendar_view_tests.erl
Normal file
@@ -0,0 +1,61 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Тесты клиентского API для HTML-представления календаря.
|
||||
%%%
|
||||
%%% Покрывает эндпоинты:
|
||||
%%% GET /v1/calendars/:calendar_id/view
|
||||
%%%
|
||||
%%% Проверяет:
|
||||
%%% - успешное получение HTML-страницы (200, text/html)
|
||||
%%% - ошибку 401 без токена
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(user_calendar_view_tests).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-export([test/0]).
|
||||
|
||||
%%%===================================================================
|
||||
%%% Главная тестовая функция
|
||||
%%%===================================================================
|
||||
|
||||
-spec test() -> ok.
|
||||
test() ->
|
||||
ct:pal("=== User Calendar View Tests ==="),
|
||||
Token = api_test_runner:get_user_token(),
|
||||
|
||||
% Создаём календарь
|
||||
CalId = api_test_runner:create_calendar(Token, #{title => <<"ViewCal">>}),
|
||||
|
||||
test_get_calendar_view(Token, CalId),
|
||||
test_get_calendar_view_unauthorized(CalId),
|
||||
|
||||
ct:pal("=== All user calendar view tests passed ==="),
|
||||
ok.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Тестовые функции
|
||||
%%%===================================================================
|
||||
|
||||
%% @doc Успешный запрос HTML-представления: 200 OK, тип text/html.
|
||||
-spec test_get_calendar_view(binary(), binary()) -> ok.
|
||||
test_get_calendar_view(Token, CalId) ->
|
||||
ct:pal(" TEST: Get calendar HTML view"),
|
||||
Path = <<"/v1/calendars/", CalId/binary, "/view?month=2026-06">>,
|
||||
Resp = api_test_runner:client_request(get, Path, Token),
|
||||
{ok, 200, Headers, Body} = Resp,
|
||||
?assert(lists:keymember("content-type", 1, Headers)),
|
||||
{"content-type", CT} = lists:keyfind("content-type", 1, Headers),
|
||||
?assert(string:str(CT, "text/html") > 0),
|
||||
% Body может быть строкой или binary, приводим к binary и проверяем непустоту
|
||||
BodyBin = iolist_to_binary(Body),
|
||||
?assert(byte_size(BodyBin) > 0),
|
||||
ct:pal(" OK: got HTML of ~p bytes", [byte_size(BodyBin)]).
|
||||
|
||||
%% @doc Запрос без токена: 401 Unauthorized.
|
||||
-spec test_get_calendar_view_unauthorized(binary()) -> ok.
|
||||
test_get_calendar_view_unauthorized(CalId) ->
|
||||
ct:pal(" TEST: Get calendar view without token"),
|
||||
Path = <<"/v1/calendars/", CalId/binary, "/view?month=2026-06">>,
|
||||
Resp = api_test_runner:client_request(get, Path, <<>>),
|
||||
?assertMatch({ok, 401, _, _}, Resp),
|
||||
ct:pal(" OK: got 401").
|
||||
51
test/api/users/user_calendars_tests.erl
Normal file
51
test/api/users/user_calendars_tests.erl
Normal file
@@ -0,0 +1,51 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Тесты клиентского API для управления календарями.
|
||||
%%% Покрывает POST /v1/calendars (создание) и GET /v1/calendars (список).
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(user_calendars_tests).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-export([test/0]).
|
||||
|
||||
-spec test() -> ok.
|
||||
test() ->
|
||||
ct:pal("=== User Calendars Tests ==="),
|
||||
Token = api_test_runner:get_user_token(),
|
||||
|
||||
% Создаём один календарь для тестов
|
||||
#{<<"id">> := CalId} = api_test_runner:client_post(<<"/v1/calendars">>, Token,
|
||||
#{title => <<"TestCal">>, type => <<"personal">>}),
|
||||
|
||||
test_create_calendar(Token),
|
||||
test_list_calendars(Token),
|
||||
test_list_calendars_unauthorized(),
|
||||
|
||||
ct:pal("=== All user calendars tests passed ==="),
|
||||
ok.
|
||||
|
||||
test_create_calendar(Token) ->
|
||||
ct:pal(" TEST: Create a new calendar"),
|
||||
Resp = api_test_runner:client_request(post, <<"/v1/calendars">>, Token,
|
||||
jsx:encode(#{title => <<"NewCal">>, type => <<"personal">>})),
|
||||
{ok, 201, _, Body} = Resp,
|
||||
#{<<"id">> := Id, <<"title">> := Title} = jsx:decode(list_to_binary(Body), [return_maps]),
|
||||
?assert(is_binary(Id)),
|
||||
?assertEqual(<<"NewCal">>, Title),
|
||||
ct:pal(" OK: created calendar ~s", [Id]).
|
||||
|
||||
test_list_calendars(Token) ->
|
||||
ct:pal(" TEST: List user calendars"),
|
||||
Calendars = api_test_runner:client_get(<<"/v1/calendars">>, Token),
|
||||
?assert(is_list(Calendars)),
|
||||
?assert(length(Calendars) >= 1),
|
||||
First = hd(Calendars),
|
||||
?assert(maps:is_key(<<"id">>, First)),
|
||||
?assert(maps:is_key(<<"title">>, First)),
|
||||
ct:pal(" OK: ~p calendars found", [length(Calendars)]).
|
||||
|
||||
test_list_calendars_unauthorized() ->
|
||||
ct:pal(" TEST: List calendars without token (401)"),
|
||||
Resp = api_test_runner:client_request(get, <<"/v1/calendars">>, <<>>),
|
||||
?assertMatch({ok, 401, _, _}, Resp),
|
||||
ct:pal(" OK: got 401").
|
||||
114
test/api/users/user_event_by_id_tests.erl
Normal file
114
test/api/users/user_event_by_id_tests.erl
Normal file
@@ -0,0 +1,114 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Тесты клиентского API для работы с конкретным событием.
|
||||
%%%
|
||||
%%% Покрывает эндпоинты:
|
||||
%%% GET /v1/events/:id
|
||||
%%% PUT /v1/events/:id
|
||||
%%% DELETE /v1/events/:id
|
||||
%%%
|
||||
%%% Проверяет:
|
||||
%%% - получение события по ID
|
||||
%%% - обновление события (владельцем)
|
||||
%%% - мягкое удаление события (статус становится deleted)
|
||||
%%% - ошибку 403 при попытке изменения чужого события
|
||||
%%% - ошибку 404 для несуществующего события
|
||||
%%% - ошибку 401 без токена
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(user_event_by_id_tests).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-export([test/0]).
|
||||
|
||||
%%%===================================================================
|
||||
%%% Главная тестовая функция
|
||||
%%%===================================================================
|
||||
|
||||
-spec test() -> ok.
|
||||
test() ->
|
||||
ct:pal("=== User Event By ID Tests ==="),
|
||||
Token = api_test_runner:get_user_token(),
|
||||
OtherToken = api_test_runner:register_and_login(
|
||||
api_test_runner:unique_email(<<"other">>), <<"pass">>),
|
||||
|
||||
% Создаём календарь и событие
|
||||
CalId = api_test_runner:create_calendar(Token, #{title => <<"EvtById">>}),
|
||||
#{<<"id">> := EventId} = api_test_runner:client_post(
|
||||
<<"/v1/calendars/", CalId/binary, "/events">>, Token,
|
||||
#{title => <<"Test Event">>,
|
||||
start_time => <<"2026-06-01T10:00:00Z">>,
|
||||
duration => 60}),
|
||||
|
||||
test_get_event(Token, EventId),
|
||||
test_update_event(Token, EventId),
|
||||
test_update_event_forbidden(OtherToken, EventId),
|
||||
test_delete_event(Token, EventId),
|
||||
test_get_event_not_found(Token),
|
||||
test_get_event_unauthorized(EventId),
|
||||
|
||||
ct:pal("=== All user event by id tests passed ==="),
|
||||
ok.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Тестовые функции
|
||||
%%%===================================================================
|
||||
|
||||
%% @doc GET /v1/events/:id – получение события.
|
||||
-spec test_get_event(binary(), binary()) -> ok.
|
||||
test_get_event(Token, EventId) ->
|
||||
ct:pal(" TEST: Get event by ID"),
|
||||
Path = <<"/v1/events/", EventId/binary>>,
|
||||
Event = api_test_runner:client_get(Path, Token),
|
||||
?assertEqual(EventId, maps:get(<<"id">>, Event)),
|
||||
?assertEqual(<<"Test Event">>, maps:get(<<"title">>, Event)),
|
||||
ct:pal(" OK: got event ~s", [EventId]).
|
||||
|
||||
%% @doc PUT /v1/events/:id – обновление события.
|
||||
-spec test_update_event(binary(), binary()) -> ok.
|
||||
test_update_event(Token, EventId) ->
|
||||
ct:pal(" TEST: Update event"),
|
||||
Path = <<"/v1/events/", EventId/binary>>,
|
||||
Updated = api_test_runner:client_put(Path, Token,
|
||||
#{title => <<"Updated Event">>, description => <<"New desc">>}),
|
||||
?assertEqual(<<"Updated Event">>, maps:get(<<"title">>, Updated)),
|
||||
?assertEqual(<<"New desc">>, maps:get(<<"description">>, Updated)),
|
||||
ct:pal(" OK").
|
||||
|
||||
%% @doc PUT /v1/events/:id – попытка обновления чужим пользователем (403).
|
||||
-spec test_update_event_forbidden(binary(), binary()) -> ok.
|
||||
test_update_event_forbidden(OtherToken, EventId) ->
|
||||
ct:pal(" TEST: Update event by non-owner"),
|
||||
Path = <<"/v1/events/", EventId/binary>>,
|
||||
Resp = api_test_runner:client_request(put, Path, OtherToken,
|
||||
jsx:encode(#{title => <<"fail">>})),
|
||||
?assertMatch({ok, 403, _, _}, Resp),
|
||||
ct:pal(" OK: got 403").
|
||||
|
||||
%% @doc DELETE /v1/events/:id – мягкое удаление события.
|
||||
-spec test_delete_event(binary(), binary()) -> ok.
|
||||
test_delete_event(Token, EventId) ->
|
||||
ct:pal(" TEST: Soft-delete event"),
|
||||
Path = <<"/v1/events/", EventId/binary>>,
|
||||
% Удаляем событие
|
||||
{ok, 200, _, _} = api_test_runner:client_request(delete, Path, Token),
|
||||
% Проверяем, что событие доступно, но его статус = deleted
|
||||
Event = api_test_runner:client_get(Path, Token),
|
||||
?assertEqual(<<"deleted">>, maps:get(<<"status">>, Event)),
|
||||
ct:pal(" OK: event soft-deleted").
|
||||
|
||||
%% @doc GET /v1/events/:id – несуществующее событие (404).
|
||||
-spec test_get_event_not_found(binary()) -> ok.
|
||||
test_get_event_not_found(Token) ->
|
||||
ct:pal(" TEST: Get non-existent event"),
|
||||
Resp = api_test_runner:client_request(get, <<"/v1/events/fakeid">>, Token),
|
||||
?assertMatch({ok, 404, _, _}, Resp),
|
||||
ct:pal(" OK: got 404").
|
||||
|
||||
%% @doc GET /v1/events/:id – без токена (401).
|
||||
-spec test_get_event_unauthorized(binary()) -> ok.
|
||||
test_get_event_unauthorized(EventId) ->
|
||||
ct:pal(" TEST: Get event without token"),
|
||||
Path = <<"/v1/events/", EventId/binary>>,
|
||||
Resp = api_test_runner:client_request(get, Path, <<>>),
|
||||
?assertMatch({ok, 401, _, _}, Resp),
|
||||
ct:pal(" OK: got 401").
|
||||
80
test/api/users/user_events_tests.erl
Normal file
80
test/api/users/user_events_tests.erl
Normal file
@@ -0,0 +1,80 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Тесты клиентского API для событий.
|
||||
%%% Покрывает POST /v1/calendars/:id/events и GET /v1/events/:id/occurrences
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(user_events_tests).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-export([test/0]).
|
||||
|
||||
-spec test() -> ok.
|
||||
test() ->
|
||||
ct:pal("=== User Events Tests ==="),
|
||||
Token = api_test_runner:get_user_token(),
|
||||
|
||||
% Создаём календарь
|
||||
CalId = api_test_runner:create_calendar(Token, #{title => <<"EventsTest">>}),
|
||||
|
||||
% Тесты
|
||||
test_create_single_event(Token, CalId),
|
||||
test_list_events_with_dates(Token, CalId),
|
||||
test_create_recurring_event(Token, CalId),
|
||||
|
||||
ct:pal("=== All user events tests passed ==="),
|
||||
ok.
|
||||
|
||||
%% @doc POST /v1/calendars/:calendar_id/events – одиночное событие.
|
||||
test_create_single_event(Token, CalId) ->
|
||||
ct:pal(" TEST: Create single event"),
|
||||
Path = <<"/v1/calendars/", CalId/binary, "/events">>,
|
||||
Body = jsx:encode(#{
|
||||
title => <<"Single Event">>,
|
||||
start_time => <<"2026-06-01T10:00:00Z">>,
|
||||
duration => 60
|
||||
}),
|
||||
Resp = api_test_runner:client_request(post, Path, Token, Body),
|
||||
{ok, 201, _, RespBody} = Resp,
|
||||
#{<<"id">> := EventId, <<"title">> := Title} = jsx:decode(list_to_binary(RespBody), [return_maps]),
|
||||
?assert(is_binary(EventId)),
|
||||
?assertEqual(<<"Single Event">>, Title),
|
||||
ct:pal(" OK: created event ~s", [EventId]).
|
||||
|
||||
%% @doc GET /v1/calendars/:calendar_id/events?from=...&to=... – список с фильтром.
|
||||
test_list_events_with_dates(Token, CalId) ->
|
||||
ct:pal(" TEST: List events with date range"),
|
||||
Path = <<"/v1/calendars/", CalId/binary, "/events?from=2026-05-01T00:00:00Z&to=2026-07-01T00:00:00Z">>,
|
||||
Events = api_test_runner:client_get(Path, Token),
|
||||
?assert(is_list(Events)),
|
||||
?assert(length(Events) >= 1),
|
||||
First = hd(Events),
|
||||
?assert(maps:is_key(<<"id">>, First)),
|
||||
?assert(maps:is_key(<<"title">>, First)),
|
||||
ct:pal(" OK: ~p events found", [length(Events)]).
|
||||
|
||||
%% @doc POST /v1/calendars/:calendar_id/events – повторяющееся событие и проверка вхождений.
|
||||
test_create_recurring_event(Token, CalId) ->
|
||||
ct:pal(" TEST: Create recurring event and check occurrences"),
|
||||
Path = <<"/v1/calendars/", CalId/binary, "/events">>,
|
||||
Body = jsx:encode(#{
|
||||
title => <<"Weekly Meeting">>,
|
||||
start_time => <<"2026-06-01T10:00:00Z">>,
|
||||
duration => 60,
|
||||
recurrence => #{
|
||||
freq => <<"WEEKLY">>,
|
||||
interval => 1
|
||||
}
|
||||
}),
|
||||
Resp = api_test_runner:client_request(post, Path, Token, Body),
|
||||
{ok, 201, _, RespBody} = Resp,
|
||||
#{<<"id">> := RecurringId} = jsx:decode(list_to_binary(RespBody), [return_maps]),
|
||||
ct:pal(" Created recurring event ~s", [RecurringId]),
|
||||
|
||||
% Запрашиваем вхождения на месяц
|
||||
OccPath = <<"/v1/events/", RecurringId/binary, "/occurrences?from=2026-06-01T00:00:00Z&to=2026-06-30T00:00:00Z">>,
|
||||
Occs = api_test_runner:client_get(OccPath, Token),
|
||||
?assert(is_list(Occs)),
|
||||
?assert(length(Occs) >= 4), % минимум 4 недели в июне
|
||||
FirstOcc = hd(Occs),
|
||||
?assert(maps:is_key(<<"start_time">>, FirstOcc)),
|
||||
ct:pal(" OK: ~p occurrences found", [length(Occs)]).
|
||||
90
test/api/users/user_login_tests.erl
Normal file
90
test/api/users/user_login_tests.erl
Normal file
@@ -0,0 +1,90 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Тесты клиентского API для входа пользователей.
|
||||
%%%
|
||||
%%% Покрывает эндпоинты:
|
||||
%%% POST /v1/login
|
||||
%%%
|
||||
%%% Проверяет:
|
||||
%%% - успешный вход с правильными email и паролем
|
||||
%%% - ошибку при неверном пароле
|
||||
%%% - ошибку при несуществующем email
|
||||
%%% - ошибку при отсутствии обязательных полей
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(user_login_tests).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-export([test/0]).
|
||||
|
||||
%%%===================================================================
|
||||
%%% Главная тестовая функция
|
||||
%%%===================================================================
|
||||
|
||||
-spec test() -> ok.
|
||||
test() ->
|
||||
ct:pal("=== Client Login Tests ==="),
|
||||
Email = api_test_runner:unique_email(<<"login">>),
|
||||
Password = <<"StrongPass1!">>,
|
||||
|
||||
% Создаём пользователя для тестов входа
|
||||
api_test_runner:register_and_login(Email, Password),
|
||||
|
||||
test_successful_login(Email, Password),
|
||||
test_wrong_password(Email),
|
||||
test_nonexistent_email(),
|
||||
test_missing_fields(),
|
||||
|
||||
ct:pal("=== All client login tests passed ==="),
|
||||
ok.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Тестовые функции
|
||||
%%%===================================================================
|
||||
|
||||
%% @doc Успешный вход: 200 OK, возвращает токен и данные пользователя.
|
||||
-spec test_successful_login(binary(), binary()) -> ok.
|
||||
test_successful_login(Email, Password) ->
|
||||
ct:pal(" TEST: Successful login"),
|
||||
Resp = api_test_runner:client_request(post, <<"/v1/login">>, <<>>,
|
||||
jsx:encode(#{email => Email, password => Password})),
|
||||
{ok, 200, _, Body} = Resp,
|
||||
#{<<"token">> := Token, <<"user">> := User} = jsx:decode(list_to_binary(Body), [return_maps]),
|
||||
?assert(is_binary(Token)),
|
||||
?assertEqual(Email, maps:get(<<"email">>, User)),
|
||||
ct:pal(" OK: user ~s logged in", [maps:get(<<"id">>, User)]).
|
||||
|
||||
%% @doc Неверный пароль: 401 Unauthorized.
|
||||
-spec test_wrong_password(binary()) -> ok.
|
||||
test_wrong_password(Email) ->
|
||||
ct:pal(" TEST: Wrong password"),
|
||||
Resp = api_test_runner:client_request(post, <<"/v1/login">>, <<>>,
|
||||
jsx:encode(#{email => Email, password => <<"WrongPass1">>})),
|
||||
{ok, 401, _, Body} = Resp,
|
||||
#{<<"error">> := Msg} = jsx:decode(list_to_binary(Body), [return_maps]),
|
||||
?assertEqual(<<"Invalid credentials">>, Msg),
|
||||
ct:pal(" OK: got 401 unauthorized").
|
||||
|
||||
%% @doc Несуществующий email: 401 Unauthorized.
|
||||
-spec test_nonexistent_email() -> ok.
|
||||
test_nonexistent_email() ->
|
||||
ct:pal(" TEST: Nonexistent email"),
|
||||
Resp = api_test_runner:client_request(post, <<"/v1/login">>, <<>>,
|
||||
jsx:encode(#{email => <<"no@such.user">>, password => <<"Anything1">>})),
|
||||
{ok, 401, _, Body} = Resp,
|
||||
#{<<"error">> := Msg} = jsx:decode(list_to_binary(Body), [return_maps]),
|
||||
?assertEqual(<<"Invalid credentials">>, Msg),
|
||||
ct:pal(" OK: got 401 unauthorized").
|
||||
|
||||
%% @doc Отсутствие обязательных полей: 400 Bad Request.
|
||||
-spec test_missing_fields() -> ok.
|
||||
test_missing_fields() ->
|
||||
ct:pal(" TEST: Missing required fields"),
|
||||
Resp1 = api_test_runner:client_request(post, <<"/v1/login">>, <<>>,
|
||||
jsx:encode(#{email => <<"a@b.com">>})),
|
||||
?assertMatch({ok, 400, _, _}, Resp1),
|
||||
|
||||
Resp2 = api_test_runner:client_request(post, <<"/v1/login">>, <<>>,
|
||||
jsx:encode(#{password => <<"NoEmail1">>})),
|
||||
?assertMatch({ok, 400, _, _}, Resp2),
|
||||
|
||||
ct:pal(" OK: 400 on missing fields").
|
||||
55
test/api/users/user_me_tests.erl
Normal file
55
test/api/users/user_me_tests.erl
Normal file
@@ -0,0 +1,55 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Тесты клиентского API для получения профиля текущего пользователя.
|
||||
%%%
|
||||
%%% Покрывает эндпоинты:
|
||||
%%% GET /v1/user/me
|
||||
%%%
|
||||
%%% Проверяет:
|
||||
%%% - успешное получение профиля с валидным токеном
|
||||
%%% - ошибку 401 при отсутствии токена
|
||||
%%% - наличие ключевых полей в ответе
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(user_me_tests).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-export([test/0]).
|
||||
|
||||
%%%===================================================================
|
||||
%%% Главная тестовая функция
|
||||
%%%===================================================================
|
||||
|
||||
-spec test() -> ok.
|
||||
test() ->
|
||||
ct:pal("=== User Profile (me) Tests ==="),
|
||||
Token = api_test_runner:get_user_token(),
|
||||
|
||||
test_get_me_success(Token),
|
||||
test_get_me_unauthorized(),
|
||||
|
||||
ct:pal("=== All user me tests passed ==="),
|
||||
ok.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Тестовые функции
|
||||
%%%===================================================================
|
||||
|
||||
%% @doc Успешное получение профиля: 200 OK, возвращает данные пользователя.
|
||||
-spec test_get_me_success(binary()) -> ok.
|
||||
test_get_me_success(Token) ->
|
||||
ct:pal(" TEST: Get current user profile"),
|
||||
User = api_test_runner:client_get(<<"/v1/user/me">>, Token),
|
||||
?assert(is_map(User)),
|
||||
?assert(maps:is_key(<<"id">>, User)),
|
||||
?assert(maps:is_key(<<"email">>, User)),
|
||||
?assert(maps:is_key(<<"role">>, User)),
|
||||
?assert(maps:is_key(<<"status">>, User)),
|
||||
ct:pal(" OK: got profile for ~s", [maps:get(<<"email">>, User)]).
|
||||
|
||||
%% @doc Отсутствие токена: 401 Unauthorized.
|
||||
-spec test_get_me_unauthorized() -> ok.
|
||||
test_get_me_unauthorized() ->
|
||||
ct:pal(" TEST: Get profile without token"),
|
||||
Resp = api_test_runner:client_request(get, <<"/v1/user/me">>, <<>>),
|
||||
?assertMatch({ok, 401, _, _}, Resp),
|
||||
ct:pal(" OK: got 401 unauthorized").
|
||||
69
test/api/users/user_my_bookings_tests.erl
Normal file
69
test/api/users/user_my_bookings_tests.erl
Normal file
@@ -0,0 +1,69 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Тесты клиентского API для получения своих бронирований.
|
||||
%%%
|
||||
%%% Покрывает эндпоинты:
|
||||
%%% GET /v1/user/bookings
|
||||
%%%
|
||||
%%% Проверяет:
|
||||
%%% - получение списка бронирований текущего пользователя
|
||||
%%% - что бронирование, созданное пользователем, присутствует в ответе
|
||||
%%% - ошибку 401 при отсутствии токена
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(user_my_bookings_tests).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-export([test/0]).
|
||||
|
||||
%%%===================================================================
|
||||
%%% Главная тестовая функция
|
||||
%%%===================================================================
|
||||
|
||||
-spec test() -> ok.
|
||||
test() ->
|
||||
ct:pal("=== User My Bookings Tests ==="),
|
||||
OwnerToken = api_test_runner:get_user_token(),
|
||||
ParticipantEmail = api_test_runner:unique_email(<<"mybooker">>),
|
||||
ParticipantToken = api_test_runner:register_and_login(ParticipantEmail, <<"pass">>),
|
||||
|
||||
% Создаём календарь и событие
|
||||
CalId = api_test_runner:create_calendar(OwnerToken, #{title => <<"MyBookTest">>}),
|
||||
#{<<"id">> := EventId} = api_test_runner:client_post(
|
||||
<<"/v1/calendars/", CalId/binary, "/events">>, OwnerToken,
|
||||
#{title => <<"Event for my booking">>,
|
||||
start_time => <<"2026-06-01T10:00:00Z">>,
|
||||
duration => 60}),
|
||||
|
||||
% Бронируем событие от имени участника и подтверждаем
|
||||
#{<<"id">> := BookingId} = api_test_runner:client_post(
|
||||
<<"/v1/events/", EventId/binary, "/bookings">>, ParticipantToken, #{}),
|
||||
api_test_runner:client_put(<<"/v1/bookings/", BookingId/binary>>, OwnerToken,
|
||||
#{action => <<"confirm">>}),
|
||||
|
||||
test_get_my_bookings(ParticipantToken, BookingId),
|
||||
test_get_my_bookings_unauthorized(),
|
||||
|
||||
ct:pal("=== All user my bookings tests passed ==="),
|
||||
ok.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Тестовые функции
|
||||
%%%===================================================================
|
||||
|
||||
%% @doc Успешное получение своих бронирований: 200 OK, содержит бронирование.
|
||||
-spec test_get_my_bookings(binary(), binary()) -> ok.
|
||||
test_get_my_bookings(Token, ExpectedBookingId) ->
|
||||
ct:pal(" TEST: Get my bookings"),
|
||||
Bookings = api_test_runner:client_get(<<"/v1/user/bookings">>, Token),
|
||||
?assert(is_list(Bookings)),
|
||||
?assert(length(Bookings) >= 1),
|
||||
?assert(lists:any(fun(B) -> maps:get(<<"id">>, B) =:= ExpectedBookingId end, Bookings)),
|
||||
ct:pal(" OK: booking ~s found in list", [ExpectedBookingId]).
|
||||
|
||||
%% @doc Запрос без токена: 401 Unauthorized.
|
||||
-spec test_get_my_bookings_unauthorized() -> ok.
|
||||
test_get_my_bookings_unauthorized() ->
|
||||
ct:pal(" TEST: Get my bookings without token"),
|
||||
Resp = api_test_runner:client_request(get, <<"/v1/user/bookings">>, <<>>),
|
||||
?assertMatch({ok, 401, _, _}, Resp),
|
||||
ct:pal(" OK: got 401").
|
||||
75
test/api/users/user_my_reviews_tests.erl
Normal file
75
test/api/users/user_my_reviews_tests.erl
Normal file
@@ -0,0 +1,75 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Тесты клиентского API для получения своих отзывов.
|
||||
%%%
|
||||
%%% Покрывает эндпоинты:
|
||||
%%% GET /v1/user/reviews
|
||||
%%%
|
||||
%%% Проверяет:
|
||||
%%% - получение списка отзывов текущего пользователя
|
||||
%%% - наличие созданного отзыва в ответе
|
||||
%%% - ошибку 401 при отсутствии токена
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(user_my_reviews_tests).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-export([test/0]).
|
||||
|
||||
%%%===================================================================
|
||||
%%% Главная тестовая функция
|
||||
%%%===================================================================
|
||||
|
||||
-spec test() -> ok.
|
||||
test() ->
|
||||
ct:pal("=== User My Reviews Tests ==="),
|
||||
OwnerToken = api_test_runner:get_user_token(),
|
||||
ParticipantEmail = api_test_runner:unique_email(<<"myreviewer">>),
|
||||
ParticipantToken = api_test_runner:register_and_login(ParticipantEmail, <<"pass">>),
|
||||
|
||||
% Создаём календарь и событие
|
||||
CalId = api_test_runner:create_calendar(OwnerToken, #{title => <<"MyRevTest">>}),
|
||||
#{<<"id">> := EventId} = api_test_runner:client_post(
|
||||
<<"/v1/calendars/", CalId/binary, "/events">>, OwnerToken,
|
||||
#{title => <<"Event for my review">>,
|
||||
start_time => <<"2026-06-01T10:00:00Z">>,
|
||||
duration => 60}),
|
||||
|
||||
% Бронируем, подтверждаем, оставляем отзыв
|
||||
#{<<"id">> := BookingId} = api_test_runner:client_post(
|
||||
<<"/v1/events/", EventId/binary, "/bookings">>, ParticipantToken, #{}),
|
||||
api_test_runner:client_put(<<"/v1/bookings/", BookingId/binary>>, OwnerToken,
|
||||
#{action => <<"confirm">>}),
|
||||
#{<<"id">> := ReviewId} = api_test_runner:client_post(
|
||||
<<"/v1/reviews">>, ParticipantToken,
|
||||
#{target_type => <<"event">>,
|
||||
target_id => EventId,
|
||||
rating => 4,
|
||||
comment => <<"Nice event!">>}),
|
||||
|
||||
test_get_my_reviews(ParticipantToken, ReviewId),
|
||||
test_get_my_reviews_unauthorized(),
|
||||
|
||||
ct:pal("=== All user my reviews tests passed ==="),
|
||||
ok.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Тестовые функции
|
||||
%%%===================================================================
|
||||
|
||||
%% @doc Успешное получение своих отзывов: 200 OK, содержит отзыв.
|
||||
-spec test_get_my_reviews(binary(), binary()) -> ok.
|
||||
test_get_my_reviews(Token, ExpectedReviewId) ->
|
||||
ct:pal(" TEST: Get my reviews"),
|
||||
Reviews = api_test_runner:client_get(<<"/v1/user/reviews">>, Token),
|
||||
?assert(is_list(Reviews)),
|
||||
?assert(length(Reviews) >= 1),
|
||||
?assert(lists:any(fun(R) -> maps:get(<<"id">>, R) =:= ExpectedReviewId end, Reviews)),
|
||||
ct:pal(" OK: review ~s found in list", [ExpectedReviewId]).
|
||||
|
||||
%% @doc Запрос без токена: 401 Unauthorized.
|
||||
-spec test_get_my_reviews_unauthorized() -> ok.
|
||||
test_get_my_reviews_unauthorized() ->
|
||||
ct:pal(" TEST: Get my reviews without token"),
|
||||
Resp = api_test_runner:client_request(get, <<"/v1/user/reviews">>, <<>>),
|
||||
?assertMatch({ok, 401, _, _}, Resp),
|
||||
ct:pal(" OK: got 401").
|
||||
99
test/api/users/user_occurrence_cancel_tests.erl
Normal file
99
test/api/users/user_occurrence_cancel_tests.erl
Normal file
@@ -0,0 +1,99 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Тесты клиентского API для отмены вхождения повторяющегося события.
|
||||
%%%
|
||||
%%% Покрывает эндпоинты:
|
||||
%%% DELETE /v1/events/:id/occurrences/:start_time
|
||||
%%%
|
||||
%%% Проверяет:
|
||||
%%% - успешную отмену конкретного вхождения
|
||||
%%% - ошибку 400 для не-recurring события
|
||||
%%% - ошибку 403 при попытке отмены чужим пользователем
|
||||
%%% - ошибку 401 без токена
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(user_occurrence_cancel_tests).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-export([test/0]).
|
||||
|
||||
%%%===================================================================
|
||||
%%% Главная тестовая функция
|
||||
%%%===================================================================
|
||||
|
||||
-spec test() -> ok.
|
||||
test() ->
|
||||
ct:pal("=== User Occurrence Cancel Tests ==="),
|
||||
OwnerToken = api_test_runner:get_user_token(),
|
||||
OtherToken = api_test_runner:register_and_login(
|
||||
api_test_runner:unique_email(<<"other">>), <<"pass">>),
|
||||
|
||||
% Создаём календарь и повторяющееся событие
|
||||
CalId = api_test_runner:create_calendar(OwnerToken, #{title => <<"OccCancel">>}),
|
||||
#{<<"id">> := RecurringId} = api_test_runner:client_post(
|
||||
<<"/v1/calendars/", CalId/binary, "/events">>, OwnerToken,
|
||||
#{title => <<"Weekly Standup">>,
|
||||
start_time => <<"2026-06-01T10:00:00Z">>,
|
||||
duration => 30,
|
||||
recurrence => #{freq => <<"WEEKLY">>, interval => 1}}),
|
||||
|
||||
% Получаем вхождения (ответ – список карт)
|
||||
OccPath = <<"/v1/events/", RecurringId/binary, "/occurrences?from=2026-06-01T00:00:00Z&to=2026-06-30T00:00:00Z">>,
|
||||
Occurrences = api_test_runner:client_get(OccPath, OwnerToken),
|
||||
?assert(is_list(Occurrences)),
|
||||
?assert(length(Occurrences) >= 1),
|
||||
#{<<"start_time">> := FirstStart} = hd(Occurrences),
|
||||
|
||||
test_cancel_occurrence(OwnerToken, RecurringId, FirstStart),
|
||||
test_cancel_occurrence_on_single_event(OwnerToken, CalId),
|
||||
test_cancel_occurrence_forbidden(OtherToken, RecurringId, FirstStart),
|
||||
test_cancel_occurrence_unauthorized(RecurringId, FirstStart),
|
||||
|
||||
ct:pal("=== All user occurrence cancel tests passed ==="),
|
||||
ok.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Тестовые функции
|
||||
%%%===================================================================
|
||||
|
||||
%% @doc Успешная отмена вхождения: 200 OK.
|
||||
-spec test_cancel_occurrence(binary(), binary(), binary()) -> ok.
|
||||
test_cancel_occurrence(Token, EventId, StartTime) ->
|
||||
ct:pal(" TEST: Cancel occurrence"),
|
||||
Path = <<"/v1/events/", EventId/binary, "/occurrences/", StartTime/binary>>,
|
||||
Resp = api_test_runner:client_request(delete, Path, Token),
|
||||
{ok, 200, _, Body} = Resp,
|
||||
#{<<"status">> := Status} = jsx:decode(list_to_binary(Body), [return_maps]),
|
||||
?assertEqual(<<"cancelled">>, Status),
|
||||
ct:pal(" OK: occurrence cancelled").
|
||||
|
||||
%% @doc Попытка отменить вхождение для одиночного события: 400.
|
||||
-spec test_cancel_occurrence_on_single_event(binary(), binary()) -> ok.
|
||||
test_cancel_occurrence_on_single_event(Token, CalId) ->
|
||||
ct:pal(" TEST: Cancel occurrence on non-recurring event"),
|
||||
#{<<"id">> := SingleId} = api_test_runner:client_post(
|
||||
<<"/v1/calendars/", CalId/binary, "/events">>, Token,
|
||||
#{title => <<"Single">>,
|
||||
start_time => <<"2026-06-02T10:00:00Z">>,
|
||||
duration => 30}),
|
||||
Path = <<"/v1/events/", SingleId/binary, "/occurrences/2026-06-02T10:00:00Z">>,
|
||||
Resp = api_test_runner:client_request(delete, Path, Token),
|
||||
?assertMatch({ok, 400, _, _}, Resp),
|
||||
ct:pal(" OK: got 400").
|
||||
|
||||
%% @doc Попытка отмены чужим пользователем: 403.
|
||||
-spec test_cancel_occurrence_forbidden(binary(), binary(), binary()) -> ok.
|
||||
test_cancel_occurrence_forbidden(OtherToken, EventId, StartTime) ->
|
||||
ct:pal(" TEST: Cancel occurrence by non-owner"),
|
||||
Path = <<"/v1/events/", EventId/binary, "/occurrences/", StartTime/binary>>,
|
||||
Resp = api_test_runner:client_request(delete, Path, OtherToken),
|
||||
?assertMatch({ok, 403, _, _}, Resp),
|
||||
ct:pal(" OK: got 403").
|
||||
|
||||
%% @doc Запрос без токена: 401.
|
||||
-spec test_cancel_occurrence_unauthorized(binary(), binary()) -> ok.
|
||||
test_cancel_occurrence_unauthorized(EventId, StartTime) ->
|
||||
ct:pal(" TEST: Cancel occurrence without token"),
|
||||
Path = <<"/v1/events/", EventId/binary, "/occurrences/", StartTime/binary>>,
|
||||
Resp = api_test_runner:client_request(delete, Path, <<>>),
|
||||
?assertMatch({ok, 401, _, _}, Resp),
|
||||
ct:pal(" OK: got 401").
|
||||
88
test/api/users/user_refresh_tests.erl
Normal file
88
test/api/users/user_refresh_tests.erl
Normal file
@@ -0,0 +1,88 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Тесты клиентского API для обновления токена.
|
||||
%%%
|
||||
%%% Покрывает эндпоинты:
|
||||
%%% POST /v1/refresh
|
||||
%%%
|
||||
%%% Проверяет:
|
||||
%%% - успешное обновление токена по валидному refresh_token
|
||||
%%% - ошибку 401 при невалидном refresh_token
|
||||
%%% - ошибку 400 при отсутствии refresh_token в теле запроса
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(user_refresh_tests).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-export([test/0]).
|
||||
|
||||
%%%===================================================================
|
||||
%%% Главная тестовая функция
|
||||
%%%===================================================================
|
||||
|
||||
-spec test() -> ok.
|
||||
test() ->
|
||||
ct:pal("=== User Refresh Tests ==="),
|
||||
Token = api_test_runner:get_user_token(),
|
||||
|
||||
% Получаем refresh_token через логин (или регистрацию)
|
||||
Email = api_test_runner:unique_email(<<"refresh">>),
|
||||
Password = <<"StrongPass1!">>,
|
||||
#{<<"refresh_token">> := RefreshToken} = register_and_get_refresh(Email, Password),
|
||||
|
||||
test_successful_refresh(RefreshToken),
|
||||
test_invalid_refresh_token(),
|
||||
test_missing_refresh_token(),
|
||||
|
||||
ct:pal("=== All user refresh tests passed ==="),
|
||||
ok.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Тестовые функции
|
||||
%%%===================================================================
|
||||
|
||||
%% @doc Успешное обновление: 200 OK, возвращает новую пару токенов.
|
||||
-spec test_successful_refresh(binary()) -> ok.
|
||||
test_successful_refresh(RefreshToken) ->
|
||||
ct:pal(" TEST: Successful token refresh"),
|
||||
Resp = api_test_runner:client_request(post, <<"/v1/refresh">>, <<>>,
|
||||
jsx:encode(#{refresh_token => RefreshToken})),
|
||||
{ok, 200, _, Body} = Resp,
|
||||
#{<<"token">> := NewToken, <<"refresh_token">> := NewRefresh} =
|
||||
jsx:decode(list_to_binary(Body), [return_maps]),
|
||||
?assert(is_binary(NewToken)),
|
||||
?assert(is_binary(NewRefresh)),
|
||||
?assertNotEqual(RefreshToken, NewRefresh),
|
||||
ct:pal(" OK: got new token pair").
|
||||
|
||||
%% @doc Невалидный refresh_token: 401 Unauthorized.
|
||||
-spec test_invalid_refresh_token() -> ok.
|
||||
test_invalid_refresh_token() ->
|
||||
ct:pal(" TEST: Invalid refresh token"),
|
||||
Resp = api_test_runner:client_request(post, <<"/v1/refresh">>, <<>>,
|
||||
jsx:encode(#{refresh_token => <<"invalid_token_here">>})),
|
||||
{ok, 401, _, _} = Resp,
|
||||
ct:pal(" OK: got 401").
|
||||
|
||||
%% @doc Отсутствие refresh_token в теле: 400 Bad Request.
|
||||
-spec test_missing_refresh_token() -> ok.
|
||||
test_missing_refresh_token() ->
|
||||
ct:pal(" TEST: Missing refresh_token field"),
|
||||
Resp = api_test_runner:client_request(post, <<"/v1/refresh">>, <<>>,
|
||||
jsx:encode(#{})),
|
||||
?assertMatch({ok, 400, _, _}, Resp),
|
||||
ct:pal(" OK: got 400").
|
||||
|
||||
%%%===================================================================
|
||||
%%% Вспомогательные функции
|
||||
%%%===================================================================
|
||||
|
||||
%% @doc Регистрирует пользователя, выполняет логин и возвращает refresh_token.
|
||||
-spec register_and_get_refresh(binary(), binary()) -> map().
|
||||
register_and_get_refresh(Email, Password) ->
|
||||
% Регистрируем
|
||||
_ = api_test_runner:client_request(post, <<"/v1/register">>, <<>>,
|
||||
jsx:encode(#{email => Email, password => Password})),
|
||||
% Логинимся, чтобы получить refresh_token
|
||||
{ok, 200, _, Body} = api_test_runner:client_request(post, <<"/v1/login">>, <<>>,
|
||||
jsx:encode(#{email => Email, password => Password})),
|
||||
jsx:decode(list_to_binary(Body), [return_maps]).
|
||||
76
test/api/users/user_register_tests.erl
Normal file
76
test/api/users/user_register_tests.erl
Normal file
@@ -0,0 +1,76 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Тесты клиентского API для регистрации пользователей.
|
||||
%%%
|
||||
%%% Покрывает эндпоинты:
|
||||
%%% POST /v1/register
|
||||
%%%
|
||||
%%% Проверяет:
|
||||
%%% - успешную регистрацию нового пользователя
|
||||
%%% - возврат JWT токена и данных пользователя
|
||||
%%% - ошибку при повторной регистрации с тем же email
|
||||
%%% - ошибку при отсутствии обязательных полей
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(user_register_tests).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-export([test/0]).
|
||||
|
||||
%%%===================================================================
|
||||
%%% Главная тестовая функция
|
||||
%%%===================================================================
|
||||
|
||||
-spec test() -> ok.
|
||||
test() ->
|
||||
ct:pal("=== Client Register Tests ==="),
|
||||
Email = api_test_runner:unique_email(<<"register">>),
|
||||
Password = <<"StrongPass1!">>,
|
||||
|
||||
test_successful_register(Email, Password),
|
||||
test_duplicate_register(Email, Password),
|
||||
test_missing_fields(),
|
||||
|
||||
ct:pal("=== All client register tests passed ==="),
|
||||
ok.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Тестовые функции
|
||||
%%%===================================================================
|
||||
|
||||
%% @doc Успешная регистрация: 201 Created, возвращает токен и пользователя.
|
||||
-spec test_successful_register(binary(), binary()) -> ok.
|
||||
test_successful_register(Email, Password) ->
|
||||
ct:pal(" TEST: Successful registration"),
|
||||
Resp = api_test_runner:client_request(post, <<"/v1/register">>, <<>>,
|
||||
jsx:encode(#{email => Email, password => Password})),
|
||||
{ok, 201, _, Body} = Resp,
|
||||
#{<<"token">> := Token, <<"user">> := User} = jsx:decode(list_to_binary(Body), [return_maps]),
|
||||
?assert(is_binary(Token)),
|
||||
?assert(maps:is_key(<<"id">>, User)),
|
||||
?assertEqual(Email, maps:get(<<"email">>, User)),
|
||||
ct:pal(" OK: user ~s created", [maps:get(<<"id">>, User)]).
|
||||
|
||||
%% @doc Повторная регистрация с тем же email: 409 Conflict.
|
||||
-spec test_duplicate_register(binary(), binary()) -> ok.
|
||||
test_duplicate_register(Email, Password) ->
|
||||
ct:pal(" TEST: Duplicate registration"),
|
||||
Resp = api_test_runner:client_request(post, <<"/v1/register">>, <<>>,
|
||||
jsx:encode(#{email => Email, password => Password})),
|
||||
{ok, 409, _, Body} = Resp,
|
||||
#{<<"error">> := ErrorMsg} = jsx:decode(list_to_binary(Body), [return_maps]),
|
||||
?assertEqual(<<"Email already exists">>, ErrorMsg),
|
||||
ct:pal(" OK: got 409 conflict").
|
||||
|
||||
%% @doc Отсутствие обязательных полей: 400 Bad Request.
|
||||
-spec test_missing_fields() -> ok.
|
||||
test_missing_fields() ->
|
||||
ct:pal(" TEST: Missing required fields"),
|
||||
Resp1 = api_test_runner:client_request(post, <<"/v1/register">>, <<>>,
|
||||
jsx:encode(#{email => <<"missing@test.local">>})),
|
||||
?assertMatch({ok, 400, _, _}, Resp1),
|
||||
|
||||
Resp2 = api_test_runner:client_request(post, <<"/v1/register">>, <<>>,
|
||||
jsx:encode(#{password => <<"NoEmail1">>})),
|
||||
?assertMatch({ok, 400, _, _}, Resp2),
|
||||
|
||||
ct:pal(" OK: 400 on missing fields").
|
||||
101
test/api/users/user_reports_tests.erl
Normal file
101
test/api/users/user_reports_tests.erl
Normal file
@@ -0,0 +1,101 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Тесты клиентского API для работы с жалобами.
|
||||
%%%
|
||||
%%% Покрывает эндпоинты:
|
||||
%%% POST /v1/reports
|
||||
%%% GET /v1/reports (требует прав администратора)
|
||||
%%%
|
||||
%%% Проверяет:
|
||||
%%% - успешное создание жалобы (201 Created)
|
||||
%%% - ошибку 400 при отсутствии обязательных полей
|
||||
%%% - ошибку 401 при создании без токена
|
||||
%%% - ошибку 403 при попытке получения списка обычным пользователем
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(user_reports_tests).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-export([test/0]).
|
||||
|
||||
%%%===================================================================
|
||||
%%% Главная тестовая функция
|
||||
%%%===================================================================
|
||||
|
||||
-spec test() -> ok.
|
||||
test() ->
|
||||
ct:pal("=== User Reports Tests ==="),
|
||||
UserToken = api_test_runner:get_user_token(),
|
||||
|
||||
% Создаём событие, на которое можно пожаловаться
|
||||
CalId = api_test_runner:create_calendar(UserToken, #{title => <<"ReportTest">>}),
|
||||
#{<<"id">> := EventId} = api_test_runner:client_post(
|
||||
<<"/v1/calendars/", CalId/binary, "/events">>, UserToken,
|
||||
#{title => <<"Event to report">>,
|
||||
start_time => <<"2026-06-01T10:00:00Z">>,
|
||||
duration => 60}),
|
||||
|
||||
test_create_report(UserToken, EventId),
|
||||
test_create_report_missing_fields(UserToken),
|
||||
test_create_report_unauthorized(EventId),
|
||||
test_list_reports_forbidden(UserToken),
|
||||
|
||||
ct:pal("=== All user reports tests passed ==="),
|
||||
ok.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Тестовые функции
|
||||
%%%===================================================================
|
||||
|
||||
%% @doc Успешное создание жалобы: 201 Created.
|
||||
-spec test_create_report(binary(), binary()) -> ok.
|
||||
test_create_report(Token, EventId) ->
|
||||
ct:pal(" TEST: Create a report"),
|
||||
Resp = api_test_runner:client_request(post, <<"/v1/reports">>, Token,
|
||||
jsx:encode(#{
|
||||
target_type => <<"event">>,
|
||||
target_id => EventId,
|
||||
reason => <<"Inappropriate content">>
|
||||
})),
|
||||
{ok, 201, _, Body} = Resp,
|
||||
#{<<"id">> := ReportId, <<"status">> := Status} = jsx:decode(list_to_binary(Body), [return_maps]),
|
||||
?assert(is_binary(ReportId)),
|
||||
?assertEqual(<<"pending">>, Status),
|
||||
ct:pal(" OK: report ~s created", [ReportId]).
|
||||
|
||||
%% @doc Отсутствие обязательных полей: 400 Bad Request.
|
||||
-spec test_create_report_missing_fields(binary()) -> ok.
|
||||
test_create_report_missing_fields(Token) ->
|
||||
ct:pal(" TEST: Create report with missing fields"),
|
||||
Resp1 = api_test_runner:client_request(post, <<"/v1/reports">>, Token,
|
||||
jsx:encode(#{target_id => <<"id">>, reason => <<"text">>})),
|
||||
?assertMatch({ok, 400, _, _}, Resp1),
|
||||
|
||||
Resp2 = api_test_runner:client_request(post, <<"/v1/reports">>, Token,
|
||||
jsx:encode(#{target_type => <<"event">>, reason => <<"text">>})),
|
||||
?assertMatch({ok, 400, _, _}, Resp2),
|
||||
|
||||
Resp3 = api_test_runner:client_request(post, <<"/v1/reports">>, Token,
|
||||
jsx:encode(#{target_type => <<"event">>, target_id => <<"id">>})),
|
||||
?assertMatch({ok, 400, _, _}, Resp3),
|
||||
ct:pal(" OK: got 400").
|
||||
|
||||
%% @doc Создание жалобы без токена: 401 Unauthorized.
|
||||
-spec test_create_report_unauthorized(binary()) -> ok.
|
||||
test_create_report_unauthorized(EventId) ->
|
||||
ct:pal(" TEST: Create report without token"),
|
||||
Resp = api_test_runner:client_request(post, <<"/v1/reports">>, <<>>,
|
||||
jsx:encode(#{
|
||||
target_type => <<"event">>,
|
||||
target_id => EventId,
|
||||
reason => <<"test">>
|
||||
})),
|
||||
?assertMatch({ok, 401, _, _}, Resp),
|
||||
ct:pal(" OK: got 401").
|
||||
|
||||
%% @doc GET /v1/reports для обычного пользователя должен вернуть 403.
|
||||
-spec test_list_reports_forbidden(binary()) -> ok.
|
||||
test_list_reports_forbidden(Token) ->
|
||||
ct:pal(" TEST: List reports as regular user"),
|
||||
Resp = api_test_runner:client_request(get, <<"/v1/reports">>, Token),
|
||||
?assertMatch({ok, 403, _, _}, Resp),
|
||||
ct:pal(" OK: got 403").
|
||||
126
test/api/users/user_review_by_id_tests.erl
Normal file
126
test/api/users/user_review_by_id_tests.erl
Normal file
@@ -0,0 +1,126 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Тесты клиентского API для работы с конкретным отзывом.
|
||||
%%%
|
||||
%%% Покрывает эндпоинты:
|
||||
%%% GET /v1/reviews/:id
|
||||
%%% PUT /v1/reviews/:id
|
||||
%%% DELETE /v1/reviews/:id
|
||||
%%%
|
||||
%%% Проверяет:
|
||||
%%% - получение отзыва по ID
|
||||
%%% - обновление отзыва (автором)
|
||||
%%% - удаление отзыва (автором)
|
||||
%%% - ошибку 403 при попытке изменения чужим пользователем
|
||||
%%% - ошибку 404 для несуществующего отзыва
|
||||
%%% - ошибку 401 без токена
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(user_review_by_id_tests).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-export([test/0]).
|
||||
|
||||
%%%===================================================================
|
||||
%%% Главная тестовая функция
|
||||
%%%===================================================================
|
||||
|
||||
-spec test() -> ok.
|
||||
test() ->
|
||||
ct:pal("=== User Review By ID Tests ==="),
|
||||
OwnerToken = api_test_runner:get_user_token(),
|
||||
ParticipantEmail = api_test_runner:unique_email(<<"reviewer">>),
|
||||
ParticipantToken = api_test_runner:register_and_login(ParticipantEmail, <<"pass">>),
|
||||
StrangerEmail = api_test_runner:unique_email(<<"stranger">>),
|
||||
StrangerToken = api_test_runner:register_and_login(StrangerEmail, <<"pass">>),
|
||||
|
||||
% Создаём календарь, событие, бронирование и отзыв
|
||||
CalId = api_test_runner:create_calendar(OwnerToken, #{title => <<"RevById">>}),
|
||||
#{<<"id">> := EventId} = api_test_runner:client_post(
|
||||
<<"/v1/calendars/", CalId/binary, "/events">>, OwnerToken,
|
||||
#{title => <<"Event for review">>,
|
||||
start_time => <<"2026-06-01T10:00:00Z">>,
|
||||
duration => 60}),
|
||||
#{<<"id">> := BookingId} = api_test_runner:client_post(
|
||||
<<"/v1/events/", EventId/binary, "/bookings">>, ParticipantToken, #{}),
|
||||
api_test_runner:client_put(<<"/v1/bookings/", BookingId/binary>>, OwnerToken,
|
||||
#{action => <<"confirm">>}),
|
||||
#{<<"id">> := ReviewId} = api_test_runner:client_post(
|
||||
<<"/v1/reviews">>, ParticipantToken,
|
||||
#{target_type => <<"event">>,
|
||||
target_id => EventId,
|
||||
rating => 5,
|
||||
comment => <<"Excellent!">>}),
|
||||
|
||||
test_get_review(ParticipantToken, ReviewId),
|
||||
test_update_review(ParticipantToken, ReviewId),
|
||||
test_update_review_forbidden(StrangerToken, ReviewId),
|
||||
test_delete_review(ParticipantToken, ReviewId),
|
||||
test_get_review_not_found(ParticipantToken),
|
||||
test_get_review_unauthorized(ReviewId),
|
||||
|
||||
ct:pal("=== All user review by id tests passed ==="),
|
||||
ok.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Тестовые функции
|
||||
%%%===================================================================
|
||||
|
||||
%% @doc GET /v1/reviews/:id – получение отзыва.
|
||||
-spec test_get_review(binary(), binary()) -> ok.
|
||||
test_get_review(Token, ReviewId) ->
|
||||
ct:pal(" TEST: Get review by ID"),
|
||||
Path = <<"/v1/reviews/", ReviewId/binary>>,
|
||||
Review = api_test_runner:client_get(Path, Token),
|
||||
?assertEqual(ReviewId, maps:get(<<"id">>, Review)),
|
||||
?assertEqual(<<"Excellent!">>, maps:get(<<"comment">>, Review)),
|
||||
ct:pal(" OK: got review").
|
||||
|
||||
%% @doc PUT /v1/reviews/:id – обновление отзыва автором.
|
||||
-spec test_update_review(binary(), binary()) -> ok.
|
||||
test_update_review(Token, ReviewId) ->
|
||||
ct:pal(" TEST: Update review"),
|
||||
Path = <<"/v1/reviews/", ReviewId/binary>>,
|
||||
Updated = api_test_runner:client_put(Path, Token,
|
||||
#{comment => <<"Updated comment">>, rating => 4}),
|
||||
?assertEqual(<<"Updated comment">>, maps:get(<<"comment">>, Updated)),
|
||||
?assertEqual(4, maps:get(<<"rating">>, Updated)),
|
||||
ct:pal(" OK").
|
||||
|
||||
%% @doc PUT /v1/reviews/:id – попытка обновления чужим пользователем (403).
|
||||
-spec test_update_review_forbidden(binary(), binary()) -> ok.
|
||||
test_update_review_forbidden(StrangerToken, ReviewId) ->
|
||||
ct:pal(" TEST: Update review by non-author"),
|
||||
Path = <<"/v1/reviews/", ReviewId/binary>>,
|
||||
Resp = api_test_runner:client_request(put, Path, StrangerToken,
|
||||
jsx:encode(#{comment => <<"fail">>})),
|
||||
?assertMatch({ok, 403, _, _}, Resp),
|
||||
ct:pal(" OK: got 403").
|
||||
|
||||
%% @doc DELETE /v1/reviews/:id – удаление отзыва автором.
|
||||
-spec test_delete_review(binary(), binary()) -> ok.
|
||||
test_delete_review(Token, ReviewId) ->
|
||||
ct:pal(" TEST: Delete review"),
|
||||
Path = <<"/v1/reviews/", ReviewId/binary>>,
|
||||
% Удаляем
|
||||
{ok, 200, _, _} = api_test_runner:client_request(delete, Path, Token),
|
||||
% Проверяем, что отзыв больше недоступен
|
||||
GetResp = api_test_runner:client_request(get, Path, Token),
|
||||
?assertMatch({ok, 404, _, _}, GetResp),
|
||||
ct:pal(" OK: review deleted").
|
||||
|
||||
%% @doc GET /v1/reviews/:id – несуществующий отзыв (404).
|
||||
-spec test_get_review_not_found(binary()) -> ok.
|
||||
test_get_review_not_found(Token) ->
|
||||
ct:pal(" TEST: Get non-existent review"),
|
||||
Resp = api_test_runner:client_request(get, <<"/v1/reviews/fakeid">>, Token),
|
||||
?assertMatch({ok, 404, _, _}, Resp),
|
||||
ct:pal(" OK: got 404").
|
||||
|
||||
%% @doc GET /v1/reviews/:id – без токена (401).
|
||||
-spec test_get_review_unauthorized(binary()) -> ok.
|
||||
test_get_review_unauthorized(ReviewId) ->
|
||||
ct:pal(" TEST: Get review without token"),
|
||||
Path = <<"/v1/reviews/", ReviewId/binary>>,
|
||||
Resp = api_test_runner:client_request(get, Path, <<>>),
|
||||
?assertMatch({ok, 401, _, _}, Resp),
|
||||
ct:pal(" OK: got 401").
|
||||
114
test/api/users/user_reviews_tests.erl
Normal file
114
test/api/users/user_reviews_tests.erl
Normal file
@@ -0,0 +1,114 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Тесты клиентского API для отзывов.
|
||||
%%%
|
||||
%%% Покрывает эндпоинты:
|
||||
%%% POST /v1/reviews
|
||||
%%% GET /v1/reviews
|
||||
%%%
|
||||
%%% Проверяет:
|
||||
%%% - создание отзыва участником подтверждённого бронирования
|
||||
%%% - ошибку при повторном отзыве на ту же цель
|
||||
%%% - ошибку при отзыве без участия (403)
|
||||
%%% - получение списка отзывов для цели
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(user_reviews_tests).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-export([test/0]).
|
||||
|
||||
%%%===================================================================
|
||||
%%% Главная тестовая функция
|
||||
%%%===================================================================
|
||||
|
||||
-spec test() -> ok.
|
||||
test() ->
|
||||
ct:pal("=== User Reviews Tests ==="),
|
||||
OwnerToken = api_test_runner:get_user_token(),
|
||||
ParticipantEmail = api_test_runner:unique_email(<<"reviewer">>),
|
||||
ParticipantToken = api_test_runner:register_and_login(ParticipantEmail, <<"pass">>),
|
||||
StrangerEmail = api_test_runner:unique_email(<<"stranger">>),
|
||||
StrangerToken = api_test_runner:register_and_login(StrangerEmail, <<"pass">>),
|
||||
|
||||
% Создаём календарь и событие
|
||||
CalId = api_test_runner:create_calendar(OwnerToken, #{title => <<"ReviewTest">>}),
|
||||
#{<<"id">> := EventId} = api_test_runner:client_post(
|
||||
<<"/v1/calendars/", CalId/binary, "/events">>, OwnerToken,
|
||||
#{title => <<"Event for review">>,
|
||||
start_time => <<"2026-06-01T10:00:00Z">>,
|
||||
duration => 60}),
|
||||
|
||||
% Бронируем и подтверждаем участие
|
||||
#{<<"id">> := BookingId} = api_test_runner:client_post(
|
||||
<<"/v1/events/", EventId/binary, "/bookings">>, ParticipantToken, #{}),
|
||||
api_test_runner:client_put(<<"/v1/bookings/", BookingId/binary>>, OwnerToken,
|
||||
#{action => <<"confirm">>}),
|
||||
|
||||
test_create_review(ParticipantToken, EventId),
|
||||
test_duplicate_review(ParticipantToken, EventId),
|
||||
test_review_without_booking(StrangerToken, EventId),
|
||||
test_list_reviews(OwnerToken, EventId),
|
||||
|
||||
ct:pal("=== All user reviews tests passed ==="),
|
||||
ok.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Тестовые функции
|
||||
%%%===================================================================
|
||||
|
||||
%% @doc Успешное создание отзыва: 201 Created.
|
||||
-spec test_create_review(binary(), binary()) -> ok.
|
||||
test_create_review(Token, EventId) ->
|
||||
ct:pal(" TEST: Create a review"),
|
||||
Resp = api_test_runner:client_request(post, <<"/v1/reviews">>, Token,
|
||||
jsx:encode(#{
|
||||
target_type => <<"event">>,
|
||||
target_id => EventId,
|
||||
rating => 5,
|
||||
comment => <<"Excellent!">>
|
||||
})),
|
||||
{ok, 201, _, Body} = Resp,
|
||||
#{<<"id">> := ReviewId, <<"status">> := Status} = jsx:decode(list_to_binary(Body), [return_maps]),
|
||||
?assert(is_binary(ReviewId)),
|
||||
?assertEqual(<<"visible">>, Status),
|
||||
ct:pal(" OK: review ~s created", [ReviewId]).
|
||||
|
||||
%% @doc Повторный отзыв на ту же цель: 409 Conflict.
|
||||
-spec test_duplicate_review(binary(), binary()) -> ok.
|
||||
test_duplicate_review(Token, EventId) ->
|
||||
ct:pal(" TEST: Duplicate review"),
|
||||
Resp = api_test_runner:client_request(post, <<"/v1/reviews">>, Token,
|
||||
jsx:encode(#{
|
||||
target_type => <<"event">>,
|
||||
target_id => EventId,
|
||||
rating => 3,
|
||||
comment => <<"Second try">>
|
||||
})),
|
||||
{ok, 409, _, _} = Resp,
|
||||
ct:pal(" OK: got 409 conflict").
|
||||
|
||||
%% @doc Попытка оставить отзыв без бронирования: 403 Forbidden.
|
||||
-spec test_review_without_booking(binary(), binary()) -> ok.
|
||||
test_review_without_booking(StrangerToken, EventId) ->
|
||||
ct:pal(" TEST: Review without booking"),
|
||||
Resp = api_test_runner:client_request(post, <<"/v1/reviews">>, StrangerToken,
|
||||
jsx:encode(#{
|
||||
target_type => <<"event">>,
|
||||
target_id => EventId,
|
||||
rating => 1,
|
||||
comment => <<"Not allowed">>
|
||||
})),
|
||||
{ok, 403, _, _} = Resp,
|
||||
ct:pal(" OK: got 403 forbidden").
|
||||
|
||||
%% @doc GET /v1/reviews?target_type=event&target_id=... – список отзывов.
|
||||
-spec test_list_reviews(binary(), binary()) -> ok.
|
||||
test_list_reviews(_, EventId) ->
|
||||
ct:pal(" TEST: List reviews for event"),
|
||||
Path = <<"/v1/reviews?target_type=event&target_id=", EventId/binary>>,
|
||||
Reviews = api_test_runner:client_get(Path, api_test_runner:get_user_token()),
|
||||
?assert(is_list(Reviews)),
|
||||
?assert(length(Reviews) >= 1),
|
||||
First = hd(Reviews),
|
||||
?assertEqual(<<"Excellent!">>, maps:get(<<"comment">>, First)),
|
||||
ct:pal(" OK: ~p reviews found", [length(Reviews)]).
|
||||
126
test/api/users/user_search_tests.erl
Normal file
126
test/api/users/user_search_tests.erl
Normal file
@@ -0,0 +1,126 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Тесты клиентского API для поиска.
|
||||
%%%
|
||||
%%% Покрывает эндпоинты:
|
||||
%%% GET /v1/search
|
||||
%%%
|
||||
%%% Проверяет:
|
||||
%%% - полнотекстовый поиск по названиям событий
|
||||
%%% - поиск с фильтрацией по типу (event/calendar)
|
||||
%%% - поиск с фильтрацией по тегам
|
||||
%%% - поиск с фильтрацией по датам (from/to)
|
||||
%%% - геопоиск (lat, lon, radius)
|
||||
%%% - пагинацию результатов
|
||||
%%% - ошибку 401 без токена
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(user_search_tests).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-export([test/0]).
|
||||
|
||||
%%%===================================================================
|
||||
%%% Главная тестовая функция
|
||||
%%%===================================================================
|
||||
|
||||
-spec test() -> ok.
|
||||
test() ->
|
||||
ct:pal("=== User Search Tests ==="),
|
||||
Token = api_test_runner:get_user_token(),
|
||||
|
||||
% Создаём тестовые данные: календарь и несколько событий с тегами
|
||||
CalId = api_test_runner:create_calendar(Token, #{title => <<"SearchTestCal">>}),
|
||||
api_test_runner:client_post(
|
||||
<<"/v1/calendars/", CalId/binary, "/events">>, Token,
|
||||
#{title => <<"Python Workshop">>,
|
||||
start_time => <<"2026-06-01T10:00:00Z">>,
|
||||
duration => 60,
|
||||
tags => [<<"python">>, <<"workshop">>]}),
|
||||
api_test_runner:client_post(
|
||||
<<"/v1/calendars/", CalId/binary, "/events">>, Token,
|
||||
#{title => <<"JavaScript Meetup">>,
|
||||
start_time => <<"2026-06-15T10:00:00Z">>,
|
||||
duration => 60,
|
||||
tags => [<<"javascript">>]}),
|
||||
|
||||
test_basic_search(Token),
|
||||
test_type_filter(Token),
|
||||
test_tag_filter(Token),
|
||||
test_date_filter(Token),
|
||||
test_geo_search(Token),
|
||||
test_search_pagination(Token),
|
||||
test_search_unauthorized(),
|
||||
|
||||
ct:pal("=== All user search tests passed ==="),
|
||||
ok.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Вспомогательная функция
|
||||
%%%===================================================================
|
||||
|
||||
%% @private Извлекает список событий из результата поиска.
|
||||
%% Ожидает ответ вида {"results": {"events": [...], "calendars": [...]}}.
|
||||
-spec extract_events(map()) -> list().
|
||||
extract_events(#{<<"results">> := #{<<"events">> := Events}}) -> Events.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Тестовые функции
|
||||
%%%===================================================================
|
||||
|
||||
test_basic_search(Token) ->
|
||||
ct:pal(" TEST: Basic search"),
|
||||
Events = extract_events(
|
||||
api_test_runner:client_get(<<"/v1/search?q=Python">>, Token)),
|
||||
?assert(is_list(Events)),
|
||||
?assert(length(Events) >= 1),
|
||||
ct:pal(" OK: ~p events found", [length(Events)]).
|
||||
|
||||
test_type_filter(Token) ->
|
||||
ct:pal(" TEST: Search with type filter"),
|
||||
Events = extract_events(
|
||||
api_test_runner:client_get(<<"/v1/search?q=Python&type=event">>, Token)),
|
||||
?assert(is_list(Events)),
|
||||
?assert(length(Events) >= 1),
|
||||
ct:pal(" OK: ~p events", [length(Events)]).
|
||||
|
||||
test_tag_filter(Token) ->
|
||||
ct:pal(" TEST: Search with tag filter"),
|
||||
Events = extract_events(
|
||||
api_test_runner:client_get(<<"/v1/search?tags=python">>, Token)),
|
||||
?assert(is_list(Events)),
|
||||
?assert(length(Events) >= 1),
|
||||
ct:pal(" OK: ~p events", [length(Events)]).
|
||||
|
||||
test_date_filter(Token) ->
|
||||
ct:pal(" TEST: Search with date range"),
|
||||
Events = extract_events(
|
||||
api_test_runner:client_get(
|
||||
<<"/v1/search?from=2026-06-01T00:00:00Z&to=2026-06-15T23:59:59Z">>, Token)),
|
||||
?assert(is_list(Events)),
|
||||
?assert(length(Events) >= 1),
|
||||
ct:pal(" OK: ~p events", [length(Events)]).
|
||||
|
||||
test_geo_search(Token) ->
|
||||
ct:pal(" TEST: Geo search"),
|
||||
Events = extract_events(
|
||||
api_test_runner:client_get(
|
||||
<<"/v1/search?lat=55.75&lon=37.61&radius=1">>, Token)),
|
||||
?assert(is_list(Events)),
|
||||
?assert(length(Events) >= 0),
|
||||
ct:pal(" OK: ~p events", [length(Events)]).
|
||||
|
||||
test_search_pagination(Token) ->
|
||||
ct:pal(" TEST: Search pagination"),
|
||||
Events1 = extract_events(
|
||||
api_test_runner:client_get(<<"/v1/search?limit=1&offset=0">>, Token)),
|
||||
?assertEqual(1, length(Events1)),
|
||||
Events2 = extract_events(
|
||||
api_test_runner:client_get(<<"/v1/search?limit=1&offset=1">>, Token)),
|
||||
?assert(length(Events2) >= 0),
|
||||
ct:pal(" OK").
|
||||
|
||||
test_search_unauthorized() ->
|
||||
ct:pal(" TEST: Search without token"),
|
||||
Resp = api_test_runner:client_request(get, <<"/v1/search?q=test">>, <<>>),
|
||||
?assertMatch({ok, 401, _, _}, Resp),
|
||||
ct:pal(" OK: got 401").
|
||||
78
test/api/users/user_subscription_tests.erl
Normal file
78
test/api/users/user_subscription_tests.erl
Normal file
@@ -0,0 +1,78 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Тесты клиентского API для управления подпиской.
|
||||
%%%
|
||||
%%% Покрывает эндпоинты:
|
||||
%%% GET /v1/subscription
|
||||
%%% POST /v1/subscription
|
||||
%%%
|
||||
%%% Проверяет:
|
||||
%%% - получение информации о подписке (200)
|
||||
%%% - активацию пробного периода (start_trial, 201)
|
||||
%%% - ошибку 409 при повторной активации
|
||||
%%% - ошибку 401 без токена
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(user_subscription_tests).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-export([test/0]).
|
||||
|
||||
%%%===================================================================
|
||||
%%% Главная тестовая функция
|
||||
%%%===================================================================
|
||||
|
||||
-spec test() -> ok.
|
||||
test() ->
|
||||
ct:pal("=== User Subscription Tests ==="),
|
||||
% Создаём уникального пользователя – у него точно нет активной подписки
|
||||
Email = api_test_runner:unique_email(<<"subtest">>),
|
||||
Token = api_test_runner:register_and_login(Email, <<"StrongPass1!">>),
|
||||
|
||||
test_get_subscription(Token),
|
||||
test_start_trial(Token),
|
||||
test_start_trial_duplicate(Token),
|
||||
test_get_subscription_unauthorized(),
|
||||
|
||||
ct:pal("=== All user subscription tests passed ==="),
|
||||
ok.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Тестовые функции
|
||||
%%%===================================================================
|
||||
|
||||
%% @doc GET /v1/subscription – получение подписки (может быть пустой).
|
||||
-spec test_get_subscription(binary()) -> ok.
|
||||
test_get_subscription(Token) ->
|
||||
ct:pal(" TEST: Get subscription"),
|
||||
Sub = api_test_runner:client_get(<<"/v1/subscription">>, Token),
|
||||
?assert(is_map(Sub)),
|
||||
ct:pal(" OK: subscription info received").
|
||||
|
||||
%% @doc POST /v1/subscription – успешная активация пробного периода.
|
||||
-spec test_start_trial(binary()) -> ok.
|
||||
test_start_trial(Token) ->
|
||||
ct:pal(" TEST: Start trial"),
|
||||
Resp = api_test_runner:client_request(post, <<"/v1/subscription">>, Token,
|
||||
jsx:encode(#{action => <<"start_trial">>})),
|
||||
{ok, 201, _, Body} = Resp,
|
||||
#{<<"status">> := Status, <<"plan">> := Plan} = jsx:decode(list_to_binary(Body), [return_maps]),
|
||||
?assertEqual(<<"active">>, Status),
|
||||
?assert(is_binary(Plan)),
|
||||
ct:pal(" OK: trial activated with plan ~s", [Plan]).
|
||||
|
||||
%% @doc POST /v1/subscription – повторная активация (409).
|
||||
-spec test_start_trial_duplicate(binary()) -> ok.
|
||||
test_start_trial_duplicate(Token) ->
|
||||
ct:pal(" TEST: Start trial again (duplicate)"),
|
||||
Resp = api_test_runner:client_request(post, <<"/v1/subscription">>, Token,
|
||||
jsx:encode(#{action => <<"start_trial">>})),
|
||||
?assertMatch({ok, 409, _, _}, Resp),
|
||||
ct:pal(" OK: got 409 conflict").
|
||||
|
||||
%% @doc GET /v1/subscription без токена (401).
|
||||
-spec test_get_subscription_unauthorized() -> ok.
|
||||
test_get_subscription_unauthorized() ->
|
||||
ct:pal(" TEST: Get subscription without token"),
|
||||
Resp = api_test_runner:client_request(get, <<"/v1/subscription">>, <<>>),
|
||||
?assertMatch({ok, 401, _, _}, Resp),
|
||||
ct:pal(" OK: got 401").
|
||||
137
test/api/users/user_tickets_tests.erl
Normal file
137
test/api/users/user_tickets_tests.erl
Normal file
@@ -0,0 +1,137 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Тесты клиентского API для работы с тикетами.
|
||||
%%%
|
||||
%%% Покрывает эндпоинты:
|
||||
%%% POST /v1/tickets
|
||||
%%% GET /v1/tickets
|
||||
%%% GET /v1/tickets/:id
|
||||
%%%
|
||||
%%% Проверяет:
|
||||
%%% - успешное создание тикета
|
||||
%%% - получение списка своих тикетов
|
||||
%%% - получение конкретного тикета по ID
|
||||
%%% - ошибку 400 при отсутствии обязательных полей
|
||||
%%% - ошибку 401 без токена
|
||||
%%% - ошибку 403 при попытке доступа к чужому тикету
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(user_tickets_tests).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-export([test/0]).
|
||||
|
||||
%%%===================================================================
|
||||
%%% Главная тестовая функция
|
||||
%%%===================================================================
|
||||
|
||||
-spec test() -> ok.
|
||||
test() ->
|
||||
ct:pal("=== User Tickets Tests ==="),
|
||||
Token = api_test_runner:get_user_token(),
|
||||
StrangerEmail = api_test_runner:unique_email(<<"stranger">>),
|
||||
StrangerToken = api_test_runner:register_and_login(StrangerEmail, <<"pass">>),
|
||||
|
||||
% Создаём тикет
|
||||
#{<<"id">> := TicketId} = api_test_runner:client_post(<<"/v1/tickets">>, Token,
|
||||
#{error_message => <<"Something broke">>, stacktrace => <<"line 42">>}),
|
||||
|
||||
test_create_ticket(Token),
|
||||
test_create_ticket_missing_fields(Token),
|
||||
test_create_ticket_unauthorized(),
|
||||
test_list_tickets(Token, TicketId),
|
||||
test_list_tickets_unauthorized(),
|
||||
test_get_ticket(Token, TicketId),
|
||||
test_get_ticket_forbidden(StrangerToken, TicketId),
|
||||
test_get_ticket_not_found(Token),
|
||||
test_get_ticket_unauthorized(TicketId),
|
||||
|
||||
ct:pal("=== All user tickets tests passed ==="),
|
||||
ok.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Тестовые функции
|
||||
%%%===================================================================
|
||||
|
||||
%% @doc Успешное создание тикета: 201 Created.
|
||||
-spec test_create_ticket(binary()) -> ok.
|
||||
test_create_ticket(Token) ->
|
||||
ct:pal(" TEST: Create a ticket"),
|
||||
Resp = api_test_runner:client_request(post, <<"/v1/tickets">>, Token,
|
||||
jsx:encode(#{error_message => <<"Test bug">>, stacktrace => <<"trace">>})),
|
||||
{ok, 201, _, Body} = Resp,
|
||||
#{<<"id">> := Id, <<"status">> := Status} = jsx:decode(list_to_binary(Body), [return_maps]),
|
||||
?assert(is_binary(Id)),
|
||||
?assertEqual(<<"open">>, Status),
|
||||
ct:pal(" OK: ticket ~s created", [Id]).
|
||||
|
||||
%% @doc Отсутствие обязательного поля error_message: 400 Bad Request.
|
||||
-spec test_create_ticket_missing_fields(binary()) -> ok.
|
||||
test_create_ticket_missing_fields(Token) ->
|
||||
ct:pal(" TEST: Create ticket without error_message"),
|
||||
Resp = api_test_runner:client_request(post, <<"/v1/tickets">>, Token,
|
||||
jsx:encode(#{stacktrace => <<"trace">>})),
|
||||
?assertMatch({ok, 400, _, _}, Resp),
|
||||
ct:pal(" OK: got 400").
|
||||
|
||||
%% @doc Создание тикета без токена: 401 Unauthorized.
|
||||
-spec test_create_ticket_unauthorized() -> ok.
|
||||
test_create_ticket_unauthorized() ->
|
||||
ct:pal(" TEST: Create ticket without token"),
|
||||
Resp = api_test_runner:client_request(post, <<"/v1/tickets">>, <<>>,
|
||||
jsx:encode(#{error_message => <<"bug">>})),
|
||||
?assertMatch({ok, 401, _, _}, Resp),
|
||||
ct:pal(" OK: got 401").
|
||||
|
||||
%% @doc GET /v1/tickets – получение списка своих тикетов.
|
||||
-spec test_list_tickets(binary(), binary()) -> ok.
|
||||
test_list_tickets(Token, ExpectedTicketId) ->
|
||||
ct:pal(" TEST: List my tickets"),
|
||||
Tickets = api_test_runner:client_get(<<"/v1/tickets">>, Token),
|
||||
?assert(is_list(Tickets)),
|
||||
?assert(length(Tickets) >= 1),
|
||||
?assert(lists:any(fun(T) -> maps:get(<<"id">>, T) =:= ExpectedTicketId end, Tickets)),
|
||||
ct:pal(" OK: my ticket found").
|
||||
|
||||
%% @doc GET /v1/tickets без токена: 401 Unauthorized.
|
||||
-spec test_list_tickets_unauthorized() -> ok.
|
||||
test_list_tickets_unauthorized() ->
|
||||
ct:pal(" TEST: List tickets without token"),
|
||||
Resp = api_test_runner:client_request(get, <<"/v1/tickets">>, <<>>),
|
||||
?assertMatch({ok, 401, _, _}, Resp),
|
||||
ct:pal(" OK: got 401").
|
||||
|
||||
%% @doc GET /v1/tickets/:id – получение своего тикета.
|
||||
-spec test_get_ticket(binary(), binary()) -> ok.
|
||||
test_get_ticket(Token, TicketId) ->
|
||||
ct:pal(" TEST: Get my ticket by ID"),
|
||||
Path = <<"/v1/tickets/", TicketId/binary>>,
|
||||
Ticket = api_test_runner:client_get(Path, Token),
|
||||
?assertEqual(TicketId, maps:get(<<"id">>, Ticket)),
|
||||
?assertEqual(<<"Something broke">>, maps:get(<<"error_message">>, Ticket)),
|
||||
ct:pal(" OK: got my ticket").
|
||||
|
||||
%% @doc GET /v1/tickets/:id – попытка доступа к чужому тикету (403).
|
||||
-spec test_get_ticket_forbidden(binary(), binary()) -> ok.
|
||||
test_get_ticket_forbidden(StrangerToken, TicketId) ->
|
||||
ct:pal(" TEST: Get ticket that is not mine"),
|
||||
Path = <<"/v1/tickets/", TicketId/binary>>,
|
||||
Resp = api_test_runner:client_request(get, Path, StrangerToken),
|
||||
?assertMatch({ok, 403, _, _}, Resp),
|
||||
ct:pal(" OK: got 403").
|
||||
|
||||
%% @doc GET /v1/tickets/:id – несуществующий тикет (404).
|
||||
-spec test_get_ticket_not_found(binary()) -> ok.
|
||||
test_get_ticket_not_found(Token) ->
|
||||
ct:pal(" TEST: Get non-existent ticket"),
|
||||
Resp = api_test_runner:client_request(get, <<"/v1/tickets/fakeid">>, Token),
|
||||
?assertMatch({ok, 404, _, _}, Resp),
|
||||
ct:pal(" OK: got 404").
|
||||
|
||||
%% @doc GET /v1/tickets/:id – без токена (401).
|
||||
-spec test_get_ticket_unauthorized(binary()) -> ok.
|
||||
test_get_ticket_unauthorized(TicketId) ->
|
||||
ct:pal(" TEST: Get ticket without token"),
|
||||
Path = <<"/v1/tickets/", TicketId/binary>>,
|
||||
Resp = api_test_runner:client_request(get, Path, <<>>),
|
||||
?assertMatch({ok, 401, _, _}, Resp),
|
||||
ct:pal(" OK: got 401").
|
||||
255
test/api/users/user_websocket_tests.erl
Normal file
255
test/api/users/user_websocket_tests.erl
Normal file
@@ -0,0 +1,255 @@
|
||||
-module(user_websocket_tests).
|
||||
-export([test/0]).
|
||||
|
||||
test() ->
|
||||
ct:pal("Testing WebSocket API..."),
|
||||
application:ensure_all_started(gun),
|
||||
|
||||
AdminToken = api_test_runner:get_admin_token(),
|
||||
UserToken = api_test_runner:get_user_token(),
|
||||
ct:pal(" AdminToken: ~s...", [binary_part(AdminToken, 0, 30)]),
|
||||
ct:pal(" UserToken: ~s...", [binary_part(UserToken, 0, 30)]),
|
||||
|
||||
% Создаём календарь и событие через новый api_test_runner
|
||||
CalId = api_test_runner:create_calendar(UserToken, #{title => <<"WS Test Calendar">>, type => <<"commercial">>}),
|
||||
ct:pal(" CalId: ~s", [CalId]),
|
||||
|
||||
EventId = api_test_runner:create_event(UserToken, CalId, #{
|
||||
title => <<"WS Test Event">>,
|
||||
start_time => <<"2026-06-01T10:00:00Z">>,
|
||||
duration => 60
|
||||
}),
|
||||
ct:pal(" EventId: ~s", [EventId]),
|
||||
|
||||
WsUrl = api_test_runner:get_base_ws_url() ++ "/ws",
|
||||
AdminWsUrl = api_test_runner:get_admin_ws_url() ++ "/admin/ws",
|
||||
|
||||
%% TEST 1: Connect to WebSocket with valid token
|
||||
ct:pal(" TEST 1: Connect WebSocket with valid token..."),
|
||||
ct:pal(" URL: ~s", [WsUrl]),
|
||||
ct:pal(" Token: ~s...", [binary_part(UserToken, 0, 30)]),
|
||||
case test_ws_connect_debug(WsUrl, UserToken) of
|
||||
{ok, WS} ->
|
||||
ct:pal(" OK - Connected"),
|
||||
|
||||
%% TEST 2: Subscribe to calendar updates
|
||||
ct:pal(" TEST 2: Subscribe to calendar..."),
|
||||
SubMsg = #{action => <<"subscribe">>, calendar_id => CalId},
|
||||
ct:pal(" Sending: ~p", [SubMsg]),
|
||||
ok = test_ws_send(WS, SubMsg),
|
||||
case test_ws_recv(WS) of
|
||||
{ok, #{<<"status">> := <<"subscribed">>}} ->
|
||||
ct:pal(" OK - Subscribed");
|
||||
{ok, Other} ->
|
||||
ct:pal(" ERROR: Unexpected response: ~p", [Other]),
|
||||
error({unexpected_response, Other});
|
||||
{error, timeout} ->
|
||||
ct:pal(" ERROR: Timeout waiting for response"),
|
||||
error(timeout)
|
||||
end,
|
||||
|
||||
test_ws_close(WS);
|
||||
{error, Reason} ->
|
||||
ct:pal(" ERROR: ~p", [Reason]),
|
||||
error({websocket_connect_failed, Reason})
|
||||
end,
|
||||
|
||||
ct:pal("~n✅ WebSocket API tests passed!"),
|
||||
|
||||
%% ============ ТЕСТЫ АДМИНСКОГО WEBSOCKET ============
|
||||
ct:pal("~n=== ADMIN WEBSOCKET TESTS ==="),
|
||||
|
||||
%% TEST 6: Admin WebSocket connection
|
||||
ct:pal(" TEST 6: Admin WebSocket connect..."),
|
||||
{ok, AdminWS} = test_ws_connect_debug(AdminWsUrl, AdminToken),
|
||||
ct:pal(" OK - Admin connected"),
|
||||
|
||||
%% TEST 7: Admin subscribe to reports channel
|
||||
ct:pal(" TEST 7: Admin subscribe to reports channel..."),
|
||||
ok = test_ws_send(AdminWS, #{action => <<"subscribe">>, channel => <<"reports">>}),
|
||||
{ok, #{<<"status">> := <<"subscribed">>}} = test_ws_recv(AdminWS),
|
||||
ct:pal(" OK - Subscribed to reports"),
|
||||
|
||||
%% TEST 8: Admin subscribe to tickets channel
|
||||
ct:pal(" TEST 8: Admin subscribe to tickets channel..."),
|
||||
ok = test_ws_send(AdminWS, #{action => <<"subscribe">>, channel => <<"tickets">>}),
|
||||
{ok, #{<<"status">> := <<"subscribed">>}} = test_ws_recv(AdminWS),
|
||||
ct:pal(" OK - Subscribed to tickets"),
|
||||
|
||||
%% TEST 9: Admin receives report notification
|
||||
ct:pal(" TEST 9: Admin receives report notification..."),
|
||||
api_test_runner:client_post(<<"/v1/reports">>, UserToken,
|
||||
#{target_type => <<"event">>, target_id => EventId, reason => <<"Test report">>}),
|
||||
{ok, #{<<"type">> := <<"report_created">>}} = test_ws_recv(AdminWS, 5000),
|
||||
ct:pal(" OK - Received report notification"),
|
||||
|
||||
%% TEST 10: Admin Ping/Pong
|
||||
ct:pal(" TEST 10: Admin Ping/Pong..."),
|
||||
ok = test_ws_send(AdminWS, #{action => <<"ping">>}),
|
||||
{ok, #{<<"status">> := <<"pong">>}} = test_ws_recv(AdminWS),
|
||||
ct:pal(" OK - Admin Ping/Pong"),
|
||||
|
||||
%% TEST 11: Admin unsubscribe
|
||||
ct:pal(" TEST 11: Admin unsubscribe from reports..."),
|
||||
ok = test_ws_send(AdminWS, #{action => <<"unsubscribe">>, channel => <<"reports">>}),
|
||||
{ok, #{<<"status">> := <<"unsubscribed">>}} = test_ws_recv(AdminWS),
|
||||
ct:pal(" OK - Unsubscribed"),
|
||||
|
||||
test_ws_close(AdminWS),
|
||||
|
||||
%% TEST 12: Admin WebSocket with user token (should fail)
|
||||
ct:pal(" TEST 12: Admin WS with user token..."),
|
||||
{error, {403, _}} = test_ws_connect_debug(AdminWsUrl, UserToken),
|
||||
ct:pal(" OK - Rejected"),
|
||||
|
||||
%% TEST 13: Admin WebSocket with invalid token
|
||||
ct:pal(" TEST 13: Admin WS with invalid token..."),
|
||||
Chars = <<"abcdefghijklmnopqrstuvwxyz0123456789">>,
|
||||
InvalidToken = << <<(binary:at(Chars, rand:uniform(byte_size(Chars)) - 1))>>
|
||||
|| _ <- lists:seq(1, 30) >>,
|
||||
{error, {401, _}} = test_ws_connect_debug(AdminWsUrl, InvalidToken),
|
||||
ct:pal(" OK - Rejected"),
|
||||
|
||||
ct:pal("~n✅ Admin WebSocket API tests passed!"),
|
||||
{?MODULE, ok}.
|
||||
|
||||
%% ============ WebSocket хелперы с отладкой ============
|
||||
test_ws_connect_debug(Url, Token) ->
|
||||
Path = case string:split(Url, "://", trailing) of
|
||||
[_, Rest] ->
|
||||
case string:split(Rest, "/", leading) of
|
||||
[_HostPort, WsPath] ->
|
||||
"/" ++ WsPath ++ "?token=" ++ binary_to_list(Token);
|
||||
_ ->
|
||||
"/ws?token=" ++ binary_to_list(Token)
|
||||
end;
|
||||
_ ->
|
||||
"/ws?token=" ++ binary_to_list(Token)
|
||||
end,
|
||||
{ok, Port} = extract_port(Url),
|
||||
{ok, Host} = extract_host(Url),
|
||||
Opts = case Port of
|
||||
443 -> #{protocols => [http],
|
||||
transport => tls,
|
||||
tls_opts => [{verify, verify_none}]};
|
||||
_ -> #{protocols => [http]}
|
||||
end,
|
||||
ct:pal(" Host: ~s", [Host]),
|
||||
ct:pal(" Port: ~p", [Port]),
|
||||
ct:pal(" Path: ~s", [Path]),
|
||||
{ok, ConnPid} = gun:open(Host, Port, Opts),
|
||||
{ok, http} = gun:await_up(ConnPid, 5000),
|
||||
Headers = [{<<"host">>, list_to_binary(Host ++ ":" ++ integer_to_list(Port))}],
|
||||
StreamRef = gun:ws_upgrade(ConnPid, Path, Headers),
|
||||
receive
|
||||
{gun_upgrade, ConnPid, StreamRef, [<<"websocket">>], _} ->
|
||||
ct:pal(" WebSocket upgrade OK"),
|
||||
{ok, ConnPid};
|
||||
{gun_response, ConnPid, StreamRef, fin, 401, _} ->
|
||||
ct:pal(" ERROR: HTTP 401 Unauthorized"),
|
||||
gun:close(ConnPid),
|
||||
{error, {401, <<"Invalid token">>}};
|
||||
{gun_response, ConnPid, StreamRef, fin, 403, _} ->
|
||||
ct:pal(" ERROR: HTTP 403 Forbidden"),
|
||||
gun:close(ConnPid),
|
||||
{error, {403, <<"Admin access required">>}};
|
||||
{gun_response, ConnPid, StreamRef, nofin, 403, _} ->
|
||||
ct:pal(" ERROR: HTTP 403 Forbidden (nofin)"),
|
||||
gun:close(ConnPid),
|
||||
{error, {403, <<"Admin access required">>}};
|
||||
{gun_response, ConnPid, StreamRef, fin, Status, _} ->
|
||||
ct:pal(" ERROR: HTTP ~p", [Status]),
|
||||
gun:close(ConnPid),
|
||||
{error, {Status, <<"WebSocket upgrade failed">>}};
|
||||
{gun_response, ConnPid, StreamRef, nofin, Status, _} ->
|
||||
ct:pal(" ERROR: HTTP ~p (nofin)", [Status]),
|
||||
gun:close(ConnPid),
|
||||
{error, {Status, <<"WebSocket upgrade failed">>}};
|
||||
{gun_error, ConnPid, Reason} ->
|
||||
ct:pal(" ERROR: ~p", [Reason]),
|
||||
gun:close(ConnPid),
|
||||
{error, Reason}
|
||||
after 5000 ->
|
||||
ct:pal(" ERROR: Timeout"),
|
||||
gun:close(ConnPid),
|
||||
{error, timeout}
|
||||
end.
|
||||
|
||||
test_ws_send(ConnPid, Data) ->
|
||||
Msg = jsx:encode(Data),
|
||||
ct:pal(" Sending: ~s", [Msg]),
|
||||
case catch gun:ws_send(ConnPid, {text, Msg}) of
|
||||
ok -> ok;
|
||||
{'EXIT', {undef, _}} ->
|
||||
gun:ws_send(ConnPid, fin, {text, Msg});
|
||||
Other ->
|
||||
ct:pal(" ERROR sending: ~p", [Other]),
|
||||
error({ws_send_failed, Other})
|
||||
end.
|
||||
|
||||
test_ws_recv(ConnPid) ->
|
||||
test_ws_recv(ConnPid, 3000).
|
||||
|
||||
test_ws_recv(ConnPid, Timeout) ->
|
||||
receive
|
||||
{gun_ws, ConnPid, _StreamRef, {text, Msg}} ->
|
||||
ct:pal(" Received (with StreamRef): ~s", [Msg]),
|
||||
{ok, jsx:decode(Msg, [return_maps])};
|
||||
{gun_ws, ConnPid, {text, Msg}} ->
|
||||
ct:pal(" Received: ~s", [Msg]),
|
||||
{ok, jsx:decode(Msg, [return_maps])};
|
||||
{gun_ws, ConnPid, _StreamRef, Frame} ->
|
||||
ct:pal(" Received frame: ~p", [Frame]),
|
||||
{ok, Frame};
|
||||
{gun_ws, ConnPid, Frame} ->
|
||||
ct:pal(" Received: ~p", [Frame]),
|
||||
{ok, Frame};
|
||||
{gun_error, ConnPid, Reason} ->
|
||||
ct:pal(" ERROR: gun_error ~p", [Reason]),
|
||||
{error, Reason};
|
||||
Other ->
|
||||
ct:pal(" Received unexpected: ~p", [Other]),
|
||||
test_ws_recv(ConnPid, Timeout)
|
||||
after Timeout ->
|
||||
{error, timeout}
|
||||
end.
|
||||
|
||||
test_ws_close(ConnPid) ->
|
||||
gun:close(ConnPid).
|
||||
|
||||
%% ========== URL parsing helpers ==========
|
||||
normalize_scheme(S) when is_binary(S) -> S;
|
||||
normalize_scheme(S) when is_list(S) -> list_to_binary(S);
|
||||
normalize_scheme(S) when is_atom(S) -> atom_to_binary(S, utf8);
|
||||
normalize_scheme(_) -> <<"unknown">>.
|
||||
|
||||
extract_host(Url) ->
|
||||
try
|
||||
Parsed = uri_string:parse(Url),
|
||||
#{scheme := SchemeRaw, host := Host} = Parsed,
|
||||
Scheme = normalize_scheme(SchemeRaw),
|
||||
if Scheme =:= <<"ws">>; Scheme =:= <<"wss">> -> ok;
|
||||
true -> throw({invalid_scheme, SchemeRaw})
|
||||
end,
|
||||
HostStr = if is_binary(Host) -> binary_to_list(Host); true -> Host end,
|
||||
{ok, HostStr}
|
||||
catch
|
||||
Class:Reason:Stacktrace ->
|
||||
{error, {parse_error, {Class, Reason}, Stacktrace}}
|
||||
end.
|
||||
|
||||
extract_port(Url) ->
|
||||
try
|
||||
Parsed = uri_string:parse(Url),
|
||||
#{scheme := SchemeRaw} = Parsed,
|
||||
Scheme = normalize_scheme(SchemeRaw),
|
||||
DefaultPort = if Scheme =:= <<"ws">> -> 80; Scheme =:= <<"wss">> -> 443; true -> throw({invalid_scheme, SchemeRaw}) end,
|
||||
case maps:find(port, Parsed) of
|
||||
{ok, P} when is_integer(P) -> {ok, P};
|
||||
{ok, P} -> {ok, try list_to_integer(binary_to_list(normalize_scheme(P))) catch _:_ -> DefaultPort end};
|
||||
error -> {ok, DefaultPort}
|
||||
end
|
||||
catch
|
||||
Class:Reason:Stacktrace ->
|
||||
{error, {parse_error, {Class, Reason}, Stacktrace}}
|
||||
end.
|
||||
Reference in New Issue
Block a user