Stage 10 final

This commit is contained in:
2026-04-22 23:15:20 +03:00
parent e3a08cfa04
commit 081dcf9588
85 changed files with 2116 additions and 160 deletions

View File

@@ -0,0 +1,107 @@
-module(admin_handler_stats_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(event, [{attributes, record_info(fields, event)}, {ram_copies, [node()]}]),
mnesia:create_table(booking, [{attributes, record_info(fields, booking)}, {ram_copies, [node()]}]),
mnesia:create_table(review, [{attributes, record_info(fields, review)}, {ram_copies, [node()]}]),
mnesia:create_table(report, [{attributes, record_info(fields, report)}, {ram_copies, [node()]}]),
mnesia:create_table(ticket, [{attributes, record_info(fields, ticket)}, {ram_copies, [node()]}]),
mnesia:create_table(subscription, [{attributes, record_info(fields, subscription)}, {ram_copies, [node()]}]),
ok.
cleanup(_) ->
mnesia:delete_table(subscription),
mnesia:delete_table(ticket),
mnesia:delete_table(report),
mnesia:delete_table(review),
mnesia:delete_table(booking),
mnesia:delete_table(event),
mnesia:delete_table(calendar),
mnesia:delete_table(user),
mnesia:stop(),
ok.
admin_stats_test_() ->
{foreach,
fun setup/0,
fun cleanup/1,
[
{"Count users", fun test_count_users/0},
{"Count calendars", fun test_count_calendars/0},
{"Count events", fun test_count_events/0},
{"Count bookings", fun test_count_bookings/0},
{"Count reviews", fun test_count_reviews/0},
{"Count reports", fun test_count_reports/0},
{"Count tickets", fun test_count_tickets/0},
{"Count subscriptions", fun test_count_subscriptions/0}
]}.
create_test_user() ->
UserId = base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}),
User = #user{id = UserId, email = <<UserId/binary, "@test.com">>, password_hash = <<"hash">>,
role = user, status = active, created_at = calendar:universal_time(), updated_at = calendar:universal_time()},
mnesia:dirty_write(User),
UserId.
test_count_users() ->
?assertEqual(0, admin_handler_stats:count_users()),
create_test_user(),
create_test_user(),
?assertEqual(2, admin_handler_stats:count_users()).
test_count_calendars() ->
?assertEqual(0, admin_handler_stats:count_calendars()),
UserId = create_test_user(),
core_calendar:create(UserId, <<"Cal1">>, <<"">>, manual),
core_calendar:create(UserId, <<"Cal2">>, <<"">>, auto),
?assertEqual(2, admin_handler_stats:count_calendars()).
test_count_events() ->
?assertEqual(0, admin_handler_stats:count_events()),
UserId = create_test_user(),
{ok, Cal} = core_calendar:create(UserId, <<"Cal">>, <<"">>, manual),
core_event:create(Cal#calendar.id, <<"Ev1">>, {{2026,6,1},{10,0,0}}, 60),
core_event:create(Cal#calendar.id, <<"Ev2">>, {{2026,6,2},{10,0,0}}, 60),
?assertEqual(2, admin_handler_stats:count_events()).
test_count_bookings() ->
?assertEqual(0, admin_handler_stats:count_bookings()),
UserId = create_test_user(),
ParticipantId = create_test_user(),
{ok, Cal} = core_calendar:create(UserId, <<"Cal">>, <<"">>, manual),
{ok, Ev} = core_event:create(Cal#calendar.id, <<"Ev">>, {{2026,6,1},{10,0,0}}, 60),
core_booking:create(Ev#event.id, ParticipantId),
core_booking:create(Ev#event.id, ParticipantId),
?assertEqual(2, admin_handler_stats:count_bookings()).
test_count_reviews() ->
?assertEqual(0, admin_handler_stats:count_reviews()),
UserId = create_test_user(),
core_review:create(UserId, calendar, <<"cal1">>, 5, <<"Great">>),
core_review:create(UserId, event, <<"ev1">>, 4, <<"Good">>),
?assertEqual(2, admin_handler_stats:count_reviews()).
test_count_reports() ->
?assertEqual(0, admin_handler_stats:count_reports()),
UserId = create_test_user(),
core_report:create(UserId, event, <<"ev1">>, <<"Bad">>),
core_report:create(UserId, calendar, <<"cal1">>, <<"Spam">>),
?assertEqual(2, admin_handler_stats:count_reports()).
test_count_tickets() ->
?assertEqual(0, admin_handler_stats:count_tickets()),
core_ticket:create_or_update(<<"Error1">>, <<"">>, #{}),
core_ticket:create_or_update(<<"Error2">>, <<"">>, #{}),
?assertEqual(2, admin_handler_stats:count_tickets()).
test_count_subscriptions() ->
?assertEqual(0, admin_handler_stats:count_subscriptions()),
UserId = create_test_user(),
core_subscription:create(UserId, trial, false),
core_subscription:create(UserId, monthly, true),
?assertEqual(2, admin_handler_stats:count_subscriptions()).

View File

@@ -0,0 +1,28 @@
-module(admin_handler_user_by_id_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()]}]),
ok.
cleanup(_) ->
mnesia:delete_table(user),
mnesia:stop(),
ok.
admin_user_by_id_test_() ->
{foreach,
fun setup/0,
fun cleanup/1,
[
{"Convert updates test", fun test_convert_updates/0}
]}.
test_convert_updates() ->
Updates = [{<<"status">>, <<"frozen">>}, {<<"role">>, <<"admin">>}, {<<"email">>, <<"test@test.com">>}],
Converted = admin_handler_user_by_id:convert_updates(Updates),
?assertEqual({status, frozen}, lists:keyfind(status, 1, Converted)),
?assertEqual({role, admin}, lists:keyfind(role, 1, Converted)),
?assertEqual({<<"email">>, <<"test@test.com">>}, lists:keyfind(<<"email">>, 1, Converted)).

View File

@@ -0,0 +1,45 @@
-module(admin_handler_users_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()]}]),
ok.
cleanup(_) ->
mnesia:delete_table(user),
mnesia:stop(),
ok.
admin_users_test_() ->
{foreach,
fun setup/0,
fun cleanup/1,
[
{"User to JSON conversion", fun test_user_to_json/0},
{"Is admin check", fun test_is_admin/0}
]}.
create_test_user(Role) ->
UserId = base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}),
User = #user{id = UserId, email = <<UserId/binary, "@test.com">>, password_hash = <<"hash">>,
role = Role, status = active, created_at = calendar:universal_time(), updated_at = calendar:universal_time()},
mnesia:dirty_write(User),
UserId.
test_user_to_json() ->
UserId = create_test_user(user),
{ok, User} = core_user:get_by_id(UserId),
Json = admin_handler_user_by_id:user_to_json(User),
?assert(is_map(Json)),
?assertEqual(UserId, maps:get(id, Json)),
?assertEqual(user, maps:get(role, Json)),
?assertEqual(active, maps:get(status, Json)).
test_is_admin() ->
AdminId = create_test_user(admin),
UserId = create_test_user(user),
?assert(admin_handler_stats:is_admin(AdminId)),
?assertNot(admin_handler_stats:is_admin(UserId)),
?assertNot(admin_handler_stats:is_admin(<<"nonexistent">>)).

View File

@@ -0,0 +1,34 @@
-module(admin_ws_handler_tests).
-include_lib("eunit/include/eunit.hrl").
-record(state, {
admin_id :: binary() | undefined
}).
setup() ->
pg:start_link(),
ok.
cleanup(_) ->
ok.
admin_ws_handler_test_() ->
{foreach,
fun setup/0,
fun cleanup/1,
[
{"Admin WebSocket info notification", fun test_admin_websocket_info/0}
]}.
test_admin_websocket_info() ->
State = #state{admin_id = <<"admin123">>},
Data = #{report_id => <<"rep123">>, reason => <<"Spam">>},
Msg = {admin_notification, report_created, Data},
case admin_ws_handler:websocket_info(Msg, State) of
{reply, {text, Reply}, _} ->
Decoded = jsx:decode(Reply, [return_maps]),
?assertEqual(<<"report_created">>, maps:get(<<"type">>, Decoded)),
?assertEqual(<<"rep123">>, maps:get(<<"report_id">>, maps:get(<<"data">>, Decoded)));
_ -> ?assert(false, "Expected reply")
end.

View File

