mirror of
https://github.com/PeWu/topola-viewer.git
synced 2026-07-30 15:41:47 +00:00
Add support for loading files from Google Drive
#vibecoded
This commit is contained in:
284
src/datasource/google_drive_service.spec.ts
Normal file
284
src/datasource/google_drive_service.spec.ts
Normal file
@@ -0,0 +1,284 @@
|
||||
/* 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user