Files
nocal/tests/microsoft_graph_tests.cpp
Bernardo Magri 98e07e287e feat(sync): reconcile secondary Graph calendars
Read documented Graph v1 calendarView pages for secondary calendars and publish each finite window only after the complete bounded listing validates.

Add schema-v2 per-window event membership so moved-out occurrences are evicted without being mislabeled as remote tombstones, while overlapping windows retain shared instances.

Verified: 18/18 fresh normal, 18/18 -Werror, and 18/18 ASan/UBSan tests.
2026-07-18 13:05:21 +01:00

846 lines
41 KiB
C++

#include "nocal/sync/microsoft_graph.hpp"
#include <openssl/evp.h>
#include <algorithm>
#include <array>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <exception>
#include <filesystem>
#include <iostream>
#include <optional>
#include <stdexcept>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include <unistd.h>
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<char> 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<TransportStep> steps;
std::vector<HttpRequest> 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<unsigned char, 32> 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<microseconds>(
(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<nocal::sync::HttpHeader>{{"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 <typename Action>
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<std::string> 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<CachedAccount>{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<MicrosoftGraphCredentials> 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<std::string> 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;
}