fix(api): resolve public calendar GET by unique short_name.
CI / test (push) Successful in 7m28s
CI / deploy-ift (push) Successful in 2m46s
CI / e2e-ift (push) Successful in 1m26s
CI / deploy-stage (push) Successful in 2m4s
CI / e2e-stage (push) Successful in 2m4s

This commit is contained in:
2026-08-14 15:00:41 +03:00
parent 658cbda4fe
commit 3d7ae0d372
8 changed files with 67 additions and 18 deletions
+24 -1
View File
@@ -1,6 +1,7 @@
-module(core_calendar). -module(core_calendar).
-include("records.hrl"). -include("records.hrl").
-export([create/4, create/5, get_by_id/1, list_by_owner/1, update/2, delete/1]). -export([create/4, create/5, get_by_id/1, get_by_short_name/1, get_by_id_or_short_name/1,
list_by_owner/1, update/2, delete/1]).
-export([count_calendars/0, list_all/0]). -export([count_calendars/0, list_all/0]).
-export([freeze/2, unfreeze/2]). -export([freeze/2, unfreeze/2]).
-export([count_calendars_by_date/2]). -export([count_calendars_by_date/2]).
@@ -78,6 +79,28 @@ get_by_id(Id) ->
[Calendar] -> {ok, Calendar} [Calendar] -> {ok, Calendar}
end. end.
%% Unique public slug. Empty name is not a slug (many Default calendars).
-spec get_by_short_name(Name :: binary()) -> {ok, #calendar{}} | {error, not_found}.
get_by_short_name(<<>>) ->
{error, not_found};
get_by_short_name(Name) when is_binary(Name) ->
case mnesia:dirty_index_read(calendar, Name, #calendar.short_name) of
[] ->
{error, not_found};
Rows ->
case [C || C <- Rows, C#calendar.status =:= active] of
[C | _] -> {ok, C};
[] -> {error, not_found}
end
end.
-spec get_by_id_or_short_name(IdOrSlug :: binary()) -> {ok, #calendar{}} | {error, not_found}.
get_by_id_or_short_name(IdOrSlug) ->
case get_by_id(IdOrSlug) of
{ok, C} -> {ok, C};
{error, not_found} -> get_by_short_name(IdOrSlug)
end.
%%%------------------------------------------------------------------- %%%-------------------------------------------------------------------
%%% @doc Список активных календарей владельца. %%% @doc Список активных календарей владельца.
%%% @end %%% @end
+2 -1
View File
@@ -121,7 +121,8 @@ get_calendar(Req) ->
case logic_calendar:get_calendar(UserId, CalendarId) of case logic_calendar:get_calendar(UserId, CalendarId) of
{ok, Calendar} -> {ok, Calendar} ->
Json0 = handler_utils:calendar_to_json(Calendar), Json0 = handler_utils:calendar_to_json(Calendar),
Following = UserId =/= <<>> andalso logic_calendar_follow:is_following(UserId, CalendarId), Following = UserId =/= <<>> andalso
logic_calendar_follow:is_following(UserId, Calendar#calendar.id),
handler_utils:send_json(Req1, 200, Json0#{following => Following}); handler_utils:send_json(Req1, 200, Json0#{following => Following});
{error, access_denied} -> {error, access_denied} ->
handler_utils:send_error(Req1, 403, <<"Access denied">>); handler_utils:send_error(Req1, 403, <<"Access denied">>);
+1 -1
View File
@@ -162,7 +162,7 @@ calendar_has_content(CalendarId) ->
%% Получение календаря с проверкой доступа %% Получение календаря с проверкой доступа
get_calendar(UserId, CalendarId) -> get_calendar(UserId, CalendarId) ->
case core_calendar:get_by_id(CalendarId) of case core_calendar:get_by_id_or_short_name(CalendarId) of
{ok, Calendar} -> {ok, Calendar} ->
case can_access(UserId, Calendar) of case can_access(UserId, Calendar) of
true -> {ok, Calendar}; true -> {ok, Calendar};
+5 -6
View File
@@ -15,7 +15,7 @@
-spec follow(UserId :: binary(), CalendarId :: binary()) -> -spec follow(UserId :: binary(), CalendarId :: binary()) ->
{ok, #calendar_follow{}} | {error, term()}. {ok, #calendar_follow{}} | {error, term()}.
follow(UserId, CalendarId) -> follow(UserId, CalendarId) ->
case core_calendar:get_by_id(CalendarId) of case core_calendar:get_by_id_or_short_name(CalendarId) of
{error, not_found} -> {error, not_found} ->
{error, not_found}; {error, not_found};
{ok, #calendar{owner_id = UserId}} -> {ok, #calendar{owner_id = UserId}} ->
@@ -27,7 +27,7 @@ follow(UserId, CalendarId) ->
true -> true ->
case Calendar#calendar.status of case Calendar#calendar.status of
active -> active ->
core_calendar_follow:follow(CalendarId, UserId); core_calendar_follow:follow(Calendar#calendar.id, UserId);
_ -> _ ->
{error, not_found} {error, not_found}
end end
@@ -40,14 +40,13 @@ follow(UserId, CalendarId) ->
%%%------------------------------------------------------------------- %%%-------------------------------------------------------------------
-spec unfollow(UserId :: binary(), CalendarId :: binary()) -> ok | {error, term()}. -spec unfollow(UserId :: binary(), CalendarId :: binary()) -> ok | {error, term()}.
unfollow(UserId, CalendarId) -> unfollow(UserId, CalendarId) ->
case core_calendar:get_by_id(CalendarId) of case core_calendar:get_by_id_or_short_name(CalendarId) of
{error, not_found} -> {error, not_found} ->
%% всё равно снимаем локальный follow, если был
core_calendar_follow:unfollow(CalendarId, UserId); core_calendar_follow:unfollow(CalendarId, UserId);
{ok, #calendar{owner_id = UserId}} -> {ok, #calendar{owner_id = UserId}} ->
{error, own_calendar}; {error, own_calendar};
{ok, _} -> {ok, Calendar} ->
core_calendar_follow:unfollow(CalendarId, UserId) core_calendar_follow:unfollow(Calendar#calendar.id, UserId)
end. end.
%%%------------------------------------------------------------------- %%%-------------------------------------------------------------------
+2 -5
View File
@@ -10,12 +10,9 @@
-spec list(ActorId :: binary(), CalendarId :: binary()) -> -spec list(ActorId :: binary(), CalendarId :: binary()) ->
{ok, [#calendar_specialist{}]} | {error, not_found | access_denied}. {ok, [#calendar_specialist{}]} | {error, not_found | access_denied}.
list(ActorId, CalendarId) -> list(ActorId, CalendarId) ->
case core_calendar:get_by_id(CalendarId) of case logic_calendar:get_calendar(ActorId, CalendarId) of
{ok, Cal} -> {ok, Cal} ->
case logic_calendar:can_access(ActorId, Cal) of {ok, core_calendar_specialist:list_by_calendar(Cal#calendar.id)};
true -> {ok, core_calendar_specialist:list_by_calendar(CalendarId)};
false -> {error, access_denied}
end;
Error -> Error Error -> Error
end. end.
+4 -4
View File
@@ -25,7 +25,7 @@ create_event(UserId, CalendarId, Title, StartTime, Duration, Description) ->
{reject, Words} -> {reject, Words} ->
{error, {content_banned, Words}}; {error, {content_banned, Words}};
{ok, Action, [Title2, Desc2], Words} -> {ok, Action, [Title2, Desc2], Words} ->
case core_event:create(CalendarId, Title2, StartTime, Duration) of case core_event:create(Calendar#calendar.id, Title2, StartTime, Duration) of
{ok, Event} -> {ok, Event} ->
case Desc2 of case Desc2 of
<<>> -> ok; <<>> -> ok;
@@ -64,7 +64,7 @@ create_recurring_event(UserId, CalendarId, Title, StartTime, Duration, RRule, De
{reject, Words} -> {reject, Words} ->
{error, {content_banned, Words}}; {error, {content_banned, Words}};
{ok, Action, [Title2, Desc2], Words} -> {ok, Action, [Title2, Desc2], Words} ->
case core_event:create_recurring(CalendarId, Title2, StartTime, Duration, RRule) of case core_event:create_recurring(Calendar#calendar.id, Title2, StartTime, Duration, RRule) of
{ok, Event} -> {ok, Event} ->
case Desc2 of case Desc2 of
<<>> -> ok; <<>> -> ok;
@@ -184,8 +184,8 @@ get_event(UserId, EventId) ->
%% Список событий календаря %% Список событий календаря
list_events(UserId, CalendarId) -> list_events(UserId, CalendarId) ->
case logic_calendar:get_calendar(UserId, CalendarId) of case logic_calendar:get_calendar(UserId, CalendarId) of
{ok, _} -> {ok, Calendar} ->
core_event:list_by_calendar(CalendarId); core_event:list_by_calendar(Calendar#calendar.id);
Error -> Error ->
Error Error
end. end.
@@ -20,6 +20,7 @@ test() ->
test_get_calendar(Token, CalId), test_get_calendar(Token, CalId),
test_get_calendar_guest(Token, CalId), test_get_calendar_guest(Token, CalId),
test_get_calendar_guest_short_name(Token, CalId),
test_get_calendar_not_found(Token), test_get_calendar_not_found(Token),
test_update_calendar(Token, CalId), test_update_calendar(Token, CalId),
test_update_calendar_settings(Token, CalId), test_update_calendar_settings(Token, CalId),
@@ -49,6 +50,20 @@ test_get_calendar_guest(Token, CalId) ->
{ok, 403, _, _} = api_test_runner:client_request(get, PPath, <<>>), {ok, 403, _, _} = api_test_runner:client_request(get, PPath, <<>>),
ct:pal(" OK: guest commercial 200, personal 403"). ct:pal(" OK: guest commercial 200, personal 403").
test_get_calendar_guest_short_name(Token, CalId) ->
ct:pal(" TEST: Guest GET commercial by short_name"),
PathId = <<"/v1/calendars/", CalId/binary>>,
Slug2 = <<"sn", (integer_to_binary(erlang:unique_integer([positive])))/binary>>,
_ = api_test_runner:client_put(PathId, Token, #{short_name => Slug2}),
PathSlug = <<"/v1/calendars/", Slug2/binary>>,
{ok, 200, _, Body} = api_test_runner:client_request(get, PathSlug, <<>>),
Dec = jsx:decode(list_to_binary(Body), [return_maps]),
?assertEqual(CalId, maps:get(<<"id">>, Dec)),
?assertEqual(Slug2, maps:get(<<"short_name">>, Dec)),
EvPath = <<"/v1/calendars/", Slug2/binary, "/events">>,
{ok, 200, _, _} = api_test_runner:client_request(get, EvPath, <<>>),
ct:pal(" OK: guest GET by short_name").
test_get_calendar_not_found(Token) -> test_get_calendar_not_found(Token) ->
ct:pal(" TEST: Get non-existent calendar (404)"), ct:pal(" TEST: Get non-existent calendar (404)"),
Resp = api_test_runner:client_request(get, <<"/v1/calendars/fakeid">>, Token), Resp = api_test_runner:client_request(get, <<"/v1/calendars/fakeid">>, Token),
+14
View File
@@ -12,6 +12,7 @@ setup() ->
{attributes, record_info(fields, calendar)}, {attributes, record_info(fields, calendar)},
{ram_copies, [node()]} {ram_copies, [node()]}
]), ]),
{atomic, ok} = mnesia:add_table_index(calendar, short_name),
mnesia:create_table(subscription, [ mnesia:create_table(subscription, [
{attributes, record_info(fields, subscription)}, {attributes, record_info(fields, subscription)},
{ram_copies, [node()]} {ram_copies, [node()]}
@@ -38,6 +39,7 @@ logic_calendar_test_() ->
{"Create calendar test", fun test_create_calendar/0}, {"Create calendar test", fun test_create_calendar/0},
{"Create commercial requires subscription", fun test_create_commercial_gate/0}, {"Create commercial requires subscription", fun test_create_commercial_gate/0},
{"Get calendar test", fun test_get_calendar/0}, {"Get calendar test", fun test_get_calendar/0},
{"Get calendar by short_name", fun test_get_calendar_by_short_name/0},
{"List calendars test", fun test_list_calendars/0}, {"List calendars test", fun test_list_calendars/0},
{"Update calendar test", fun test_update_calendar/0}, {"Update calendar test", fun test_update_calendar/0},
{"Persist calendar settings (week_patterns)", fun test_update_settings/0}, {"Persist calendar settings (week_patterns)", fun test_update_settings/0},
@@ -104,6 +106,18 @@ test_get_calendar() ->
?assertMatch({error, access_denied}, ?assertMatch({error, access_denied},
logic_calendar:get_calendar(OtherUserId, Calendar#calendar.id)). logic_calendar:get_calendar(OtherUserId, Calendar#calendar.id)).
test_get_calendar_by_short_name() ->
UserId = create_test_user(),
{ok, _} = logic_subscription:start_trial(UserId),
{ok, Cal} = logic_calendar:create_calendar(UserId, <<"Studio">>, <<>>, manual, commercial),
Slug = <<"bizcal-unit-", (Cal#calendar.id)/binary>>,
{ok, _} = core_calendar:update(Cal#calendar.id, [{short_name, Slug}]),
{ok, Found} = logic_calendar:get_calendar(<<>>, Slug),
?assertEqual(Cal#calendar.id, Found#calendar.id),
{ok, Found2} = logic_calendar:get_calendar(UserId, Slug),
?assertEqual(Cal#calendar.id, Found2#calendar.id),
?assertEqual({error, not_found}, logic_calendar:get_calendar(<<>>, <<"no-such-slug">>)).
test_list_calendars() -> test_list_calendars() ->
UserId = create_test_user(), UserId = create_test_user(),
{ok, _} = logic_calendar:create_calendar(UserId, <<"Calendar 1">>, <<"">>, manual), {ok, _} = logic_calendar:create_calendar(UserId, <<"Calendar 1">>, <<"">>, manual),