mirror of
https://github.com/PeWu/topola-viewer.git
synced 2026-07-19 18:21:51 +00:00
285 lines
8.8 KiB
TypeScript
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();
|
|
});
|
|
});
|
|
});
|