Files
topola-viewer/src/datasource/google_drive_service.spec.ts
2026-05-31 09:59:12 +02:00

285 lines
8.8 KiB
TypeScript

/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-var-requires */
import {beforeEach, describe, expect, it, jest} from '@jest/globals';
import {
GOOGLE_DRIVE_CACHE_KEY_PREFIX,
GOOGLE_DRIVE_TOKEN_EXPIRES_AT_KEY,
GOOGLE_DRIVE_TOKEN_KEY,
GoogleDriveService,
clearGoogleDriveCache,
isGoogleDriveConfigured,
} from './google_drive_service';
import {mockSessionStorage} from './test_helpers';
describe('GoogleDriveService', () => {
let service: GoogleDriveService;
let sessionStorageMock: {[key: string]: string};
let mockTokenClient: any;
let initTokenClientSpy: any;
let revokeSpy: any;
let headMock: any;
let documentMock: any;
beforeEach(() => {
sessionStorageMock = mockSessionStorage();
// Mock document
headMock = {
appendChild: jest.fn((el: any) => {
if (el.onload) {
setTimeout(() => el.onload(), 0);
}
}),
};
documentMock = {
querySelector: jest.fn().mockReturnValue(null),
createElement: jest.fn().mockImplementation(() => ({
src: '',
async: false,
defer: false,
onload: null,
onerror: null,
remove: jest.fn(),
})),
head: headMock,
};
Object.defineProperty(global, 'document', {
value: documentMock,
writable: true,
configurable: true,
});
// Mock global window objects
(global as any).window = global;
(global as any).gapi = {
load: jest.fn((apiName: string, config: any) => {
if (config && config.callback) {
config.callback();
}
}),
};
mockTokenClient = {
requestAccessToken: jest.fn(),
};
initTokenClientSpy = jest.fn().mockReturnValue(mockTokenClient);
revokeSpy = jest.fn((token: string, callback: () => void) => callback());
(global as any).google = {
accounts: {
oauth2: {
initTokenClient: initTokenClientSpy,
revoke: revokeSpy,
},
},
picker: {
DocsView: jest.fn().mockImplementation(() => ({
setMimeTypes: jest.fn().mockReturnThis(),
})),
PickerBuilder: jest.fn().mockImplementation(() => ({
addView: jest.fn().mockReturnThis(),
setOAuthToken: jest.fn().mockReturnThis(),
setDeveloperKey: jest.fn().mockReturnThis(),
setCallback: jest.fn().mockReturnThis(),
setSize: jest.fn().mockReturnThis(),
build: jest.fn().mockReturnValue({
setVisible: jest.fn(),
}),
})),
ViewId: {
DOCS: 'docs',
},
},
};
// Set up env vars for tests
process.env.VITE_GOOGLE_CLIENT_ID = 'test-client-id';
process.env.VITE_GOOGLE_API_KEY = 'test-api-key';
// Reset modules so that module-scoped variables (like loadingScripts and retryCounts)
// are fresh for every test.
jest.resetModules();
const {
GoogleDriveService: FreshGoogleDriveService,
} = require('./google_drive_service');
service = new FreshGoogleDriveService();
jest.clearAllMocks();
});
describe('isGoogleDriveConfigured', () => {
it('returns true when client ID and API key are configured', () => {
expect(isGoogleDriveConfigured()).toBe(true);
});
it('returns false when client ID is missing', () => {
process.env.VITE_GOOGLE_CLIENT_ID = '';
expect(isGoogleDriveConfigured()).toBe(false);
});
});
describe('init', () => {
it('resolves immediately if not configured', async () => {
process.env.VITE_GOOGLE_CLIENT_ID = '';
await expect(service.init()).resolves.not.toThrow();
expect(initTokenClientSpy).not.toHaveBeenCalled();
});
it('successfully initializes scripts and token client', async () => {
await service.init();
expect(initTokenClientSpy).toHaveBeenCalledWith({
client_id: 'test-client-id',
scope: 'https://www.googleapis.com/auth/drive.file',
callback: expect.any(Function),
});
expect(gapi.load).toHaveBeenCalledWith('picker', expect.any(Object));
});
it('throws error and clears initPromise on load failure', async () => {
const consoleErrorSpy = jest
.spyOn(console, 'error')
.mockImplementation(() => {
// No-op
});
const oldGapi = (global as any).gapi;
delete (global as any).gapi;
await expect(service.init()).rejects.toThrow();
(global as any).gapi = oldGapi;
await expect(service.init()).resolves.not.toThrow();
expect(consoleErrorSpy).toHaveBeenCalled();
consoleErrorSpy.mockRestore();
});
});
describe('getAccessToken', () => {
it('returns null when no token is cached', () => {
expect(service.getAccessToken()).toBeNull();
});
it('returns null when token is expired', () => {
sessionStorageMock[GOOGLE_DRIVE_TOKEN_KEY] = 'expired-token';
sessionStorageMock[GOOGLE_DRIVE_TOKEN_EXPIRES_AT_KEY] = String(
Date.now() - 1000,
);
expect(service.getAccessToken()).toBeNull();
});
it('returns token when cached token is valid', () => {
sessionStorageMock[GOOGLE_DRIVE_TOKEN_KEY] = 'valid-token';
sessionStorageMock[GOOGLE_DRIVE_TOKEN_EXPIRES_AT_KEY] = String(
Date.now() + 60000,
);
expect(service.getAccessToken()).toBe('valid-token');
});
});
describe('requestToken', () => {
it('returns cached valid token without initiating login popup', async () => {
sessionStorageMock[GOOGLE_DRIVE_TOKEN_KEY] = 'valid-token';
sessionStorageMock[GOOGLE_DRIVE_TOKEN_EXPIRES_AT_KEY] = String(
Date.now() + 60000,
);
const token = await service.requestToken();
expect(token).toBe('valid-token');
expect(mockTokenClient.requestAccessToken).not.toHaveBeenCalled();
});
it('triggers requestAccessToken when no valid token cached', async () => {
const tokenPromise = service.requestToken();
// Wait for async script loaders and GAPI load callback in init() to execute
await new Promise((resolve) => setTimeout(resolve, 0));
const callback = initTokenClientSpy.mock.calls[0][0].callback;
callback({
access_token: 'new-auth-token',
expires_in: '3600',
});
const token = await tokenPromise;
expect(token).toBe('new-auth-token');
expect(sessionStorageMock[GOOGLE_DRIVE_TOKEN_KEY]).toBe('new-auth-token');
expect(mockTokenClient.requestAccessToken).toHaveBeenCalledWith({
prompt: '',
});
});
it('rejects previous pending promise when superseded by a forced select request', async () => {
const firstTokenPromise = service.requestToken(false);
const secondTokenPromise = service.requestToken(true);
await expect(firstTokenPromise).rejects.toThrow(
'Authentication superseded by a new request.',
);
const callback = initTokenClientSpy.mock.calls[0][0].callback;
callback({
access_token: 'superseded-flow-token',
expires_in: '3600',
});
const secondToken = await secondTokenPromise;
expect(secondToken).toBe('superseded-flow-token');
});
});
describe('signOut', () => {
it('revokes the token and clears storage keys', async () => {
sessionStorageMock[GOOGLE_DRIVE_TOKEN_KEY] = 'my-token';
sessionStorageMock[GOOGLE_DRIVE_TOKEN_EXPIRES_AT_KEY] = '9999999999999';
await service.signOut();
expect(revokeSpy).toHaveBeenCalledWith('my-token', expect.any(Function));
expect(sessionStorageMock[GOOGLE_DRIVE_TOKEN_KEY]).toBeUndefined();
expect(
sessionStorageMock[GOOGLE_DRIVE_TOKEN_EXPIRES_AT_KEY],
).toBeUndefined();
});
});
describe('clearGoogleDriveCache', () => {
it('clears all google-drive: keys except the specified key', () => {
sessionStorageMock[`${GOOGLE_DRIVE_CACHE_KEY_PREFIX}file1`] = 'data1';
sessionStorageMock[`${GOOGLE_DRIVE_CACHE_KEY_PREFIX}file2`] = 'data2';
sessionStorageMock['other-key'] = 'other-data';
clearGoogleDriveCache(`${GOOGLE_DRIVE_CACHE_KEY_PREFIX}file1`);
expect(sessionStorageMock[`${GOOGLE_DRIVE_CACHE_KEY_PREFIX}file1`]).toBe(
'data1',
);
expect(
sessionStorageMock[`${GOOGLE_DRIVE_CACHE_KEY_PREFIX}file2`],
).toBeUndefined();
expect(sessionStorageMock['other-key']).toBe('other-data');
});
it('clears all google-drive: keys if no key is excepted', () => {
sessionStorageMock[`${GOOGLE_DRIVE_CACHE_KEY_PREFIX}file1`] = 'data1';
sessionStorageMock[`${GOOGLE_DRIVE_CACHE_KEY_PREFIX}file2`] = 'data2';
clearGoogleDriveCache();
expect(
sessionStorageMock[`${GOOGLE_DRIVE_CACHE_KEY_PREFIX}file1`],
).toBeUndefined();
expect(
sessionStorageMock[`${GOOGLE_DRIVE_CACHE_KEY_PREFIX}file2`],
).toBeUndefined();
});
});
});