Add strict /me identity, atomic paged calendar discovery, and resumable primary-calendar v1 delta synchronization through the durable provider cache. Keep opaque continuations on trusted Graph URLs and retain credentials only per operation. Document that stable Graph v1 delta covers only the primary calendar; secondary calendars still require a v1 reconciliation or beta dependency decision. Verified: 18/18 normal, 18/18 -Werror, and 18/18 ASan/UBSan tests.
697 lines
32 KiB
C++
697 lines
32 KiB
C++
#include "nocal/sync/microsoft_graph.hpp"
|
|
|
|
#include <nlohmann/json.hpp>
|
|
#include <openssl/evp.h>
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <chrono>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <exception>
|
|
#include <filesystem>
|
|
#include <functional>
|
|
#include <iostream>
|
|
#include <limits>
|
|
#include <optional>
|
|
#include <span>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include <unistd.h>
|
|
|
|
namespace {
|
|
|
|
using namespace std::chrono;
|
|
using nocal::storage::CacheSnapshot;
|
|
using nocal::storage::CachedCalendar;
|
|
using nocal::storage::CachedEvent;
|
|
using nocal::storage::PullPage;
|
|
using nocal::storage::SyncCache;
|
|
using nocal::storage::SyncCheckpoint;
|
|
using nocal::sync::HttpHeader;
|
|
using nocal::sync::HttpMethod;
|
|
using nocal::sync::HttpRequest;
|
|
using nocal::sync::HttpResponse;
|
|
using nocal::sync::HttpTransport;
|
|
using nocal::sync::MicrosoftGraphCredentials;
|
|
using nocal::sync::MicrosoftGraphSync;
|
|
using nocal::sync::MicrosoftGraphWindow;
|
|
|
|
constexpr std::string_view graph_prefix = "https://graph.microsoft.com/";
|
|
constexpr std::string_view primary_remote_id = "remote-primary-calendar";
|
|
constexpr std::string_view primary_local_prefix = "graph-calendar-";
|
|
constexpr std::string_view event_local_prefix = "graph-event-";
|
|
|
|
void require(bool condition, std::string_view message) {
|
|
if (!condition) {
|
|
throw std::runtime_error(std::string{message});
|
|
}
|
|
}
|
|
|
|
template <typename Function>
|
|
std::string require_throws_redacted(Function&& function, std::string_view message,
|
|
std::span<const std::string_view> sensitive = {}) {
|
|
try {
|
|
function();
|
|
} catch (const std::exception& error) {
|
|
const std::string description = error.what();
|
|
for (const auto value : sensitive) {
|
|
require(value.empty() || description.find(value) == std::string::npos,
|
|
std::string{message}.append(": sensitive data leaked in exception"));
|
|
}
|
|
return description;
|
|
}
|
|
throw std::runtime_error(std::string{message}.append(": no exception"));
|
|
}
|
|
|
|
class TempDirectory {
|
|
public:
|
|
TempDirectory() {
|
|
std::string pattern =
|
|
(std::filesystem::temp_directory_path() / "nocal-graph-security.XXXXXX").string();
|
|
std::vector<char> writable(pattern.begin(), pattern.end());
|
|
writable.push_back('\0');
|
|
const char* created = ::mkdtemp(writable.data());
|
|
if (created == nullptr) {
|
|
throw std::runtime_error("mkdtemp failed");
|
|
}
|
|
path_ = created;
|
|
}
|
|
|
|
TempDirectory(const TempDirectory&) = delete;
|
|
TempDirectory& operator=(const TempDirectory&) = delete;
|
|
~TempDirectory() {
|
|
std::error_code ignored;
|
|
std::filesystem::remove_all(path_, ignored);
|
|
}
|
|
|
|
[[nodiscard]] const std::filesystem::path& path() const noexcept { return path_; }
|
|
|
|
private:
|
|
std::filesystem::path path_;
|
|
};
|
|
|
|
struct ScriptStep {
|
|
HttpResponse response;
|
|
std::optional<std::string> exception;
|
|
};
|
|
|
|
class ScriptedTransport final : public HttpTransport {
|
|
public:
|
|
HttpResponse send(const HttpRequest& request) override {
|
|
requests.push_back(request);
|
|
if (position >= steps.size()) {
|
|
throw std::runtime_error("unscripted Graph request");
|
|
}
|
|
ScriptStep& step = steps[position++];
|
|
if (step.exception) {
|
|
throw std::runtime_error(*step.exception);
|
|
}
|
|
return step.response;
|
|
}
|
|
|
|
void respond(std::string body, int status = 200) {
|
|
steps.push_back({HttpResponse{status, {}, std::move(body)}, std::nullopt});
|
|
}
|
|
|
|
void fail(std::string description) {
|
|
steps.push_back({HttpResponse{}, std::move(description)});
|
|
}
|
|
|
|
std::vector<ScriptStep> steps;
|
|
std::vector<HttpRequest> requests;
|
|
std::size_t position{0};
|
|
};
|
|
|
|
struct Rig {
|
|
explicit Rig(const std::filesystem::path& root)
|
|
: cache(root / ("graph-" + std::to_string(sequence++) + ".db")), graph(transport, cache) {}
|
|
|
|
static inline int sequence = 0;
|
|
ScriptedTransport transport;
|
|
SyncCache cache;
|
|
MicrosoftGraphSync graph;
|
|
MicrosoftGraphCredentials credentials{"local-account", "SENSITIVE_ACCESS_TOKEN"};
|
|
};
|
|
|
|
[[nodiscard]] std::string profile_body(
|
|
std::string_view remote_subject = "remote-user", std::string_view display = "Alice") {
|
|
return nlohmann::json{{"id", remote_subject}, {"displayName", display},
|
|
{"userPrincipalName", "alice@example.test"}}
|
|
.dump();
|
|
}
|
|
|
|
[[nodiscard]] nlohmann::json calendar_json(std::string_view id, std::string_view name,
|
|
bool primary, bool can_edit = true) {
|
|
return {{"id", id}, {"name", name}, {"color", "auto"}, {"hexColor", "#123456"},
|
|
{"canEdit", can_edit}, {"isDefaultCalendar", primary}};
|
|
}
|
|
|
|
[[nodiscard]] std::string calendar_page(const nlohmann::json& calendars,
|
|
std::optional<std::string> next = std::nullopt) {
|
|
nlohmann::json page{{"value", calendars}};
|
|
if (next) {
|
|
page["@odata.nextLink"] = *next;
|
|
}
|
|
return page.dump();
|
|
}
|
|
|
|
[[nodiscard]] nlohmann::json live_event(std::string_view id = "remote-event-1",
|
|
std::string_view start = "2026-07-18T09:00:00.1234567",
|
|
std::string_view end = "2026-07-18T10:00:00.1234567",
|
|
std::string_view zone = "UTC") {
|
|
return {{"id", id}, {"iCalUId", "uid-1"}, {"subject", "Planning"},
|
|
{"@odata.etag", "etag-1"}, {"changeKey", "change-1"}, {"isAllDay", false},
|
|
{"start", {{"dateTime", start}, {"timeZone", zone}}},
|
|
{"end", {{"dateTime", end}, {"timeZone", zone}}},
|
|
{"location", {{"displayName", "Room 1"}}}, {"bodyPreview", "Agenda"}};
|
|
}
|
|
|
|
[[nodiscard]] std::string delta_page(const nlohmann::json& events,
|
|
std::optional<std::string> next, std::optional<std::string> delta) {
|
|
nlohmann::json page{{"value", events}};
|
|
if (next) {
|
|
page["@odata.nextLink"] = *next;
|
|
}
|
|
if (delta) {
|
|
page["@odata.deltaLink"] = *delta;
|
|
}
|
|
return page.dump();
|
|
}
|
|
|
|
[[nodiscard]] MicrosoftGraphWindow july_window() {
|
|
const auto start = sys_days{year{2026} / July / 18};
|
|
return {duration_cast<microseconds>(start.time_since_epoch()).count(),
|
|
duration_cast<microseconds>((start + days{1}).time_since_epoch()).count()};
|
|
}
|
|
|
|
[[nodiscard]] const HttpHeader* header(const HttpRequest& request, std::string_view name) {
|
|
const auto found = std::ranges::find_if(request.headers, [&](const HttpHeader& value) {
|
|
if (value.name.size() != name.size()) {
|
|
return false;
|
|
}
|
|
for (std::size_t index = 0; index < name.size(); ++index) {
|
|
const auto lower = [](unsigned char character) {
|
|
return character >= 'A' && character <= 'Z'
|
|
? static_cast<unsigned char>(character + ('a' - 'A'))
|
|
: character;
|
|
};
|
|
if (lower(static_cast<unsigned char>(value.name[index]))
|
|
!= lower(static_cast<unsigned char>(name[index]))) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
});
|
|
return found == request.headers.end() ? nullptr : &*found;
|
|
}
|
|
|
|
void require_safe_request(const HttpRequest& request, std::string_view token) {
|
|
require(request.method == HttpMethod::get, "Graph read adapter sent a non-GET request");
|
|
require(request.body.empty(), "Graph GET request unexpectedly contains a body");
|
|
const HttpHeader* authorization = header(request, "Authorization");
|
|
require(authorization != nullptr && authorization->value == "Bearer " + std::string{token},
|
|
"Graph request is missing the exact bearer credential");
|
|
require(request.url.starts_with(graph_prefix), "Graph request left the trusted origin");
|
|
require(request.url.find("/beta/") == std::string::npos,
|
|
"Graph adapter called the beta API");
|
|
}
|
|
|
|
void seed_account(Rig& rig) {
|
|
rig.transport.respond(profile_body());
|
|
static_cast<void>(rig.graph.refresh_account(rig.credentials));
|
|
}
|
|
|
|
void seed_calendars(Rig& rig, bool include_other = true) {
|
|
seed_account(rig);
|
|
nlohmann::json values = nlohmann::json::array(
|
|
{calendar_json(primary_remote_id, "Primary", true)});
|
|
if (include_other) {
|
|
values.push_back(calendar_json("remote-other-calendar", "Other", false, false));
|
|
}
|
|
rig.transport.respond(calendar_page(values));
|
|
rig.graph.refresh_calendars(rig.credentials);
|
|
}
|
|
|
|
[[nodiscard]] std::string sha256_hex(std::string_view value) {
|
|
std::array<unsigned char, 32> digest{};
|
|
unsigned int size = 0;
|
|
require(EVP_Digest(value.data(), value.size(), digest.data(), &size, EVP_sha256(), nullptr) == 1
|
|
&& size == digest.size(),
|
|
"could not hash Graph local identifier fixture");
|
|
constexpr char hexadecimal[] = "0123456789abcdef";
|
|
std::string result;
|
|
result.reserve(digest.size() * 2);
|
|
for (const unsigned char byte : digest) {
|
|
result.push_back(hexadecimal[byte >> 4U]);
|
|
result.push_back(hexadecimal[byte & 0x0fU]);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] std::string local_calendar_id(
|
|
std::string_view account, std::string_view remote) {
|
|
std::string input{"calendar", 8};
|
|
input.push_back('\0');
|
|
input += account;
|
|
input.push_back('\0');
|
|
input += remote;
|
|
return std::string{primary_local_prefix} + sha256_hex(input);
|
|
}
|
|
|
|
[[nodiscard]] std::string local_event_id(
|
|
std::string_view calendar_id, std::string_view remote) {
|
|
std::string input{"event", 5};
|
|
input.push_back('\0');
|
|
input += calendar_id;
|
|
input.push_back('\0');
|
|
input += remote;
|
|
return std::string{event_local_prefix} + sha256_hex(input);
|
|
}
|
|
|
|
void test_credentials_and_request_security(const std::filesystem::path& root) {
|
|
const std::vector<MicrosoftGraphCredentials> invalid{
|
|
{"", "token"}, {"bad\naccount", "token"}, {"SENSITIVE_ACCOUNT_ID_123", ""},
|
|
{"SENSITIVE_ACCOUNT_ID_123", " "},
|
|
{"SENSITIVE_ACCOUNT_ID_123", "token\r\nInjected: yes"}};
|
|
for (const auto& credentials : invalid) {
|
|
Rig rig{root};
|
|
const CacheSnapshot before = rig.cache.snapshot();
|
|
const std::array<std::string_view, 2> sensitive{
|
|
credentials.account_id, credentials.access_token};
|
|
require_throws_redacted(
|
|
[&] { static_cast<void>(rig.graph.refresh_account(credentials)); },
|
|
"invalid Graph credentials were accepted", sensitive);
|
|
require(rig.transport.requests.empty(), "invalid credentials reached the transport");
|
|
require(rig.cache.snapshot() == before, "invalid credentials changed the cache");
|
|
}
|
|
|
|
Rig rig{root};
|
|
rig.transport.respond(profile_body());
|
|
const auto account = rig.graph.refresh_account(rig.credentials);
|
|
require(account.id == rig.credentials.account_id && account.provider == "microsoft-graph"
|
|
&& account.remote_subject == "remote-user" && account.display_name == "Alice",
|
|
"valid profile did not map to the frozen account contract");
|
|
require(rig.transport.requests.size() == 1, "profile refresh sent the wrong request count");
|
|
require_safe_request(rig.transport.requests[0], rig.credentials.access_token);
|
|
|
|
rig.transport.respond(profile_body("remote-user", ""));
|
|
const auto fallback = rig.graph.refresh_account(rig.credentials);
|
|
require(fallback.display_name == "alice@example.test",
|
|
"empty displayName did not fall back to userPrincipalName");
|
|
}
|
|
|
|
void test_account_failures_are_atomic_and_redacted(const std::filesystem::path& root) {
|
|
Rig rig{root};
|
|
rig.transport.respond(profile_body());
|
|
static_cast<void>(rig.graph.refresh_account(rig.credentials));
|
|
const CacheSnapshot baseline = rig.cache.snapshot();
|
|
|
|
const std::string response_secret = "SENSITIVE_PROFILE_BODY";
|
|
rig.transport.respond(response_secret, 401);
|
|
const std::array<std::string_view, 2> response_sensitive{
|
|
rig.credentials.access_token, response_secret};
|
|
require_throws_redacted([&] { static_cast<void>(rig.graph.refresh_account(rig.credentials)); },
|
|
"non-2xx profile response was accepted", response_sensitive);
|
|
require(rig.cache.snapshot() == baseline, "non-2xx profile response mutated account cache");
|
|
|
|
rig.transport.fail("transport detail " + rig.credentials.access_token);
|
|
const std::array<std::string_view, 1> token_sensitive{rig.credentials.access_token};
|
|
require_throws_redacted([&] { static_cast<void>(rig.graph.refresh_account(rig.credentials)); },
|
|
"profile transport failure did not propagate safely", token_sensitive);
|
|
require(rig.cache.snapshot() == baseline, "profile transport failure mutated account cache");
|
|
|
|
const std::vector<std::string> malformed{"{", "[]", R"({"id":"x"})",
|
|
R"({"id":"first","id":"second","displayName":"name"})",
|
|
std::string(8U * 1024U * 1024U + 1U, 'x')};
|
|
for (const auto& body : malformed) {
|
|
rig.transport.respond(body);
|
|
const std::array<std::string_view, 2> sensitive{body, rig.credentials.access_token};
|
|
require_throws_redacted(
|
|
[&] { static_cast<void>(rig.graph.refresh_account(rig.credentials)); },
|
|
"malformed profile body was accepted", sensitive);
|
|
require(rig.cache.snapshot() == baseline, "malformed profile body mutated account cache");
|
|
}
|
|
}
|
|
|
|
void test_calendar_listing_atomicity_and_duplicates(const std::filesystem::path& root) {
|
|
Rig rig{root};
|
|
seed_calendars(rig);
|
|
const CacheSnapshot baseline = rig.cache.snapshot();
|
|
const std::string next =
|
|
"https://graph.microsoft.com/v1.0/me/calendars?$skiptoken=partial-cursor";
|
|
rig.transport.respond(calendar_page(
|
|
nlohmann::json::array({calendar_json(primary_remote_id, "Changed", true)}), next));
|
|
rig.transport.fail("later calendar page failed SENSITIVE_ACCESS_TOKEN");
|
|
const std::array<std::string_view, 3> sensitive{
|
|
rig.credentials.access_token, next, primary_remote_id};
|
|
require_throws_redacted([&] { rig.graph.refresh_calendars(rig.credentials); },
|
|
"partial calendar listing was accepted", sensitive);
|
|
require(rig.cache.snapshot() == baseline,
|
|
"partial calendar listing changed or deactivated cached calendars");
|
|
|
|
const std::vector<std::string> invalid_pages{
|
|
calendar_page(nlohmann::json::array(
|
|
{calendar_json("duplicate", "One", true),
|
|
calendar_json("duplicate", "Two", false)})),
|
|
calendar_page(nlohmann::json::array(
|
|
{calendar_json("primary-a", "One", true),
|
|
calendar_json("primary-b", "Two", true)})),
|
|
R"({"value":{},"@odata.nextLink":null})",
|
|
R"({"value":[],"value":[]})",
|
|
"{",
|
|
std::string(8U * 1024U * 1024U + 1U, 'x')};
|
|
for (const auto& body : invalid_pages) {
|
|
rig.transport.respond(body);
|
|
const std::array<std::string_view, 2> page_sensitive{body, rig.credentials.access_token};
|
|
require_throws_redacted([&] { rig.graph.refresh_calendars(rig.credentials); },
|
|
"invalid calendar listing was accepted", page_sensitive);
|
|
require(rig.cache.snapshot() == baseline, "invalid calendar listing mutated cache");
|
|
}
|
|
}
|
|
|
|
void test_continuation_url_rejection(const std::filesystem::path& root) {
|
|
const std::vector<std::string> unsafe{
|
|
"http://graph.microsoft.com/v1.0/next",
|
|
"https://evil.example.test/v1.0/next",
|
|
"https://graph.microsoft.com.evil.test/v1.0/next",
|
|
"https://user@graph.microsoft.com/v1.0/next",
|
|
"https://graph.microsoft.com:443/v1.0/next",
|
|
"https://graph.microsoft.com/v1.0/next#fragment",
|
|
"https://graph.microsoft.com/v1.0/next\nInjected: yes",
|
|
std::string{"https://graph.microsoft.com/"} + std::string(16U * 1024U, 'x')};
|
|
for (const auto& url : unsafe) {
|
|
Rig rig{root};
|
|
seed_calendars(rig);
|
|
const std::size_t requests_before = rig.transport.requests.size();
|
|
const CacheSnapshot baseline = rig.cache.snapshot();
|
|
rig.transport.respond(calendar_page(nlohmann::json::array(), url));
|
|
const std::array<std::string_view, 3> sensitive{
|
|
url, rig.credentials.access_token, primary_remote_id};
|
|
require_throws_redacted([&] { rig.graph.refresh_calendars(rig.credentials); },
|
|
"unsafe Graph continuation was accepted", sensitive);
|
|
require(rig.transport.requests.size() == requests_before + 1,
|
|
"unsafe continuation reached a credentialed request");
|
|
require(rig.cache.snapshot() == baseline, "unsafe continuation mutated cache");
|
|
}
|
|
|
|
Rig rig{root};
|
|
seed_calendars(rig);
|
|
const std::string repeated =
|
|
"https://graph.microsoft.com/v1.0/me/calendars?$skiptoken=repeated";
|
|
const std::size_t before = rig.transport.requests.size();
|
|
rig.transport.respond(calendar_page(nlohmann::json::array(), repeated));
|
|
rig.transport.respond(calendar_page(nlohmann::json::array(), repeated));
|
|
const std::array<std::string_view, 2> sensitive{repeated, rig.credentials.access_token};
|
|
require_throws_redacted([&] { rig.graph.refresh_calendars(rig.credentials); },
|
|
"repeated Graph continuation was accepted", sensitive);
|
|
require(rig.transport.requests.size() == before + 2,
|
|
"repeated continuation triggered another credentialed request");
|
|
}
|
|
|
|
void test_window_and_primary_preconditions(const std::filesystem::path& root) {
|
|
const std::vector<MicrosoftGraphWindow> invalid{{0, 0}, {1, 0},
|
|
{std::numeric_limits<std::int64_t>::min(), 0},
|
|
{0, std::numeric_limits<std::int64_t>::max()}};
|
|
for (const auto window : invalid) {
|
|
Rig rig{root};
|
|
seed_calendars(rig);
|
|
const std::size_t before = rig.transport.requests.size();
|
|
require_throws_redacted(
|
|
[&] { rig.graph.sync_primary_calendar_window(rig.credentials, window); },
|
|
"invalid Graph window was accepted");
|
|
require(rig.transport.requests.size() == before,
|
|
"invalid Graph window reached the transport");
|
|
}
|
|
|
|
Rig no_primary{root};
|
|
seed_account(no_primary);
|
|
no_primary.transport.respond(calendar_page(
|
|
nlohmann::json::array({calendar_json("not-primary", "Other", false)})));
|
|
no_primary.graph.refresh_calendars(no_primary.credentials);
|
|
const std::size_t before = no_primary.transport.requests.size();
|
|
require_throws_redacted(
|
|
[&] { no_primary.graph.sync_primary_calendar_window(
|
|
no_primary.credentials, july_window()); },
|
|
"sync without a primary calendar was accepted");
|
|
require(no_primary.transport.requests.size() == before,
|
|
"missing-primary sync reached the transport");
|
|
}
|
|
|
|
void test_delta_request_and_strict_page_shape(const std::filesystem::path& root) {
|
|
Rig rig{root};
|
|
seed_calendars(rig);
|
|
const CacheSnapshot baseline = rig.cache.snapshot();
|
|
const std::vector<std::string> invalid{
|
|
R"({"value":[]})",
|
|
R"({"value":[],"@odata.nextLink":"https://graph.microsoft.com/v1.0/next","@odata.deltaLink":"https://graph.microsoft.com/v1.0/delta"})",
|
|
R"({"value":{},"@odata.deltaLink":"https://graph.microsoft.com/v1.0/delta"})",
|
|
R"({"value":[],"value":[],"@odata.deltaLink":"https://graph.microsoft.com/v1.0/delta"})",
|
|
"{", std::string(8U * 1024U * 1024U + 1U, 'x')};
|
|
for (const auto& body : invalid) {
|
|
rig.transport.respond(body);
|
|
const std::array<std::string_view, 2> sensitive{body, rig.credentials.access_token};
|
|
require_throws_redacted(
|
|
[&] { rig.graph.sync_primary_calendar_window(rig.credentials, july_window()); },
|
|
"invalid delta page was accepted", sensitive);
|
|
require(rig.cache.snapshot() == baseline,
|
|
"invalid delta page advanced cursor or changed cache");
|
|
}
|
|
|
|
const std::string delta = "https://graph.microsoft.com/v1.0/me/calendarView/delta?$deltatoken=done";
|
|
rig.transport.respond(delta_page(nlohmann::json::array(), std::nullopt, delta));
|
|
const std::size_t request_index = rig.transport.requests.size();
|
|
rig.graph.sync_primary_calendar_window(rig.credentials, july_window());
|
|
const HttpRequest& request = rig.transport.requests[request_index];
|
|
require_safe_request(request, rig.credentials.access_token);
|
|
require(request.url.find("/v1.0/me/calendarView/delta?") != std::string::npos,
|
|
"initial sync did not use stable primary calendarView/delta");
|
|
require(request.url.find("startDateTime=2026-07-18T00%3A00%3A00.000000Z")
|
|
!= std::string::npos
|
|
&& request.url.find("endDateTime=2026-07-19T00%3A00%3A00.000000Z")
|
|
!= std::string::npos,
|
|
"initial delta window was not canonically encoded");
|
|
require(request.url.find("/beta/") == std::string::npos
|
|
&& request.url.find("/calendars/") == std::string::npos,
|
|
"adapter used beta or undocumented per-calendar delta");
|
|
const HttpHeader* prefer = header(request, "Prefer");
|
|
require(prefer != nullptr && prefer->value.find("UTC") != std::string::npos,
|
|
"delta request is missing its UTC Prefer header");
|
|
}
|
|
|
|
void test_delta_continuation_security(const std::filesystem::path& root) {
|
|
{
|
|
Rig rig{root};
|
|
seed_calendars(rig);
|
|
const CacheSnapshot baseline = rig.cache.snapshot();
|
|
const std::string unsafe = "https://evil.example.test/v1.0/delta?SENSITIVE_CURSOR";
|
|
const std::string body = delta_page(
|
|
nlohmann::json::array({live_event("SENSITIVE_REMOTE_ID")}), unsafe, std::nullopt);
|
|
const std::size_t before = rig.transport.requests.size();
|
|
rig.transport.respond(body);
|
|
const std::array<std::string_view, 4> sensitive{
|
|
unsafe, body, "SENSITIVE_REMOTE_ID", rig.credentials.access_token};
|
|
require_throws_redacted(
|
|
[&] { rig.graph.sync_primary_calendar_window(rig.credentials, july_window()); },
|
|
"unsafe delta continuation was accepted", sensitive);
|
|
require(rig.transport.requests.size() == before + 1,
|
|
"unsafe delta continuation reached a credentialed request");
|
|
require(rig.cache.snapshot() == baseline,
|
|
"page with unsafe delta continuation was committed");
|
|
}
|
|
|
|
{
|
|
Rig rig{root};
|
|
seed_calendars(rig);
|
|
const std::string repeated =
|
|
"https://graph.microsoft.com/v1.0/me/calendarView/delta?$skiptoken=repeated";
|
|
const std::size_t before = rig.transport.requests.size();
|
|
rig.transport.respond(delta_page(nlohmann::json::array(), repeated, std::nullopt));
|
|
rig.transport.respond(delta_page(nlohmann::json::array(), repeated, std::nullopt));
|
|
const std::array<std::string_view, 2> sensitive{repeated, rig.credentials.access_token};
|
|
require_throws_redacted(
|
|
[&] { rig.graph.sync_primary_calendar_window(rig.credentials, july_window()); },
|
|
"repeated delta continuation was accepted", sensitive);
|
|
require(rig.transport.requests.size() == before + 2,
|
|
"repeated delta continuation triggered a third credentialed request");
|
|
}
|
|
}
|
|
|
|
void test_authoritative_timestamp_offsets(const std::filesystem::path& root) {
|
|
Rig rig{root};
|
|
seed_calendars(rig);
|
|
const auto event = live_event("offset-event", "2026-07-18T09:00:00.1234567Z",
|
|
"2026-07-18T11:00:00.1234567+01:00", "Unknown/But-Ignored");
|
|
rig.transport.respond(delta_page(nlohmann::json::array({event}), std::nullopt,
|
|
"https://graph.microsoft.com/v1.0/me/calendarView/delta?$deltatoken=offset"));
|
|
rig.graph.sync_primary_calendar_window(rig.credentials, july_window());
|
|
const CacheSnapshot snapshot = rig.cache.snapshot();
|
|
require(snapshot.instances.size() == 1, "valid explicit-offset event was not cached");
|
|
const auto day = sys_days{year{2026} / July / 18};
|
|
const auto expected_start =
|
|
duration_cast<microseconds>((day + 9h).time_since_epoch()).count() + 123'456;
|
|
const auto expected_end =
|
|
duration_cast<microseconds>((day + 10h).time_since_epoch()).count() + 123'456;
|
|
require(snapshot.instances[0].start_epoch_microseconds == expected_start
|
|
&& snapshot.instances[0].end_epoch_microseconds == expected_end,
|
|
"explicit offset authority or seventh-digit truncation is incorrect");
|
|
}
|
|
|
|
void test_timestamp_identity_and_interval_rejection(const std::filesystem::path& root) {
|
|
const std::vector<nlohmann::json> invalid_events{
|
|
live_event("unknown-zone", "2026-07-18T09:00:00", "2026-07-18T10:00:00",
|
|
"Europe/London"),
|
|
live_event("invalid-date", "2026-02-30T09:00:00", "2026-02-30T10:00:00"),
|
|
live_event("leap-second", "2026-07-18T09:00:60Z", "2026-07-18T10:00:00Z",
|
|
"Ignored"),
|
|
live_event("bad-offset", "2026-07-18T09:00:00+25:00",
|
|
"2026-07-18T10:00:00+25:00", "Ignored"),
|
|
live_event("backwards", "2026-07-18T10:00:00", "2026-07-18T09:00:00"),
|
|
};
|
|
for (const auto& event : invalid_events) {
|
|
Rig rig{root};
|
|
seed_calendars(rig);
|
|
const CacheSnapshot baseline = rig.cache.snapshot();
|
|
const std::string delta =
|
|
"https://graph.microsoft.com/v1.0/me/calendarView/delta?$deltatoken=invalid";
|
|
const std::string body =
|
|
delta_page(nlohmann::json::array({event}), std::nullopt, delta);
|
|
rig.transport.respond(body);
|
|
const std::array<std::string_view, 3> sensitive{
|
|
body, event.at("id").get_ref<const std::string&>(), rig.credentials.access_token};
|
|
require_throws_redacted(
|
|
[&] { rig.graph.sync_primary_calendar_window(rig.credentials, july_window()); },
|
|
"invalid Graph event timestamp was accepted", sensitive);
|
|
require(rig.cache.snapshot() == baseline,
|
|
"invalid Graph event advanced cursor or changed cache");
|
|
}
|
|
|
|
Rig duplicate{root};
|
|
seed_calendars(duplicate);
|
|
const CacheSnapshot baseline = duplicate.cache.snapshot();
|
|
duplicate.transport.respond(delta_page(
|
|
nlohmann::json::array({live_event("duplicate"), live_event("duplicate")}),
|
|
std::nullopt,
|
|
"https://graph.microsoft.com/v1.0/me/calendarView/delta?$deltatoken=duplicate"));
|
|
require_throws_redacted(
|
|
[&] { duplicate.graph.sync_primary_calendar_window(
|
|
duplicate.credentials, july_window()); },
|
|
"duplicate Graph event identity was accepted");
|
|
require(duplicate.cache.snapshot() == baseline,
|
|
"duplicate Graph event advanced cursor or changed cache");
|
|
}
|
|
|
|
void test_resume_after_later_failure(const std::filesystem::path& root) {
|
|
Rig rig{root};
|
|
seed_calendars(rig);
|
|
const std::string next = "https://graph.microsoft.com/v1.0/me/calendarView/delta?$skiptoken=page-2";
|
|
rig.transport.respond(
|
|
delta_page(nlohmann::json::array({live_event()}), next, std::nullopt));
|
|
rig.transport.fail("later transport failed " + next + " remote-event-1 SENSITIVE_ACCESS_TOKEN");
|
|
const std::size_t start_request = rig.transport.requests.size();
|
|
const std::array<std::string_view, 3> sensitive{
|
|
next, "remote-event-1", rig.credentials.access_token};
|
|
require_throws_redacted(
|
|
[&] { rig.graph.sync_primary_calendar_window(rig.credentials, july_window()); },
|
|
"later delta transport failure was accepted", sensitive);
|
|
const CacheSnapshot committed = rig.cache.snapshot();
|
|
require(committed.events.size() == 1 && committed.instances.size() == 1
|
|
&& committed.checkpoints.size() == 1 && committed.checkpoints[0].cursor == next
|
|
&& !committed.checkpoints[0].complete,
|
|
"validated first delta page was not committed atomically before later failure");
|
|
require(rig.transport.requests.size() == start_request + 2,
|
|
"paged delta failure sent an unexpected request count");
|
|
|
|
const std::string final =
|
|
"https://graph.microsoft.com/v1.0/me/calendarView/delta?$deltatoken=complete";
|
|
rig.transport.respond(delta_page(nlohmann::json::array(), std::nullopt, final));
|
|
const std::size_t resume_index = rig.transport.requests.size();
|
|
rig.graph.sync_primary_calendar_window(rig.credentials, july_window());
|
|
require(rig.transport.requests[resume_index].url == next,
|
|
"later sync did not resume from the last committed opaque cursor");
|
|
require(rig.cache.snapshot().checkpoints[0].cursor == final
|
|
&& rig.cache.snapshot().checkpoints[0].complete,
|
|
"resumed delta did not commit its final cursor");
|
|
}
|
|
|
|
void test_tombstone_preserves_payload(const std::filesystem::path& root) {
|
|
Rig rig{root};
|
|
seed_calendars(rig);
|
|
const std::string first_delta =
|
|
"https://graph.microsoft.com/v1.0/me/calendarView/delta?$deltatoken=first";
|
|
rig.transport.respond(
|
|
delta_page(nlohmann::json::array({live_event()}), std::nullopt, first_delta));
|
|
rig.graph.sync_primary_calendar_window(rig.credentials, july_window());
|
|
const std::string raw = rig.cache.snapshot().events[0].raw_payload;
|
|
|
|
const std::string second_delta =
|
|
"https://graph.microsoft.com/v1.0/me/calendarView/delta?$deltatoken=second";
|
|
const nlohmann::json removed =
|
|
{{"id", "remote-event-1"}, {"@removed", {{"reason", "deleted"}}}};
|
|
rig.transport.respond(
|
|
delta_page(nlohmann::json::array({removed}), std::nullopt, second_delta));
|
|
rig.graph.sync_primary_calendar_window(rig.credentials, july_window());
|
|
const CacheSnapshot snapshot = rig.cache.snapshot();
|
|
require(snapshot.events.size() == 1 && snapshot.events[0].deleted
|
|
&& snapshot.events[0].raw_payload == raw && snapshot.instances.empty(),
|
|
"Graph tombstone did not retain raw payload and remove instances");
|
|
}
|
|
|
|
void test_cache_failure_does_not_advance_cursor(const std::filesystem::path& root) {
|
|
Rig rig{root};
|
|
seed_calendars(rig, false);
|
|
const std::string calendar_id =
|
|
local_calendar_id(rig.credentials.account_id, primary_remote_id);
|
|
const std::string poisoned_id = local_event_id(calendar_id, "target-remote-event");
|
|
const std::string prior =
|
|
"https://graph.microsoft.com/v1.0/me/calendarView/delta?$skiptoken=prior";
|
|
CachedEvent poisoned{poisoned_id, calendar_id, "different-remote-id", "uid", "", "",
|
|
R"({"poisoned":true})", false};
|
|
rig.cache.apply_pull_page(PullPage{{poisoned}, {},
|
|
SyncCheckpoint{calendar_id, "2026-07-18T00:00:00.000000Z",
|
|
"2026-07-19T00:00:00.000000Z", prior, false}});
|
|
const CacheSnapshot baseline = rig.cache.snapshot();
|
|
|
|
const std::string next =
|
|
"https://graph.microsoft.com/v1.0/me/calendarView/delta?$deltatoken=must-not-commit";
|
|
const std::string body = delta_page(
|
|
nlohmann::json::array({live_event("target-remote-event")}), std::nullopt, next);
|
|
rig.transport.respond(body);
|
|
const std::array<std::string_view, 4> sensitive{
|
|
prior, next, "target-remote-event", rig.credentials.access_token};
|
|
require_throws_redacted(
|
|
[&] { rig.graph.sync_primary_calendar_window(rig.credentials, july_window()); },
|
|
"Graph cache identity failure was accepted", sensitive);
|
|
require(rig.cache.snapshot() == baseline,
|
|
"cache failure advanced Graph cursor or partially replaced event state");
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main() {
|
|
try {
|
|
TempDirectory temporary;
|
|
test_credentials_and_request_security(temporary.path());
|
|
test_account_failures_are_atomic_and_redacted(temporary.path());
|
|
test_calendar_listing_atomicity_and_duplicates(temporary.path());
|
|
test_continuation_url_rejection(temporary.path());
|
|
test_window_and_primary_preconditions(temporary.path());
|
|
test_delta_request_and_strict_page_shape(temporary.path());
|
|
test_delta_continuation_security(temporary.path());
|
|
test_authoritative_timestamp_offsets(temporary.path());
|
|
test_timestamp_identity_and_interval_rejection(temporary.path());
|
|
test_resume_after_later_failure(temporary.path());
|
|
test_tombstone_preserves_payload(temporary.path());
|
|
test_cache_failure_does_not_advance_cursor(temporary.path());
|
|
} catch (const std::exception& error) {
|
|
std::cerr << "Microsoft Graph security test failure: " << error.what() << '\n';
|
|
return 1;
|
|
}
|
|
std::cout << "Microsoft Graph security tests passed\n";
|
|
return 0;
|
|
}
|