feat: calendar follow API (POST/DELETE follow, GET following). Refs EventHub/EventHubFront#9
CI / test (push) Failing after 6m39s
CI / deploy-ift (push) Has been skipped
CI / e2e-ift (push) Has been skipped
CI / deploy-stage (push) Has been skipped
CI / e2e-stage (push) Has been skipped

This commit is contained in:
2026-07-20 22:10:48 +03:00
parent 4932f9abae
commit 59220b955e
16 changed files with 599 additions and 4 deletions
+113
View File
@@ -0,0 +1,113 @@
%%%-------------------------------------------------------------------
%%% @doc Тесты follow / unfollow чужого календаря.
%%%
%%% POST /v1/calendars/:id/follow
%%% DELETE /v1/calendars/:id/follow
%%% GET /v1/user/following
%%% GET /v1/calendars/:id (поле following)
%%% @end
%%%-------------------------------------------------------------------
-module(user_calendar_follow_tests).
-include_lib("eunit/include/eunit.hrl").
-export([test/0]).
-spec test() -> ok.
test() ->
ct:pal("=== User Calendar Follow Tests ==="),
OwnerEmail = api_test_runner:unique_email(<<"flowner">>),
OwnerToken = api_test_runner:register_and_login(OwnerEmail, <<"pass">>),
FollowerEmail = api_test_runner:unique_email(<<"fluser">>),
FollowerToken = api_test_runner:register_and_login(FollowerEmail, <<"pass">>),
%% commercial calendar requires subscription
{ok, 201, _, _} = api_test_runner:client_request(
post, <<"/v1/subscription">>, OwnerToken,
jsx:encode(#{action => <<"start_trial">>})),
CalId = api_test_runner:create_calendar(OwnerToken, #{
title => <<"FollowMe">>,
type => <<"commercial">>
}),
FollowPath = <<"/v1/calendars/", CalId/binary, "/follow">>,
test_follow(FollowerToken, FollowPath, CalId),
test_follow_idempotent(FollowerToken, FollowPath, CalId),
test_get_calendar_following(FollowerToken, CalId, true),
test_list_following(FollowerToken, CalId),
test_own_forbidden(OwnerToken, FollowPath),
test_unfollow(FollowerToken, FollowPath, CalId),
test_unfollow_idempotent(FollowerToken, FollowPath, CalId),
test_get_calendar_following(FollowerToken, CalId, false),
test_list_following_empty(FollowerToken),
test_unauthorized(FollowPath),
ct:pal("=== All user calendar follow tests passed ==="),
ok.
%%%===================================================================
%%% Cases
%%%===================================================================
test_follow(Token, Path, CalId) ->
ct:pal(" TEST: POST follow"),
{ok, 200, _, Body} = api_test_runner:client_request(post, Path, Token, <<"{}">>),
Resp = jsx:decode(list_to_binary(Body), [return_maps]),
?assertEqual(CalId, maps:get(<<"calendar_id">>, Resp)),
?assertEqual(true, maps:get(<<"following">>, Resp)),
ct:pal(" OK").
test_follow_idempotent(Token, Path, CalId) ->
ct:pal(" TEST: POST follow idempotent"),
{ok, 200, _, Body} = api_test_runner:client_request(post, Path, Token, <<"{}">>),
Resp = jsx:decode(list_to_binary(Body), [return_maps]),
?assertEqual(CalId, maps:get(<<"calendar_id">>, Resp)),
?assertEqual(true, maps:get(<<"following">>, Resp)),
ct:pal(" OK").
test_get_calendar_following(Token, CalId, Expected) ->
ct:pal(" TEST: GET calendar following=~p", [Expected]),
Cal = api_test_runner:client_get(<<"/v1/calendars/", CalId/binary>>, Token),
?assertEqual(Expected, maps:get(<<"following">>, Cal)),
ct:pal(" OK").
test_list_following(Token, CalId) ->
ct:pal(" TEST: GET /v1/user/following"),
List = api_test_runner:client_get(<<"/v1/user/following">>, Token),
?assert(is_list(List)),
Match = [C || C <- List, maps:get(<<"id">>, C) =:= CalId],
?assertMatch([_], Match),
[C] = Match,
?assertEqual(true, maps:get(<<"following">>, C)),
ct:pal(" OK").
test_own_forbidden(Token, Path) ->
ct:pal(" TEST: follow own calendar forbidden"),
Resp = api_test_runner:client_request(post, Path, Token, <<"{}">>),
?assertMatch({ok, 403, _, _}, Resp),
ct:pal(" OK").
test_unfollow(Token, Path, CalId) ->
ct:pal(" TEST: DELETE unfollow"),
Resp = api_test_runner:client_delete(Path, Token),
?assertEqual(CalId, maps:get(<<"calendar_id">>, Resp)),
?assertEqual(false, maps:get(<<"following">>, Resp)),
ct:pal(" OK").
test_unfollow_idempotent(Token, Path, CalId) ->
ct:pal(" TEST: DELETE unfollow idempotent"),
Resp = api_test_runner:client_delete(Path, Token),
?assertEqual(CalId, maps:get(<<"calendar_id">>, Resp)),
?assertEqual(false, maps:get(<<"following">>, Resp)),
ct:pal(" OK").
test_list_following_empty(Token) ->
ct:pal(" TEST: GET following empty"),
List = api_test_runner:client_get(<<"/v1/user/following">>, Token),
?assertEqual([], List),
ct:pal(" OK").
test_unauthorized(Path) ->
ct:pal(" TEST: unauthorized"),
Resp = api_test_runner:client_request(post, Path, <<>>, <<"{}">>),
?assertMatch({ok, 401, _, _}, Resp),
ct:pal(" OK").
Regular → Executable
+4
View File
@@ -40,6 +40,7 @@ all() ->
user_test_reviews,
user_test_review_by_id,
user_test_review_vote,
user_test_calendar_follow,
user_test_my_reviews,
user_test_search,
user_test_refresh,
@@ -140,6 +141,9 @@ user_test_review_by_id(_Config) ->
user_test_review_vote(_Config) ->
user_review_vote_tests:test().
user_test_calendar_follow(_Config) ->
user_calendar_follow_tests:test().
user_test_my_reviews(_Config) ->
user_my_reviews_tests:test().
+49
View File
@@ -0,0 +1,49 @@
-module(core_calendar_follow_tests).
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
-define(TABLES, [calendar_follow]).
setup() ->
eh_test_support:start_mnesia(),
eh_test_support:ensure_tables(?TABLES),
ok.
cleanup(_) ->
eh_test_support:delete_tables(?TABLES),
eh_test_support:stop_mnesia(),
ok.
core_calendar_follow_test_() ->
{foreach, fun setup/0, fun cleanup/1, [
{"follow and is_following", fun test_follow/0},
{"follow idempotent", fun test_follow_idempotent/0},
{"unfollow", fun test_unfollow/0},
{"list_by_user", fun test_list_by_user/0}
]}.
test_follow() ->
{ok, F} = core_calendar_follow:follow(<<"cal1">>, <<"u1">>),
?assertEqual(<<"cal1">>, F#calendar_follow.calendar_id),
?assertEqual(<<"u1">>, F#calendar_follow.user_id),
?assert(core_calendar_follow:is_following(<<"u1">>, <<"cal1">>)),
?assertNot(core_calendar_follow:is_following(<<"u2">>, <<"cal1">>)).
test_follow_idempotent() ->
{ok, F1} = core_calendar_follow:follow(<<"cal1">>, <<"u1">>),
{ok, F2} = core_calendar_follow:follow(<<"cal1">>, <<"u1">>),
?assertEqual(F1#calendar_follow.id, F2#calendar_follow.id),
?assertEqual(1, length(core_calendar_follow:list_by_user(<<"u1">>))).
test_unfollow() ->
{ok, _} = core_calendar_follow:follow(<<"cal1">>, <<"u1">>),
ok = core_calendar_follow:unfollow(<<"cal1">>, <<"u1">>),
?assertNot(core_calendar_follow:is_following(<<"u1">>, <<"cal1">>)),
ok = core_calendar_follow:unfollow(<<"cal1">>, <<"u1">>).
test_list_by_user() ->
{ok, _} = core_calendar_follow:follow(<<"cal1">>, <<"u1">>),
{ok, _} = core_calendar_follow:follow(<<"cal2">>, <<"u1">>),
{ok, _} = core_calendar_follow:follow(<<"cal1">>, <<"u2">>),
?assertEqual(2, length(core_calendar_follow:list_by_user(<<"u1">>))),
?assertEqual(2, length(core_calendar_follow:list_by_calendar(<<"cal1">>))).
Regular → Executable
+2
View File
@@ -126,6 +126,8 @@ table_opts(calendar) ->
[{ram_copies, [node()]}, {attributes, record_info(fields, calendar)}];
table_opts(calendar_share) ->
[{ram_copies, [node()]}, {attributes, record_info(fields, calendar_share)}];
table_opts(calendar_follow) ->
[{ram_copies, [node()]}, {attributes, record_info(fields, calendar_follow)}];
table_opts(calendar_specialist) ->
[{ram_copies, [node()]}, {attributes, record_info(fields, calendar_specialist)}];
table_opts(event) ->
+2 -1
View File
@@ -10,7 +10,8 @@
"20260716230000_ticket_source_and_hash_index",
"20260717180000_stats_counters",
"20260717190000_admin_stats_indexes",
"20260719210000_review_vote"
"20260719210000_review_vote",
"20260720210000_calendar_follow"
]).
setup() ->