@@ -0,0 +1,148 @@
-module(booking_integration_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(event, [
{attributes, record_info(fields, event)},
{ram_copies, [node()]}
]),
mnesia:create_table(booking, [
{attributes, record_info(fields, booking)},
{ram_copies, [node()]}
]),
ok.
cleanup(_) ->
mnesia:delete_table(booking),
mnesia:delete_table(event),
mnesia:delete_table(calendar),
mnesia:delete_table(user),
mnesia:stop(),
ok.
booking_integration_test_() ->
{foreach,
fun setup/0,
fun cleanup/1,
[
{"Full booking flow with auto confirmation", fun test_auto_booking_flow/0},
{"Full booking flow with manual confirmation", fun test_manual_booking_flow/0},
{"Capacity management test", fun test_capacity_management/0},
{"Multiple bookings test", fun test_multiple_bookings/0}
]}.
create_user() ->
UserId = base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}),
User = #user{
id = UserId,
email = <<UserId/binary, "@test.com">>,
password_hash = <<"hash">>,
role = user,
status = active,
created_at = calendar:universal_time(),
updated_at = calendar:universal_time()
},
mnesia:dirty_write(User),
UserId.
test_auto_booking_flow() ->
OwnerId = create_user(),
ParticipantId = create_user(),
{ok, Calendar} = core_calendar:create(OwnerId, <<"Auto">>, <<"">>, auto),
StartTime = {{2026, 6, 1}, {10, 0, 0}},
{ok, Event} = core_event:create(Calendar#calendar.id, <<"Event">>, StartTime, 60),
{ok, Booking} = logic_booking:create_booking(ParticipantId, Event#event.id),
timer:sleep(100),
{ok, Updated} = core_booking:get_by_id(Booking#booking.id),
?assertEqual(confirmed, Updated#booking.status),
{ok, EventBookings} = logic_booking:list_event_bookings(OwnerId, Event#event.id),
?assertEqual(1, length(EventBookings)),
{ok, UserBookings} = logic_booking:list_user_bookings(ParticipantId),
?assertEqual(1, length(UserBookings)).
test_manual_booking_flow() ->
OwnerId = create_user(),
ParticipantId = create_user(),
{ok, Calendar} = core_calendar:create(OwnerId, <<"Manual">>, <<"">>, manual),
StartTime = {{2026, 6, 1}, {10, 0, 0}},
{ok, Event} = core_event:create(Calendar#calendar.id, <<"Event">>, StartTime, 60),
{ok, Booking} = logic_booking:create_booking(ParticipantId, Event#event.id),
?assertEqual(pending, Booking#booking.status),
{ok, Confirmed} = logic_booking:confirm_booking(OwnerId, Booking#booking.id, confirm),
?assertEqual(confirmed, Confirmed#booking.status),
{ok, Cancelled} = logic_booking:cancel_booking(ParticipantId, Booking#booking.id),
?assertEqual(cancelled, Cancelled#booking.status).
test_capacity_management() ->
OwnerId = create_user(),
Participant1Id = create_user(),
Participant2Id = create_user(),
Participant3Id = create_user(),
{ok, Calendar} = core_calendar:create(OwnerId, <<"Test">>, <<"">>, auto),
StartTime = {{2026, 6, 1}, {10, 0, 0}},
{ok, Event} = core_event:create(Calendar#calendar.id, <<"Event">>, StartTime, 60),
{ok, _} = core_event:update(Event#event.id, [{capacity, 2}]),
{ok, Booking1} = logic_booking:create_booking(Participant1Id, Event#event.id),
{ok, _Booking2} = logic_booking:create_booking(Participant2Id, Event#event.id),
{error, event_full} = logic_booking:create_booking(Participant3Id, Event#event.id),
% Участник 1 отменяет своё бронирование
{ok, _} = logic_booking:cancel_booking(Participant1Id, Booking1#booking.id),
% Теперь третий может записаться
{ok, _} = logic_booking:create_booking(Participant3Id, Event#event.id).
test_multiple_bookings() ->
OwnerId = create_user(),
ParticipantId = create_user(),
{ok, Calendar} = core_calendar:create(OwnerId, <<"Test">>, <<"">>, manual),
StartTime1 = {{2026, 6, 1}, {10, 0, 0}},
StartTime2 = {{2026, 6, 2}, {10, 0, 0}},
StartTime3 = {{2026, 6, 3}, {10, 0, 0}},
{ok, Event1} = core_event:create(Calendar#calendar.id, <<"Event1">>, StartTime1, 60),
{ok, Event2} = core_event:create(Calendar#calendar.id, <<"Event2">>, StartTime2, 60),
{ok, Event3} = core_event:create(Calendar#calendar.id, <<"Event3">>, StartTime3, 60),
{ok, B1} = logic_booking:create_booking(ParticipantId, Event1#event.id),
{ok, B2} = logic_booking:create_booking(ParticipantId, Event2#event.id),
{ok, _B3} = logic_booking:create_booking(ParticipantId, Event3#event.id),
{ok, _} = logic_booking:confirm_booking(OwnerId, B1#booking.id, confirm),
{ok, _} = logic_booking:confirm_booking(OwnerId, B2#booking.id, confirm),
{ok, UserBookings} = logic_booking:list_user_bookings(ParticipantId),
?assertEqual(3, length(UserBookings)),
ConfirmedCount = length([B || B <- UserBookings, B#booking.status =:= confirmed]),
?assertEqual(2, ConfirmedCount),
PendingCount = length([B || B <- UserBookings, B#booking.status =:= pending]),
?assertEqual(1, PendingCount).

View File

@@ -0,0 +1,80 @@
-module(core_banned_word_tests).
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
setup() ->
mnesia:start(),
mnesia:create_table(banned_word, [
{attributes, record_info(fields, banned_word)},
{ram_copies, [node()]}
]),
ok.
cleanup(_) ->
mnesia:delete_table(banned_word),
mnesia:stop(),
ok.
core_banned_word_test_() ->
{foreach,
fun setup/0,
fun cleanup/1,
[
{"Add banned word test", fun test_add_word/0},
{"Add duplicate word test", fun test_add_duplicate/0},
{"Remove banned word test", fun test_remove_word/0},
{"List banned words test", fun test_list_words/0},
{"Is banned test", fun test_is_banned/0},
{"Check text test", fun test_check_text/0},
{"Filter text test", fun test_filter_text/0}
]}.
test_add_word() ->
Word = <<"badword">>,
{ok, BannedWord} = core_banned_word:add(Word),
?assertEqual(Word, BannedWord#banned_word.word),
?assert(is_binary(BannedWord#banned_word.id)).
test_add_duplicate() ->
Word = <<"badword">>,
{ok, _} = core_banned_word:add(Word),
{error, already_exists} = core_banned_word:add(Word),
{error, already_exists} = core_banned_word:add(<<"BADWORD">>). % case insensitive
test_remove_word() ->
Word = <<"badword">>,
{ok, _} = core_banned_word:add(Word),
{ok, removed} = core_banned_word:remove(Word),
{error, not_found} = core_banned_word:remove(<<"nonexistent">>).
test_list_words() ->
{ok, _} = core_banned_word:add(<<"word1">>),
{ok, _} = core_banned_word:add(<<"word2">>),
{ok, _} = core_banned_word:add(<<"word3">>),
{ok, Words} = core_banned_word:list_all(),
?assertEqual(3, length(Words)),
?assert(lists:member(<<"word1">>, Words)).
test_is_banned() ->
Word = <<"badword">>,
?assertNot(core_banned_word:is_banned(Word)),
{ok, _} = core_banned_word:add(Word),
?assert(core_banned_word:is_banned(Word)),
?assert(core_banned_word:is_banned(<<"BADWORD">>)). % case insensitive
test_check_text() ->
{ok, _} = core_banned_word:add(<<"bad">>),
{ok, _} = core_banned_word:add(<<"spam">>),
?assertNot(core_banned_word:check_text(<<"Hello world">>)),
?assert(core_banned_word:check_text(<<"This is bad">>)),
?assert(core_banned_word:check_text(<<"This is SPAM">>)).
test_filter_text() ->
{ok, _} = core_banned_word:add(<<"bad">>),
{ok, _} = core_banned_word:add(<<"spam">>),
?assertEqual(<<"Hello world">>, core_banned_word:filter_text(<<"Hello world">>)),
?assertEqual(<<"This is ***">>, core_banned_word:filter_text(<<"This is bad">>)),
?assertEqual(<<"*** and ***">>, core_banned_word:filter_text(<<"bad and spam">>)).

View File

@@ -0,0 +1,126 @@
-module(core_booking_tests).
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
setup() ->
mnesia:start(),
mnesia:create_table(booking, [
{attributes, record_info(fields, booking)},
{ram_copies, [node()]}
]),
ok.
cleanup(_) ->
mnesia:delete_table(booking),
mnesia:stop(),
ok.
core_booking_test_() ->
{foreach,
fun setup/0,
fun cleanup/1,
[
{"Create booking test", fun test_create_booking/0},
{"Get booking by id test", fun test_get_by_id/0},
{"Get booking by event and user test", fun test_get_by_event_and_user/0},
{"List bookings by event test", fun test_list_by_event/0},
{"List bookings by user test", fun test_list_by_user/0},
{"Update booking status test", fun test_update_status/0},
{"Delete booking test", fun test_delete_booking/0}
]}.
test_create_booking() ->
EventId = <<"event123">>,
UserId = <<"user123">>,
{ok, Booking} = core_booking:create(EventId, UserId),
?assertEqual(EventId, Booking#booking.event_id),
?assertEqual(UserId, Booking#booking.user_id),
?assertEqual(pending, Booking#booking.status),
?assertEqual(undefined, Booking#booking.confirmed_at),
?assert(is_binary(Booking#booking.id)),
?assert(Booking#booking.created_at =/= undefined),
?assert(Booking#booking.updated_at =/= undefined).
test_get_by_id() ->
EventId = <<"event123">>,
UserId = <<"user123">>,
{ok, Booking} = core_booking:create(EventId, UserId),
{ok, Found} = core_booking:get_by_id(Booking#booking.id),
?assertEqual(Booking#booking.id, Found#booking.id),
{error, not_found} = core_booking:get_by_id(<<"nonexistent">>).
test_get_by_event_and_user() ->
EventId = <<"event123">>,
UserId1 = <<"user1">>,
UserId2 = <<"user2">>,
{ok, Booking1} = core_booking:create(EventId, UserId1),
{ok, _Booking2} = core_booking:create(EventId, UserId2),
{ok, Found} = core_booking:get_by_event_and_user(EventId, UserId1),
?assertEqual(Booking1#booking.id, Found#booking.id),
{error, not_found} = core_booking:get_by_event_and_user(EventId, <<"user3">>).
test_list_by_event() ->
EventId1 = <<"event1">>,
EventId2 = <<"event2">>,
UserId = <<"user123">>,
{ok, _} = core_booking:create(EventId1, UserId),
{ok, _} = core_booking:create(EventId1, <<"user2">>),
{ok, _} = core_booking:create(EventId2, UserId),
{ok, Bookings1} = core_booking:list_by_event(EventId1),
?assertEqual(2, length(Bookings1)),
{ok, Bookings2} = core_booking:list_by_event(EventId2),
?assertEqual(1, length(Bookings2)).
test_list_by_user() ->
EventId = <<"event123">>,
UserId1 = <<"user1">>,
UserId2 = <<"user2">>,
{ok, _} = core_booking:create(EventId, UserId1),
{ok, _} = core_booking:create(EventId, UserId1),
{ok, _} = core_booking:create(EventId, UserId2),
{ok, Bookings1} = core_booking:list_by_user(UserId1),
?assertEqual(2, length(Bookings1)),
{ok, Bookings2} = core_booking:list_by_user(UserId2),
?assertEqual(1, length(Bookings2)).
test_update_status() ->
EventId = <<"event123">>,
UserId = <<"user123">>,
{ok, Booking} = core_booking:create(EventId, UserId),
timer:sleep(2000), % 2 секунды
{ok, Confirmed} = core_booking:update_status(Booking#booking.id, confirmed),
?assertEqual(confirmed, Confirmed#booking.status),
?assert(Confirmed#booking.confirmed_at =/= undefined),
?assert(Confirmed#booking.updated_at > Booking#booking.updated_at),
timer:sleep(2000), % 2 секунды
{ok, Cancelled} = core_booking:update_status(Booking#booking.id, cancelled),
?assertEqual(cancelled, Cancelled#booking.status),
?assert(Cancelled#booking.updated_at > Confirmed#booking.updated_at),
{error, not_found} = core_booking:update_status(<<"nonexistent">>, confirmed).
test_delete_booking() ->
EventId = <<"event123">>,
UserId = <<"user123">>,
{ok, Booking} = core_booking:create(EventId, UserId),
{ok, deleted} = core_booking:delete(Booking#booking.id),
{error, not_found} = core_booking:get_by_id(Booking#booking.id).

View File

@@ -0,0 +1,90 @@
-module(core_calendar_tests).
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
setup() ->
mnesia:start(),
mnesia:create_table(calendar, [
{attributes, record_info(fields, calendar)},
{ram_copies, [node()]}
]),
ok.
cleanup(_) ->
mnesia:delete_table(calendar),
mnesia:stop(),
ok.
core_calendar_test_() ->
{foreach,
fun setup/0,
fun cleanup/1,
[
{"Create calendar test", fun test_create_calendar/0},
{"Get calendar by id test", fun test_get_by_id/0},
{"List calendars by owner test", fun test_list_by_owner/0},
{"Update calendar test", fun test_update_calendar/0},
{"Delete calendar test", fun test_delete_calendar/0}
]}.
test_create_calendar() ->
OwnerId = <<"owner123">>,
Title = <<"Test Calendar">>,
Description = <<"Test Description">>,
Confirmation = auto,
{ok, Calendar} = core_calendar:create(OwnerId, Title, Description, Confirmation),
?assertEqual(OwnerId, Calendar#calendar.owner_id),
?assertEqual(Title, Calendar#calendar.title),
?assertEqual(Description, Calendar#calendar.description),
?assertEqual(personal, Calendar#calendar.type),
?assertEqual(Confirmation, Calendar#calendar.confirmation),
?assertEqual(active, Calendar#calendar.status),
?assert(is_binary(Calendar#calendar.id)),
?assert(Calendar#calendar.created_at =/= undefined),
?assert(Calendar#calendar.updated_at =/= undefined).
test_get_by_id() ->
OwnerId = <<"owner123">>,
{ok, Calendar} = core_calendar:create(OwnerId, <<"Test">>, <<"Desc">>, manual),
{ok, Found} = core_calendar:get_by_id(Calendar#calendar.id),
?assertEqual(Calendar#calendar.id, Found#calendar.id),
{error, not_found} = core_calendar:get_by_id(<<"nonexistent">>).
test_list_by_owner() ->
OwnerId = <<"owner123">>,
OtherOwner = <<"other456">>,
{ok, _} = core_calendar:create(OwnerId, <<"Calendar 1">>, <<"">>, manual),
{ok, _} = core_calendar:create(OwnerId, <<"Calendar 2">>, <<"">>, auto),
{ok, _} = core_calendar:create(OtherOwner, <<"Other Calendar">>, <<"">>, manual),
{ok, Calendars} = core_calendar:list_by_owner(OwnerId),
?assertEqual(2, length(Calendars)).
test_update_calendar() ->
OwnerId = <<"owner123">>,
{ok, Calendar} = core_calendar:create(OwnerId, <<"Original">>, <<"">>, manual),
timer:sleep(2000),
Updates = [{title, <<"Updated">>}, {description, <<"New Desc">>}, {confirmation, auto}],
{ok, Updated} = core_calendar:update(Calendar#calendar.id, Updates),
?assertEqual(<<"Updated">>, Updated#calendar.title),
?assertEqual(<<"New Desc">>, Updated#calendar.description),
?assertEqual(auto, Updated#calendar.confirmation),
?assert(Updated#calendar.updated_at > Calendar#calendar.updated_at),
{error, not_found} = core_calendar:update(<<"nonexistent">>, Updates).
test_delete_calendar() ->
OwnerId = <<"owner123">>,
{ok, Calendar} = core_calendar:create(OwnerId, <<"Test">>, <<"">>, manual),
{ok, Deleted} = core_calendar:delete(Calendar#calendar.id),
?assertEqual(deleted, Deleted#calendar.status),
{ok, ActiveCalendars} = core_calendar:list_by_owner(OwnerId),
?assertEqual(0, length(ActiveCalendars)).

View File

@@ -0,0 +1,104 @@
-module(core_event_recurring_tests).
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
setup() ->
mnesia:start(),
mnesia:create_table(event, [
{attributes, record_info(fields, event)},
{ram_copies, [node()]}
]),
mnesia:create_table(recurrence_exception, [
{attributes, record_info(fields, recurrence_exception)},
{ram_copies, [node()]}
]),
ok.
cleanup(_) ->
mnesia:delete_table(recurrence_exception),
mnesia:delete_table(event),
mnesia:stop(),
ok.
core_event_recurring_test_() ->
{foreach,
fun setup/0,
fun cleanup/1,
[
{"Create recurring event test", fun test_create_recurring/0},
{"Materialize occurrence test", fun test_materialize_occurrence/0},
{"Materialize existing occurrence test", fun test_materialize_existing/0},
{"Materialize non-recurring test", fun test_materialize_non_recurring/0}
]}.
test_create_recurring() ->
CalendarId = <<"calendar123">>,
Title = <<"Weekly Meeting">>,
StartTime = {{2026, 4, 20}, {10, 0, 0}},
Duration = 60,
RRule = #{<<"freq">> => <<"WEEKLY">>, <<"interval">> => 1},
{ok, Event} = core_event:create_recurring(CalendarId, Title, StartTime, Duration, RRule),
?assertEqual(CalendarId, Event#event.calendar_id),
?assertEqual(Title, Event#event.title),
?assertEqual(recurring, Event#event.event_type),
?assertEqual(false, Event#event.is_instance),
?assert(is_binary(Event#event.recurrence_rule)),
% Проверяем, что правило сохранилось
Decoded = jsx:decode(Event#event.recurrence_rule, [return_maps]),
RRuleMap = case Decoded of
Map when is_map(Map) -> Map;
{ok, Map} -> Map
end,
?assertEqual(<<"WEEKLY">>, maps:get(<<"freq">>, RRuleMap)).
test_materialize_occurrence() ->
CalendarId = <<"calendar123">>,
MasterId = create_recurring_event(CalendarId),
OccurrenceStart = {{2026, 4, 27}, {10, 0, 0}},
SpecialistId = <<"specialist123">>,
{ok, Instance} = core_event:materialize_occurrence(MasterId, OccurrenceStart, SpecialistId),
?assertEqual(CalendarId, Instance#event.calendar_id),
?assertEqual(MasterId, Instance#event.master_id),
?assertEqual(OccurrenceStart, Instance#event.start_time),
?assertEqual(SpecialistId, Instance#event.specialist_id),
?assertEqual(single, Instance#event.event_type),
?assertEqual(true, Instance#event.is_instance).
test_materialize_existing() ->
CalendarId = <<"calendar123">>,
MasterId = create_recurring_event(CalendarId),
OccurrenceStart = {{2026, 4, 27}, {10, 0, 0}},
SpecialistId1 = <<"specialist1">>,
SpecialistId2 = <<"specialist2">>,
% Первая материализация
{ok, Instance1} = core_event:materialize_occurrence(MasterId, OccurrenceStart, SpecialistId1),
% Вторая материализация - должна вернуть существующий экземпляр
{ok, Instance2} = core_event:materialize_occurrence(MasterId, OccurrenceStart, SpecialistId2),
?assertEqual(Instance1#event.id, Instance2#event.id),
?assertEqual(SpecialistId1, Instance2#event.specialist_id).
test_materialize_non_recurring() ->
CalendarId = <<"calendar123">>,
{ok, SingleEvent} = core_event:create(CalendarId, <<"Single">>, {{2026, 4, 20}, {10, 0, 0}}, 60),
{error, not_recurring} = core_event:materialize_occurrence(
SingleEvent#event.id, {{2026, 4, 27}, {10, 0, 0}}, <<"spec">>
).
create_recurring_event(CalendarId) ->
{ok, Event} = core_event:create_recurring(
CalendarId,
<<"Weekly Meeting">>,
{{2026, 4, 20}, {10, 0, 0}},
60,
#{<<"freq">> => <<"WEEKLY">>, <<"interval">> => 1}
),
Event#event.id.

View File

@@ -0,0 +1,98 @@
-module(core_event_tests).
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
setup() ->
mnesia:start(),
mnesia:create_table(event, [
{attributes, record_info(fields, event)},
{ram_copies, [node()]}
]),
ok.
cleanup(_) ->
mnesia:delete_table(event),
mnesia:stop(),
ok.
core_event_test_() ->
{foreach,
fun setup/0,
fun cleanup/1,
[
{"Create event test", fun test_create_event/0},
{"Get event by id test", fun test_get_by_id/0},
{"List events by calendar test", fun test_list_by_calendar/0},
{"Update event test", fun test_update_event/0},
{"Delete event test", fun test_delete_event/0}
]}.
test_create_event() ->
CalendarId = <<"calendar123">>,
Title = <<"Test Event">>,
StartTime = {{2026, 4, 25}, {10, 0, 0}},
Duration = 60,
{ok, Event} = core_event:create(CalendarId, Title, StartTime, Duration),
?assertEqual(CalendarId, Event#event.calendar_id),
?assertEqual(Title, Event#event.title),
?assertEqual(StartTime, Event#event.start_time),
?assertEqual(Duration, Event#event.duration),
?assertEqual(single, Event#event.event_type),
?assertEqual(active, Event#event.status),
?assertEqual(false, Event#event.is_instance),
?assert(is_binary(Event#event.id)),
?assert(Event#event.created_at =/= undefined),
?assert(Event#event.updated_at =/= undefined).
test_get_by_id() ->
CalendarId = <<"calendar123">>,
{ok, Event} = core_event:create(CalendarId, <<"Test">>, {{2026, 4, 25}, {10, 0, 0}}, 60),
{ok, Found} = core_event:get_by_id(Event#event.id),
?assertEqual(Event#event.id, Found#event.id),
{error, not_found} = core_event:get_by_id(<<"nonexistent">>).
test_list_by_calendar() ->
CalendarId1 = <<"calendar1">>,
CalendarId2 = <<"calendar2">>,
{ok, _} = core_event:create(CalendarId1, <<"Event 1">>, {{2026, 4, 25}, {10, 0, 0}}, 60),
{ok, _} = core_event:create(CalendarId1, <<"Event 2">>, {{2026, 4, 26}, {11, 0, 0}}, 90),
{ok, _} = core_event:create(CalendarId2, <<"Event 3">>, {{2026, 4, 27}, {12, 0, 0}}, 30),
{ok, Events1} = core_event:list_by_calendar(CalendarId1),
?assertEqual(2, length(Events1)),
{ok, Events2} = core_event:list_by_calendar(CalendarId2),
?assertEqual(1, length(Events2)),
{ok, Events3} = core_event:list_by_calendar(<<"empty">>),
?assertEqual(0, length(Events3)).
test_update_event() ->
CalendarId = <<"calendar123">>,
{ok, Event} = core_event:create(CalendarId, <<"Original">>, {{2026, 4, 25}, {10, 0, 0}}, 60),
timer:sleep(2000),
Updates = [{title, <<"Updated">>}, {capacity, 100}, {description, <<"New desc">>}],
{ok, Updated} = core_event:update(Event#event.id, Updates),
?assertEqual(<<"Updated">>, Updated#event.title),
?assertEqual(100, Updated#event.capacity),
?assertEqual(<<"New desc">>, Updated#event.description),
?assert(Updated#event.updated_at > Event#event.updated_at),
{error, not_found} = core_event:update(<<"nonexistent">>, Updates).
test_delete_event() ->
CalendarId = <<"calendar123">>,
{ok, Event} = core_event:create(CalendarId, <<"Test">>, {{2026, 4, 25}, {10, 0, 0}}, 60),
{ok, Deleted} = core_event:delete(Event#event.id),
?assertEqual(deleted, Deleted#event.status),
% Удалённое событие не возвращается в списке активных
{ok, ActiveEvents} = core_event:list_by_calendar(CalendarId),
?assertEqual(0, length(ActiveEvents)).

View File

@@ -0,0 +1,101 @@
-module(core_report_tests).
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
setup() ->
mnesia:start(),
mnesia:create_table(report, [
{attributes, record_info(fields, report)},
{ram_copies, [node()]}
]),
ok.
cleanup(_) ->
mnesia:delete_table(report),
mnesia:stop(),
ok.
core_report_test_() ->
{foreach,
fun setup/0,
fun cleanup/1,
[
{"Create report test", fun test_create_report/0},
{"Get report by id test", fun test_get_by_id/0},
{"List reports by target test", fun test_list_by_target/0},
{"List reports by reporter test", fun test_list_by_reporter/0},
{"Update report status test", fun test_update_status/0},
{"Get count by target test", fun test_get_count_by_target/0}
]}.
test_create_report() ->
ReporterId = <<"user123">>,
TargetType = event,
TargetId = <<"event123">>,
Reason = <<"Inappropriate content">>,
{ok, Report} = core_report:create(ReporterId, TargetType, TargetId, Reason),
?assertEqual(ReporterId, Report#report.reporter_id),
?assertEqual(TargetType, Report#report.target_type),
?assertEqual(TargetId, Report#report.target_id),
?assertEqual(Reason, Report#report.reason),
?assertEqual(pending, Report#report.status),
?assertEqual(undefined, Report#report.resolved_at),
?assertEqual(undefined, Report#report.resolved_by),
?assert(is_binary(Report#report.id)).
test_get_by_id() ->
ReporterId = <<"user123">>,
{ok, Report} = core_report:create(ReporterId, event, <<"ev1">>, <<"Bad">>),
{ok, Found} = core_report:get_by_id(Report#report.id),
?assertEqual(Report#report.id, Found#report.id),
{error, not_found} = core_report:get_by_id(<<"nonexistent">>).
test_list_by_target() ->
User1 = <<"user1">>,
User2 = <<"user2">>,
EventId = <<"event123">>,
{ok, _} = core_report:create(User1, event, EventId, <<"Reason 1">>),
{ok, _} = core_report:create(User2, event, EventId, <<"Reason 2">>),
{ok, _} = core_report:create(User1, calendar, <<"cal1">>, <<"Reason 3">>),
{ok, Reports} = core_report:list_by_target(event, EventId),
?assertEqual(2, length(Reports)).
test_list_by_reporter() ->
User1 = <<"user1">>,
User2 = <<"user2">>,
{ok, _} = core_report:create(User1, event, <<"ev1">>, <<"">>),
{ok, _} = core_report:create(User1, event, <<"ev2">>, <<"">>),
{ok, _} = core_report:create(User2, event, <<"ev3">>, <<"">>),
{ok, Reports} = core_report:list_by_reporter(User1),
?assertEqual(2, length(Reports)).
test_update_status() ->
ReporterId = <<"user123">>,
AdminId = <<"admin123">>,
{ok, Report} = core_report:create(ReporterId, event, <<"ev1">>, <<"">>),
{ok, Updated} = core_report:update_status(Report#report.id, reviewed, AdminId),
?assertEqual(reviewed, Updated#report.status),
?assertEqual(AdminId, Updated#report.resolved_by),
?assert(Updated#report.resolved_at =/= undefined).
test_get_count_by_target() ->
User1 = <<"user1">>,
User2 = <<"user2">>,
EventId = <<"event123">>,
?assertEqual(0, core_report:get_count_by_target(event, EventId)),
{ok, _} = core_report:create(User1, event, EventId, <<"">>),
?assertEqual(1, core_report:get_count_by_target(event, EventId)),
{ok, _} = core_report:create(User2, event, EventId, <<"">>),
?assertEqual(2, core_report:get_count_by_target(event, EventId)).

View File

@@ -0,0 +1,131 @@
-module(core_review_tests).
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
setup() ->
mnesia:start(),
mnesia:create_table(review, [
{attributes, record_info(fields, review)},
{ram_copies, [node()]}
]),
ok.
cleanup(_) ->
mnesia:delete_table(review),
mnesia:stop(),
ok.
core_review_test_() ->
{foreach,
fun setup/0,
fun cleanup/1,
[
{"Create review test", fun test_create_review/0},
{"Get review by id test", fun test_get_by_id/0},
{"List reviews by target test", fun test_list_by_target/0},
{"List reviews by user test", fun test_list_by_user/0},
{"Update review test", fun test_update_review/0},
{"Delete review test", fun test_delete_review/0},
{"Hide/unhide review test", fun test_hide_unhide/0},
{"Average rating test", fun test_average_rating/0},
{"Has user reviewed test", fun test_has_user_reviewed/0}
]}.
test_create_review() ->
UserId = <<"user123">>,
TargetType = event,
TargetId = <<"event123">>,
Rating = 5,
Comment = <<"Great event!">>,
{ok, Review} = core_review:create(UserId, TargetType, TargetId, Rating, Comment),
?assertEqual(UserId, Review#review.user_id),
?assertEqual(TargetType, Review#review.target_type),
?assertEqual(TargetId, Review#review.target_id),
?assertEqual(Rating, Review#review.rating),
?assertEqual(Comment, Review#review.comment),
?assertEqual(visible, Review#review.status),
?assert(is_binary(Review#review.id)).
test_get_by_id() ->
UserId = <<"user123">>,
{ok, Review} = core_review:create(UserId, event, <<"ev1">>, 4, <<"Good">>),
{ok, Found} = core_review:get_by_id(Review#review.id),
?assertEqual(Review#review.id, Found#review.id),
{error, not_found} = core_review:get_by_id(<<"nonexistent">>).
test_list_by_target() ->
UserId1 = <<"user1">>,
UserId2 = <<"user2">>,
EventId = <<"event123">>,
{ok, _} = core_review:create(UserId1, event, EventId, 5, <<"Awesome">>),
{ok, _} = core_review:create(UserId2, event, EventId, 3, <<"Okay">>),
{ok, _} = core_review:create(UserId1, calendar, <<"cal1">>, 4, <<"Nice">>),
{ok, Reviews} = core_review:list_by_target(event, EventId),
?assertEqual(2, length(Reviews)).
test_list_by_user() ->
UserId1 = <<"user1">>,
UserId2 = <<"user2">>,
{ok, _} = core_review:create(UserId1, event, <<"ev1">>, 5, <<"">>),
{ok, _} = core_review:create(UserId1, event, <<"ev2">>, 4, <<"">>),
{ok, _} = core_review:create(UserId2, event, <<"ev3">>, 3, <<"">>),
{ok, Reviews} = core_review:list_by_user(UserId1),
?assertEqual(2, length(Reviews)).
test_update_review() ->
UserId = <<"user123">>,
{ok, Review} = core_review:create(UserId, event, <<"ev1">>, 3, <<"Old">>),
timer:sleep(2000),
Updates = [{rating, 5}, {comment, <<"Updated!">>}],
{ok, Updated} = core_review:update(Review#review.id, Updates),
?assertEqual(5, Updated#review.rating),
?assertEqual(<<"Updated!">>, Updated#review.comment),
?assert(Updated#review.updated_at > Review#review.updated_at).
test_delete_review() ->
UserId = <<"user123">>,
{ok, Review} = core_review:create(UserId, event, <<"ev1">>, 4, <<"">>),
{ok, deleted} = core_review:delete(Review#review.id),
{error, not_found} = core_review:get_by_id(Review#review.id).
test_hide_unhide() ->
UserId = <<"user123">>,
{ok, Review} = core_review:create(UserId, event, <<"ev1">>, 4, <<"">>),
{ok, Hidden} = core_review:hide(Review#review.id),
?assertEqual(hidden, Hidden#review.status),
{ok, Unhidden} = core_review:unhide(Review#review.id),
?assertEqual(visible, Unhidden#review.status).
test_average_rating() ->
UserId1 = <<"user1">>,
UserId2 = <<"user2">>,
EventId = <<"event123">>,
{ok, _} = core_review:create(UserId1, event, EventId, 5, <<"">>),
{ok, _} = core_review:create(UserId2, event, EventId, 3, <<"">>),
{Avg, Count} = core_review:get_average_rating(event, EventId),
?assertEqual(4.0, Avg),
?assertEqual(2, Count).
test_has_user_reviewed() ->
UserId = <<"user123">>,
EventId = <<"event123">>,
?assertNot(core_review:has_user_reviewed(UserId, event, EventId)),
{ok, _} = core_review:create(UserId, event, EventId, 5, <<"">>),
?assert(core_review:has_user_reviewed(UserId, event, EventId)).

View File

@@ -0,0 +1,90 @@
-module(core_subscription_tests).
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
setup() ->
mnesia:start(),
mnesia:create_table(subscription, [
{attributes, record_info(fields, subscription)},
{ram_copies, [node()]}
]),
mnesia:create_table(calendar, [
{attributes, record_info(fields, calendar)},
{ram_copies, [node()]}
]),
ok.
cleanup(_) ->
mnesia:delete_table(calendar),
mnesia:delete_table(subscription),
mnesia:stop(),
ok.
core_subscription_test_() ->
{foreach,
fun setup/0,
fun cleanup/1,
[
{"Create trial subscription", fun test_create_trial/0},
{"Create paid subscription", fun test_create_paid/0},
{"Get active by user", fun test_get_active_by_user/0},
{"List by user", fun test_list_by_user/0},
{"Update status", fun test_update_status/0},
{"Check expired", fun test_check_expired/0}
]}.
test_create_trial() ->
UserId = <<"user123">>,
{ok, Sub} = core_subscription:create(UserId, trial, false),
?assertEqual(UserId, Sub#subscription.user_id),
?assertEqual(trial, Sub#subscription.plan),
?assertEqual(false, Sub#subscription.trial_used),
?assertEqual(active, Sub#subscription.status).
test_create_paid() ->
UserId = <<"user123">>,
{ok, Sub} = core_subscription:create(UserId, monthly, true),
?assertEqual(UserId, Sub#subscription.user_id),
?assertEqual(monthly, Sub#subscription.plan),
?assertEqual(true, Sub#subscription.trial_used),
?assertEqual(active, Sub#subscription.status).
test_get_active_by_user() ->
UserId = <<"user123">>,
{ok, Sub1} = core_subscription:create(UserId, trial, false),
{ok, Sub2} = core_subscription:create(UserId, monthly, true),
core_subscription:update_status(Sub1#subscription.id, expired),
{ok, Active} = core_subscription:get_active_by_user(UserId),
?assertEqual(Sub2#subscription.id, Active#subscription.id).
test_list_by_user() ->
UserId = <<"user123">>,
{ok, _} = core_subscription:create(UserId, trial, false),
{ok, _} = core_subscription:create(UserId, monthly, true),
{ok, Subs} = core_subscription:list_by_user(UserId),
?assertEqual(2, length(Subs)).
test_update_status() ->
UserId = <<"user123">>,
{ok, Sub} = core_subscription:create(UserId, trial, false),
{ok, Cancelled} = core_subscription:update_status(Sub#subscription.id, cancelled),
?assertEqual(cancelled, Cancelled#subscription.status).
test_check_expired() ->
UserId = <<"user123">>,
% Создаём подписку с истёкшим сроком
{ok, Sub} = core_subscription:create(UserId, trial, false),
% Принудительно устанавливаем expires_at в прошлое
Past = {{2020, 1, 1}, {0, 0, 0}},
mnesia:dirty_write(Sub#subscription{expires_at = Past}),
core_subscription:check_expired(),
{ok, Updated} = core_subscription:get_by_id(Sub#subscription.id),
?assertEqual(expired, Updated#subscription.status).

View File

@@ -0,0 +1,121 @@
-module(core_ticket_tests).
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
setup() ->
mnesia:start(),
mnesia:create_table(ticket, [
{attributes, record_info(fields, ticket)},
{ram_copies, [node()]}
]),
ok.
cleanup(_) ->
mnesia:delete_table(ticket),
mnesia:stop(),
ok.
core_ticket_test_() ->
{foreach,
fun setup/0,
fun cleanup/1,
[
{"Create ticket test", fun test_create_ticket/0},
{"Update existing ticket test", fun test_update_ticket/0},
{"Get ticket by id test", fun test_get_by_id/0},
{"Get ticket by error hash test", fun test_get_by_error_hash/0},
{"List all tickets test", fun test_list_all/0},
{"List by status test", fun test_list_by_status/0},
{"Update status test", fun test_update_status/0},
{"Assign ticket test", fun test_assign_ticket/0},
{"Add resolution test", fun test_add_resolution/0}
]}.
test_create_ticket() ->
ErrorMsg = <<"Test error">>,
Stacktrace = <<"line 1\nline 2">>,
Context = #{user_id => <<"user123">>},
{ok, Ticket} = core_ticket:create_or_update(ErrorMsg, Stacktrace, Context),
?assertEqual(ErrorMsg, Ticket#ticket.error_message),
?assertEqual(Stacktrace, Ticket#ticket.stacktrace),
?assertEqual(1, Ticket#ticket.count),
?assertEqual(open, Ticket#ticket.status),
?assert(is_binary(Ticket#ticket.id)),
?assert(is_binary(Ticket#ticket.error_hash)).
test_update_ticket() ->
ErrorMsg = <<"Test error">>,
Stacktrace = <<"line 1">>,
Context = #{},
{ok, Ticket1} = core_ticket:create_or_update(ErrorMsg, Stacktrace, Context),
?assertEqual(1, Ticket1#ticket.count),
{ok, Ticket2} = core_ticket:create_or_update(ErrorMsg, Stacktrace, Context),
?assertEqual(Ticket1#ticket.id, Ticket2#ticket.id),
?assertEqual(2, Ticket2#ticket.count),
?assert(Ticket2#ticket.last_seen >= Ticket1#ticket.last_seen).
test_get_by_id() ->
{ok, Ticket} = core_ticket:create_or_update(<<"Error">>, <<"">>, #{}),
{ok, Found} = core_ticket:get_by_id(Ticket#ticket.id),
?assertEqual(Ticket#ticket.id, Found#ticket.id),
{error, not_found} = core_ticket:get_by_id(<<"nonexistent">>).
test_get_by_error_hash() ->
ErrorMsg = <<"Unique error">>,
Stacktrace = <<"stack">>,
{ok, Ticket} = core_ticket:create_or_update(ErrorMsg, Stacktrace, #{}),
{ok, Found} = core_ticket:get_by_error_hash(Ticket#ticket.error_hash),
?assertEqual(Ticket#ticket.id, Found#ticket.id),
{error, not_found} = core_ticket:get_by_error_hash(<<"badhash">>).
test_list_all() ->
{ok, _} = core_ticket:create_or_update(<<"Error 1">>, <<"">>, #{}),
{ok, _} = core_ticket:create_or_update(<<"Error 2">>, <<"">>, #{}),
{ok, _} = core_ticket:create_or_update(<<"Error 3">>, <<"">>, #{}),
{ok, Tickets} = core_ticket:list_all(),
?assertEqual(3, length(Tickets)).
test_list_by_status() ->
{ok, T1} = core_ticket:create_or_update(<<"E1">>, <<"">>, #{}),
{ok, T2} = core_ticket:create_or_update(<<"E2">>, <<"">>, #{}),
core_ticket:update_status(T2#ticket.id, resolved),
{ok, Open} = core_ticket:list_by_status(open),
?assertEqual(1, length(Open)),
{ok, Resolved} = core_ticket:list_by_status(resolved),
?assertEqual(1, length(Resolved)).
test_update_status() ->
{ok, Ticket} = core_ticket:create_or_update(<<"Error">>, <<"">>, #{}),
{ok, Updated} = core_ticket:update_status(Ticket#ticket.id, in_progress),
?assertEqual(in_progress, Updated#ticket.status),
{ok, Resolved} = core_ticket:update_status(Ticket#ticket.id, resolved),
?assertEqual(resolved, Resolved#ticket.status).
test_assign_ticket() ->
AdminId = <<"admin123">>,
{ok, Ticket} = core_ticket:create_or_update(<<"Error">>, <<"">>, #{}),
{ok, Assigned} = core_ticket:assign(Ticket#ticket.id, AdminId),
?assertEqual(AdminId, Assigned#ticket.assigned_to),
?assertEqual(in_progress, Assigned#ticket.status).
test_add_resolution() ->
Note = <<"Fixed in version 1.0">>,
{ok, Ticket} = core_ticket:create_or_update(<<"Error">>, <<"">>, #{}),
{ok, Updated} = core_ticket:add_resolution(Ticket#ticket.id, Note),
?assertEqual(Note, Updated#ticket.resolution_note).

View File

@@ -0,0 +1,23 @@
-module(handler_search_tests).
-include_lib("eunit/include/eunit.hrl").
handler_search_test_() ->
case is_server_running() of
true ->
[
{"Search API requires authentication", fun test_search_requires_auth/0}
];
false ->
io:format("Skipping handler tests: server not running~n"),
[]
end.
is_server_running() ->
case httpc:request(get, {"http://localhost:8080/health", []}, [], [{timeout, 1000}]) of
{ok, {{_, 200, _}, _, _}} -> true;
_ -> false
end.
test_search_requires_auth() ->
{ok, {{_, 401, _}, _, Body}} = httpc:request(get, {"http://localhost:8080/v1/search?type=event", []}, [], []),
?assertMatch(#{<<"error">> := _}, jsx:decode(Body, [return_maps])).

View File

@@ -0,0 +1,46 @@
-module(logic_auth_tests).
-include_lib("eunit/include/eunit.hrl").
logic_auth_test_() ->
[
{"Password hash test", fun test_password_hash/0},
{"JWT generate and verify test", fun test_jwt/0},
{"JWT expired test", fun test_jwt_expired/0},
{"Refresh token test", fun test_refresh_token/0}
].
test_password_hash() ->
Password = <<"secret123">>,
{ok, Hash} = logic_auth:hash_password(Password),
?assert(is_binary(Hash)),
{ok, true} = logic_auth:verify_password(Password, Hash),
{ok, false} = logic_auth:verify_password(<<"wrong">>, Hash).
test_jwt() ->
UserId = <<"user123">>,
Role = user,
Token = logic_auth:generate_jwt(UserId, Role),
?assert(is_binary(Token)),
{ok, Claims} = logic_auth:verify_jwt(Token),
?assertEqual(UserId, maps:get(<<"user_id">>, Claims)),
?assertEqual(<<"user">>, maps:get(<<"role">>, Claims)),
?assert(maps:is_key(<<"exp">>, Claims)),
?assert(maps:is_key(<<"iat">>, Claims)),
% Проверка невалидного токена
{error, invalid_token} = logic_auth:verify_jwt(<<"invalid.token.here">>).
test_jwt_expired() ->
% Пропускаем для простоты, так как требует мока времени
ok.
test_refresh_token() ->
{Token, ExpiresAt} = logic_auth:generate_refresh_token(<<"user123">>),
?assert(is_binary(Token)),
?assert(size(Token) >= 32),
?assert(is_tuple(ExpiresAt)),
% Проверяем, что срок действия в будущем
Now = calendar:universal_time(),
?assert(ExpiresAt > Now).

View File

@@ -0,0 +1,212 @@
-module(logic_booking_tests).
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
-define(TEST_EMAIL, <<"test@example.com">>).
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(event, [
{attributes, record_info(fields, event)},
{ram_copies, [node()]}
]),
mnesia:create_table(booking, [
{attributes, record_info(fields, booking)},
{ram_copies, [node()]}
]),
ok.
cleanup(_) ->
mnesia:delete_table(booking),
mnesia:delete_table(event),
mnesia:delete_table(calendar),
mnesia:delete_table(user),
mnesia:stop(),
ok.
logic_booking_test_() ->
{foreach,
fun setup/0,
fun cleanup/1,
[
{"Create booking with auto confirmation", fun test_create_booking_auto/0},
{"Create booking with manual confirmation", fun test_create_booking_manual/0},
{"Create booking with timeout confirmation", fun test_create_booking_timeout/0},
{"Create duplicate booking", fun test_create_duplicate_booking/0},
{"Create booking for inactive event", fun test_booking_inactive_event/0},
{"Create booking when event is full", fun test_booking_event_full/0},
{"Confirm booking by owner", fun test_confirm_booking/0},
{"Decline booking by owner", fun test_decline_booking/0},
{"Cancel booking by participant", fun test_cancel_booking/0},
{"Unauthorized confirm attempt", fun test_unauthorized_confirm/0},
{"List event bookings", fun test_list_event_bookings/0},
{"List user bookings", fun test_list_user_bookings/0}
]}.
%% Вспомогательные функции
create_test_user(Role) ->
UserId = base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}),
User = #user{
id = UserId,
email = <<UserId/binary, "@test.com">>,
password_hash = <<"hash">>,
role = Role,
status = active,
created_at = calendar:universal_time(),
updated_at = calendar:universal_time()
},
mnesia:dirty_write(User),
UserId.
create_test_calendar(OwnerId, Confirmation) ->
{ok, Calendar} = core_calendar:create(OwnerId, <<"Test Calendar">>, <<"">>, Confirmation),
Calendar#calendar.id.
create_test_event(CalendarId) ->
StartTime = {{2026, 6, 1}, {10, 0, 0}},
{ok, Event} = core_event:create(CalendarId, <<"Test Event">>, StartTime, 60),
Event#event.id.
create_test_event_with_capacity(CalendarId, Capacity) ->
StartTime = {{2026, 6, 1}, {10, 0, 0}},
{ok, Event} = core_event:create(CalendarId, <<"Test Event">>, StartTime, 60),
{ok, Updated} = core_event:update(Event#event.id, [{capacity, Capacity}]),
Updated#event.id.
%% Тесты
test_create_booking_auto() ->
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, auto),
EventId = create_test_event(CalendarId),
{ok, Booking} = logic_booking:create_booking(ParticipantId, EventId),
timer:sleep(100),
{ok, Updated} = core_booking:get_by_id(Booking#booking.id),
?assertEqual(confirmed, Updated#booking.status).
test_create_booking_manual() ->
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, manual),
EventId = create_test_event(CalendarId),
{ok, Booking} = logic_booking:create_booking(ParticipantId, EventId),
?assertEqual(pending, Booking#booking.status).
test_create_booking_timeout() ->
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, {timeout, 1}),
EventId = create_test_event(CalendarId),
{ok, Booking} = logic_booking:create_booking(ParticipantId, EventId),
?assertEqual(pending, Booking#booking.status),
timer:sleep(1500),
{ok, Updated} = core_booking:get_by_id(Booking#booking.id),
?assertEqual(confirmed, Updated#booking.status).
test_create_duplicate_booking() ->
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, manual),
EventId = create_test_event(CalendarId),
{ok, _} = logic_booking:create_booking(ParticipantId, EventId),
{error, already_booked} = logic_booking:create_booking(ParticipantId, EventId).
test_booking_inactive_event() ->
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, manual),
EventId = create_test_event(CalendarId),
{ok, _} = core_event:update(EventId, [{status, cancelled}]),
{error, event_not_active} = logic_booking:create_booking(ParticipantId, EventId).
test_booking_event_full() ->
OwnerId = create_test_user(user),
Participant1Id = create_test_user(user),
Participant2Id = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, auto),
EventId = create_test_event_with_capacity(CalendarId, 1),
{ok, _} = logic_booking:create_booking(Participant1Id, EventId),
{error, event_full} = logic_booking:create_booking(Participant2Id, EventId).
test_confirm_booking() ->
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, manual),
EventId = create_test_event(CalendarId),
{ok, Booking} = logic_booking:create_booking(ParticipantId, EventId),
{ok, Confirmed} = logic_booking:confirm_booking(OwnerId, Booking#booking.id, confirm),
?assertEqual(confirmed, Confirmed#booking.status).
test_decline_booking() ->
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, manual),
EventId = create_test_event(CalendarId),
{ok, Booking} = logic_booking:create_booking(ParticipantId, EventId),
{ok, Declined} = logic_booking:confirm_booking(OwnerId, Booking#booking.id, decline),
?assertEqual(cancelled, Declined#booking.status).
test_cancel_booking() ->
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, manual),
EventId = create_test_event(CalendarId),
{ok, Booking} = logic_booking:create_booking(ParticipantId, EventId),
{ok, Cancelled} = logic_booking:cancel_booking(ParticipantId, Booking#booking.id),
?assertEqual(cancelled, Cancelled#booking.status).
test_unauthorized_confirm() ->
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
OtherId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, manual),
EventId = create_test_event(CalendarId),
{ok, Booking} = logic_booking:create_booking(ParticipantId, EventId),
{error, access_denied} = logic_booking:confirm_booking(OtherId, Booking#booking.id, confirm).
test_list_event_bookings() ->
OwnerId = create_test_user(user),
Participant1Id = create_test_user(user),
Participant2Id = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, manual),
EventId = create_test_event(CalendarId),
{ok, _} = logic_booking:create_booking(Participant1Id, EventId),
{ok, _} = logic_booking:create_booking(Participant2Id, EventId),
{ok, Bookings} = logic_booking:list_event_bookings(OwnerId, EventId),
?assertEqual(2, length(Bookings)).
test_list_user_bookings() ->
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, manual),
EventId1 = create_test_event(CalendarId),
EventId2 = create_test_event(CalendarId),
{ok, _} = logic_booking:create_booking(ParticipantId, EventId1),
{ok, _} = logic_booking:create_booking(ParticipantId, EventId2),
{ok, Bookings} = logic_booking:list_user_bookings(ParticipantId),
?assertEqual(2, length(Bookings)).

View File

@@ -0,0 +1,124 @@
-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()]}
]),
ok.
cleanup(_) ->
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},
{"Get calendar test", fun test_get_calendar/0},
{"List calendars test", fun test_list_calendars/0},
{"Update calendar test", fun test_update_calendar/0},
{"Delete calendar test", fun test_delete_calendar/0},
{"Access control test", fun test_access_control/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,
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_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_calendar:create_calendar(UserId, <<"Calendar 2">>, <<"">>, auto),
{ok, Calendars} = logic_calendar:list_calendars(UserId),
?assertEqual(2, length(Calendars)).
test_update_calendar() ->
UserId = create_test_user(),
{ok, Calendar} = logic_calendar:create_calendar(UserId, <<"Original">>, <<"">>, manual),
Updates = [{title, <<"Updated">>}, {type, commercial}, {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_delete_calendar() ->
UserId = create_test_user(),
{ok, Calendar} = logic_calendar:create_calendar(UserId, <<"Test">>, <<"">>, manual),
{ok, Deleted} = logic_calendar:delete_calendar(UserId, Calendar#calendar.id),
?assertEqual(deleted, Deleted#calendar.status),
?assertMatch({error, access_denied}, logic_calendar:get_calendar(UserId, Calendar#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, CommercialCalendar} = logic_calendar:update_calendar(OwnerId, PersonalCalendar#calendar.id, [{type, 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)).

View File

@@ -0,0 +1,151 @@
-module(logic_event_recurring_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(event, [
{attributes, record_info(fields, event)},
{ram_copies, [node()]}
]),
mnesia:create_table(recurrence_exception, [
{attributes, record_info(fields, recurrence_exception)},
{ram_copies, [node()]}
]),
ok.
cleanup(_) ->
mnesia:delete_table(recurrence_exception),
mnesia:delete_table(event),
mnesia:delete_table(calendar),
mnesia:delete_table(user),
mnesia:stop(),
ok.
logic_event_recurring_test_() ->
{foreach,
fun setup/0,
fun cleanup/1,
[
{"Create recurring event test", fun test_create_recurring_event/0},
{"Create recurring event invalid RRULE test", fun test_create_recurring_invalid/0},
{"Get occurrences test", fun test_get_occurrences/0},
{"Cancel occurrence test", fun test_cancel_occurrence/0},
{"Get occurrences with cancelled test", fun test_occurrences_with_cancelled/0},
{"Materialize for booking test", fun test_materialize_for_booking/0}
]}.
create_test_user_and_calendar() ->
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,
created_at = calendar:universal_time(),
updated_at = calendar:universal_time()
},
mnesia:dirty_write(User),
{ok, Calendar} = logic_calendar:create_calendar(UserId, <<"Test Calendar">>, <<"">>, manual),
{UserId, Calendar#calendar.id}.
test_create_recurring_event() ->
{UserId, CalendarId} = create_test_user_and_calendar(),
Title = <<"Weekly Meeting">>,
StartTime = {{2026, 6, 1}, {10, 0, 0}},
Duration = 60,
RRule = #{<<"freq">> => <<"WEEKLY">>, <<"interval">> => 1},
{ok, Event} = logic_event:create_recurring_event(UserId, CalendarId, Title, StartTime, Duration, RRule),
?assertEqual(CalendarId, Event#event.calendar_id),
?assertEqual(Title, Event#event.title),
?assertEqual(recurring, Event#event.event_type).
test_create_recurring_invalid() ->
{UserId, CalendarId} = create_test_user_and_calendar(),
Title = <<"Invalid">>,
StartTime = {{2026, 6, 1}, {10, 0, 0}},
Duration = 60,
InvalidRRule = #{<<"freq">> => <<"YEARLY">>, <<"interval">> => 1},
{error, invalid_rrule} = logic_event:create_recurring_event(
UserId, CalendarId, Title, StartTime, Duration, InvalidRRule
).
test_get_occurrences() ->
{UserId, CalendarId} = create_test_user_and_calendar(),
StartTime = {{2026, 6, 1}, {10, 0, 0}},
RRule = #{<<"freq">> => <<"WEEKLY">>, <<"interval">> => 1},
{ok, Event} = logic_event:create_recurring_event(
UserId, CalendarId, <<"Weekly">>, StartTime, 60, RRule
),
RangeEnd = {{2026, 6, 29}, {10, 0, 0}},
{ok, Occurrences} = logic_event:get_occurrences(UserId, Event#event.id, RangeEnd),
?assertEqual(5, length(Occurrences)).
test_cancel_occurrence() ->
{UserId, CalendarId} = create_test_user_and_calendar(),
StartTime = {{2026, 6, 1}, {10, 0, 0}},
RRule = #{<<"freq">> => <<"WEEKLY">>, <<"interval">> => 1},
{ok, Event} = logic_event:create_recurring_event(
UserId, CalendarId, <<"Weekly">>, StartTime, 60, RRule
),
CancelTime = {{2026, 6, 8}, {10, 0, 0}},
{ok, cancelled} = logic_event:cancel_occurrence(UserId, Event#event.id, CancelTime).
test_occurrences_with_cancelled() ->
{UserId, CalendarId} = create_test_user_and_calendar(),
StartTime = {{2026, 6, 1}, {10, 0, 0}},
RRule = #{<<"freq">> => <<"WEEKLY">>, <<"interval">> => 1},
{ok, Event} = logic_event:create_recurring_event(
UserId, CalendarId, <<"Weekly">>, StartTime, 60, RRule
),
CancelTime = {{2026, 6, 8}, {10, 0, 0}},
{ok, cancelled} = logic_event:cancel_occurrence(UserId, Event#event.id, CancelTime),
RangeEnd = {{2026, 6, 29}, {10, 0, 0}},
{ok, Occurrences} = logic_event:get_occurrences(UserId, Event#event.id, RangeEnd),
?assertEqual(4, length(Occurrences)),
Starts = [O || {virtual, O} <- Occurrences],
?assertNot(lists:member(CancelTime, Starts)).
test_materialize_for_booking() ->
{UserId, CalendarId} = create_test_user_and_calendar(),
StartTime = {{2026, 6, 1}, {10, 0, 0}},
RRule = #{<<"freq">> => <<"WEEKLY">>, <<"interval">> => 1},
{ok, Event} = logic_event:create_recurring_event(
UserId, CalendarId, <<"Weekly">>, StartTime, 60, RRule
),
OccurrenceStart = {{2026, 6, 8}, {10, 0, 0}},
SpecialistId = <<"specialist123">>,
{ok, Instance} = logic_event:materialize_for_booking(
Event#event.id, OccurrenceStart, SpecialistId
),
?assertEqual(Event#event.id, Instance#event.master_id),
?assertEqual(OccurrenceStart, Instance#event.start_time),
?assertEqual(SpecialistId, Instance#event.specialist_id),
?assertEqual(true, Instance#event.is_instance).

View File

@@ -0,0 +1,106 @@
-module(logic_event_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(event, [
{attributes, record_info(fields, event)},
{ram_copies, [node()]}
]),
ok.
cleanup(_) ->
mnesia:delete_table(event),
mnesia:delete_table(calendar),
mnesia:delete_table(user),
mnesia:stop(),
ok.
logic_event_test_() ->
{foreach,
fun setup/0,
fun cleanup/1,
[
{"Create event test", fun test_create_event/0},
{"Get event test", fun test_get_event/0},
{"List events test", fun test_list_events/0},
{"Update event test", fun test_update_event/0},
{"Delete event test", fun test_delete_event/0},
{"Event time validation test", fun test_time_validation/0}
]}.
create_test_user_and_calendar() ->
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,
created_at = calendar:universal_time(),
updated_at = calendar:universal_time()
},
mnesia:dirty_write(User),
{ok, Calendar} = logic_calendar:create_calendar(UserId, <<"Test Calendar">>, <<"">>, manual),
{UserId, Calendar#calendar.id}.
test_create_event() ->
{UserId, CalendarId} = create_test_user_and_calendar(),
Title = <<"Test Event">>,
StartTime = {{2026, 5, 1}, {10, 0, 0}},
Duration = 60,
{ok, Event} = logic_event:create_event(UserId, CalendarId, Title, StartTime, Duration),
?assertEqual(CalendarId, Event#event.calendar_id),
?assertEqual(Title, Event#event.title),
?assertEqual(single, Event#event.event_type).
test_get_event() ->
{UserId, CalendarId} = create_test_user_and_calendar(),
{ok, Event} = logic_event:create_event(UserId, CalendarId, <<"Test">>, {{2026, 5, 1}, {10, 0, 0}}, 60),
{ok, Found} = logic_event:get_event(UserId, Event#event.id),
?assertEqual(Event#event.id, Found#event.id).
test_list_events() ->
{UserId, CalendarId} = create_test_user_and_calendar(),
{ok, _} = logic_event:create_event(UserId, CalendarId, <<"Event 1">>, {{2026, 5, 1}, {10, 0, 0}}, 60),
{ok, _} = logic_event:create_event(UserId, CalendarId, <<"Event 2">>, {{2026, 5, 2}, {11, 0, 0}}, 90),
{ok, Events} = logic_event:list_events(UserId, CalendarId),
?assertEqual(2, length(Events)).
test_update_event() ->
{UserId, CalendarId} = create_test_user_and_calendar(),
{ok, Event} = logic_event:create_event(UserId, CalendarId, <<"Original">>, {{2026, 5, 1}, {10, 0, 0}}, 60),
Updates = [{title, <<"Updated">>}, {capacity, 50}],
{ok, Updated} = logic_event:update_event(UserId, Event#event.id, Updates),
?assertEqual(<<"Updated">>, Updated#event.title),
?assertEqual(50, Updated#event.capacity).
test_delete_event() ->
{UserId, CalendarId} = create_test_user_and_calendar(),
{ok, Event} = logic_event:create_event(UserId, CalendarId, <<"Test">>, {{2026, 5, 1}, {10, 0, 0}}, 60),
{ok, Deleted} = logic_event:delete_event(UserId, Event#event.id),
?assertEqual(deleted, Deleted#event.status).
test_time_validation() ->
{UserId, CalendarId} = create_test_user_and_calendar(),
PastTime = {{2020, 1, 1}, {10, 0, 0}},
{error, event_in_past} = logic_event:create_event(UserId, CalendarId, <<"Past">>, PastTime, 60),
FutureTime = {{2030, 1, 1}, {10, 0, 0}},
?assertEqual(ok, logic_event:validate_event_time(FutureTime)).

View File

@@ -0,0 +1,146 @@
-module(logic_moderation_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(event, [{attributes, record_info(fields, event)}, {ram_copies, [node()]}]),
mnesia:create_table(report, [{attributes, record_info(fields, report)}, {ram_copies, [node()]}]),
mnesia:create_table(banned_word, [{attributes, record_info(fields, banned_word)}, {ram_copies, [node()]}]),
ok.
cleanup(_) ->
mnesia:delete_table(banned_word),
mnesia:delete_table(report),
mnesia:delete_table(event),
mnesia:delete_table(calendar),
mnesia:delete_table(user),
mnesia:stop(),
ok.
logic_moderation_test_() ->
{foreach,
fun setup/0,
fun cleanup/1,
[
{"Create report test", fun test_create_report/0},
{"Get reports test", fun test_get_reports/0},
{"Resolve report test", fun test_resolve_report/0},
{"Add banned word test", fun test_add_banned_word/0},
{"Remove banned word test", fun test_remove_banned_word/0},
{"Auto freeze by reports test", fun test_auto_freeze/0},
{"Freeze/unfreeze calendar test", fun test_freeze_calendar/0},
{"Freeze/unfreeze event test", fun test_freeze_event/0},
{"Check content test", fun test_check_content/0}
]}.
create_test_user(Role) ->
UserId = base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}),
User = #user{id = UserId, email = <<UserId/binary, "@test.com">>, password_hash = <<"hash">>,
role = Role, status = active, created_at = calendar:universal_time(), updated_at = calendar:universal_time()},
mnesia:dirty_write(User),
UserId.
create_test_calendar(OwnerId) ->
{ok, Calendar} = core_calendar:create(OwnerId, <<"Test">>, <<"">>, manual),
Calendar#calendar.id.
create_test_event(CalendarId) ->
{ok, Event} = core_event:create(CalendarId, <<"Event">>, {{2026, 6, 1}, {10, 0, 0}}, 60),
Event#event.id.
test_create_report() ->
ReporterId = create_test_user(user),
OwnerId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId),
EventId = create_test_event(CalendarId),
{ok, Report} = logic_moderation:create_report(ReporterId, event, EventId, <<"Bad content">>),
?assertEqual(ReporterId, Report#report.reporter_id),
?assertEqual(pending, Report#report.status).
test_get_reports() ->
AdminId = create_test_user(admin),
ReporterId = create_test_user(user),
OwnerId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId),
EventId = create_test_event(CalendarId),
{ok, _} = logic_moderation:create_report(ReporterId, event, EventId, <<"">>),
{ok, Reports} = logic_moderation:get_reports(AdminId),
?assertEqual(1, length(Reports)).
test_resolve_report() ->
AdminId = create_test_user(admin),
ReporterId = create_test_user(user),
OwnerId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId),
EventId = create_test_event(CalendarId),
{ok, Report} = logic_moderation:create_report(ReporterId, event, EventId, <<"">>),
{ok, Resolved} = logic_moderation:resolve_report(AdminId, Report#report.id, reviewed),
?assertEqual(reviewed, Resolved#report.status),
?assertEqual(AdminId, Resolved#report.resolved_by).
test_add_banned_word() ->
AdminId = create_test_user(admin),
{ok, _} = logic_moderation:add_banned_word(AdminId, <<"badword">>),
?assert(core_banned_word:is_banned(<<"badword">>)).
test_remove_banned_word() ->
AdminId = create_test_user(admin),
{ok, _} = logic_moderation:add_banned_word(AdminId, <<"badword">>),
{ok, removed} = logic_moderation:remove_banned_word(AdminId, <<"badword">>),
?assertNot(core_banned_word:is_banned(<<"badword">>)).
test_auto_freeze() ->
Reporter1 = create_test_user(user),
Reporter2 = create_test_user(user),
Reporter3 = create_test_user(user),
OwnerId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId),
EventId = create_test_event(CalendarId),
% 3 жалобы должны заморозить событие
{ok, _} = logic_moderation:create_report(Reporter1, event, EventId, <<"">>),
{ok, _} = logic_moderation:create_report(Reporter2, event, EventId, <<"">>),
{ok, _} = logic_moderation:create_report(Reporter3, event, EventId, <<"">>),
{ok, Event} = core_event:get_by_id(EventId),
?assertEqual(frozen, Event#event.status).
test_freeze_calendar() ->
AdminId = create_test_user(admin),
OwnerId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId),
{ok, Frozen} = logic_moderation:freeze_calendar(AdminId, CalendarId),
?assertEqual(frozen, Frozen#calendar.status),
{ok, Unfrozen} = logic_moderation:unfreeze_calendar(AdminId, CalendarId),
?assertEqual(active, Unfrozen#calendar.status).
test_freeze_event() ->
AdminId = create_test_user(admin),
OwnerId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId),
EventId = create_test_event(CalendarId),
{ok, Frozen} = logic_moderation:freeze_event(AdminId, EventId),
?assertEqual(frozen, Frozen#event.status),
{ok, Unfrozen} = logic_moderation:unfreeze_event(AdminId, EventId),
?assertEqual(active, Unfrozen#event.status).
test_check_content() ->
AdminId = create_test_user(admin),
{ok, _} = logic_moderation:add_banned_word(AdminId, <<"bad">>),
?assertNot(logic_moderation:check_content(<<"Hello">>)),
?assert(logic_moderation:check_content(<<"This is bad">>)),
?assertEqual(<<"Hello">>, logic_moderation:auto_moderate(<<"Hello">>)),
?assertEqual(<<"This is ***">>, logic_moderation:auto_moderate(<<"This is bad">>)).

View File

@@ -0,0 +1,108 @@
-module(logic_notification_tests).
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
setup() ->
pg:start_link(),
mnesia:start(),
mnesia:create_table(booking, [{attributes, record_info(fields, booking)}, {ram_copies, [node()]}]),
mnesia:create_table(calendar, [{attributes, record_info(fields, calendar)}, {ram_copies, [node()]}]),
mnesia:create_table(event, [{attributes, record_info(fields, event)}, {ram_copies, [node()]}]),
ok.
cleanup(_) ->
mnesia:delete_table(event),
mnesia:delete_table(calendar),
mnesia:delete_table(booking),
mnesia:stop(),
ok.
logic_notification_test_() ->
{foreach,
fun setup/0,
fun cleanup/1,
[
{"Notify booking", fun test_notify_booking/0},
{"Notify calendar update", fun test_notify_calendar_update/0},
{"Notify event update", fun test_notify_event_update/0},
{"Notify admin", fun test_notify_admin/0}
]}.
create_test_booking() ->
Booking = #booking{
id = base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}),
event_id = <<"event123">>,
user_id = <<"user123">>,
status = pending,
confirmed_at = undefined,
created_at = calendar:universal_time(),
updated_at = calendar:universal_time()
},
mnesia:dirty_write(Booking),
Booking.
create_test_calendar() ->
Calendar = #calendar{
id = base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}),
owner_id = <<"owner123">>,
title = <<"Test Calendar">>,
description = <<"">>,
tags = [],
type = personal,
confirmation = manual,
rating_avg = 0.0,
rating_count = 0,
status = active,
created_at = calendar:universal_time(),
updated_at = calendar:universal_time()
},
mnesia:dirty_write(Calendar),
Calendar.
create_test_event() ->
Event = #event{
id = base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}),
calendar_id = <<"cal123">>,
title = <<"Test Event">>,
description = <<"">>,
event_type = single,
start_time = {{2026, 6, 1}, {10, 0, 0}},
duration = 60,
recurrence_rule = undefined,
master_id = undefined,
is_instance = false,
specialist_id = undefined,
location = undefined,
tags = [],
capacity = undefined,
online_link = undefined,
status = active,
rating_avg = 0.0,
rating_count = 0,
created_at = calendar:universal_time(),
updated_at = calendar:universal_time()
},
mnesia:dirty_write(Event),
Event.
test_notify_booking() ->
Booking = create_test_booking(),
UserId = <<"user123">>,
% Функция возвращает список пидов (может быть пустым)
Result = logic_notification:notify_booking(UserId, Booking),
?assert(is_list(Result)).
test_notify_calendar_update() ->
Calendar = create_test_calendar(),
Result = logic_notification:notify_calendar_update(Calendar),
?assert(is_list(Result)).
test_notify_event_update() ->
Event = create_test_event(),
Result = logic_notification:notify_event_update(Event),
?assert(is_list(Result)).
test_notify_admin() ->
Result = logic_notification:notify_admin(report_created, #{report_id => <<"rep123">>}),
?assertEqual(ok, Result).

View File

@@ -0,0 +1,110 @@
-module(logic_recurrence_tests).
-include_lib("eunit/include/eunit.hrl").
logic_recurrence_test_() ->
[
{"Parse RRULE test", fun test_parse_rrule/0},
{"Validate RRULE test", fun test_validate_rrule/0},
{"Generate daily occurrences test", fun test_daily_occurrences/0},
{"Generate weekly occurrences test", fun test_weekly_occurrences/0},
{"Generate monthly occurrences test", fun test_monthly_occurrences/0},
{"Generate with count limit test", fun test_count_limit/0},
{"Generate with until limit test", fun test_until_limit/0}
].
test_parse_rrule() ->
RRule = #{
<<"freq">> => <<"WEEKLY">>,
<<"interval">> => 2,
<<"until">> => <<"2026-12-31T00:00:00Z">>,
<<"byday">> => [<<"MO">>, <<"WE">>, <<"FR">>]
},
{ok, Parsed} = logic_recurrence:parse_rrule(RRule),
?assertEqual(<<"WEEKLY">>, maps:get(freq, Parsed)),
?assertEqual(2, maps:get(interval, Parsed)),
?assertEqual(<<"2026-12-31T00:00:00Z">>, maps:get(until, Parsed)),
?assertEqual([<<"MO">>, <<"WE">>, <<"FR">>], maps:get(byday, Parsed)).
test_validate_rrule() ->
ValidRule = #{
freq => <<"DAILY">>,
interval => 1
},
?assert(logic_recurrence:validate_rrule(ValidRule)),
InvalidFreq = #{
freq => <<"YEARLY">>,
interval => 1
},
?assertNot(logic_recurrence:validate_rrule(InvalidFreq)),
InvalidInterval = #{
freq => <<"DAILY">>,
interval => 0
},
?assertNot(logic_recurrence:validate_rrule(InvalidInterval)).
test_daily_occurrences() ->
StartTime = {{2026, 4, 20}, {10, 0, 0}},
RRule = #{
freq => <<"DAILY">>,
interval => 1
},
RangeEnd = {{2026, 4, 25}, {10, 0, 0}},
Occurrences = logic_recurrence:generate_occurrences(StartTime, RRule, RangeEnd),
?assertEqual(6, length(Occurrences)),
?assertEqual({{2026, 4, 20}, {10, 0, 0}}, lists:nth(1, Occurrences)),
?assertEqual({{2026, 4, 25}, {10, 0, 0}}, lists:nth(6, Occurrences)).
test_weekly_occurrences() ->
StartTime = {{2026, 4, 20}, {10, 0, 0}}, % Monday
RRule = #{
freq => <<"WEEKLY">>,
interval => 1
},
RangeEnd = {{2026, 5, 11}, {10, 0, 0}},
Occurrences = logic_recurrence:generate_occurrences(StartTime, RRule, RangeEnd),
?assertEqual(4, length(Occurrences)),
?assertEqual({{2026, 4, 20}, {10, 0, 0}}, lists:nth(1, Occurrences)),
?assertEqual({{2026, 5, 11}, {10, 0, 0}}, lists:nth(4, Occurrences)).
test_monthly_occurrences() ->
StartTime = {{2026, 4, 20}, {10, 0, 0}},
RRule = #{
freq => <<"MONTHLY">>,
interval => 1
},
RangeEnd = {{2026, 7, 20}, {10, 0, 0}},
Occurrences = logic_recurrence:generate_occurrences(StartTime, RRule, RangeEnd),
?assertEqual(4, length(Occurrences)),
?assertEqual({{2026, 4, 20}, {10, 0, 0}}, lists:nth(1, Occurrences)),
?assertEqual({{2026, 7, 20}, {10, 0, 0}}, lists:nth(4, Occurrences)).
test_count_limit() ->
StartTime = {{2026, 4, 20}, {10, 0, 0}},
RRule = #{
freq => <<"DAILY">>,
interval => 1,
count => 3
},
RangeEnd = {{2026, 12, 31}, {10, 0, 0}},
Occurrences = logic_recurrence:generate_occurrences(StartTime, RRule, RangeEnd),
?assertEqual(3, length(Occurrences)).
test_until_limit() ->
StartTime = {{2026, 4, 20}, {10, 0, 0}},
RRule = #{
freq => <<"DAILY">>,
interval => 1,
until => <<"2026-04-22T10:00:00Z">>
},
RangeEnd = {{2026, 12, 31}, {10, 0, 0}},
Occurrences = logic_recurrence:generate_occurrences(StartTime, RRule, RangeEnd),
% 20, 21, 22 апреля = 3 дня
?assertEqual(3, length(Occurrences)).

View File

@@ -0,0 +1,161 @@
-module(logic_review_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(event, [{attributes, record_info(fields, event)}, {ram_copies, [node()]}]),
mnesia:create_table(booking, [{attributes, record_info(fields, booking)}, {ram_copies, [node()]}]),
mnesia:create_table(review, [{attributes, record_info(fields, review)}, {ram_copies, [node()]}]),
ok.
cleanup(_) ->
mnesia:delete_table(review),
mnesia:delete_table(booking),
mnesia:delete_table(event),
mnesia:delete_table(calendar),
mnesia:delete_table(user),
mnesia:stop(),
ok.
logic_review_test_() ->
{foreach,
fun setup/0,
fun cleanup/1,
[
{"Create review for event", fun test_create_event_review/0},
{"Create review for calendar", fun test_create_calendar_review/0},
{"Cannot review without booking", fun test_cannot_review_without_booking/0},
{"Cannot review twice", fun test_cannot_review_twice/0},
{"Update own review", fun test_update_own_review/0},
{"Cannot update others review", fun test_cannot_update_others/0},
{"Delete own review", fun test_delete_own_review/0},
{"Admin can hide review", fun test_admin_hide_review/0},
{"Rating updates target", fun test_rating_updates_target/0}
]}.
create_test_user(Role) ->
UserId = base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}),
User = #user{id = UserId, email = <<UserId/binary, "@test.com">>, password_hash = <<"hash">>,
role = Role, status = active, created_at = calendar:universal_time(), updated_at = calendar:universal_time()},
mnesia:dirty_write(User),
UserId.
create_test_calendar(OwnerId) ->
{ok, Calendar} = core_calendar:create(OwnerId, <<"Test">>, <<"">>, manual),
Calendar#calendar.id.
create_test_event(CalendarId) ->
{ok, Event} = core_event:create(CalendarId, <<"Event">>, {{2026, 6, 1}, {10, 0, 0}}, 60),
Event#event.id.
create_booking(UserId, EventId) ->
{ok, Booking} = core_booking:create(EventId, UserId),
core_booking:update_status(Booking#booking.id, confirmed),
Booking#booking.id.
test_create_event_review() ->
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId),
EventId = create_test_event(CalendarId),
create_booking(ParticipantId, EventId),
{ok, Review} = logic_review:create_review(ParticipantId, event, EventId, 5, <<"Great!">>),
?assertEqual(5, Review#review.rating),
% Проверяем, что рейтинг события обновился
{ok, Event} = core_event:get_by_id(EventId),
?assertEqual(5.0, Event#event.rating_avg),
?assertEqual(1, Event#event.rating_count).
test_create_calendar_review() ->
OwnerId = create_test_user(user),
ReviewerId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId),
{ok, Review} = logic_review:create_review(ReviewerId, calendar, CalendarId, 4, <<"Nice">>),
?assertEqual(4, Review#review.rating).
test_cannot_review_without_booking() ->
OwnerId = create_test_user(user),
UserId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId),
EventId = create_test_event(CalendarId),
{error, cannot_review} = logic_review:create_review(UserId, event, EventId, 5, <<"">>).
test_cannot_review_twice() ->
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId),
EventId = create_test_event(CalendarId),
create_booking(ParticipantId, EventId),
{ok, _} = logic_review:create_review(ParticipantId, event, EventId, 5, <<"">>),
{error, already_reviewed} = logic_review:create_review(ParticipantId, event, EventId, 4, <<"">>).
test_update_own_review() ->
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId),
EventId = create_test_event(CalendarId),
create_booking(ParticipantId, EventId),
{ok, Review} = logic_review:create_review(ParticipantId, event, EventId, 3, <<"">>),
{ok, Updated} = logic_review:update_review(ParticipantId, Review#review.id, [{rating, 5}]),
?assertEqual(5, Updated#review.rating).
test_cannot_update_others() ->
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
OtherId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId),
EventId = create_test_event(CalendarId),
create_booking(ParticipantId, EventId),
{ok, Review} = logic_review:create_review(ParticipantId, event, EventId, 3, <<"">>),
{error, access_denied} = logic_review:update_review(OtherId, Review#review.id, [{rating, 5}]).
test_delete_own_review() ->
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId),
EventId = create_test_event(CalendarId),
create_booking(ParticipantId, EventId),
{ok, Review} = logic_review:create_review(ParticipantId, event, EventId, 3, <<"">>),
{ok, deleted} = logic_review:delete_review(ParticipantId, Review#review.id).
test_admin_hide_review() ->
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
AdminId = create_test_user(admin),
CalendarId = create_test_calendar(OwnerId),
EventId = create_test_event(CalendarId),
create_booking(ParticipantId, EventId),
{ok, Review} = logic_review:create_review(ParticipantId, event, EventId, 3, <<"Bad">>),
{ok, Hidden} = logic_review:hide_review(AdminId, Review#review.id),
?assertEqual(hidden, Hidden#review.status).
test_rating_updates_target() ->
OwnerId = create_test_user(user),
P1 = create_test_user(user),
P2 = create_test_user(user),
CalendarId = create_test_calendar(OwnerId),
EventId = create_test_event(CalendarId),
create_booking(P1, EventId),
create_booking(P2, EventId),
{ok, _} = logic_review:create_review(P1, event, EventId, 5, <<"">>),
{ok, Event1} = core_event:get_by_id(EventId),
?assertEqual(5.0, Event1#event.rating_avg),
?assertEqual(1, Event1#event.rating_count),
{ok, _} = logic_review:create_review(P2, event, EventId, 3, <<"">>),
{ok, Event2} = core_event:get_by_id(EventId),
?assertEqual(4.0, Event2#event.rating_avg),
?assertEqual(2, Event2#event.rating_count).

View File

@@ -0,0 +1,249 @@
-module(logic_search_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(event, [
{attributes, record_info(fields, event)},
{ram_copies, [node()]}
]),
ok.
cleanup(_) ->
mnesia:delete_table(event),
mnesia:delete_table(calendar),
mnesia:delete_table(user),
mnesia:stop(),
ok.
logic_search_test_() ->
{foreach,
fun setup/0,
fun cleanup/1,
[
{"Search events by text", fun test_search_events_by_text/0},
{"Search events by tags", fun test_search_events_by_tags/0},
{"Search events by date range", fun test_search_events_by_date/0},
{"Search events by location", fun test_search_events_by_location/0},
{"Combined search", fun test_combined_search/0},
{"Search calendars", fun test_search_calendars/0},
{"Search all", fun test_search_all/0},
{"Pagination", fun test_pagination/0},
{"Sorting", fun test_sorting/0},
{"Access control in search", fun test_access_control/0},
{"Empty search results", fun test_empty_search/0}
]}.
%% Вспомогательные функции
create_test_user(Role) ->
UserId = base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}),
User = #user{
id = UserId,
email = <<UserId/binary, "@test.com">>,
password_hash = <<"hash">>,
role = Role,
status = active,
created_at = calendar:universal_time(),
updated_at = calendar:universal_time()
},
mnesia:dirty_write(User),
UserId.
create_test_calendar(OwnerId, Type, Tags) ->
{ok, Calendar} = core_calendar:create(OwnerId, <<"Test Calendar">>, <<"Description">>, manual),
core_calendar:update(Calendar#calendar.id, [{type, Type}, {tags, Tags}]),
{ok, Updated} = core_calendar:get_by_id(Calendar#calendar.id),
Updated#calendar.id.
create_test_event(CalendarId, Title, Description, StartTime, Tags, Location) ->
{ok, Event} = core_event:create(CalendarId, Title, StartTime, 60),
Updates = [{description, Description}, {tags, Tags}],
Updates2 = case Location of
undefined -> Updates;
_ -> [{location, Location} | Updates]
end,
core_event:update(Event#event.id, Updates2),
{ok, Updated} = core_event:get_by_id(Event#event.id),
Updated#event.id.
%% Тесты
test_search_events_by_text() ->
OwnerId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, personal, []),
StartTime = {{2026, 6, 1}, {10, 0, 0}},
create_test_event(CalendarId, <<"Python Workshop">>, <<"Learn Python">>, StartTime, [], undefined),
create_test_event(CalendarId, <<"JavaScript Conference">>, <<"JS for everyone">>, StartTime, [], undefined),
create_test_event(CalendarId, <<"Yoga Class">>, <<"Morning yoga">>, StartTime, [], undefined),
Params = #{},
{ok, Total, Results} = logic_search:search(<<"event">>, <<"Python">>, OwnerId, Params),
?assertEqual(1, Total),
?assertEqual(1, length(Results)),
{ok, Total2, _Results2} = logic_search:search(<<"event">>, <<"conference">>, OwnerId, Params),
?assertEqual(1, Total2).
test_search_events_by_tags() ->
OwnerId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, personal, []),
StartTime = {{2026, 6, 1}, {10, 0, 0}},
create_test_event(CalendarId, <<"Event 1">>, <<"">>, StartTime, [<<"python">>, <<"workshop">>], undefined),
create_test_event(CalendarId, <<"Event 2">>, <<"">>, StartTime, [<<"javascript">>, <<"conference">>], undefined),
create_test_event(CalendarId, <<"Event 3">>, <<"">>, StartTime, [<<"python">>, <<"advanced">>], undefined),
% Поиск по одному тегу
Params = #{tags => <<"python">>},
{ok, Total, _} = logic_search:search(<<"event">>, undefined, OwnerId, Params),
?assertEqual(2, Total), % Event 1 и Event 3
% Поиск по тегу, который есть только у одного события
Params2 = #{tags => <<"workshop">>},
{ok, Total2, _} = logic_search:search(<<"event">>, undefined, OwnerId, Params2),
?assertEqual(1, Total2), % Только Event 1
% Поиск по тегу, которого нет
Params3 = #{tags => <<"nonexistent">>},
{ok, Total3, _} = logic_search:search(<<"event">>, undefined, OwnerId, Params3),
?assertEqual(0, Total3).
test_search_events_by_date() ->
OwnerId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, personal, []),
create_test_event(CalendarId, <<"June Event">>, <<"">>, {{2026, 6, 15}, {10, 0, 0}}, [], undefined),
create_test_event(CalendarId, <<"July Event">>, <<"">>, {{2026, 7, 15}, {10, 0, 0}}, [], undefined),
create_test_event(CalendarId, <<"August Event">>, <<"">>, {{2026, 8, 15}, {10, 0, 0}}, [], undefined),
Params = #{from => {{2026, 6, 1}, {0, 0, 0}}, to => {{2026, 6, 30}, {23, 59, 59}}},
{ok, Total, _} = logic_search:search(<<"event">>, undefined, OwnerId, Params),
?assertEqual(1, Total).
test_search_events_by_location() ->
OwnerId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, personal, []),
StartTime = {{2026, 6, 1}, {10, 0, 0}},
MoscowLoc = #location{address = <<"Moscow">>, lat = 55.7558, lon = 37.6173},
SpbLoc = #location{address = <<"SPb">>, lat = 59.9343, lon = 30.3351},
create_test_event(CalendarId, <<"Moscow Event">>, <<"">>, StartTime, [], MoscowLoc),
create_test_event(CalendarId, <<"SPb Event">>, <<"">>, StartTime, [], SpbLoc),
Params = #{lat => 55.7558, lon => 37.6173, radius => 10},
{ok, Total, _Results} = logic_search:search(<<"event">>, undefined, OwnerId, Params),
?assertEqual(1, Total),
Params2 = #{lat => 59.9343, lon => 30.3351, radius => 10},
{ok, Total2, _} = logic_search:search(<<"event">>, undefined, OwnerId, Params2),
?assertEqual(1, Total2).
test_combined_search() ->
OwnerId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, personal, []),
StartTime = {{2026, 6, 15}, {10, 0, 0}},
create_test_event(CalendarId, <<"Python Workshop">>, <<"Learn Python">>, StartTime, [<<"python">>, <<"workshop">>], undefined),
create_test_event(CalendarId, <<"Python Advanced">>, <<"Advanced Python">>, {{2026, 7, 15}, {10, 0, 0}}, [<<"python">>, <<"advanced">>], undefined),
Params = #{from => {{2026, 6, 1}, {0, 0, 0}}, to => {{2026, 6, 30}, {23, 59, 59}}, tags => <<"python">>},
{ok, Total, _} = logic_search:search(<<"event">>, <<"Workshop">>, OwnerId, Params),
?assertEqual(1, Total).
test_search_calendars() ->
OwnerId = create_test_user(user),
create_test_calendar(OwnerId, personal, [<<"tech">>, <<"programming">>]),
create_test_calendar(OwnerId, commercial, [<<"yoga">>, <<"wellness">>]),
Params = #{tags => <<"tech">>},
{ok, Total, _} = logic_search:search(<<"calendar">>, undefined, OwnerId, Params),
?assertEqual(1, Total),
{ok, Total2, _} = logic_search:search(<<"calendar">>, <<"Calendar">>, OwnerId, #{}),
?assertEqual(2, Total2).
test_search_all() ->
OwnerId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, personal, []),
StartTime = {{2026, 6, 1}, {10, 0, 0}},
create_test_event(CalendarId, <<"Test Event">>, <<"">>, StartTime, [], undefined),
{ok, Total, Results} = logic_search:search(undefined, <<"Test">>, OwnerId, #{}),
?assert(Total >= 2),
?assert(maps:is_key(events, Results)),
?assert(maps:is_key(calendars, Results)).
test_pagination() ->
OwnerId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, personal, []),
StartTime = {{2026, 6, 1}, {10, 0, 0}},
lists:foreach(fun(I) ->
Title = iolist_to_binary(["Event ", integer_to_binary(I)]),
create_test_event(CalendarId, Title, <<"">>, StartTime, [], undefined)
end, [1, 2, 3, 4, 5]),
Params = #{limit => 2, offset => 0},
{ok, Total, Results} = logic_search:search(<<"event">>, undefined, OwnerId, Params),
?assertEqual(5, Total),
?assertEqual(2, length(Results)),
Params2 = #{limit => 2, offset => 2},
{ok, _, Results2} = logic_search:search(<<"event">>, undefined, OwnerId, Params2),
?assertEqual(2, length(Results2)),
Params3 = #{limit => 2, offset => 4},
{ok, _, Results3} = logic_search:search(<<"event">>, undefined, OwnerId, Params3),
?assertEqual(1, length(Results3)).
test_sorting() ->
OwnerId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, personal, []),
create_test_event(CalendarId, <<"Early">>, <<"">>, {{2026, 6, 1}, {10, 0, 0}}, [], undefined),
create_test_event(CalendarId, <<"Late">>, <<"">>, {{2026, 6, 30}, {10, 0, 0}}, [], undefined),
Params = #{sort => <<"start_time">>, order => <<"asc">>},
{ok, _, [First | _]} = logic_search:search(<<"event">>, undefined, OwnerId, Params),
?assertMatch(#{title := <<"Early">>}, First),
Params2 = #{sort => <<"start_time">>, order => <<"desc">>},
{ok, _, [First2 | _]} = logic_search:search(<<"event">>, undefined, OwnerId, Params2),
?assertMatch(#{title := <<"Late">>}, First2).
test_access_control() ->
OwnerId = create_test_user(user),
OtherId = create_test_user(user),
PersonalId = create_test_calendar(OwnerId, personal, []),
CommercialId = create_test_calendar(OwnerId, commercial, []),
StartTime = {{2026, 6, 1}, {10, 0, 0}},
create_test_event(PersonalId, <<"Personal Event">>, <<"">>, StartTime, [], undefined),
create_test_event(CommercialId, <<"Public Event">>, <<"">>, StartTime, [], undefined),
% Другой пользователь видит только коммерческие события
{ok, Total, Results} = logic_search:search(<<"event">>, undefined, OtherId, #{}),
?assertEqual(1, Total),
[Event] = Results,
?assertMatch(#{title := <<"Public Event">>}, Event).
test_empty_search() ->
OwnerId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, personal, []),
StartTime = {{2026, 6, 1}, {10, 0, 0}},
create_test_event(CalendarId, <<"Test">>, <<"">>, StartTime, [], undefined),
{ok, Total, Results} = logic_search:search(<<"event">>, <<"nonexistent">>, OwnerId, #{}),
?assertEqual(0, Total),
?assertEqual([], Results).

View File

@@ -0,0 +1,120 @@
-module(logic_subscription_tests).
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
logic_subscription_test_() ->
{foreach,
fun setup/0,
fun cleanup/1,
[
{"Start trial", fun test_start_trial/0},
{"Start trial duplicate", fun test_start_trial_duplicate/0},
{"Activate subscription (no trial)", fun test_activate_subscription_no_trial/0},
{"Activate subscription (after trial)", fun test_activate_subscription_after_trial/0},
{"Check subscription - free", fun test_check_free/0},
{"Check subscription - trial", fun test_check_trial/0},
{"Check subscription - paid", fun test_check_paid/0},
{"Can create commercial - free (auto-trial)", fun test_can_create_commercial_free/0},
{"Can create commercial - trial", fun test_can_create_commercial_trial/0},
{"Can create commercial - paid", fun test_can_create_commercial_paid/0},
{"Get user subscription - free", fun test_get_user_subscription_free/0},
{"Get user subscription - trial", fun test_get_user_subscription_trial/0},
{"Get user subscription - paid", fun test_get_user_subscription_paid/0}
]}.
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()]}]),
ok.
cleanup(_) ->
mnesia:delete_table(subscription),
mnesia:delete_table(calendar),
mnesia:delete_table(user),
mnesia:stop(),
ok.
create_test_user() ->
UserId = base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}),
User = #user{
id = UserId,
email = <<UserId/binary, "@test.com">>,
password_hash = <<"hash">>,
role = user,
status = active,
created_at = calendar:universal_time(),
updated_at = calendar:universal_time()
},
mnesia:dirty_write(User),
UserId.
%% ============ Тесты ============
test_start_trial() ->
UserId = create_test_user(),
{ok, Sub} = logic_subscription:start_trial(UserId),
?assertEqual(trial, Sub#subscription.plan),
?assertEqual(true, Sub#subscription.trial_used).
test_start_trial_duplicate() ->
UserId = create_test_user(),
{ok, _} = logic_subscription:start_trial(UserId),
{error, already_has_subscription} = logic_subscription:start_trial(UserId).
test_activate_subscription_no_trial() ->
UserId = create_test_user(),
{ok, Sub} = logic_subscription:activate_subscription(UserId, monthly, #{card => "4242"}),
?assertEqual(monthly, Sub#subscription.plan),
?assertEqual(false, Sub#subscription.trial_used).
test_activate_subscription_after_trial() ->
UserId = create_test_user(),
{ok, _} = logic_subscription:start_trial(UserId),
{ok, Sub} = logic_subscription:activate_subscription(UserId, monthly, #{card => "4242"}),
?assertEqual(monthly, Sub#subscription.plan),
?assertEqual(true, Sub#subscription.trial_used).
test_check_free() ->
UserId = create_test_user(),
{ok, free, free} = logic_subscription:check_user_subscription(UserId).
test_check_trial() ->
UserId = create_test_user(),
{ok, _} = logic_subscription:start_trial(UserId),
{ok, active, trial} = logic_subscription:check_user_subscription(UserId).
test_check_paid() ->
UserId = create_test_user(),
{ok, _} = logic_subscription:activate_subscription(UserId, monthly, #{card => "4242"}),
{ok, active, monthly} = logic_subscription:check_user_subscription(UserId).
test_can_create_commercial_free() ->
% Новый пользователь автоматически получает пробный период при проверке
UserId = create_test_user(),
?assert(logic_subscription:can_create_commercial_calendar(UserId)).
test_can_create_commercial_trial() ->
UserId = create_test_user(),
{ok, _} = logic_subscription:start_trial(UserId),
?assert(logic_subscription:can_create_commercial_calendar(UserId)).
test_can_create_commercial_paid() ->
UserId = create_test_user(),
{ok, _} = logic_subscription:activate_subscription(UserId, monthly, #{card => "4242"}),
?assert(logic_subscription:can_create_commercial_calendar(UserId)).
test_get_user_subscription_free() ->
UserId = create_test_user(),
{ok, #{status := free}} = logic_subscription:get_user_subscription(UserId).
test_get_user_subscription_trial() ->
UserId = create_test_user(),
{ok, _} = logic_subscription:start_trial(UserId),
{ok, #{status := active, plan := trial}} = logic_subscription:get_user_subscription(UserId).
test_get_user_subscription_paid() ->
UserId = create_test_user(),
{ok, _} = logic_subscription:activate_subscription(UserId, annual, #{card => "4242"}),
{ok, #{status := active, plan := annual}} = logic_subscription:get_user_subscription(UserId).

View File

@@ -0,0 +1,105 @@
-module(logic_ticket_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(ticket, [{attributes, record_info(fields, ticket)}, {ram_copies, [node()]}]),
ok.
cleanup(_) ->
mnesia:delete_table(ticket),
mnesia:delete_table(user),
mnesia:stop(),
ok.
logic_ticket_test_() ->
{foreach,
fun setup/0,
fun cleanup/1,
[
{"Report error test", fun test_report_error/0},
{"List tickets admin only", fun test_list_tickets_admin_only/0},
{"Update status test", fun test_update_status/0},
{"Assign ticket test", fun test_assign_ticket/0},
{"Resolve ticket test", fun test_resolve_ticket/0},
{"Close ticket test", fun test_close_ticket/0},
{"Get statistics test", fun test_get_statistics/0}
]}.
create_test_user(Role) ->
UserId = base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}),
User = #user{id = UserId, email = <<UserId/binary, "@test.com">>, password_hash = <<"hash">>,
role = Role, status = active, created_at = calendar:universal_time(), updated_at = calendar:universal_time()},
mnesia:dirty_write(User),
UserId.
test_report_error() ->
{ok, Ticket} = logic_ticket:report_error(<<"Test error">>, <<"stack">>, #{}),
?assertEqual(<<"Test error">>, Ticket#ticket.error_message),
?assertEqual(1, Ticket#ticket.count),
{ok, Ticket2} = logic_ticket:report_error(<<"Test error">>, <<"stack">>, #{}),
?assertEqual(2, Ticket2#ticket.count).
test_list_tickets_admin_only() ->
AdminId = create_test_user(admin),
UserId = create_test_user(user),
{ok, _} = logic_ticket:report_error(<<"E1">>, <<"">>, #{}),
{ok, _} = logic_ticket:report_error(<<"E2">>, <<"">>, #{}),
{ok, Tickets} = logic_ticket:list_tickets(AdminId),
?assertEqual(2, length(Tickets)),
{error, access_denied} = logic_ticket:list_tickets(UserId).
test_update_status() ->
AdminId = create_test_user(admin),
UserId = create_test_user(user),
{ok, Ticket} = logic_ticket:report_error(<<"Error">>, <<"">>, #{}),
{ok, Updated} = logic_ticket:update_status(AdminId, Ticket#ticket.id, in_progress),
?assertEqual(in_progress, Updated#ticket.status),
{error, access_denied} = logic_ticket:update_status(UserId, Ticket#ticket.id, resolved).
test_assign_ticket() ->
AdminId = create_test_user(admin),
AssignToId = create_test_user(admin),
{ok, Ticket} = logic_ticket:report_error(<<"Error">>, <<"">>, #{}),
{ok, Assigned} = logic_ticket:assign_ticket(AdminId, Ticket#ticket.id, AssignToId),
?assertEqual(AssignToId, Assigned#ticket.assigned_to),
?assertEqual(in_progress, Assigned#ticket.status).
test_resolve_ticket() ->
AdminId = create_test_user(admin),
{ok, Ticket} = logic_ticket:report_error(<<"Error">>, <<"">>, #{}),
{ok, Resolved} = logic_ticket:resolve_ticket(AdminId, Ticket#ticket.id, <<"Fixed">>),
?assertEqual(<<"Fixed">>, Resolved#ticket.resolution_note),
?assertEqual(resolved, Resolved#ticket.status).
test_close_ticket() ->
AdminId = create_test_user(admin),
{ok, Ticket} = logic_ticket:report_error(<<"Error">>, <<"">>, #{}),
{ok, Closed} = logic_ticket:close_ticket(AdminId, Ticket#ticket.id),
?assertEqual(closed, Closed#ticket.status).
test_get_statistics() ->
AdminId = create_test_user(admin),
{ok, _} = logic_ticket:report_error(<<"E1">>, <<"">>, #{}),
{ok, _} = logic_ticket:report_error(<<"E2">>, <<"">>, #{}),
{ok, T3} = logic_ticket:report_error(<<"E3">>, <<"">>, #{}),
logic_ticket:update_status(AdminId, T3#ticket.id, resolved),
Stats = logic_ticket:get_statistics(AdminId),
?assertEqual(3, maps:get(total_tickets, Stats)),
?assertEqual(2, maps:get(open, Stats)),
?assertEqual(1, maps:get(resolved, Stats)),
?assertEqual(3, maps:get(total_errors, Stats)).

View File

@@ -0,0 +1,61 @@
-module(ws_handler_tests).
-include_lib("eunit/include/eunit.hrl").
-record(state, {
user_id :: binary() | undefined,
subscriptions = [] :: [binary()]
}).
setup() ->
pg:start_link(),
ok.
cleanup(_) ->
ok.
ws_handler_test_() ->
{foreach,
fun setup/0,
fun cleanup/1,
[
{"WebSocket info notification - booking", fun test_websocket_info_booking/0},
{"WebSocket info notification - calendar", fun test_websocket_info_calendar/0},
{"WebSocket info notification - event", fun test_websocket_info_event/0}
]}.
test_websocket_info_booking() ->
State = #state{user_id = <<"user123">>, subscriptions = []},
Data = #{booking_id => <<"book123">>, event_id => <<"ev123">>, status => confirmed},
Msg = {notification, booking_update, Data},
case ws_handler:websocket_info(Msg, State) of
{reply, {text, Reply}, _} ->
Decoded = jsx:decode(Reply, [return_maps]),
?assertEqual(<<"booking_update">>, maps:get(<<"type">>, Decoded)),
?assertEqual(<<"book123">>, maps:get(<<"booking_id">>, maps:get(<<"data">>, Decoded)));
_ -> ?assert(false, "Expected reply")
end.
test_websocket_info_calendar() ->
State = #state{user_id = <<"user123">>, subscriptions = [<<"cal123">>]},
Data = #{calendar_id => <<"cal123">>, title => <<"Updated">>},
Msg = {notification, calendar_update, Data},
case ws_handler:websocket_info(Msg, State) of
{reply, {text, Reply}, _} ->
Decoded = jsx:decode(Reply, [return_maps]),
?assertEqual(<<"calendar_update">>, maps:get(<<"type">>, Decoded));
_ -> ?assert(false, "Expected reply")
end.
test_websocket_info_event() ->
State = #state{user_id = <<"user123">>, subscriptions = []},
Data = #{event_id => <<"ev123">>, title => <<"Updated Event">>},
Msg = {notification, event_update, Data},
case ws_handler:websocket_info(Msg, State) of
{reply, {text, Reply}, _} ->
Decoded = jsx:decode(Reply, [return_maps]),
?assertEqual(<<"event_update">>, maps:get(<<"type">>, Decoded));
_ -> ?assert(false, "Expected reply")
end.