Skip to content

Commit

Permalink
fix: storage access error when cookies are blocked (#1872)
Browse files Browse the repository at this point in the history
* fix: storage access error when cookies are blocked

* test: add tests to improve coverage
  • Loading branch information
saikumarrs authored Oct 3, 2024
1 parent 0ce61dd commit 966dbc2
Show file tree
Hide file tree
Showing 6 changed files with 141 additions and 18 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { ILogger } from '@rudderstack/analytics-js-common/types/Logger';
import {
isStorageQuotaExceeded,
isStorageAvailable,
Expand All @@ -24,4 +25,78 @@ describe('Capabilities Detection - Storage', () => {
it('should detect if memoryStorage is available', () => {
expect(isStorageAvailable('memoryStorage', new InMemoryStorage())).toBeTruthy();
});

it('should log a warning when storage is unavailable', () => {
const mockLogger = {
warn: jest.fn(),
} as ILogger;

// Store the original descriptor so we can restore it later
const originalLsDescriptor = Object.getOwnPropertyDescriptor(window, 'localStorage');
const originalSessionStorageDescriptor = Object.getOwnPropertyDescriptor(
window,
'sessionStorage',
);

// Mock using Object.defineProperty
Object.defineProperty(window, 'localStorage', {
get: jest.fn(),
set: jest.fn(),
configurable: true,
});

// Mock using Object.defineProperty
Object.defineProperty(window, 'sessionStorage', {
get: jest.fn(),
set: jest.fn(),
configurable: true,
});

isStorageAvailable('localStorage', undefined, mockLogger);
expect(mockLogger.warn).toHaveBeenCalledWith(
'CapabilitiesManager:: The "localStorage" storage type is unavailable.',
undefined,
);

isStorageAvailable('sessionStorage', undefined, mockLogger);
expect(mockLogger.warn).toHaveBeenCalledWith(
'CapabilitiesManager:: The "sessionStorage" storage type is unavailable.',
undefined,
);

// Restore the original document.cookie descriptor
Object.defineProperty(window, 'localStorage', originalLsDescriptor as PropertyDescriptor);
Object.defineProperty(
window,
'sessionStorage',
originalSessionStorageDescriptor as PropertyDescriptor,
);
});

it('should log a warning when the local storage is full', () => {
const mockLogger = {
warn: jest.fn(),
} as ILogger;

// Store the original descriptor so we can restore it later
const originalLsDescriptor = Object.getOwnPropertyDescriptor(window, 'localStorage');

// Mock using Object.defineProperty
Object.defineProperty(window, 'localStorage', {
get: jest.fn(() => {
throw new DOMException('StorageQuotaExceeded', 'QuotaExceededError');
}),
set: jest.fn(),
configurable: true,
});

isStorageAvailable('localStorage', undefined, mockLogger);
expect(mockLogger.warn).toHaveBeenCalledWith(
'CapabilitiesManager:: The "localStorage" storage type is full.',
new DOMException('StorageQuotaExceeded', 'QuotaExceededError'),
);

// Restore the original document.cookie descriptor
Object.defineProperty(window, 'localStorage', originalLsDescriptor as PropertyDescriptor);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,26 @@ describe('SessionStorage', () => {
expect(engine.keys()).toStrictEqual([]);
expect(engine.length).toStrictEqual(0);
});

it('APIs should respond appropriate when session storage is not available', () => {
const sessionStorageEngine = getStorageEngine('sessionStorage');

sessionStorageEngine.store = undefined;

sessionStorageEngine.setItem('a', '1');
expect(sessionStorageEngine.length).toBe(0);
expect(sessionStorageEngine.getItem('a')).toBeNull();

sessionStorageEngine.removeItem('a');

expect(sessionStorageEngine.length).toBe(0);

// clear all entries
sessionStorageEngine.clear();
expect(sessionStorageEngine.length).toBe(0);

expect(sessionStorageEngine.key(0)).toBeNull();

expect(sessionStorageEngine.keys()).toEqual([]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ const isStorageAvailable = (
let storage;
let testData;

const msgPrefix = STORAGE_UNAVAILABILITY_ERROR_PREFIX(CAPABILITIES_MANAGER, type);
let reason = 'unavailable';
let isAccessible = true;
let errObj;

try {
switch (type) {
case MEMORY_STORAGE:
Expand All @@ -53,25 +58,29 @@ const isStorageAvailable = (
return false;
}

if (!storage) {
return false;
if (storage) {
storage.setItem(testData, 'true');
if (storage.getItem(testData)) {
storage.removeItem(testData);
return true;
}
}

storage.setItem(testData, 'true');
if (storage.getItem(testData)) {
storage.removeItem(testData);
return true;
}
return false;
isAccessible = false;
} catch (err) {
const msgPrefix = STORAGE_UNAVAILABILITY_ERROR_PREFIX(CAPABILITIES_MANAGER, type);
let reason = 'unavailable';
isAccessible = false;
errObj = err;
if (isStorageQuotaExceeded(err)) {
reason = 'full';
}
logger?.warn(`${msgPrefix}${reason}.`, err);
return false;
}

if (!isAccessible) {
logger?.warn(`${msgPrefix}${reason}.`, errObj);
}

// if we've have reached here, it means the storage is not available
return false;
};

export { isStorageQuotaExceeded, isStorageAvailable };
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class CookieStorage implements IStorage {
if (options.sameDomainCookiesOnly) {
delete this.options.domain;
}
this.isSupportAvailable = isStorageAvailable(COOKIE_STORAGE, this, this.logger);
this.isSupportAvailable = isStorageAvailable(COOKIE_STORAGE, this);
this.isEnabled = Boolean(this.options.enabled && this.isSupportAvailable);
return this.options;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class LocalStorage implements IStorage {

configure(options: Partial<ILocalStorageOptions>): ILocalStorageOptions {
this.options = mergeDeepRight(this.options, options);
this.isSupportAvailable = isStorageAvailable(LOCAL_STORAGE, this, this.logger);
this.isSupportAvailable = isStorageAvailable(LOCAL_STORAGE, this);
this.isEnabled = Boolean(this.options.enabled && this.isSupportAvailable);
return this.options;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class SessionStorage implements IStorage {
isSupportAvailable = true;
isEnabled = true;
length = 0;
store = globalThis.sessionStorage;
store?: Storage;

constructor(options: ISessionStorageOptions = {}, logger?: ILogger) {
this.options = getDefaultSessionStorageOptions();
Expand All @@ -30,37 +30,54 @@ class SessionStorage implements IStorage {

configure(options: Partial<ISessionStorageOptions>): ISessionStorageOptions {
this.options = mergeDeepRight(this.options, options);
this.isSupportAvailable = isStorageAvailable(SESSION_STORAGE, this, this.logger);
this.isSupportAvailable = isStorageAvailable(SESSION_STORAGE);
// when storage is blocked by the user, even accessing the property throws an error
if (this.isSupportAvailable) {
this.store = globalThis.sessionStorage;
}
this.isEnabled = Boolean(this.options.enabled && this.isSupportAvailable);
return this.options;
}

setItem(key: string, value: any) {
if (!this.store) {
return;
}
this.store.setItem(key, value);
this.length = this.store.length;
}

getItem(key: string): any {
if (!this.store) {
return null;
}
const value = this.store.getItem(key);
return isUndefined(value) ? null : value;
}

removeItem(key: string) {
if (!this.store) {
return;
}
this.store.removeItem(key);
this.length = this.store.length;
}

clear() {
this.store.clear();
this.store?.clear();
this.length = 0;
}

key(index: number): Nullable<string> {
return this.store.key(index);
return this.store?.key(index) ?? null;
}

keys(): string[] {
const keys: string[] = [];
if (!this.store) {
return keys;
}

for (let i = 0; i < this.store.length; i += 1) {
const key = this.store.key(i);
if (key !== null) {
Expand Down

0 comments on commit 966dbc2

Please sign in to comment.