import assert from 'node:assert/strict'; import test from 'node:test'; import { attachTokenRefreshPersistence, handleProviderReauthRequiredImpl, persistRefreshedProviderTokensImpl, } from './stateAndSecurityMethods.ts'; import { inspectProviderRemoteStateImpl } from './authMethods.ts'; import { ONEDRIVE_REAUTH_REQUIRED_MARKER, isProviderReadyForSync, type OAuthTokens, type ProviderConnection, } from '../../../domain/sync.ts'; const newTokens = (): OAuthTokens => ({ accessToken: 'fresh-access', refreshToken: 'rotated-refresh', expiresAt: Date.now() + 3_600_000, tokenType: 'Bearer', }); function createManager() { const saved: Array<{ provider: string; tokens?: OAuthTokens }> = []; let notified = 0; const manager = { providerDecryptSeq: { onedrive: 0, google: 0 } as Record, adapters: new Map void }>(), state: { providers: { onedrive: { provider: 'onedrive', status: 'connected', tokens: { accessToken: 'old', refreshToken: 'old-refresh', tokenType: 'Bearer' }, account: { id: 'u1' }, resourceId: 'file-1', } as ProviderConnection, } as Record, }, saveProviderConnection: async (provider: string, connection: { tokens?: OAuthTokens }) => { saved.push({ provider, tokens: connection.tokens }); }, notifyStateChange: () => { notified += 1; }, }; return { manager, saved, getNotified: () => notified }; } test('persistRefreshedProviderTokens updates state, persists, and notifies', async () => { const { manager, saved, getNotified } = createManager(); const tokens = newTokens(); persistRefreshedProviderTokensImpl.call(manager, 'onedrive', tokens); // saveProviderConnection is fire-and-forget; let microtasks flush. await Promise.resolve(); assert.deepEqual(manager.state.providers.onedrive.tokens, tokens); // Other fields are preserved. assert.equal(manager.state.providers.onedrive.account?.id, 'u1'); assert.equal(manager.state.providers.onedrive.resourceId, 'file-1'); assert.equal(manager.state.providers.onedrive.status, 'connected'); assert.equal(saved.length, 1); assert.equal(saved[0].provider, 'onedrive'); assert.deepEqual(saved[0].tokens, tokens); // Decrypt sequence is bumped so an in-flight decrypt cannot clobber the write. assert.equal(manager.providerDecryptSeq.onedrive, 1); assert.equal(getNotified(), 1); }); test('persistRefreshedProviderTokens is a no-op when the provider was disconnected', async () => { const { manager, saved } = createManager(); // Simulate a disconnect happening during the async refresh. manager.state.providers.onedrive = { provider: 'onedrive', status: 'disconnected' }; persistRefreshedProviderTokensImpl.call(manager, 'onedrive', newTokens()); await Promise.resolve(); assert.equal(saved.length, 0); assert.equal(manager.state.providers.onedrive.tokens, undefined); }); test('attachTokenRefreshPersistence wires adapters that expose setOnTokensRefreshed', () => { const { manager, saved } = createManager(); let registered: ((tokens: OAuthTokens) => void) | null = null; const adapter = { setOnTokensRefreshed(cb: (tokens: OAuthTokens) => void) { registered = cb; }, }; attachTokenRefreshPersistence.call(manager, 'onedrive', adapter as never); assert.equal(typeof registered, 'function'); // Invoking the registered callback persists, proving the wiring is correct. const tokens = newTokens(); registered!(tokens); assert.equal(saved.length, 1); assert.deepEqual(saved[0].tokens, tokens); }); test('attachTokenRefreshPersistence is a no-op for adapters without the hook', () => { const { manager } = createManager(); // Adapter without setOnTokensRefreshed (e.g. GitHub) must not throw. assert.doesNotThrow(() => attachTokenRefreshPersistence.call(manager, 'github', {} as never), ); }); test('attachTokenRefreshPersistence persists Google tokens refreshed mid-session', async () => { // End-to-end: the real GoogleDriveAdapter refreshes during an operation and // the rotated tokens reach saveProviderConnection (the regression #1208 fixed // for OneDrive — Google previously had no setOnTokensRefreshed hook). const { manager, saved } = createManager(); manager.providerDecryptSeq.google = 0; manager.state.providers.google = { provider: 'google', status: 'connected', tokens: { accessToken: 'old', refreshToken: 'google-refresh', tokenType: 'Bearer' }, account: { id: 'g1' }, resourceId: 'gfile-1', } as ProviderConnection; const g = globalThis as typeof globalThis & { window?: unknown }; const originalWindow = g.window; g.window = { netcatty: { // Google's refresh response omits refresh_token — the adapter must carry // the previous one forward so the persisted connection stays refreshable. googleRefreshAccessToken: async () => ({ accessToken: 'fresh-access', expiresAt: Date.now() + 3_600_000, tokenType: 'Bearer', }), googleDriveDownloadSyncFile: async () => ({ syncedFile: { meta: { version: 1 }, payload: 'x' }, }), }, } as unknown as Window & typeof globalThis; try { const { GoogleDriveAdapter } = await import('../adapters/GoogleDriveAdapter.ts'); const adapter = new GoogleDriveAdapter( { accessToken: 'old', refreshToken: 'google-refresh', // Expired so the operation forces a refresh. expiresAt: Date.now() - 60_000, tokenType: 'Bearer', }, 'gfile-1', ); attachTokenRefreshPersistence.call(manager, 'google', adapter as never); await adapter.download(); // persistRefreshedProviderTokens fires saveProviderConnection fire-and-forget. await Promise.resolve(); assert.equal(saved.length, 1); assert.equal(saved[0].provider, 'google'); assert.equal(saved[0].tokens?.accessToken, 'fresh-access'); // Original refresh token preserved despite the omitted refresh_token. assert.equal(saved[0].tokens?.refreshToken, 'google-refresh'); // State updated and other fields preserved. assert.equal(manager.state.providers.google.tokens?.accessToken, 'fresh-access'); assert.equal(manager.state.providers.google.account?.id, 'g1'); assert.equal(manager.state.providers.google.resourceId, 'gfile-1'); assert.equal(manager.providerDecryptSeq.google, 1); } finally { g.window = originalWindow; } }); test('handleProviderReauthRequired clears OneDrive tokens and stops it being sync-ready', async () => { const { manager, saved } = createManager(); let signedOut = false; manager.adapters.set('onedrive', { signOut: () => { signedOut = true; } }); const handled = handleProviderReauthRequiredImpl.call( manager, 'onedrive', new Error( `Download failed: ${ONEDRIVE_REAUTH_REQUIRED_MARKER}: OneDrive session expired, please reconnect. (AADSTS70000)`, ), ); await Promise.resolve(); assert.equal(handled, true); assert.equal(signedOut, true); // Stale adapter is evicted. assert.equal(manager.adapters.has('onedrive'), false); const conn = manager.state.providers.onedrive; assert.equal(conn.tokens, undefined); assert.equal(conn.status, 'error'); // Account is preserved for display; error message is cleaned of the marker. assert.equal(conn.account?.id, 'u1'); assert.ok(conn.error && !conn.error.includes(ONEDRIVE_REAUTH_REQUIRED_MARKER)); assert.match(conn.error ?? '', /please reconnect/); // Crucial: cleared tokens => not ready for sync => auto-sync won't retry. assert.equal(isProviderReadyForSync(conn), false); // Persisted the cleared connection. assert.equal(saved.length, 1); assert.equal(saved[0].tokens, undefined); }); test('handleProviderReauthRequired ignores non-OneDrive providers and unrelated errors', () => { const { manager } = createManager(); // Wrong provider. assert.equal( handleProviderReauthRequiredImpl.call( manager, 'google', new Error(`${ONEDRIVE_REAUTH_REQUIRED_MARKER}: x`), ), false, ); // OneDrive but an ordinary (retryable) error — must not clear tokens. assert.equal( handleProviderReauthRequiredImpl.call(manager, 'onedrive', new Error('network timeout')), false, ); assert.ok(manager.state.providers.onedrive.tokens); }); test('inspectProviderRemoteState clears OneDrive tokens on a reauth-required download error', async () => { const { manager, saved } = createManager(); // Provide the manager surface inspectProviderRemoteState touches. Object.assign(manager, { handleProviderReauthRequired(provider: string, error: unknown) { return handleProviderReauthRequiredImpl.call(manager, provider as never, error); }, createSyncedFileSignature: async () => null, loadSyncAnchor: () => null, }); const adapter = { resourceId: 'file-1', download: async () => { throw new Error( `Download failed: ${ONEDRIVE_REAUTH_REQUIRED_MARKER}: OneDrive session expired, please reconnect.`, ); }, }; const result = await inspectProviderRemoteStateImpl.call(manager, 'onedrive', adapter as never); await Promise.resolve(); // The inspection reports an error (so callers fail closed)... assert.ok(result.error); // ...and the dead credentials were cleared so the provider is no longer ready. assert.equal(manager.state.providers.onedrive.tokens, undefined); assert.equal(isProviderReadyForSync(manager.state.providers.onedrive), false); assert.equal(saved.length, 1); });