Files
NetMesh/infrastructure/services/cloudSync/persistRefreshedTokens.test.ts

264 lines
9.4 KiB
TypeScript
Raw Normal View History

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<string, number>,
adapters: new Map<string, { signOut: () => 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<string, ProviderConnection>,
},
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);
});