#include "nocal/sync/microsoft_graph.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { using namespace std::chrono; using nocal::storage::CachedAccount; using nocal::storage::CachedCalendar; using nocal::storage::CachedEvent; using nocal::storage::CachedEventInstance; using nocal::storage::CompleteWindow; using nocal::storage::SyncCheckpoint; using nocal::storage::SyncCache; using nocal::sync::HttpMethod; using nocal::sync::HttpRequest; using nocal::sync::HttpResponse; using nocal::sync::HttpTransport; using nocal::sync::MicrosoftGraphCredentials; using nocal::sync::MicrosoftGraphError; using nocal::sync::MicrosoftGraphSync; using nocal::sync::MicrosoftGraphWindow; void require(bool condition, std::string_view message) { if (!condition) { throw std::runtime_error(std::string(message)); } } class TemporaryDirectory { public: TemporaryDirectory() { path_ = std::filesystem::temp_directory_path() / ("nocal-graph-tests-" + std::to_string(::getpid()) + "-XXXXXX"); std::string pattern = path_.string(); std::vector writable(pattern.begin(), pattern.end()); writable.push_back('\0'); char* created = ::mkdtemp(writable.data()); require(created != nullptr, "unable to create temporary Graph test directory"); path_ = created; } ~TemporaryDirectory() { std::error_code ignored; std::filesystem::remove_all(path_, ignored); } [[nodiscard]] std::filesystem::path database(std::string_view name) const { return path_ / std::string(name); } private: std::filesystem::path path_; }; struct TransportStep { HttpResponse response; bool fail{false}; }; class ScriptedTransport final : public HttpTransport { public: HttpResponse send(const HttpRequest& request) override { requests.push_back(request); if (next >= steps.size()) { throw std::runtime_error("UNEXPECTED_REQUEST_SECRET"); } const TransportStep& step = steps[next++]; if (step.fail) { throw std::runtime_error("SENSITIVE_TRANSPORT_DETAIL"); } return step.response; } std::vector steps; std::vector requests; std::size_t next{0}; }; [[nodiscard]] HttpResponse json_response(std::string body, int status = 200) { return {status, {{"Content-Type", "application/json"}}, std::move(body)}; } [[nodiscard]] MicrosoftGraphCredentials credentials() { return {"account-local", "ACCESS_TOKEN_SECRET"}; } [[nodiscard]] std::string hash(std::string_view input) { std::array digest{}; unsigned int size = 0; require(EVP_Digest(input.data(), input.size(), digest.data(), &size, EVP_sha256(), nullptr) == 1 && size == digest.size(), "test SHA-256 failed"); static constexpr char hexadecimal[] = "0123456789abcdef"; std::string output; for (const unsigned char byte : digest) { output.push_back(hexadecimal[byte >> 4U]); output.push_back(hexadecimal[byte & 0x0fU]); } return output; } [[nodiscard]] std::string expected_id(std::string_view prefix, std::string_view type, std::string_view parent, std::string_view remote = {}) { std::string input(type); input.push_back('\0'); input += parent; if (!remote.empty()) { input.push_back('\0'); input += remote; } return std::string(prefix) + hash(input); } [[nodiscard]] std::string calendar_id(std::string_view remote) { return expected_id("graph-calendar-", "calendar", "account-local", remote); } [[nodiscard]] std::int64_t epoch_microseconds( year_month_day date, hours hour = 0h, minutes minute = 0min, seconds second = 0s, microseconds fractional = 0us) { return duration_cast( (sys_days{date} + hour + minute + second + fractional).time_since_epoch()) .count(); } void require_graph_headers(const HttpRequest& request) { require(request.method == HttpMethod::get && request.body.empty(), "Graph request was not a bodyless GET"); require(request.headers == std::vector{{"Authorization", "Bearer ACCESS_TOKEN_SECRET"}, {"Accept", "application/json"}, {"Content-Type", "application/json"}, {"Prefer", "outlook.timezone=\"UTC\", IdType=\"ImmutableId\""}}, "Graph request headers were not exact"); } template std::string expect_graph_error(Action&& action) { try { action(); } catch (const MicrosoftGraphError& error) { return error.what(); } throw std::runtime_error("expected MicrosoftGraphError was not thrown"); } void seed_account(SyncCache& cache) { cache.upsert_account({"account-local", "microsoft-graph", "remote-user", "User"}); } void seed_primary(SyncCache& cache, std::string remote = "primary-remote") { seed_account(cache); const CachedCalendar calendar{calendar_id(remote), "account-local", std::move(remote), "Primary", "#112233", false, true, "{\"id\":\"primary-remote\",\"isDefaultCalendar\":true}"}; cache.replace_calendars_after_complete_listing("account-local", {&calendar, 1}); } [[nodiscard]] std::string seed_secondary( SyncCache& cache, std::string remote = "Secondary /Case?x") { seed_account(cache); const std::string local_id = calendar_id(remote); const CachedCalendar calendar{local_id, "account-local", std::move(remote), "Secondary", "#445566", false, true, "{\"isDefaultCalendar\":false}"}; cache.replace_calendars_after_complete_listing("account-local", {&calendar, 1}); return local_id; } [[nodiscard]] std::string event_object(std::string_view id, std::string_view subject, std::string_view start = "2026-07-18T09:00:00.1234567Z", std::string_view end = "2026-07-18T10:00:00.1234567Z") { return "{\"id\":\"" + std::string(id) + "\",\"iCalUId\":\"series-uid\",\"@odata.etag\":\"etag\"," "\"changeKey\":\"change\",\"subject\":\"" + std::string(subject) + "\",\"location\":{\"displayName\":\"Room\"}," "\"bodyPreview\":\"Preview\",\"isAllDay\":false,\"type\":\"occurrence\"," "\"start\":{\"dateTime\":\"" + std::string(start) + "\",\"timeZone\":\"Ignored\"},\"end\":{\"dateTime\":\"" + std::string(end) + "\",\"timeZone\":\"Ignored\"}}"; } [[nodiscard]] std::string view_page( std::string events, std::optional next = std::nullopt) { std::string body = "{\"value\":[" + std::move(events) + "]"; if (next) { body += ",\"@odata.nextLink\":\"" + *next + "\""; } return body + "}"; } [[nodiscard]] MicrosoftGraphWindow july_window() { return {epoch_microseconds(2026y / July / 1), epoch_microseconds(2026y / August / 1)}; } [[nodiscard]] const CachedEvent& event_by_remote( const nocal::storage::CacheSnapshot& snapshot, std::string_view remote) { const auto found = std::find_if(snapshot.events.begin(), snapshot.events.end(), [&](const CachedEvent& event) { return event.remote_id == remote; }); require(found != snapshot.events.end(), "expected cached event is missing"); return *found; } void test_profile_mapping_headers_and_atomic_validation(const TemporaryDirectory& temporary) { SyncCache cache(temporary.database("profile.db")); ScriptedTransport transport; transport.steps.push_back({json_response( "{\"id\":\"remote-subject\",\"displayName\":\"\"," "\"userPrincipalName\":\"user@example.test\"}")}); MicrosoftGraphSync graph(transport, cache); const CachedAccount account = graph.refresh_account(credentials()); require(account == CachedAccount{"account-local", "microsoft-graph", "remote-subject", "user@example.test"}, "Graph profile was not mapped exactly"); require(cache.snapshot().accounts == std::vector{account}, "Graph profile was not persisted"); require(transport.requests.size() == 1 && transport.requests[0].url == "https://graph.microsoft.com/v1.0/me", "Graph profile used the wrong route"); require_graph_headers(transport.requests[0]); const auto baseline = cache.snapshot(); ScriptedTransport invalid_transport; invalid_transport.steps.push_back( {json_response("{\"id\":\"SENSITIVE_REMOTE\",\"id\":\"duplicate\"," "\"displayName\":\"Secret\"}")}); MicrosoftGraphSync invalid_graph(invalid_transport, cache); const std::string message = expect_graph_error( [&] { (void)invalid_graph.refresh_account(credentials()); }); require(cache.snapshot() == baseline, "invalid profile changed cached account"); require(message.find("SENSITIVE") == std::string::npos && message.find("ACCESS_TOKEN_SECRET") == std::string::npos, "profile error leaked provider data"); } void test_paged_listing_atomicity_primary_and_stable_ids( const TemporaryDirectory& temporary) { SyncCache cache(temporary.database("listing.db")); seed_account(cache); const CachedCalendar old{"old-local", "account-local", "old-remote", "Old", "", false, true, "{\"isDefaultCalendar\":false}"}; cache.replace_calendars_after_complete_listing("account-local", {&old, 1}); const auto baseline = cache.snapshot(); const std::string next = "https://graph.microsoft.com/v1.0/me/calendars?$skiptoken=opaque-one"; ScriptedTransport partial; partial.steps = {{json_response( "{\"value\":[{\"id\":\"A\",\"name\":\"First\"," "\"hexColor\":\"#abcdef\",\"color\":\"lightBlue\"," "\"canEdit\":true,\"isDefaultCalendar\":true}]," "\"@odata.nextLink\":\"" + next + "\"}")}, {json_response("{\"value\":\"SENSITIVE_BAD_PAGE\"}")}}; MicrosoftGraphSync partial_graph(partial, cache); const std::string partial_message = expect_graph_error([&] { partial_graph.refresh_calendars(credentials()); }); require(cache.snapshot() == baseline, "partial calendar listing changed the cache"); require(partial_message.find("SENSITIVE") == std::string::npos, "calendar parse error leaked response body"); ScriptedTransport success; success.steps = {{partial.steps[0].response}, {json_response( "{\"value\":[{\"id\":\"a\",\"name\":\"Second\"," "\"hexColor\":null,\"color\":\"darkRed\",\"canEdit\":false," "\"isDefaultCalendar\":false}]}")}}; MicrosoftGraphSync success_graph(success, cache); success_graph.refresh_calendars(credentials()); const auto snapshot = cache.snapshot(); require(snapshot.calendars.size() == 3, "completed listing did not retain omitted calendar"); const auto find_remote = [&](std::string_view remote) -> const CachedCalendar& { const auto found = std::find_if(snapshot.calendars.begin(), snapshot.calendars.end(), [&](const CachedCalendar& value) { return value.remote_id == remote; }); require(found != snapshot.calendars.end(), "listed calendar is missing"); return *found; }; require(!find_remote("old-remote").active, "omitted calendar was not deactivated"); require(find_remote("A").id == calendar_id("A") && find_remote("A").color == "#abcdef" && !find_remote("A").read_only, "primary calendar mapping or ID is wrong"); require(find_remote("a").id == calendar_id("a") && find_remote("a").color == "darkRed" && find_remote("a").read_only && calendar_id("A") != calendar_id("a"), "case-sensitive calendar identity or fallback mapping is wrong"); require(success.requests.size() == 2 && success.requests[1].url == next, "calendar pagination did not use the opaque nextLink"); for (const HttpRequest& request : success.requests) { require_graph_headers(request); } const auto completed = cache.snapshot(); ScriptedTransport multiple; multiple.steps.push_back({json_response( "{\"value\":[{\"id\":\"one\",\"name\":\"One\",\"canEdit\":true," "\"isDefaultCalendar\":true},{\"id\":\"two\",\"name\":\"Two\"," "\"canEdit\":true,\"isDefaultCalendar\":true}]}")}); MicrosoftGraphSync multiple_graph(multiple, cache); expect_graph_error([&] { multiple_graph.refresh_calendars(credentials()); }); require(cache.snapshot() == completed, "multiple-primary listing changed cache"); } void test_delta_mapping_incremental_tombstone_and_routes( const TemporaryDirectory& temporary) { SyncCache cache(temporary.database("delta.db")); seed_primary(cache); const std::string next = "https://graph.microsoft.com/v1.0/me/calendarView/delta?$skiptoken=NEXT_OPAQUE"; const std::string delta = "https://graph.microsoft.com/v1.0/me/calendarView/delta?$deltatoken=DELTA_OPAQUE"; ScriptedTransport transport; transport.steps = {{json_response( "{\"value\":[{\"id\":\"occurrence-1\"," "\"iCalUId\":\"series-uid\",\"@odata.etag\":\"etag-1\"," "\"changeKey\":\"change-1\",\"subject\":\"Occurrence\"," "\"location\":{\"displayName\":\"Room\"}," "\"bodyPreview\":\"Preview\",\"isAllDay\":false," "\"type\":\"occurrence\"," "\"start\":{\"dateTime\":\"2026-07-18T10:00:00.1234567+01:00\"," "\"timeZone\":\"Ignored/Zone\"}," "\"end\":{\"dateTime\":\"2026-07-18T10:00:01.1234567\"," "\"timeZone\":\"UTC\"}}],\"@odata.nextLink\":\"" + next + "\"}")}, {json_response("{\"value\":[],\"@odata.deltaLink\":\"" + delta + "\"}")}}; MicrosoftGraphSync graph(transport, cache); const MicrosoftGraphWindow window{ epoch_microseconds(2026y / July / 1), epoch_microseconds(2026y / August / 1)}; graph.sync_primary_calendar_window(credentials(), window); auto snapshot = cache.snapshot(); require(transport.requests.size() == 2 && transport.requests[0].url == "https://graph.microsoft.com/v1.0/me/calendarView/delta?" "startDateTime=2026-07-01T00%3A00%3A00.000000Z&" "endDateTime=2026-08-01T00%3A00%3A00.000000Z" && transport.requests[1].url == next, "initial delta used the wrong documented route or canonical window"); for (const HttpRequest& request : transport.requests) { require_graph_headers(request); require(request.url.find("/beta/") == std::string::npos && request.url.find("/calendars/") == std::string::npos, "delta used beta or a per-calendar route"); } const std::string local_calendar = calendar_id("primary-remote"); const std::string local_event = expected_id("graph-event-", "event", local_calendar, "occurrence-1"); require(snapshot.events.size() == 1 && snapshot.events[0] == nocal::storage::CachedEvent{local_event, local_calendar, "occurrence-1", "series-uid", "etag-1", "change-1", snapshot.events[0].raw_payload, false}, "delta event mapping or deterministic identity is wrong"); require(snapshot.instances.size() == 1 && snapshot.instances[0].id == expected_id("graph-instance-", "instance", local_event) && snapshot.instances[0].start_epoch_microseconds == epoch_microseconds(2026y / July / 18, 9h, 0min, 0s, 123456us) && snapshot.instances[0].end_epoch_microseconds == epoch_microseconds(2026y / July / 18, 10h, 0min, 1s, 123456us) && snapshot.instances[0].location == "Room" && snapshot.instances[0].time_zone == "UTC", "delta occurrence timestamp or instance mapping is wrong"); require(snapshot.checkpoints.size() == 1 && snapshot.checkpoints[0].complete && snapshot.checkpoints[0].cursor == delta, "delta checkpoint was not committed complete"); ScriptedTransport incremental; const std::string delta_two = "https://graph.microsoft.com/v1.0/me/calendarView/delta?$deltatoken=DELTA_TWO"; incremental.steps.push_back({json_response( "{\"value\":[{\"id\":\"occurrence-1\",\"@removed\":{\"reason\":\"deleted\"}}]," "\"@odata.deltaLink\":\"" + delta_two + "\"}")}); MicrosoftGraphSync incremental_graph(incremental, cache); incremental_graph.sync_primary_calendar_window(credentials(), window); snapshot = cache.snapshot(); require(incremental.requests.size() == 1 && incremental.requests[0].url == delta, "incremental sync did not resume the exact delta cursor"); require(snapshot.events.size() == 1 && snapshot.events[0].deleted && !snapshot.events[0].raw_payload.empty() && snapshot.instances.empty(), "tombstone did not retain raw payload and remove instances"); } void test_page_resume_and_primary_only_failure(const TemporaryDirectory& temporary) { SyncCache cache(temporary.database("resume.db")); seed_primary(cache); const MicrosoftGraphWindow window{ epoch_microseconds(2026y / January / 1), epoch_microseconds(2026y / February / 1)}; const std::string next = "https://graph.microsoft.com/v1.0/me/calendarView/delta?$skiptoken=resume-here"; ScriptedTransport first; first.steps = {{json_response( "{\"value\":[{\"id\":\"event-one\",\"isAllDay\":false," "\"start\":{\"dateTime\":\"2026-01-02T00:00:00Z\"}," "\"end\":{\"dateTime\":\"2026-01-02T01:00:00Z\"}}]," "\"@odata.nextLink\":\"" + next + "\"}")}, {{}, true}}; MicrosoftGraphSync first_graph(first, cache); const std::string message = expect_graph_error( [&] { first_graph.sync_primary_calendar_window(credentials(), window); }); auto snapshot = cache.snapshot(); require(message.find("SENSITIVE") == std::string::npos && message.find("ACCESS_TOKEN_SECRET") == std::string::npos && message.find("resume-here") == std::string::npos, "later-page error leaked sensitive state"); require(snapshot.events.size() == 1 && snapshot.checkpoints.size() == 1 && snapshot.checkpoints[0].cursor == next && !snapshot.checkpoints[0].complete, "first page and resume cursor were not committed before later failure"); const std::string final = "https://graph.microsoft.com/v1.0/me/calendarView/delta?$deltatoken=finished"; ScriptedTransport resumed; resumed.steps.push_back( {json_response("{\"value\":[],\"@odata.deltaLink\":\"" + final + "\"}")}); MicrosoftGraphSync resumed_graph(resumed, cache); resumed_graph.sync_primary_calendar_window(credentials(), window); require(resumed.requests.size() == 1 && resumed.requests[0].url == next, "sync did not resume from the committed next cursor"); SyncCache no_primary_cache(temporary.database("no-primary.db")); seed_account(no_primary_cache); const CachedCalendar secondary{calendar_id("secondary"), "account-local", "secondary", "Secondary", "", false, true, "{\"isDefaultCalendar\":false}"}; no_primary_cache.replace_calendars_after_complete_listing( "account-local", {&secondary, 1}); ScriptedTransport no_network; MicrosoftGraphSync no_primary_graph(no_network, no_primary_cache); expect_graph_error( [&] { no_primary_graph.sync_primary_calendar_window(credentials(), window); }); require(no_network.requests.empty(), "missing primary calendar caused a network request"); } void test_invalid_inputs_continuations_and_timestamps(const TemporaryDirectory& temporary) { SyncCache cache(temporary.database("invalid.db")); seed_primary(cache); const auto baseline = cache.snapshot(); ScriptedTransport transport; MicrosoftGraphSync graph(transport, cache); const MicrosoftGraphWindow valid{ epoch_microseconds(2026y / January / 1), epoch_microseconds(2026y / January / 2)}; const std::vector invalid_credentials = { {"", "token"}, {"account", "bad\r\ntoken"}, {"account", " "}}; for (const MicrosoftGraphCredentials& invalid : invalid_credentials) { expect_graph_error([&] { graph.sync_primary_calendar_window(invalid, valid); }); } expect_graph_error([&] { graph.sync_primary_calendar_window(credentials(), {valid.start_epoch_microseconds, valid.start_epoch_microseconds}); }); require(transport.requests.empty() && cache.snapshot() == baseline, "invalid credentials/window caused side effects"); ScriptedTransport bad_timestamp; bad_timestamp.steps.push_back({json_response( "{\"value\":[{\"id\":\"SENSITIVE_EVENT_ID\",\"isAllDay\":false," "\"start\":{\"dateTime\":\"2026-02-30T10:00:00\",\"timeZone\":\"UTC\"}," "\"end\":{\"dateTime\":\"2026-03-01T10:00:00\",\"timeZone\":\"UTC\"}}]," "\"@odata.deltaLink\":" "\"https://graph.microsoft.com/v1.0/me/calendarView/delta?$deltatoken=x\"}")}); MicrosoftGraphSync bad_timestamp_graph(bad_timestamp, cache); const std::string timestamp_message = expect_graph_error( [&] { bad_timestamp_graph.sync_primary_calendar_window(credentials(), valid); }); require(timestamp_message.find("SENSITIVE") == std::string::npos && cache.snapshot() == baseline, "invalid timestamp leaked or committed event data"); } void test_secondary_route_pagination_and_occurrence_mapping( const TemporaryDirectory& temporary) { SyncCache cache(temporary.database("secondary-route.db")); const std::string local_calendar = seed_secondary(cache); const std::string route = "https://graph.microsoft.com/v1.0/me/calendars/" "Secondary%20%2FCase%3Fx/calendarView"; const std::string next = route + "?$skiptoken=page-two"; ScriptedTransport transport; transport.steps = {{json_response(view_page(event_object("occurrence-one", "First"), next))}, {json_response(view_page(event_object("occurrence-two", "Second", "2026-07-19T11:00:00Z", "2026-07-19T12:00:00Z")))}}; MicrosoftGraphSync graph(transport, cache); graph.sync_secondary_calendar_window(credentials(), local_calendar, july_window()); require(transport.requests.size() == 2 && transport.requests[0].url == route + "?startDateTime=2026-07-01T00%3A00%3A00.000000Z&" "endDateTime=2026-08-01T00%3A00%3A00.000000Z&$top=1000" && transport.requests[1].url == next, "secondary sync used the wrong encoded calendarView route or pagination URL"); for (const HttpRequest& request : transport.requests) { require_graph_headers(request); require(request.url.find("/beta/") == std::string::npos && request.url.find("/delta") == std::string::npos, "secondary sync used beta or a delta route"); } const auto snapshot = cache.snapshot(); require(snapshot.events.size() == 2 && snapshot.instances.size() == 2, "two-page secondary view was not committed completely"); const CachedEvent& first = event_by_remote(snapshot, "occurrence-one"); require(first.id == expected_id("graph-event-", "event", local_calendar, "occurrence-one") && first.uid == "series-uid" && !first.deleted, "secondary recurrence occurrence mapping or stable ID is wrong"); const auto first_instance = std::find_if(snapshot.instances.begin(), snapshot.instances.end(), [&](const CachedEventInstance& instance) { return instance.event_id == first.id; }); require(first_instance != snapshot.instances.end() && first_instance->id == expected_id("graph-instance-", "instance", first.id) && first_instance->title == "First", "secondary occurrence instance mapping is wrong"); require(snapshot.checkpoints.size() == 1 && snapshot.checkpoints[0].calendar_id == local_calendar && snapshot.checkpoints[0].window_start == "2026-07-01T00:00:00.000000Z" && snapshot.checkpoints[0].window_end == "2026-08-01T00:00:00.000000Z" && snapshot.checkpoints[0].cursor == "graph-calendar-view-v1" && snapshot.checkpoints[0].complete, "secondary complete-window checkpoint is wrong"); } void test_secondary_update_empty_and_moved_out(const TemporaryDirectory& temporary) { SyncCache cache(temporary.database("secondary-replace.db")); const std::string calendar = seed_secondary(cache, "secondary-replace"); ScriptedTransport transport; transport.steps.push_back({json_response(view_page(event_object("event-a", "Old A") + "," + event_object("event-b", "Moved B")))}); transport.steps.push_back( {json_response(view_page(event_object("event-a", "Updated A")))}); transport.steps.push_back({json_response(view_page({}))}); MicrosoftGraphSync graph(transport, cache); graph.sync_secondary_calendar_window(credentials(), calendar, july_window()); graph.sync_secondary_calendar_window(credentials(), calendar, july_window()); auto snapshot = cache.snapshot(); const CachedEvent& updated = event_by_remote(snapshot, "event-a"); const CachedEvent& moved = event_by_remote(snapshot, "event-b"); require(!updated.deleted && !moved.deleted, "secondary update or moved-out omission was treated as a tombstone"); require(std::ranges::any_of(snapshot.instances, [&](const CachedEventInstance& instance) { return instance.event_id == updated.id && instance.title == "Updated A"; }), "secondary event update did not replace its instance"); require(std::ranges::none_of(snapshot.instances, [&](const CachedEventInstance& instance) { return instance.event_id == moved.id; }), "moved-out event retained an instance in the completed window"); graph.sync_secondary_calendar_window(credentials(), calendar, july_window()); snapshot = cache.snapshot(); require(snapshot.instances.empty(), "empty secondary window did not evict membership"); require(std::ranges::all_of(snapshot.events, [](const CachedEvent& event) { return !event.deleted; }), "empty window marked omitted secondary events deleted"); } void test_secondary_overlapping_window_retention(const TemporaryDirectory& temporary) { SyncCache cache(temporary.database("secondary-overlap.db")); const std::string calendar = seed_secondary(cache, "secondary-overlap"); ScriptedTransport transport; transport.steps.push_back( {json_response(view_page(event_object("shared-event", "Shared")))}); transport.steps.push_back({json_response(view_page({}))}); MicrosoftGraphSync graph(transport, cache); graph.sync_secondary_calendar_window(credentials(), calendar, july_window()); const auto seeded = cache.snapshot(); require(seeded.events.size() == 1 && seeded.instances.size() == 1, "overlap fixture was not cached"); CompleteWindow overlap{{seeded.events[0]}, {seeded.instances[0]}, SyncCheckpoint{calendar, "2026-07-15T00:00:00.000000Z", "2026-08-15T00:00:00.000000Z", "graph-calendar-view-v1", true}}; cache.replace_complete_window(overlap); graph.sync_secondary_calendar_window(credentials(), calendar, july_window()); const auto retained = cache.snapshot(); require(retained.instances.size() == 1 && retained.instances[0].event_id == seeded.events[0].id, "empty overlapping window removed an instance still referenced by another window"); } void test_secondary_paged_failures_are_atomic_and_redacted( const TemporaryDirectory& temporary) { SyncCache cache(temporary.database("secondary-failure.db")); const std::string calendar = seed_secondary(cache, "secondary-failure"); ScriptedTransport initial; initial.steps.push_back( {json_response(view_page(event_object("existing-event", "Existing")))}); MicrosoftGraphSync initial_graph(initial, cache); initial_graph.sync_secondary_calendar_window(credentials(), calendar, july_window()); const auto baseline = cache.snapshot(); const std::string route = "https://graph.microsoft.com/v1.0/me/calendars/secondary-failure/calendarView"; const std::string next = route + "?$skiptoken=SENSITIVE_CURSOR"; ScriptedTransport transport_failure; transport_failure.steps = { {json_response(view_page(event_object("SENSITIVE_REMOTE_EVENT", "New"), next))}, {{}, true}}; MicrosoftGraphSync transport_graph(transport_failure, cache); const std::string transport_message = expect_graph_error([&] { transport_graph.sync_secondary_calendar_window(credentials(), calendar, july_window()); }); require(cache.snapshot() == baseline, "later secondary transport failure changed prior window"); require(transport_message.find("SENSITIVE") == std::string::npos && transport_message.find("ACCESS_TOKEN_SECRET") == std::string::npos && transport_message.find("secondary-failure") == std::string::npos, "secondary transport failure leaked token, cursor, remote ID, or calendar ID"); ScriptedTransport malformed; malformed.steps = { {json_response(view_page(event_object("new-first-page", "New"), next))}, {json_response("{\"value\":\"SENSITIVE_MALFORMED_BODY\"")}}; MicrosoftGraphSync malformed_graph(malformed, cache); const std::string malformed_message = expect_graph_error([&] { malformed_graph.sync_secondary_calendar_window(credentials(), calendar, july_window()); }); require(cache.snapshot() == baseline, "later malformed secondary page changed prior window"); require(malformed_message.find("SENSITIVE") == std::string::npos, "secondary malformed response leaked provider body"); ScriptedTransport duplicate; duplicate.steps = { {json_response(view_page(event_object("duplicate-event", "First"), next))}, {json_response(view_page(event_object("duplicate-event", "Second")))}}; MicrosoftGraphSync duplicate_graph(duplicate, cache); expect_graph_error([&] { duplicate_graph.sync_secondary_calendar_window(credentials(), calendar, july_window()); }); require(cache.snapshot() == baseline, "duplicate event across secondary pages changed prior window"); ScriptedTransport rejected; rejected.steps.push_back({json_response("SENSITIVE_NON_2XX_BODY", 429)}); MicrosoftGraphSync rejected_graph(rejected, cache); const std::string rejected_message = expect_graph_error([&] { rejected_graph.sync_secondary_calendar_window(credentials(), calendar, july_window()); }); require(cache.snapshot() == baseline, "secondary non-2xx response changed prior window"); require(rejected_message.find("SENSITIVE") == std::string::npos, "secondary non-2xx error leaked its response body"); } void test_secondary_preconditions_do_not_use_network(const TemporaryDirectory& temporary) { const auto require_no_network_failure = [&](SyncCache& cache, MicrosoftGraphCredentials operation_credentials, std::string calendar_id_value, std::string_view message) { ScriptedTransport transport; MicrosoftGraphSync graph(transport, cache); expect_graph_error([&] { graph.sync_secondary_calendar_window( operation_credentials, std::move(calendar_id_value), july_window()); }); require(transport.requests.empty(), message); }; SyncCache primary(temporary.database("secondary-primary-reject.db")); seed_primary(primary); require_no_network_failure(primary, credentials(), calendar_id("primary-remote"), "primary calendar secondary sync reached network"); SyncCache wrong_account(temporary.database("secondary-account-reject.db")); const std::string wrong_account_calendar = seed_secondary(wrong_account, "wrong-account"); require_no_network_failure(wrong_account, {"different-account", "ACCESS_TOKEN_SECRET"}, wrong_account_calendar, "wrong-account secondary sync reached network"); SyncCache inactive(temporary.database("secondary-inactive-reject.db")); const std::string inactive_id = seed_secondary(inactive, "inactive"); inactive.replace_calendars_after_complete_listing("account-local", {}); require_no_network_failure(inactive, credentials(), inactive_id, "inactive secondary sync reached network"); SyncCache missing(temporary.database("secondary-missing-reject.db")); seed_account(missing); require_no_network_failure(missing, credentials(), "missing-calendar", "missing secondary sync reached network"); SyncCache malformed(temporary.database("secondary-malformed-reject.db")); seed_account(malformed); const CachedCalendar malformed_calendar{calendar_id("malformed"), "account-local", "malformed", "Malformed", "", false, true, "{"}; malformed.replace_calendars_after_complete_listing( "account-local", {&malformed_calendar, 1}); require_no_network_failure(malformed, credentials(), malformed_calendar.id, "malformed-calendar secondary sync reached network"); SyncCache invalid_window(temporary.database("secondary-window-reject.db")); const std::string valid_secondary = seed_secondary(invalid_window, "invalid-window"); ScriptedTransport transport; MicrosoftGraphSync graph(transport, invalid_window); expect_graph_error([&] { graph.sync_secondary_calendar_window(credentials(), valid_secondary, {july_window().start_epoch_microseconds, july_window().start_epoch_microseconds}); }); require(transport.requests.empty(), "invalid secondary window reached network"); } void test_secondary_continuation_security(const TemporaryDirectory& temporary) { const std::string route = "https://graph.microsoft.com/v1.0/me/calendars/secondary-links/calendarView"; const std::vector unsafe{ "http://graph.microsoft.com/v1.0/me/calendars/secondary-links/calendarView?$skip=x", "https://evil.example/v1.0/me/calendars/secondary-links/calendarView?$skip=x", "https://user@graph.microsoft.com/v1.0/me/calendars/secondary-links/calendarView?$skip=x", "https://graph.microsoft.com:443/v1.0/me/calendars/secondary-links/calendarView?$skip=x", "https://graph.microsoft.com/v1.0/me/calendars/other/calendarView?$skip=x", route + "?$skip=x#fragment", route + "?$skip=x\\nInjected:true", route + "?" + std::string(16U * 1024U, 'x')}; int sequence = 0; for (const std::string& next : unsafe) { SyncCache cache(temporary.database( "secondary-link-" + std::to_string(sequence++) + ".db")); const std::string calendar = seed_secondary(cache, "secondary-links"); const auto baseline = cache.snapshot(); ScriptedTransport transport; transport.steps.push_back({json_response( view_page(event_object("SENSITIVE_REMOTE_ID", "Sensitive"), next))}); MicrosoftGraphSync graph(transport, cache); const std::string message = expect_graph_error([&] { graph.sync_secondary_calendar_window(credentials(), calendar, july_window()); }); require(transport.requests.size() == 1, "unsafe secondary continuation received credentials"); require(cache.snapshot() == baseline, "unsafe secondary continuation changed cache"); require(message.find("SENSITIVE") == std::string::npos && message.find("ACCESS_TOKEN_SECRET") == std::string::npos, "unsafe secondary continuation error leaked secrets"); } SyncCache repeated_cache(temporary.database("secondary-link-repeated.db")); const std::string repeated_calendar = seed_secondary(repeated_cache, "secondary-links"); const auto repeated_baseline = repeated_cache.snapshot(); const std::string repeated = route + "?$skiptoken=repeated"; ScriptedTransport repeated_transport; repeated_transport.steps = { {json_response(view_page({}, repeated))}, {json_response(view_page({}, repeated))}}; MicrosoftGraphSync repeated_graph(repeated_transport, repeated_cache); expect_graph_error([&] { repeated_graph.sync_secondary_calendar_window( credentials(), repeated_calendar, july_window()); }); require(repeated_transport.requests.size() == 2, "repeated secondary continuation triggered another credentialed request"); require(repeated_cache.snapshot() == repeated_baseline, "repeated secondary continuation changed cache"); SyncCache delta_cache(temporary.database("secondary-delta-reject.db")); const std::string delta_calendar = seed_secondary(delta_cache, "secondary-links"); const auto delta_baseline = delta_cache.snapshot(); ScriptedTransport delta_transport; delta_transport.steps.push_back({json_response( "{\"value\":[],\"@odata.deltaLink\":\"" + route + "?$delta=x\"}")}); MicrosoftGraphSync delta_graph(delta_transport, delta_cache); expect_graph_error([&] { delta_graph.sync_secondary_calendar_window(credentials(), delta_calendar, july_window()); }); require(delta_cache.snapshot() == delta_baseline, "secondary calendarView accepted or committed a deltaLink"); } void test_secondary_aggregate_response_bound(const TemporaryDirectory& temporary) { SyncCache cache(temporary.database("secondary-aggregate.db")); const std::string calendar = seed_secondary(cache, "secondary-aggregate"); const auto baseline = cache.snapshot(); const std::string route = "https://graph.microsoft.com/v1.0/me/calendars/secondary-aggregate/calendarView"; ScriptedTransport transport; const std::string padding(7U * 1024U * 1024U, 'x'); for (int page = 0; page < 10; ++page) { const std::string next = route + "?$skiptoken=aggregate-" + std::to_string(page + 1); transport.steps.push_back({json_response("{\"value\":[],\"padding\":\"" + padding + "\",\"@odata.nextLink\":\"" + next + "\"}")}); } MicrosoftGraphSync graph(transport, cache); expect_graph_error([&] { graph.sync_secondary_calendar_window(credentials(), calendar, july_window()); }); require(transport.requests.size() == 10, "secondary aggregate response bound fired at the wrong page"); require(cache.snapshot() == baseline, "aggregate secondary response overflow changed prior cache state"); } void test_secondary_cache_failure_is_atomic(const TemporaryDirectory& temporary) { SyncCache cache(temporary.database("secondary-cache-failure.db")); const std::string calendar = seed_secondary(cache, "secondary-cache-failure"); const std::string target_id = expected_id("graph-event-", "event", calendar, "target-remote"); CompleteWindow poisoned{{CachedEvent{target_id, calendar, "different-remote", "uid", "", "", "{\"poisoned\":true}", false}}, {}, SyncCheckpoint{calendar, "2026-07-01T00:00:00.000000Z", "2026-08-01T00:00:00.000000Z", "graph-calendar-view-v1", true}}; cache.replace_complete_window(poisoned); const auto baseline = cache.snapshot(); ScriptedTransport transport; transport.steps.push_back( {json_response(view_page(event_object("target-remote", "Target")))}); MicrosoftGraphSync graph(transport, cache); const std::string message = expect_graph_error([&] { graph.sync_secondary_calendar_window(credentials(), calendar, july_window()); }); require(cache.snapshot() == baseline, "secondary cache failure partially replaced the complete window"); require(message.find("target-remote") == std::string::npos && message.find("ACCESS_TOKEN_SECRET") == std::string::npos, "secondary cache failure leaked remote ID or token"); } } // namespace int main() { try { TemporaryDirectory temporary; test_profile_mapping_headers_and_atomic_validation(temporary); test_paged_listing_atomicity_primary_and_stable_ids(temporary); test_delta_mapping_incremental_tombstone_and_routes(temporary); test_page_resume_and_primary_only_failure(temporary); test_invalid_inputs_continuations_and_timestamps(temporary); test_secondary_route_pagination_and_occurrence_mapping(temporary); test_secondary_update_empty_and_moved_out(temporary); test_secondary_overlapping_window_retention(temporary); test_secondary_paged_failures_are_atomic_and_redacted(temporary); test_secondary_preconditions_do_not_use_network(temporary); test_secondary_continuation_security(temporary); test_secondary_aggregate_response_bound(temporary); test_secondary_cache_failure_is_atomic(temporary); } catch (const std::exception& error) { std::cerr << "Microsoft Graph tests failed: " << error.what() << '\n'; return 1; } std::cout << "Microsoft Graph tests passed\n"; return 0; }