Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Storage tests #28

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 175 additions & 0 deletions src/services/storage/Storage.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import { Storage } from './Storage';
import { StorageAdapter, UnavailableAdapter } from './__mocks__';

interface StorageState {
firstItem: string;
secondItem: string;
}

interface StorageStateV1 {
first: string;
second: string;
}

interface StorageStateV2 {
first: number;
second: number;
}

beforeEach(() => {
StorageAdapter.mockClear();
UnavailableAdapter.mockClear();
});

describe('Storage', () => {
const adapter = new StorageAdapter();

const initialState: StorageState = {
firstItem: '123',
secondItem: '456',
};

const storage = new Storage<[StorageState]>('test', adapter, initialState, []);

describe('Constructor', () => {
it('Throw error, if namespace already exist', () => {
expect(() => new Storage<[StorageState]>('test', adapter, initialState, [])).toThrow(Error);
});

it('Save FallbackAdapter as adapter, if adapter is not available', () => {
const unavailableAdapter = new UnavailableAdapter();

const unavailableStorage = new Storage<[StorageState]>(
'unavailableStorage',
unavailableAdapter,
initialState,
[],
);

expect(unavailableStorage.get()).toEqual({ firstItem: '123', secondItem: '456' });
});
});

describe('Migrations', () => {
const mockAdapter = new StorageAdapter();

beforeEach(() => {
Storage.namespaces = [];
});

it('Set initial state from first version', () => {
const initialStateV0: StorageState = {
firstItem: '123',
secondItem: '456',
};

const storageV0 = new Storage<[StorageState]>(
'localStorage',
mockAdapter,
initialStateV0,
[],
);

expect(storageV0.get()).toEqual({ firstItem: '123', secondItem: '456' });
});

it('If pass one function in array, it run migration on next version', () => {
const initialStateV1: StorageStateV1 = {
first: '12345',
second: '56789',
};

const storageV1 = new Storage<[StorageState, StorageStateV1]>(
'localStorage',
mockAdapter,
initialStateV1,
[data => ({ first: data.firstItem, second: data.secondItem })],
);

expect(storageV1.get()).toEqual({ first: '123', second: '456' });
});

it('If pass two functions in array, it run migration on two next versions', () => {
const initialStateV2: StorageStateV2 = {
first: 12345,
second: 56789,
};

const storageV2 = new Storage<[StorageState, StorageStateV1, StorageStateV2]>(
'localStorage',
mockAdapter,
initialStateV2,
[
data => ({ first: data.firstItem, second: data.secondItem }),
data => ({ first: Number(data.first), second: Number(data.second) }),
],
);

expect(storageV2.get()).toEqual({ first: 123, second: 456 });
});

it('If migrations not passed, do not change storage', () => {
mockAdapter.setItem('V3:__storage_version__', JSON.stringify(3));
mockAdapter.setItem('V3:firstItem', JSON.stringify('12345'));
mockAdapter.setItem('V3:secondItem', JSON.stringify('56789'));

const storageV3 = new Storage<[StorageState]>('V3', mockAdapter, initialState, []);

expect(storageV3.get()).toEqual({ firstItem: '12345', secondItem: '56789' });
});
});

describe('set method', () => {
it('Accept and save object with items', () => {
storage.set({ firstItem: '12345', secondItem: '56789' });

expect(storage.getItem('firstItem')).toEqual('12345');
expect(storage.getItem('secondItem')).toEqual('56789');
});
});

describe('get method', () => {
it('Return object with all items', () => {
storage.set({ firstItem: '12345', secondItem: '56789' });

expect(storage.get()).toEqual({ firstItem: '12345', secondItem: '56789' });
});
});

describe('setItem method', () => {
it('Save item in storage', () => {
storage.setItem('secondItem', 'testItem');

expect(storage.getItem('secondItem')).toEqual('testItem');
});
});

describe('getItem method', () => {
it('Get item from storage by key', () => {
storage.setItem('firstItem', '12345');

expect(storage.getItem('firstItem')).toEqual('12345');
});

it('If data does not exist, getItem method throw Error', () => {
storage.reset();

expect(() => storage.getItem('firstItem')).toThrow(Error);
});
});

describe('reset method', () => {
it('Remove all data with current namespace', () => {
adapter.setItem('localData:__storage_version__', JSON.stringify(0));
adapter.setItem('localData:firstItem', JSON.stringify('12345'));
adapter.setItem('localData:secondItem', JSON.stringify('56789'));
adapter.setItem('data:firstItem', JSON.stringify('56789'));

const testStorage = new Storage<[StorageState]>('localData', adapter, initialState, []);
testStorage.reset();

expect(storage.get()).toEqual({});
expect(JSON.parse(adapter.getItem('data:firstItem') || '')).toEqual('56789');
});
});
});
12 changes: 7 additions & 5 deletions src/services/storage/Storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ function entries<Object>(obj: Object) {

interface IData {
[key: string]: any;
version?: number;
__storage_version__?: number;
}

class Storage<States extends IData[]> {
Expand Down Expand Up @@ -48,11 +48,13 @@ class Storage<States extends IData[]> {

public get(): Tuple.Last<States> {
return this.adapter.getAllKeys().reduce((acc, currentKey) => {
if (!this.isCurrentNamespaceKey(currentKey)) {
const key = this.getShortKey(currentKey);
const isVersionKey = key === '__storage_version__';

if (!this.isCurrentNamespaceKey(currentKey) || isVersionKey) {
return acc;
}

const key = this.getShortKey(currentKey);
const data = this.getItem(key);

return {
Expand Down Expand Up @@ -118,14 +120,14 @@ class Storage<States extends IData[]> {

private getVersion(): number | undefined | null {
try {
return this.getItem('version');
return this.getItem('__storage_version__');
} catch {
return null;
}
}

private saveVersion(version: number) {
this.setItem('version', version);
this.setItem('__storage_version__', version);
}

private getFullKey<Key extends keyof Tuple.Last<States>>(key: Key): string {
Expand Down
33 changes: 33 additions & 0 deletions src/services/storage/__mocks__/StorageAdapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { StorageAdapter } from '../types';

interface IStorageAdapter extends StorageAdapter {
state: Record<string, string>;
}

const MockStorageAdapter = jest.fn<IStorageAdapter, []>().mockImplementation(() => {
return {
state: {},

checkAvailability() {
return true;
},

setItem(key: string, value: string) {
this.state[key] = value;
},

getItem(key: string): string {
return this.state[key];
},

removeItem(key: string): void {
delete this.state[key];
},

getAllKeys(): string[] {
return Object.keys(this.state);
},
};
});

export { MockStorageAdapter as StorageAdapter };
33 changes: 33 additions & 0 deletions src/services/storage/__mocks__/UnavailableAdapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { StorageAdapter } from '../types';

interface IUnavailableAdapter extends StorageAdapter {
state: Record<string, string>;
}

const UnavailableAdapter = jest.fn<IUnavailableAdapter, []>().mockImplementation(() => {
return {
state: {},

checkAvailability() {
return false;
},

setItem(key: string, value: string) {
this.state[key] = value;
},

getItem(key: string): string {
return this.state[key];
},

removeItem(key: string): void {
delete this.state[key];
},

getAllKeys(): string[] {
return Object.keys(this.state);
},
};
});

export { UnavailableAdapter };
2 changes: 2 additions & 0 deletions src/services/storage/__mocks__/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { StorageAdapter } from './StorageAdapter';
export { UnavailableAdapter } from './UnavailableAdapter';