Files
EventHubBack/test/unit/logic_calendar_tests.erl
T
aleksey 0e4e886cd4
CI / test (push) Successful in 8m35s
CI / deploy-ift (push) Successful in 21m2s
CI / e2e-ift (push) Successful in 2m3s
CI / deploy-stage (push) Successful in 2m9s
CI / e2e-stage (push) Successful in 1m16s
feat(specialists): owner is non-deletable specialist on commercial create
Bootstrap calendar_specialist for owner (active); DELETE owner → 403;
toggle via status. Fixes solo assign-to-slot. Refs EventHub/EventHubBack#66
2026-08-01 12:18:36 +03:00

301 lines
12 KiB
Erlang
Executable File

-module(logic_calendar_tests).
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
setup() ->
mnesia:start(),
mnesia:create_table(user, [
{attributes, record_info(fields, user)},
{ram_copies, [node()]}
]),
mnesia:create_table(calendar, [
{attributes, record_info(fields, calendar)},
{ram_copies, [node()]}
]),
mnesia:create_table(subscription, [
{attributes, record_info(fields, subscription)},
{ram_copies, [node()]}
]),
mnesia:create_table(calendar_specialist, [
{attributes, record_info(fields, calendar_specialist)},
{ram_copies, [node()]}
]),
ok.
cleanup(_) ->
mnesia:delete_table(calendar_specialist),
mnesia:delete_table(subscription),
mnesia:delete_table(calendar),
mnesia:delete_table(user),
mnesia:stop(),
ok.
logic_calendar_test_() ->
{foreach,
fun setup/0,
fun cleanup/1,
[
{"Create calendar test", fun test_create_calendar/0},
{"Create commercial requires subscription", fun test_create_commercial_gate/0},
{"Get calendar test", fun test_get_calendar/0},
{"List calendars test", fun test_list_calendars/0},
{"Update calendar test", fun test_update_calendar/0},
{"Persist calendar settings (week_patterns)", fun test_update_settings/0},
{"Reject non-map settings", fun test_update_settings_invalid/0},
{"Org defaults settings round-trip", fun test_update_settings_org_defaults/0},
{"Reject invalid default_location shape", fun test_update_settings_invalid_location/0},
{"Delete calendar test", fun test_delete_calendar/0},
{"Access control test", fun test_access_control/0},
{"Ensure default calendar test", fun test_ensure_default_calendar/0},
{"Second personal rejected", fun test_personal_exists/0},
{"Personal type change forbidden", fun test_default_calendar_type_change/0},
{"Backfill extras to commercial or delete", fun test_backfill_extras/0}
]}.
create_test_user() ->
UserId = base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}),
User = #user{
id = UserId,
email = <<"test@example.com">>,
password_hash = <<"hash">>,
role = user,
status = active,
nickname = <<>>,
created_at = calendar:universal_time(),
updated_at = calendar:universal_time()
},
mnesia:dirty_write(User),
UserId.
test_create_calendar() ->
UserId = create_test_user(),
Title = <<"Test Calendar">>,
Description = <<"Test Description">>,
Confirmation = auto,
{ok, Calendar} = logic_calendar:create_calendar(UserId, Title, Description, Confirmation),
?assertEqual(UserId, Calendar#calendar.owner_id),
?assertEqual(Title, Calendar#calendar.title),
?assertEqual(personal, Calendar#calendar.type),
?assertEqual(Confirmation, Calendar#calendar.confirmation).
test_create_commercial_gate() ->
UserId = create_test_user(),
?assertMatch({error, subscription_required},
logic_calendar:create_calendar(UserId, <<"Studio">>, <<>>, manual, commercial)),
{ok, Personal} = logic_calendar:create_calendar(UserId, <<"Personal">>, <<>>, manual, personal),
?assertEqual(personal, Personal#calendar.type),
{ok, _} = logic_subscription:start_trial(UserId),
{ok, Commercial} = logic_calendar:create_calendar(UserId, <<"Studio">>, <<>>, manual, commercial),
?assertEqual(commercial, Commercial#calendar.type).
test_get_calendar() ->
UserId = create_test_user(),
{ok, Calendar} = logic_calendar:create_calendar(UserId, <<"Test">>, <<"">>, manual),
case logic_calendar:get_calendar(UserId, Calendar#calendar.id) of
{ok, Found} ->
?assertEqual(Calendar#calendar.id, Found#calendar.id);
Other ->
?assert(false, {unexpected_result, Other})
end,
OtherUserId = create_test_user(),
?assertMatch({error, access_denied},
logic_calendar:get_calendar(OtherUserId, Calendar#calendar.id)).
test_list_calendars() ->
UserId = create_test_user(),
{ok, _} = logic_calendar:create_calendar(UserId, <<"Calendar 1">>, <<"">>, manual),
{ok, _} = logic_subscription:start_trial(UserId),
{ok, _} = logic_calendar:create_calendar(UserId, <<"Calendar 2">>, <<"">>, auto, commercial),
{ok, Calendars} = logic_calendar:list_calendars(UserId),
?assertEqual(2, length(Calendars)).
test_update_calendar() ->
UserId = create_test_user(),
{ok, _} = logic_subscription:start_trial(UserId),
{ok, Calendar} = logic_calendar:create_calendar(
UserId, <<"Original">>, <<"">>, manual, commercial),
Updates = [{title, <<"Updated">>}, {confirmation, auto}],
{ok, Updated} = logic_calendar:update_calendar(UserId, Calendar#calendar.id, Updates),
?assertEqual(<<"Updated">>, Updated#calendar.title),
?assertEqual(commercial, Updated#calendar.type),
?assertEqual(auto, Updated#calendar.confirmation),
OtherUserId = create_test_user(),
?assertMatch({error, access_denied},
logic_calendar:update_calendar(OtherUserId, Calendar#calendar.id, Updates)).
test_update_settings() ->
UserId = create_test_user(),
{ok, Calendar} = logic_calendar:create_calendar(UserId, <<"Studio">>, <<"">>, manual),
Patterns = [
#{
<<"id">> => <<"p1">>,
<<"name">> => <<"Weekdays">>,
<<"updated_at">> => <<"2026-07-29T12:00:00Z">>,
<<"slots">> => [
#{<<"weekday">> => 1, <<"time">> => <<"10:00">>, <<"duration">> => 60, <<"title">> => <<"Open">>}
]
}
],
Settings = #{<<"week_patterns">> => Patterns},
{ok, Updated} = logic_calendar:update_calendar(
UserId, Calendar#calendar.id, [{settings, Settings}]),
?assertEqual(Settings, Updated#calendar.settings),
{ok, Reloaded} = logic_calendar:get_calendar(UserId, Calendar#calendar.id),
?assertEqual(Settings, Reloaded#calendar.settings),
%% binary key path (as from raw JSON list before convert_field)
Settings2 = Settings#{<<"foo">> => <<"bar">>},
{ok, Updated2} = logic_calendar:update_calendar(
UserId, Calendar#calendar.id, [{<<"settings">>, Settings2}]),
?assertEqual(Settings2, Updated2#calendar.settings).
test_update_settings_invalid() ->
UserId = create_test_user(),
{ok, Calendar} = logic_calendar:create_calendar(UserId, <<"Studio">>, <<"">>, manual),
%% non-map settings silently dropped by validate_updates — calendar unchanged
{ok, Same} = logic_calendar:update_calendar(
UserId, Calendar#calendar.id, [{settings, [<<"not-a-map">>]}]),
?assertEqual(#{}, Same#calendar.settings).
test_update_settings_org_defaults() ->
UserId = create_test_user(),
{ok, Calendar} = logic_calendar:create_calendar(UserId, <<"Salon">>, <<"">>, manual),
Patterns = [
#{
<<"id">> => <<"p1">>,
<<"name">> => <<"Weekdays">>,
<<"slots">> => [
#{<<"weekday">> => 1, <<"time">> => <<"10:00">>, <<"duration">> => 60}
]
}
],
Settings = #{
<<"week_patterns">> => Patterns,
<<"default_location">> => #{
<<"address">> => <<"ул. Пример, 1">>,
<<"lat">> => 55.75,
<<"lon">> => 37.61
},
<<"default_duration_minutes">> => 90,
<<"default_recurrence">> => #{
<<"enabled">> => true,
<<"freq">> => <<"WEEKLY">>,
<<"interval">> => 1
}
},
{ok, Updated} = logic_calendar:update_calendar(
UserId, Calendar#calendar.id, [{settings, Settings}]),
?assertEqual(Settings, Updated#calendar.settings),
{ok, Reloaded} = logic_calendar:get_calendar(UserId, Calendar#calendar.id),
?assertEqual(Settings, Reloaded#calendar.settings),
%% null clears recurrence default
Settings2 = Settings#{<<"default_recurrence">> => null},
{ok, Updated2} = logic_calendar:update_calendar(
UserId, Calendar#calendar.id, [{settings, Settings2}]),
?assertEqual(null, maps:get(<<"default_recurrence">>, Updated2#calendar.settings)).
test_update_settings_invalid_location() ->
UserId = create_test_user(),
{ok, Calendar} = logic_calendar:create_calendar(UserId, <<"Salon">>, <<"">>, manual),
?assertMatch({error, {invalid_settings, <<"default_location">>}},
logic_calendar:update_calendar(UserId, Calendar#calendar.id, [
{settings, #{<<"default_location">> => #{<<"address">> => <<"">>}}}
])),
?assertMatch({error, {invalid_settings, <<"default_location">>}},
logic_calendar:update_calendar(UserId, Calendar#calendar.id, [
{settings, #{<<"default_location">> => #{<<"address">> => <<"X">>, <<"lat">> => 1}}}
])),
?assertMatch({error, {invalid_settings, <<"default_duration_minutes">>}},
logic_calendar:update_calendar(UserId, Calendar#calendar.id, [
{settings, #{<<"default_duration_minutes">> => 0}}
])),
?assertEqual(#{},
begin
{ok, Same} = logic_calendar:get_calendar(UserId, Calendar#calendar.id),
Same#calendar.settings
end),
?assertMatch({error, {invalid_settings, <<"default_location">>}},
logic_calendar:normalize_settings(#{<<"default_location">> => <<"street">>})).
test_delete_calendar() ->
UserId = create_test_user(),
{ok, Personal} = logic_calendar:create_calendar(UserId, <<"Test">>, <<"">>, manual),
?assertMatch({error, default_calendar},
logic_calendar:delete_calendar(UserId, Personal#calendar.id)),
{ok, _} = logic_subscription:start_trial(UserId),
{ok, Commercial} = logic_calendar:create_calendar(
UserId, <<"Studio">>, <<"">>, manual, commercial),
{ok, Deleted} = logic_calendar:delete_calendar(UserId, Commercial#calendar.id),
?assertEqual(deleted, Deleted#calendar.status),
?assertMatch({error, access_denied},
logic_calendar:get_calendar(UserId, Commercial#calendar.id)).
test_access_control() ->
OwnerId = create_test_user(),
OtherId = create_test_user(),
{ok, PersonalCalendar} = logic_calendar:create_calendar(OwnerId, <<"Personal">>, <<"">>, manual),
?assert(logic_calendar:can_edit(OwnerId, PersonalCalendar)),
?assertNot(logic_calendar:can_edit(OtherId, PersonalCalendar)),
?assertNot(logic_calendar:can_access(OtherId, PersonalCalendar)),
{ok, _} = logic_subscription:start_trial(OwnerId),
{ok, CommercialCalendar} = logic_calendar:create_calendar(
OwnerId, <<"Studio">>, <<"">>, manual, commercial),
?assert(logic_calendar:can_access(OtherId, CommercialCalendar)),
?assertNot(logic_calendar:can_edit(OtherId, CommercialCalendar)),
{ok, Frozen} = core_calendar:update(CommercialCalendar#calendar.id, [{status, frozen}]),
?assertNot(logic_calendar:can_access(OtherId, Frozen)),
?assertNot(logic_calendar:can_access(OwnerId, Frozen)).
test_ensure_default_calendar() ->
UserId = create_test_user(),
ok = logic_calendar:ensure_default_calendar(UserId),
{ok, Calendars} = logic_calendar:list_calendars(UserId),
?assertEqual(1, length(Calendars)),
Cal = hd(Calendars),
?assertEqual(personal, Cal#calendar.type),
?assertEqual(<<"Default">>, Cal#calendar.title),
?assertEqual(<<>>, Cal#calendar.short_name),
?assertEqual(active, Cal#calendar.status),
ok = logic_calendar:ensure_default_calendar(UserId),
{ok, Calendars2} = logic_calendar:list_calendars(UserId),
?assertEqual(1, length(Calendars2)),
?assertEqual(<<"Default">>, (hd(Calendars2))#calendar.title).
test_personal_exists() ->
UserId = create_test_user(),
{ok, _} = logic_calendar:create_calendar(UserId, <<"One">>, <<"">>, manual, personal),
?assertMatch({error, personal_exists},
logic_calendar:create_calendar(UserId, <<"Two">>, <<"">>, manual, personal)).
test_default_calendar_type_change() ->
UserId = create_test_user(),
{ok, Personal} = logic_calendar:create_calendar(UserId, <<"One">>, <<"">>, manual, personal),
{ok, _} = logic_subscription:start_trial(UserId),
?assertMatch({error, default_calendar},
logic_calendar:update_calendar(UserId, Personal#calendar.id, [{type, commercial}])).
test_backfill_extras() ->
UserId = create_test_user(),
{ok, Keep0} = core_calendar:create(UserId, <<"Old">>, <<"">>, manual, personal),
{ok, Extra0} = core_calendar:create(UserId, <<"Extra">>, <<"">>, manual, personal),
%% datetime resolution is 1s — pin order explicitly
mnesia:dirty_write(Keep0#calendar{created_at = {{2020, 1, 1}, {0, 0, 0}}}),
mnesia:dirty_write(Extra0#calendar{created_at = {{2020, 1, 2}, {0, 0, 0}}}),
ok = logic_calendar:backfill_single_personal(),
{ok, List} = logic_calendar:list_calendars(UserId),
Personals = [C || C <- List, C#calendar.type =:= personal],
?assertEqual(1, length(Personals)),
?assertEqual(Keep0#calendar.id, (hd(Personals))#calendar.id),
?assertEqual(<<"Default">>, (hd(Personals))#calendar.title),
{ok, ExtraAfter} = core_calendar:get_by_id(Extra0#calendar.id),
?assertEqual(deleted, ExtraAfter#calendar.status).