-module(logic_calendar_share_tests). -include_lib("eunit/include/eunit.hrl"). -include("records.hrl"). -define(TABLES, [user, calendar, calendar_share, calendar_share_invite, notification, subscription]). 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_calendar_share_test_() -> {foreach, fun setup/0, fun cleanup/1, [ {"personal invite accept mirror default", fun test_personal_invite_accept/0}, {"commercial deputy write can_edit", fun test_commercial_deputy/0}, {"decline invite", fun test_decline/0}, {"duplicate pending", fun test_duplicate_pending/0}, {"revoke share", fun test_revoke/0}, {"personal access via share read", fun test_personal_share_access/0}, {"list calendars includes shared", fun test_list_includes_shared/0}, {"search excludes personal", fun test_search_excludes_personal/0} ]}. seed_owner_personal() -> Id = base64:encode(crypto:strong_rand_bytes(12), #{padding => false}), Owner = eh_test_support:make_user(#{ id => Id, email => <<"own-", Id/binary, "@ex.com">>, status => active}), mnesia:dirty_write(Owner), {ok, Cal} = core_calendar:create(Owner#user.id, <<"Default">>, <<>>, manual, personal), {Owner#user.id, Cal#calendar.id}. seed_owner_commercial() -> Id = base64:encode(crypto:strong_rand_bytes(12), #{padding => false}), Owner = eh_test_support:make_user(#{ id => Id, email => <<"co-", Id/binary, "@ex.com">>, status => active}), mnesia:dirty_write(Owner), {ok, _} = core_subscription:create(Owner#user.id, monthly, true), {ok, Cal} = core_calendar:create(Owner#user.id, <<"Studio">>, <<>>, manual, commercial), {Owner#user.id, Cal#calendar.id}. make_active_user(Email) -> Id = base64:encode(crypto:strong_rand_bytes(12), #{padding => false}), U = eh_test_support:make_user(#{ id => Id, email => Email, status => active, nickname => <<"n-", Id/binary>>}), mnesia:dirty_write(U), U. test_personal_invite_accept() -> {OwnerId, CalId} = seed_owner_personal(), Co = make_active_user(<<"co@ex.com">>), {ok, Inv} = logic_calendar_share_invite:create(OwnerId, CalId, #{user_id => Co#user.id}, #{rights => write}), ?assertEqual(pending, Inv#calendar_share_invite.status), {ok, Inv2, Share} = logic_calendar_share_invite:accept(Co#user.id, Inv#calendar_share_invite.id, #{}), ?assertEqual(accepted, Inv2#calendar_share_invite.status), ?assertEqual(write, Share#calendar_share.rights), ?assertEqual(true, Share#calendar_share.mirror_to_default), {ok, Cal} = core_calendar:get_by_id(CalId), ?assert(logic_calendar:can_edit(Co#user.id, Cal)), ?assert(logic_calendar:can_access(Co#user.id, Cal)). test_commercial_deputy() -> {OwnerId, CalId} = seed_owner_commercial(), Dep = make_active_user(<<"dep@ex.com">>), {ok, Inv} = logic_calendar_share_invite:create(OwnerId, CalId, #{user_id => Dep#user.id}, #{rights => write}), {ok, _, Share} = logic_calendar_share_invite:accept(Dep#user.id, Inv#calendar_share_invite.id, #{}), ?assertEqual(false, Share#calendar_share.mirror_to_default), {ok, Cal} = core_calendar:get_by_id(CalId), ?assert(logic_calendar:can_edit(Dep#user.id, Cal)), ?assertEqual({error, access_denied}, logic_calendar:delete_calendar(Dep#user.id, CalId)). test_decline() -> {OwnerId, CalId} = seed_owner_personal(), U = make_active_user(<<"d@ex.com">>), {ok, Inv} = logic_calendar_share_invite:create(OwnerId, CalId, #{user_id => U#user.id}, #{}), {ok, Inv2} = logic_calendar_share_invite:decline(U#user.id, Inv#calendar_share_invite.id), ?assertEqual(declined, Inv2#calendar_share_invite.status), ?assertEqual({error, not_found}, core_calendar_share:get(CalId, U#user.id)). test_duplicate_pending() -> {OwnerId, CalId} = seed_owner_personal(), U = make_active_user(<<"dup@ex.com">>), {ok, _} = logic_calendar_share_invite:create(OwnerId, CalId, #{user_id => U#user.id}, #{}), ?assertEqual({error, already_pending}, logic_calendar_share_invite:create(OwnerId, CalId, #{user_id => U#user.id}, #{})). test_revoke() -> {OwnerId, CalId} = seed_owner_personal(), U = make_active_user(<<"rev@ex.com">>), {ok, Inv} = logic_calendar_share_invite:create(OwnerId, CalId, #{user_id => U#user.id}, #{}), {ok, _, _} = logic_calendar_share_invite:accept(U#user.id, Inv#calendar_share_invite.id, #{}), ok = logic_calendar_share:revoke(OwnerId, CalId, U#user.id), {ok, Cal} = core_calendar:get_by_id(CalId), ?assertNot(logic_calendar:can_access(U#user.id, Cal)). test_personal_share_access() -> {OwnerId, CalId} = seed_owner_personal(), U = make_active_user(<<"read@ex.com">>), Stranger = make_active_user(<<"str@ex.com">>), {ok, Inv} = logic_calendar_share_invite:create(OwnerId, CalId, #{user_id => U#user.id}, #{rights => read}), {ok, _, _} = logic_calendar_share_invite:accept(U#user.id, Inv#calendar_share_invite.id, #{mirror_to_default => false}), {ok, Cal} = core_calendar:get_by_id(CalId), ?assert(logic_calendar:can_access(U#user.id, Cal)), ?assertNot(logic_calendar:can_edit(U#user.id, Cal)), ?assertNot(logic_calendar:can_access(Stranger#user.id, Cal)). test_list_includes_shared() -> {OwnerId, CalId} = seed_owner_personal(), U = make_active_user(<<"list@ex.com">>), {ok, Inv} = logic_calendar_share_invite:create(OwnerId, CalId, #{user_id => U#user.id}, #{}), {ok, _, _} = logic_calendar_share_invite:accept(U#user.id, Inv#calendar_share_invite.id, #{}), {ok, List} = logic_calendar:list_calendars(U#user.id), Ids = [C#calendar.id || C <- List], ?assert(lists:member(CalId, Ids)). test_search_excludes_personal() -> {_OwnerId, CalId} = seed_owner_personal(), {ok, Cal} = core_calendar:get_by_id(CalId), ?assertEqual(false, logic_search_discoverable(Cal)). %% Access private helper via same rule as logic_search (duplicated check). logic_search_discoverable(#calendar{type = personal}) -> false; logic_search_discoverable(#calendar{type = commercial} = C) -> logic_calendar:booking_open(C); logic_search_discoverable(_) -> false.