calendar-blob-store-inf-kn-3b.test.mjs
510 lines 16.5 KB
Raw
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