-module(logic_search_tests). -include_lib("eunit/include/eunit.hrl"). -include("records.hrl"). -define(TABLES, [user, calendar, event]). setup() -> eh_test_support:start_mnesia(), eh_test_support:ensure_tables(?TABLES), ok. cleanup(_) -> eh_test_support:delete_tables(?TABLES), eh_test_support:stop_mnesia(), ok. 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}, {"Discovery tops without filters", fun test_discovery_returns_tops/0}, {"Query switches to filtered search", fun test_discovery_with_q_uses_filter/0} ]}. %% Вспомогательные функции create_test_user(Role) -> UserId = base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}), User = #user{ id = UserId, email = <>, 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. add_days(DateTime, Days) -> Sec = calendar:datetime_to_gregorian_seconds(DateTime) + Days * 86400, calendar:gregorian_seconds_to_datetime(Sec). events_from({ok, Total, #{<<"events">> := Events}}) -> {Total, Events}. calendars_from({ok, Total, #{<<"calendars">> := Calendars}}) -> {Total, Calendars}. %% Тесты test_search_events_by_text() -> OwnerId = create_test_user(user), CalendarId = create_test_calendar(OwnerId, personal, []), StartTime = eh_test_support:future_start(), 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 = #{}, {Total, Results} = events_from(logic_search:search(<<"event">>, <<"Python">>, OwnerId, Params)), ?assertEqual(1, Total), ?assertEqual(1, length(Results)), {Total2, _} = events_from(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 = eh_test_support:future_start(), 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), {Total, _} = events_from(logic_search:search(<<"event">>, undefined, OwnerId, #{tags => <<"python">>})), ?assertEqual(2, Total), {Total2, _} = events_from(logic_search:search(<<"event">>, undefined, OwnerId, #{tags => <<"workshop">>})), ?assertEqual(1, Total2), {Total3, _} = events_from(logic_search:search(<<"event">>, undefined, OwnerId, #{tags => <<"nonexistent">>})), ?assertEqual(0, Total3). test_search_events_by_date() -> OwnerId = create_test_user(user), CalendarId = create_test_calendar(OwnerId, personal, []), Base = eh_test_support:future_start(), create_test_event(CalendarId, <<"Near Event">>, <<"">>, Base, [], undefined), create_test_event(CalendarId, <<"Mid Event">>, <<"">>, add_days(Base, 30), [], undefined), create_test_event(CalendarId, <<"Far Event">>, <<"">>, add_days(Base, 60), [], undefined), Params = #{from => add_days(Base, -1), to => add_days(Base, 7)}, {Total, _} = events_from(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 = eh_test_support:future_start(), 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), {Total, _} = events_from(logic_search:search(<<"event">>, undefined, OwnerId, #{lat => 55.7558, lon => 37.6173, radius => 10})), ?assertEqual(1, Total), {Total2, _} = events_from(logic_search:search(<<"event">>, undefined, OwnerId, #{lat => 59.9343, lon => 30.3351, radius => 10})), ?assertEqual(1, Total2). test_combined_search() -> OwnerId = create_test_user(user), CalendarId = create_test_calendar(OwnerId, personal, []), Base = eh_test_support:future_start(), create_test_event(CalendarId, <<"Python Workshop">>, <<"Learn Python">>, Base, [<<"python">>, <<"workshop">>], undefined), create_test_event(CalendarId, <<"Python Advanced">>, <<"Advanced Python">>, add_days(Base, 30), [<<"python">>, <<"advanced">>], undefined), Params = #{from => add_days(Base, -1), to => add_days(Base, 7), tags => <<"python">>}, {Total, _} = events_from(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">>]), {Total, _} = calendars_from(logic_search:search(<<"calendar">>, undefined, OwnerId, #{tags => <<"tech">>})), ?assertEqual(1, Total), {Total2, _} = calendars_from(logic_search:search(<<"calendar">>, <<"Calendar">>, OwnerId, #{})), ?assertEqual(2, Total2). test_search_all() -> OwnerId = create_test_user(user), CalendarId = create_test_calendar(OwnerId, personal, []), StartTime = eh_test_support:future_start(), 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 = eh_test_support:future_start(), 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]), {Total, Results} = events_from(logic_search:search(<<"event">>, undefined, OwnerId, #{limit => 2, offset => 0})), ?assertEqual(5, Total), ?assertEqual(2, length(Results)), {_, Results2} = events_from(logic_search:search(<<"event">>, undefined, OwnerId, #{limit => 2, offset => 2})), ?assertEqual(2, length(Results2)), {_, Results3} = events_from(logic_search:search(<<"event">>, undefined, OwnerId, #{limit => 2, offset => 4})), ?assertEqual(1, length(Results3)). test_sorting() -> OwnerId = create_test_user(user), CalendarId = create_test_calendar(OwnerId, personal, []), Base = eh_test_support:future_start(), create_test_event(CalendarId, <<"Early">>, <<"">>, Base, [], undefined), create_test_event(CalendarId, <<"Late">>, <<"">>, add_days(Base, 29), [], undefined), {_, [First | _]} = events_from(logic_search:search(<<"event">>, undefined, OwnerId, #{sort => <<"start_time">>, order => <<"asc">>})), ?assertMatch(#{title := <<"Early">>}, First), {_, [First2 | _]} = events_from(logic_search:search(<<"event">>, undefined, OwnerId, #{sort => <<"start_time">>, order => <<"desc">>})), ?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 = eh_test_support:future_start(), create_test_event(PersonalId, <<"Personal Event">>, <<"">>, StartTime, [], undefined), create_test_event(CommercialId, <<"Public Event">>, <<"">>, StartTime, [], undefined), {Total, Results} = events_from(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 = eh_test_support:future_start(), create_test_event(CalendarId, <<"Test">>, <<"">>, StartTime, [], undefined), {Total, Results} = events_from(logic_search:search(<<"event">>, <<"nonexistent">>, OwnerId, #{})), ?assertEqual(0, Total), ?assertEqual([], Results). test_discovery_returns_tops() -> OwnerId = create_test_user(user), ViewerId = create_test_user(user), CalendarId = create_test_calendar(OwnerId, commercial, []), StartTime = eh_test_support:future_start(), LowId = create_test_event(CalendarId, <<"Low Rated">>, <<"">>, StartTime, [], undefined), HighId = create_test_event(CalendarId, <<"High Rated">>, <<"">>, StartTime, [], undefined), {ok, _} = core_event:update(LowId, [{rating_avg, 1.0}, {rating_count, 1}]), {ok, _} = core_event:update(HighId, [{rating_avg, 5.0}, {rating_count, 10}]), stats_tops:init_tables(), stats_tops:rebuild(), {Total, [First | _]} = events_from(logic_search:search(<<"event">>, undefined, ViewerId, #{})), ?assertEqual(2, Total), ?assertMatch(#{title := <<"High Rated">>}, First). test_discovery_with_q_uses_filter() -> OwnerId = create_test_user(user), CalendarId = create_test_calendar(OwnerId, personal, []), StartTime = eh_test_support:future_start(), LowId = create_test_event(CalendarId, <<"Alpha">>, <<"">>, StartTime, [], undefined), HighId = create_test_event(CalendarId, <<"Beta">>, <<"">>, StartTime, [], undefined), {ok, _} = core_event:update(LowId, [{rating_avg, 1.0}]), {ok, _} = core_event:update(HighId, [{rating_avg, 5.0}]), stats_tops:init_tables(), stats_tops:rebuild(), {Total, Results} = events_from(logic_search:search(<<"event">>, <<"Alpha">>, OwnerId, #{})), ?assertEqual(1, Total), ?assertMatch([#{title := <<"Alpha">>}], Results).