calendar-blob-store-inf-kn-3b.test.mjs
sha256:b5f647cb9c409f563d4671fe3fc05ddea01fabfed9b41fc11cb923588e1c1baf
mirror: GitHub Phase A durable MCP OAuth (#270)
Human
minor
⚠ breaking
10 days ago
| 1 | /** |
| 2 | * Hosted calendar store persistence (Netlify Blobs) — INF-KN-3b. |
| 3 | * |
| 4 | * Tiers: unit, integration, data-integrity, security. |
| 5 | */ |
| 6 | import { describe, it, beforeEach, afterEach } from 'node:test'; |
| 7 | import assert from 'node:assert/strict'; |
| 8 | import fs from 'node:fs'; |
| 9 | import path from 'node:path'; |
| 10 | import { fileURLToPath } from 'node:url'; |
| 11 | |
| 12 | import { |
| 13 | CALENDAR_STORE_BLOB_KEY, |
| 14 | calendarOAuthBlobKey, |
| 15 | hydrateCalendarStoresFromBlob, |
| 16 | mergeCalendarStoreJson, |
| 17 | persistCalendarStoresToBlob, |
| 18 | withCalendarBlobSync, |
| 19 | } from '../hub/bridge/calendar-blob-store.mjs'; |
| 20 | import { getCalendarStorePath, saveCalendarStore } from '../lib/calendar/event-store.mjs'; |
| 21 | import { writeOAuthTokenVault } from '../lib/calendar/oauth-token-vault.mjs'; |
| 22 | |
| 23 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); |
| 24 | const tmpRoot = path.join(__dirname, 'fixtures', 'tmp-calendar-blob-inf-kn-3b'); |
| 25 | |
| 26 | /** |
| 27 | * @returns {{ |
| 28 | * get: (k: string, o?: { type?: string, consistency?: string }) => Promise<string|null>, |
| 29 | * set: (k: string, v: string) => Promise<void>, |
| 30 | * map: Map<string, string>, |
| 31 | * getCalls: Array<{ key: string, consistency?: string }>, |
| 32 | * }} |
| 33 | */ |
| 34 | function makeMockBlobStore() { |
| 35 | const map = new Map(); |
| 36 | /** @type {Array<{ key: string, consistency?: string }>} */ |
| 37 | const getCalls = []; |
| 38 | return { |
| 39 | map, |
| 40 | getCalls, |
| 41 | async get(key, opts) { |
| 42 | getCalls.push({ |
| 43 | key, |
| 44 | ...(opts?.consistency !== undefined ? { consistency: opts.consistency } : {}), |
| 45 | }); |
| 46 | const v = map.get(key); |
| 47 | if (v == null) return null; |
| 48 | return opts?.type === 'text' || typeof v === 'string' ? v : null; |
| 49 | }, |
| 50 | async set(key, value) { |
| 51 | map.set(key, value); |
| 52 | }, |
| 53 | }; |
| 54 | } |
| 55 | |
| 56 | describe('INF-KN-3b calendar blob store — unit', () => { |
| 57 | it('calendarOAuthBlobKey namespaces connector token blobs', () => { |
| 58 | assert.equal(calendarOAuthBlobKey('conn_abc12345'), 'calendar/oauth/conn_abc12345.enc'); |
| 59 | }); |
| 60 | |
| 61 | it('mergeCalendarStoreJson keeps fresher pending OAuth over stale connected blob', () => { |
| 62 | const blob = JSON.stringify({ |
| 63 | vaults: { |
| 64 | default: { |
| 65 | connectors: [{ |
| 66 | connector_id: 'conn_same', |
| 67 | provider: 'google', |
| 68 | display_name: 'Old', |
| 69 | status: 'connected', |
| 70 | oauth_ref: 'conn_same', |
| 71 | account_sub: 'sub', |
| 72 | sync_cursors: {}, |
| 73 | last_sync_at: '2026-07-01T00:00:00.000Z', |
| 74 | last_sync_error: 'none', |
| 75 | revoked_at: null, |
| 76 | oauth_pending: null, |
| 77 | }], |
| 78 | source_calendars: [], |
| 79 | events: [], |
| 80 | }, |
| 81 | }, |
| 82 | }); |
| 83 | const local = JSON.stringify({ |
| 84 | vaults: { |
| 85 | default: { |
| 86 | connectors: [{ |
| 87 | connector_id: 'conn_same', |
| 88 | provider: 'google', |
| 89 | display_name: 'Pending', |
| 90 | status: 'pending', |
| 91 | oauth_ref: null, |
| 92 | account_sub: null, |
| 93 | sync_cursors: {}, |
| 94 | last_sync_at: null, |
| 95 | last_sync_error: null, |
| 96 | revoked_at: null, |
| 97 | oauth_pending: { |
| 98 | state: 'state-abc', |
| 99 | code_verifier: 'v', |
| 100 | return_url: 'https://scooling.netlify.app/settings/calendar/connect/callback', |
| 101 | expires_at: '2099-01-01T00:00:00.000Z', |
| 102 | }, |
| 103 | }], |
| 104 | source_calendars: [], |
| 105 | events: [], |
| 106 | }, |
| 107 | }, |
| 108 | }); |
| 109 | const merged = JSON.parse(mergeCalendarStoreJson(local, blob)); |
| 110 | const connector = merged.vaults.default.connectors[0]; |
| 111 | assert.equal(connector.status, 'pending'); |
| 112 | assert.equal(connector.oauth_pending.state, 'state-abc'); |
| 113 | }); |
| 114 | }); |
| 115 | |
| 116 | describe('INF-KN-3b calendar blob store — integration', () => { |
| 117 | /** @type {string} */ |
| 118 | let dataDir; |
| 119 | |
| 120 | beforeEach(() => { |
| 121 | dataDir = path.join(tmpRoot, `run-${Date.now()}-${Math.random().toString(36).slice(2)}`); |
| 122 | fs.mkdirSync(dataDir, { recursive: true }); |
| 123 | }); |
| 124 | |
| 125 | afterEach(() => { |
| 126 | fs.rmSync(dataDir, { recursive: true, force: true }); |
| 127 | }); |
| 128 | |
| 129 | it('hydrate + persist round-trips calendar store and connected OAuth token blobs', async () => { |
| 130 | const blobStore = makeMockBlobStore(); |
| 131 | const connectorId = 'conn_test12345678'; |
| 132 | const secret = 'a'.repeat(32); |
| 133 | |
| 134 | saveCalendarStore(dataDir, { |
| 135 | vaults: { |
| 136 | default: { |
| 137 | connectors: [{ |
| 138 | connector_id: connectorId, |
| 139 | provider: 'google', |
| 140 | display_name: 'Google Calendar', |
| 141 | status: 'connected', |
| 142 | oauth_ref: connectorId, |
| 143 | account_sub: 'sub-1', |
| 144 | sync_cursors: {}, |
| 145 | last_sync_at: null, |
| 146 | last_sync_error: 'none', |
| 147 | revoked_at: null, |
| 148 | }], |
| 149 | source_calendars: [], |
| 150 | events: [], |
| 151 | }, |
| 152 | }, |
| 153 | }); |
| 154 | writeOAuthTokenVault(dataDir, connectorId, secret, { |
| 155 | refresh_token: 'refresh-token', |
| 156 | scope: 'calendar.readonly', |
| 157 | token_type: 'Bearer', |
| 158 | obtained_at: '2026-07-07T12:00:00.000Z', |
| 159 | account_sub: 'sub-1', |
| 160 | }); |
| 161 | |
| 162 | await persistCalendarStoresToBlob(blobStore, dataDir); |
| 163 | assert.ok(blobStore.map.has(CALENDAR_STORE_BLOB_KEY)); |
| 164 | assert.ok(blobStore.map.has(calendarOAuthBlobKey(connectorId))); |
| 165 | |
| 166 | fs.rmSync(getCalendarStorePath(dataDir)); |
| 167 | fs.rmSync(path.join(dataDir, 'calendar_oauth'), { recursive: true, force: true }); |
| 168 | |
| 169 | await hydrateCalendarStoresFromBlob(blobStore, dataDir); |
| 170 | assert.ok(fs.existsSync(getCalendarStorePath(dataDir))); |
| 171 | assert.ok(fs.existsSync(path.join(dataDir, 'calendar_oauth', `${connectorId}.enc`))); |
| 172 | }); |
| 173 | |
| 174 | it('withCalendarBlobSync survives simulated cold start between write and read', async () => { |
| 175 | const blobStore = makeMockBlobStore(); |
| 176 | const connectorId = 'conn_coldstart12'; |
| 177 | |
| 178 | await withCalendarBlobSync({ |
| 179 | blobStore, |
| 180 | dataDir, |
| 181 | run: () => { |
| 182 | saveCalendarStore(dataDir, { |
| 183 | vaults: { |
| 184 | default: { |
| 185 | connectors: [{ |
| 186 | connector_id: connectorId, |
| 187 | provider: 'google', |
| 188 | display_name: 'Work', |
| 189 | status: 'connected', |
| 190 | oauth_ref: connectorId, |
| 191 | account_sub: 'sub-2', |
| 192 | sync_cursors: {}, |
| 193 | last_sync_at: '2026-07-07T12:00:00.000Z', |
| 194 | last_sync_error: 'none', |
| 195 | revoked_at: null, |
| 196 | }], |
| 197 | source_calendars: [{ |
| 198 | source_calendar_id: 'cal_g_primary', |
| 199 | connector_id: connectorId, |
| 200 | display_name: 'Primary', |
| 201 | color: null, |
| 202 | user_group: null, |
| 203 | provider: 'google', |
| 204 | enabled_for_sync: true, |
| 205 | enabled_for_display: true, |
| 206 | enabled_for_agents: false, |
| 207 | agent_context_tier_max: 0, |
| 208 | }], |
| 209 | events: [], |
| 210 | }, |
| 211 | }, |
| 212 | }); |
| 213 | return { ok: true }; |
| 214 | }, |
| 215 | }); |
| 216 | |
| 217 | fs.rmSync(dataDir, { recursive: true, force: true }); |
| 218 | fs.mkdirSync(dataDir, { recursive: true }); |
| 219 | |
| 220 | const listed = await withCalendarBlobSync({ |
| 221 | blobStore, |
| 222 | dataDir, |
| 223 | persist: false, |
| 224 | run: () => { |
| 225 | const raw = fs.readFileSync(getCalendarStorePath(dataDir), 'utf8'); |
| 226 | return JSON.parse(raw); |
| 227 | }, |
| 228 | }); |
| 229 | |
| 230 | assert.equal(listed.vaults.default.source_calendars[0].display_name, 'Primary'); |
| 231 | }); |
| 232 | |
| 233 | it('does not hydrate OAuth blobs for revoked connectors', async () => { |
| 234 | const blobStore = makeMockBlobStore(); |
| 235 | const connectorId = 'conn_revoked1234'; |
| 236 | blobStore.map.set( |
| 237 | calendarOAuthBlobKey(connectorId), |
| 238 | 'stale-blob-should-not-load', |
| 239 | ); |
| 240 | saveCalendarStore(dataDir, { |
| 241 | vaults: { |
| 242 | default: { |
| 243 | connectors: [{ |
| 244 | connector_id: connectorId, |
| 245 | provider: 'google', |
| 246 | display_name: 'Google Calendar', |
| 247 | status: 'revoked', |
| 248 | oauth_ref: null, |
| 249 | account_sub: 'sub-3', |
| 250 | sync_cursors: {}, |
| 251 | last_sync_at: null, |
| 252 | last_sync_error: 'none', |
| 253 | revoked_at: '2026-07-07T12:00:00.000Z', |
| 254 | }], |
| 255 | source_calendars: [], |
| 256 | events: [], |
| 257 | }, |
| 258 | }, |
| 259 | }); |
| 260 | await persistCalendarStoresToBlob(blobStore, dataDir); |
| 261 | |
| 262 | fs.rmSync(path.join(dataDir, 'calendar_oauth'), { recursive: true, force: true }); |
| 263 | await hydrateCalendarStoresFromBlob(blobStore, dataDir); |
| 264 | |
| 265 | assert.equal( |
| 266 | fs.existsSync(path.join(dataDir, 'calendar_oauth', `${connectorId}.enc`)), |
| 267 | false, |
| 268 | ); |
| 269 | }); |
| 270 | |
| 271 | it('hydrateCalendarStoresFromBlob requests strong consistency for calendar store', async () => { |
| 272 | const blobStore = makeMockBlobStore(); |
| 273 | blobStore.map.set( |
| 274 | CALENDAR_STORE_BLOB_KEY, |
| 275 | JSON.stringify({ |
| 276 | vaults: { |
| 277 | default: { |
| 278 | connectors: [{ |
| 279 | connector_id: 'conn_pending_oauth', |
| 280 | provider: 'google', |
| 281 | display_name: 'Google Calendar', |
| 282 | status: 'pending', |
| 283 | oauth_ref: null, |
| 284 | account_sub: null, |
| 285 | sync_cursors: {}, |
| 286 | last_sync_at: null, |
| 287 | last_sync_error: null, |
| 288 | revoked_at: null, |
| 289 | oauth_pending: { |
| 290 | state: 'oauth-state-xyz', |
| 291 | code_verifier: 'verifier', |
| 292 | return_url: 'https://scooling.netlify.app/settings/calendar/connect/callback', |
| 293 | expires_at: '2099-01-01T00:00:00.000Z', |
| 294 | }, |
| 295 | }], |
| 296 | source_calendars: [], |
| 297 | events: [], |
| 298 | }, |
| 299 | }, |
| 300 | }), |
| 301 | ); |
| 302 | |
| 303 | await hydrateCalendarStoresFromBlob(blobStore, dataDir); |
| 304 | |
| 305 | const storeCall = blobStore.getCalls.find((c) => c.key === CALENDAR_STORE_BLOB_KEY); |
| 306 | assert.ok(storeCall); |
| 307 | assert.equal(storeCall.consistency, 'strong'); |
| 308 | |
| 309 | const onDisk = JSON.parse(fs.readFileSync(getCalendarStorePath(dataDir), 'utf8')); |
| 310 | assert.equal(onDisk.vaults.default.connectors[0].oauth_pending.state, 'oauth-state-xyz'); |
| 311 | }); |
| 312 | |
| 313 | it('hydrate merges warm-lambda local pending with strong blob so state survives', async () => { |
| 314 | const blobStore = makeMockBlobStore(); |
| 315 | // Stale eventual edge view (no pending). |
| 316 | blobStore.map.set( |
| 317 | CALENDAR_STORE_BLOB_KEY, |
| 318 | JSON.stringify({ |
| 319 | vaults: { |
| 320 | default: { |
| 321 | connectors: [], |
| 322 | source_calendars: [], |
| 323 | events: [], |
| 324 | }, |
| 325 | }, |
| 326 | }), |
| 327 | ); |
| 328 | // Warm local disk still has the pending written before cold start completes. |
| 329 | saveCalendarStore(dataDir, { |
| 330 | vaults: { |
| 331 | default: { |
| 332 | connectors: [{ |
| 333 | connector_id: 'conn_warm_pending', |
| 334 | provider: 'google', |
| 335 | display_name: 'Google Calendar', |
| 336 | status: 'pending', |
| 337 | oauth_ref: null, |
| 338 | account_sub: null, |
| 339 | sync_cursors: {}, |
| 340 | last_sync_at: null, |
| 341 | last_sync_error: null, |
| 342 | revoked_at: null, |
| 343 | oauth_pending: { |
| 344 | state: 'warm-state-123', |
| 345 | code_verifier: 'cv', |
| 346 | return_url: 'https://scooling.netlify.app/settings/calendar/connect/callback', |
| 347 | expires_at: '2099-07-08T00:00:00.000Z', |
| 348 | }, |
| 349 | }], |
| 350 | source_calendars: [], |
| 351 | events: [], |
| 352 | }, |
| 353 | }, |
| 354 | }); |
| 355 | |
| 356 | await hydrateCalendarStoresFromBlob(blobStore, dataDir); |
| 357 | const onDisk = JSON.parse(fs.readFileSync(getCalendarStorePath(dataDir), 'utf8')); |
| 358 | assert.equal(onDisk.vaults.default.connectors.length, 1); |
| 359 | assert.equal(onDisk.vaults.default.connectors[0].oauth_pending.state, 'warm-state-123'); |
| 360 | }); |
| 361 | }); |
| 362 | |
| 363 | describe('INF-KN-3b calendar blob store — security', () => { |
| 364 | it('calendar store blob key is separate from encrypted OAuth token blob keys', () => { |
| 365 | assert.equal(CALENDAR_STORE_BLOB_KEY, 'calendar/hub_calendar_store.json'); |
| 366 | assert.ok(!CALENDAR_STORE_BLOB_KEY.includes('.enc')); |
| 367 | assert.equal(calendarOAuthBlobKey('conn_x'), 'calendar/oauth/conn_x.enc'); |
| 368 | assert.notEqual(CALENDAR_STORE_BLOB_KEY, calendarOAuthBlobKey('conn_x')); |
| 369 | }); |
| 370 | }); |
| 371 | |
| 372 | describe('INF-KN-3b calendar blob store — data-integrity', () => { |
| 373 | it('mergeCalendarStoreJson round-trips vault ids without dropping either side', () => { |
| 374 | const local = JSON.stringify({ |
| 375 | vaults: { |
| 376 | vault_a: { |
| 377 | connectors: [{ |
| 378 | connector_id: 'conn_a', |
| 379 | provider: 'google', |
| 380 | display_name: 'A', |
| 381 | status: 'pending', |
| 382 | oauth_ref: null, |
| 383 | account_sub: null, |
| 384 | sync_cursors: {}, |
| 385 | last_sync_at: null, |
| 386 | last_sync_error: null, |
| 387 | revoked_at: null, |
| 388 | oauth_pending: { |
| 389 | state: 'state-a', |
| 390 | code_verifier: 'v', |
| 391 | return_url: 'https://scooling.netlify.app/settings/calendar/connect/callback', |
| 392 | expires_at: '2099-01-01T00:00:00.000Z', |
| 393 | }, |
| 394 | }], |
| 395 | source_calendars: [], |
| 396 | events: [], |
| 397 | }, |
| 398 | }, |
| 399 | }); |
| 400 | const blob = JSON.stringify({ |
| 401 | vaults: { |
| 402 | vault_b: { |
| 403 | connectors: [{ |
| 404 | connector_id: 'conn_b', |
| 405 | provider: 'google', |
| 406 | display_name: 'B', |
| 407 | status: 'connected', |
| 408 | oauth_ref: 'conn_b', |
| 409 | account_sub: 'sub-b', |
| 410 | sync_cursors: {}, |
| 411 | last_sync_at: '2026-07-08T00:00:00.000Z', |
| 412 | last_sync_error: 'none', |
| 413 | revoked_at: null, |
| 414 | }], |
| 415 | source_calendars: [], |
| 416 | events: [], |
| 417 | }, |
| 418 | }, |
| 419 | }); |
| 420 | const merged = JSON.parse(mergeCalendarStoreJson(local, blob)); |
| 421 | assert.ok(merged.vaults.vault_a); |
| 422 | assert.ok(merged.vaults.vault_b); |
| 423 | assert.equal(merged.vaults.vault_a.connectors[0].oauth_pending.state, 'state-a'); |
| 424 | assert.equal(merged.vaults.vault_b.connectors[0].status, 'connected'); |
| 425 | }); |
| 426 | }); |
| 427 | |
| 428 | describe('INF-KN-3b calendar blob store — performance', () => { |
| 429 | it('mergeCalendarStoreJson stays under 50ms for 200 connectors', () => { |
| 430 | const connectors = Array.from({ length: 200 }, (_, i) => ({ |
| 431 | connector_id: `conn_${i}`, |
| 432 | provider: 'google', |
| 433 | display_name: `C${i}`, |
| 434 | status: i % 2 === 0 ? 'pending' : 'connected', |
| 435 | oauth_ref: i % 2 === 0 ? null : `conn_${i}`, |
| 436 | account_sub: null, |
| 437 | sync_cursors: {}, |
| 438 | last_sync_at: null, |
| 439 | last_sync_error: null, |
| 440 | revoked_at: null, |
| 441 | oauth_pending: i % 2 === 0 |
| 442 | ? { |
| 443 | state: `state-${i}`, |
| 444 | code_verifier: 'v', |
| 445 | return_url: 'https://scooling.netlify.app/settings/calendar/connect/callback', |
| 446 | expires_at: '2099-01-01T00:00:00.000Z', |
| 447 | } |
| 448 | : null, |
| 449 | })); |
| 450 | const payload = JSON.stringify({ |
| 451 | vaults: { default: { connectors, source_calendars: [], events: [] } }, |
| 452 | }); |
| 453 | const started = performance.now(); |
| 454 | const merged = mergeCalendarStoreJson(payload, payload); |
| 455 | const elapsed = performance.now() - started; |
| 456 | assert.ok(JSON.parse(merged).vaults.default.connectors.length === 200); |
| 457 | assert.ok(elapsed < 50, `merge took ${elapsed}ms`); |
| 458 | }); |
| 459 | }); |
| 460 | |
| 461 | describe('INF-KN-3b calendar blob store — stress', () => { |
| 462 | it('repeated hydrate+persist does not drop pending oauth state', async () => { |
| 463 | const dataDir = path.join(tmpRoot, `stress-${Date.now()}`); |
| 464 | fs.mkdirSync(dataDir, { recursive: true }); |
| 465 | try { |
| 466 | const blobStore = makeMockBlobStore(); |
| 467 | for (let i = 0; i < 25; i += 1) { |
| 468 | await withCalendarBlobSync({ |
| 469 | blobStore, |
| 470 | dataDir, |
| 471 | run: () => { |
| 472 | saveCalendarStore(dataDir, { |
| 473 | vaults: { |
| 474 | default: { |
| 475 | connectors: [{ |
| 476 | connector_id: 'conn_stress', |
| 477 | provider: 'google', |
| 478 | display_name: 'G', |
| 479 | status: 'pending', |
| 480 | oauth_ref: null, |
| 481 | account_sub: null, |
| 482 | sync_cursors: {}, |
| 483 | last_sync_at: null, |
| 484 | last_sync_error: null, |
| 485 | revoked_at: null, |
| 486 | oauth_pending: { |
| 487 | state: `state-round-${i}`, |
| 488 | code_verifier: 'v', |
| 489 | return_url: 'https://scooling.netlify.app/settings/calendar/connect/callback', |
| 490 | expires_at: '2099-01-01T00:00:00.000Z', |
| 491 | }, |
| 492 | }], |
| 493 | source_calendars: [], |
| 494 | events: [], |
| 495 | }, |
| 496 | }, |
| 497 | }); |
| 498 | return { ok: true }; |
| 499 | }, |
| 500 | }); |
| 501 | fs.rmSync(getCalendarStorePath(dataDir), { force: true }); |
| 502 | await hydrateCalendarStoresFromBlob(blobStore, dataDir); |
| 503 | const onDisk = JSON.parse(fs.readFileSync(getCalendarStorePath(dataDir), 'utf8')); |
| 504 | assert.equal(onDisk.vaults.default.connectors[0].oauth_pending.state, `state-round-${i}`); |
| 505 | } |
| 506 | } finally { |
| 507 | fs.rmSync(dataDir, { recursive: true, force: true }); |
| 508 | } |
| 509 | }); |
| 510 | }); |
File History
1 commit
sha256:b5f647cb9c409f563d4671fe3fc05ddea01fabfed9b41fc11cb923588e1c1baf
mirror: GitHub Phase A durable MCP OAuth (#270)
Human
minor
⚠
10 days ago