/
githubmirror
/
amphtml
Обзор
Документация
Войти
/
githubmirror
/
amphtml
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
test/unit/test-storage.js
774 строки
22 KB
Daniel Rozenberg
❄️ Unskip some tests that no longer fail (#39329)
09 авг 2023, 22:33
Не верифицирован
09 авг 2023, 22:33
1986291
Код
Авторство
О чём код?
import {AmpDocSingle} from '#service/ampdoc-impl'; import { LocalStorageBinding, Storage, Store, ViewerStorageBinding, } from '#service/storage-impl'; import {dev} from '#utils/log'; import {FakePerformance} from '#testing/fake-dom'; describes.sandboxed('Storage', {}, (env) => { let storage; let binding; let bindingMock; let viewer; let viewerMock; let windowApi; let ampdoc; let viewerBroadcastHandler; let clock; describe('Storage', () => { beforeEach(() => { viewerBroadcastHandler = undefined; viewer = { onBroadcast: (handler) => { viewerBroadcastHandler = handler; }, broadcast: () => {}, }; viewerMock = env.sandbox.mock(viewer); windowApi = { document: {}, location: 'https://acme.com/document1', performance: new FakePerformance(window), }; ampdoc = new AmpDocSingle(windowApi); binding = { loadBlob: () => {}, saveBlob: () => {}, }; bindingMock = env.sandbox.mock(binding); storage = new Storage(ampdoc, viewer, binding); storage.start_(); }); function expectStorage(keyValues) { const list = []; for (const k in keyValues) { list.push( storage.get(k).then((value) => { const expectedValue = keyValues[k]; expect(value).to.equal(expectedValue, `For "${k}"`); }) ); } return Promise.all(list); } it('should configure store correctly', () => { const store1 = new Store({}); store1.set('key1', 'value1'); store1.set('key2', 'value2'); bindingMock .expects('loadBlob') .withExactArgs('https://acme.com') .returns(Promise.resolve(btoa(JSON.stringify(store1.obj)))) .once(); return storage .get('key1') .then(() => { return storage.storePromise_; }) .then((store) => { expect(store.maxValues_).to.equal(8); }); }); it('should initialize empty store with prototype-less objects', () => { bindingMock .expects('loadBlob') .withExactArgs('https://acme.com') .returns(Promise.resolve(null)) .once(); return storage .get('key1') .then(() => { return storage.storePromise_; }) .then((store) => { expect(store.obj.__proto__).to.be.undefined; expect(store.values_.__proto__).to.be.undefined; }); }); it('should restore store with prototype-less objects', () => { const store1 = new Store({}); store1.set('key1', 'value1'); store1.set('key2', 'value2'); bindingMock .expects('loadBlob') .withExactArgs('https://acme.com') .returns(Promise.resolve(btoa(JSON.stringify(store1.obj)))) .once(); return storage .get('key1') .then(() => { return storage.storePromise_; }) .then((store) => { expect(store.obj.__proto__).to.be.undefined; expect(store.values_.__proto__).to.be.undefined; }); }); it('should get the value first time and reuse store', () => { const store1 = new Store({}); store1.set('key1', 'value1'); store1.set('key2', 'value2'); bindingMock .expects('loadBlob') .withExactArgs('https://acme.com') .returns(Promise.resolve(btoa(JSON.stringify(store1.obj)))) .once(); expect(storage.storePromise_).to.not.exist; const promise = storage.get('key1'); return promise.then((value) => { expect(value).to.equal('value1'); const store1Promise = storage.storePromise_; expect(store1Promise).to.exist; // Repeat. return storage.get('key2').then((value2) => { expect(value2).to.equal('value2'); expect(storage.storePromise_).to.equal(store1Promise); }); }); }); it('should get the value from first ever request and reuse store', () => { bindingMock .expects('loadBlob') .withExactArgs('https://acme.com') .returns(Promise.resolve(null)) .once(); expect(storage.storePromise_).to.not.exist; const promise = storage.get('key1'); return promise.then((value) => { expect(value).to.be.undefined; const store1Promise = storage.storePromise_; expect(store1Promise).to.exist; // Repeat. return storage.get('key2').then((value2) => { expect(value2).to.be.undefined; expect(storage.storePromise_).to.equal(store1Promise); }); }); }); it('should recover from binding failure', () => { expectAsyncConsoleError(/Failed to load store/); bindingMock .expects('loadBlob') .withExactArgs('https://acme.com') .returns(Promise.reject('intentional')) .once(); expect(storage.storePromise_).to.not.exist; const promise = storage.get('key1'); return promise.then((value) => { expect(value).to.be.undefined; expect(storage.storePromise_).to.exist; }); }); it('should recover from binding error', () => { expectAsyncConsoleError(/Failed to load store/); bindingMock .expects('loadBlob') .withExactArgs('https://acme.com') .returns(Promise.resolve('UNKNOWN FORMAT')) .once(); expect(storage.storePromise_).to.not.exist; const promise = storage.get('key1'); return promise.then((value) => { expect(value).to.be.undefined; expect(storage.storePromise_).to.exist; }); }); it('should save the value first time and reuse store', () => { const store1 = new Store({}); store1.set('key1', 'value1'); store1.set('key2', 'value2'); bindingMock .expects('loadBlob') .withExactArgs('https://acme.com') .returns(Promise.resolve(btoa(JSON.stringify(store1.obj)))) .once(); bindingMock .expects('saveBlob') .withExactArgs( 'https://acme.com', env.sandbox.match((arg) => { const store2 = new Store(JSON.parse(atob(arg))); return ( store2.get('key1') !== undefined && store2.get('key2') !== undefined ); }) ) .returns(Promise.resolve()) .twice(); viewerMock .expects('broadcast') .withExactArgs( env.sandbox.match((arg) => { return ( arg['type'] == 'amp-storage-reset' && arg['origin'] == 'https://acme.com' ); }) ) .twice(); expect(storage.storePromise_).to.not.exist; const promise = storage.set('key1', true); return promise .then(() => { const store1Promise = storage.storePromise_; expect(store1Promise).to.exist; // Repeat. return storage.set('key2', true).then(() => { expect(storage.storePromise_).to.equal(store1Promise); }); }) .then(() => { return expectStorage({ 'key1': true, 'key2': true, }); }); }); it('should remove the key first time and reuse store', () => { const store1 = new Store({}); store1.set('key1', 'value1'); store1.set('key2', 'value2'); bindingMock .expects('loadBlob') .withExactArgs('https://acme.com') .returns(Promise.resolve(btoa(JSON.stringify(store1.obj)))) .once(); bindingMock .expects('saveBlob') .withExactArgs( 'https://acme.com', env.sandbox.match((arg) => { const store2 = new Store(JSON.parse(atob(arg))); return store2.get('key1') === undefined; }) ) .returns(Promise.resolve()) .twice(); viewerMock .expects('broadcast') .withExactArgs( env.sandbox.match((arg) => { return ( arg['type'] == 'amp-storage-reset' && arg['origin'] == 'https://acme.com' ); }) ) .twice(); expect(storage.storePromise_).to.not.exist; const promise = storage.remove('key1'); return promise .then(() => { const store1Promise = storage.storePromise_; expect(store1Promise).to.exist; // Repeat. return storage.remove('key2').then(() => { expect(storage.storePromise_).to.equal(store1Promise); }); }) .then(() => { return expectStorage({ 'key1': undefined, 'key2': undefined, }); }); }); it('should get unexpired value based on duration', async () => { clock = env.sandbox.useFakeTimers(); const store1 = new Store({}); store1.set('key1', 'value1'); expect(store1.values_).to.deep.equal({ 'key1': {v: 'value1', t: 0}, }); bindingMock .expects('loadBlob') .withExactArgs('https://acme.com') .returns(Promise.resolve(btoa(JSON.stringify(store1.obj)))) .once(); bindingMock .expects('saveBlob') .withExactArgs( 'https://acme.com', env.sandbox.match((arg) => { const store2 = new Store(JSON.parse(atob(arg))); return store2.get('key1') === undefined; }) ) .returns(Promise.resolve()) .twice(); viewerMock .expects('broadcast') .withExactArgs( env.sandbox.match((arg) => { return ( arg['type'] == 'amp-storage-reset' && arg['origin'] == 'https://acme.com' ); }) ) .once(); expect(await storage.get('key1', 10)).to.equal('value1'); clock.tick(100); expect(await storage.get('key1', 5)).to.be.undefined; }); it('should react to reset messages', () => { const store1 = new Store({}); store1.set('key1', 'value1'); bindingMock .expects('loadBlob') .withExactArgs('https://acme.com') .returns(Promise.resolve(btoa(JSON.stringify(store1.obj)))) .twice(); return storage.get('key1').then((value) => { expect(value).to.equal('value1'); const store1Promise = storage.storePromise_; expect(store1Promise).to.exist; // Issue broadcast event. viewerBroadcastHandler({ 'type': 'amp-storage-reset', 'origin': 'https://acme.com', }); expect(storage.storePromise_).to.not.exist; return storage.get('key1').then((value) => { expect(value).to.equal('value1'); expect(storage.storePromise_).to.exist; }); }); }); it('should ignore unrelated reset messages', () => { const store1 = new Store({}); store1.set('key1', 'value1'); bindingMock .expects('loadBlob') .withExactArgs('https://acme.com') .returns(Promise.resolve(btoa(JSON.stringify(store1.obj)))) .twice(); return storage.get('key1').then((value) => { expect(value).to.equal('value1'); const store1Promise = storage.storePromise_; expect(store1Promise).to.exist; // Issue broadcast event. viewerBroadcastHandler({ 'type': 'amp-storage-reset', 'origin': 'OTHER', }); expect(storage.storePromise_).to.exist; }); }); }); }); describes.sandboxed('Store', {}, (env) => { let clock; let store; beforeEach(() => { clock = env.sandbox.useFakeTimers(); store = new Store({}, 2); }); it('should get undefined with empty store', () => { expect(store.get('key1')).to.be.undefined; expect(Object.keys(store.values_).length).to.equal(0); expect(store.values_).to.deep.equal({}); }); it('should set a new value with timestamp', () => { store.set('key2', 'value2'); clock.tick(101); store.set('key1', 'value1'); expect(store.get('key1')).to.equal('value1'); expect(Object.keys(store.values_).length).to.equal(2); expect(store.values_['key2']['t']).to.equal(0); expect(store.values_['key1']['t']).to.equal(101); expect(store.values_).to.deep.equal({ 'key2': {v: 'value2', t: 0}, 'key1': {v: 'value1', t: 101}, }); }); it('should overwrite a value with new timestamp', () => { store.set('key1', 'value1'); store.set('key2', 'value2'); clock.tick(101); store.set('key1', 'value1b'); expect(store.get('key1')).to.equal('value1b'); expect(Object.keys(store.values_).length).to.equal(2); expect(store.values_['key1']['t']).to.equal(101); expect(store.values_['key2']['t']).to.equal(0); expect(store.values_).to.deep.equal({ 'key1': {v: 'value1b', t: 101}, 'key2': {v: 'value2', t: 0}, }); }); it('should update a value w/o changing timestamp', () => { store.set('key1', 'value1', true); clock.tick(101); store.set('key2', 'value2', true); store.set('key1', 'value1b', true); expect(store.get('key1')).to.equal('value1b'); expect(Object.keys(store.values_).length).to.equal(2); expect(store.values_['key1']['t']).to.equal(0); expect(store.values_['key2']['t']).to.equal(101); expect(store.values_).to.deep.equal({ 'key1': {v: 'value1b', t: 0}, 'key2': {v: 'value2', t: 101}, }); }); it('should remove a value', () => { store.set('key1', 'value1'); store.set('key2', 'value2'); clock.tick(101); expect(Object.keys(store.values_).length).to.equal(2); store.remove('key1'); expect(store.get('key1')).to.be.undefined; expect(store.get('key2')).to.be.equal('value2'); expect(Object.keys(store.values_).length).to.equal(1); expect(store.values_['key2']['t']).to.equal(0); expect(store.values_).to.deep.equal({ 'key2': {v: 'value2', t: 0}, }); }); it('should store limited amount of values', () => { clock.tick(1); store.set('k1', 1); expect(Object.keys(store.values_).length).to.equal(1); clock.tick(1); store.set('k2', 2); expect(Object.keys(store.values_).length).to.equal(2); // The oldest (k2) will be removed. clock.tick(1); store.set('k1', 4); store.set('k3', 3); expect(Object.keys(store.values_).length).to.equal(2); expect(store.get('k3')).to.equal(3); expect(store.get('k1')).to.equal(4); expect(store.get('k2')).to.be.undefined; // The new oldest (k1) will be removed clock.tick(1); store.set('k4', 4); expect(Object.keys(store.values_).length).to.equal(2); expect(store.get('k4')).to.equal(4); expect(store.get('k3')).to.equal(3); expect(store.get('k1')).to.be.undefined; expect(store.get('k2')).to.be.undefined; }); it('should prohibit unsafe values', () => { allowConsoleError(() => { expect(() => { store.set('__proto__', 'value1'); }).to.throw(/Name is not allowed/); expect(() => { store.set('prototype', 'value1'); }).to.throw(/Name is not allowed/); }); }); }); describes.sandboxed('LocalStorageBinding', {}, (env) => { let windowApi; let localStorageMock; let binding; beforeEach(() => { windowApi = { localStorage: { getItem: () => {}, setItem: () => {}, }, }; localStorageMock = env.sandbox.mock(windowApi.localStorage); binding = new LocalStorageBinding(windowApi); }); it('should throw if localStorage is not supported', () => { const errorSpy = env.sandbox.spy(dev(), 'expectedError'); expect(errorSpy).to.have.not.been.called; new LocalStorageBinding(windowApi); expect(errorSpy).to.have.not.been.called; delete windowApi.localStorage; allowConsoleError(() => new LocalStorageBinding(windowApi)); expect(errorSpy).to.be.calledOnce; expect(errorSpy.args[0][1].message).to.match(/localStorage not supported/); }); it('should load store when available', () => { localStorageMock .expects('getItem') .withExactArgs('amp-store:https://acme.com') .returns('BLOB1') .once(); return binding.loadBlob('https://acme.com').then((blob) => { expect(blob).to.equal('BLOB1'); }); }); it('should load default store when not yet available', () => { localStorageMock .expects('getItem') .withExactArgs('amp-store:https://acme.com') .returns(undefined) .once(); return binding.loadBlob('https://acme.com').then((blob) => { expect(blob).to.not.exist; }); }); it('should reject on local storage failure w/ localStorage support', () => { binding.isLocalStorageSupported_ = true; localStorageMock .expects('getItem') .withExactArgs('amp-store:https://acme.com') .throws(new Error('unknown')) .once(); return binding .loadBlob('https://acme.com') .then( () => 'SUCCESS', () => 'ERROR' ) .then((res) => { expect(res).to.equal('ERROR'); }); }); it('should succeed loadBlob w/o localStorage support', () => { binding.isLocalStorageSupported_ = false; localStorageMock .expects('getItem') .withExactArgs('amp-store:https://acme.com') .throws(new Error('unknown')) .once(); return binding .loadBlob('https://acme.com') .then( (res) => `SUCCESS ${res}`, () => 'ERROR' ) .then((res) => { // Resolves with null expect(res).to.equal('SUCCESS null'); }); }); it('should bypass loading from localStorage if getItem throws', () => { expectAsyncConsoleError(/localStorage not supported/); localStorageMock.expects('getItem').throws(new Error('unknown')).once(); binding = new LocalStorageBinding(windowApi); localStorageMock.expects('getItem').never(); return binding .loadBlob('https://acme.com') .then( () => 'SUCCESS', () => 'ERROR' ) .then((res) => { expect(res).to.equal('SUCCESS'); }); }); it('should save store', () => { localStorageMock .expects('setItem') .withExactArgs('amp-store:https://acme.com', 'BLOB1') .once(); return binding.saveBlob('https://acme.com', 'BLOB1'); }); it('should reject on save store failure', () => { localStorageMock .expects('setItem') .withExactArgs('amp-store:https://acme.com', 'BLOB1') .throws(new Error('unknown')) .once(); return binding .saveBlob('https://acme.com', 'BLOB1') .then( () => 'SUCCESS', () => 'ERROR' ) .then((res) => { expect(res).to.equal('ERROR'); }); }); it('should succeed saveBlob w/o localStorage support', () => { binding.isLocalStorageSupported_ = false; localStorageMock .expects('setItem') .withExactArgs('amp-store:https://acme.com', 'BLOB1') .throws(new Error('unknown')) .once(); // Never reaches setItem return binding .saveBlob('https://acme.com', 'BLOB1') .then( () => 'SUCCESS', () => 'ERROR' ) .then((res) => { expect(res).to.equal('SUCCESS'); }); }); it('should bypass saving to localStorage if getItem throws', () => { expectAsyncConsoleError(/localStorage not supported/); const setItemSpy = env.sandbox.spy(windowApi.localStorage, 'setItem'); localStorageMock.expects('getItem').throws(new Error('unknown')).once(); binding = new LocalStorageBinding(windowApi); // Never reaches setItem return binding .saveBlob('https://acme.com', 'BLOB1') .then( () => 'SUCCESS', () => 'ERROR' ) .then((res) => { expect(setItemSpy).to.have.not.been.called; expect(res).to.equal('SUCCESS'); }); }); }); describes.sandboxed('ViewerStorageBinding', {}, (env) => { let viewer; let viewerMock; let binding; beforeEach(() => { viewer = { sendMessageAwaitResponse: () => {}, }; viewerMock = env.sandbox.mock(viewer); binding = new ViewerStorageBinding(viewer); }); it('should load store from viewer', () => { viewerMock .expects('sendMessageAwaitResponse') .withExactArgs( 'loadStore', env.sandbox.match((arg) => { return arg['origin'] == 'https://acme.com'; }) ) .returns(Promise.resolve({'blob': 'BLOB1'})) .once(); return binding.loadBlob('https://acme.com').then((blob) => { expect(blob).to.equal('BLOB1'); }); }); it('should load default store when not yet available', () => { viewerMock .expects('sendMessageAwaitResponse') .withExactArgs( 'loadStore', env.sandbox.match((arg) => { return arg['origin'] == 'https://acme.com'; }) ) .returns(Promise.resolve({})) .once(); return binding.loadBlob('https://acme.com').then((blob) => { expect(blob).to.not.exist; }); }); it('should reject on viewer failure', () => { viewerMock .expects('sendMessageAwaitResponse') .withExactArgs( 'loadStore', env.sandbox.match((arg) => { return arg['origin'] == 'https://acme.com'; }) ) .returns(Promise.reject('unknown')) .once(); return binding .loadBlob('https://acme.com') .then( () => 'SUCCESS', () => 'ERROR' ) .then((res) => { expect(res).to.equal('ERROR'); }); }); it('should save store', () => { viewerMock .expects('sendMessageAwaitResponse') .withExactArgs( 'saveStore', env.sandbox.match((arg) => { return arg['origin'] == 'https://acme.com' && arg['blob'] == 'BLOB1'; }) ) .returns(Promise.resolve()) .once(); return binding.saveBlob('https://acme.com', 'BLOB1'); }); it('should reject on save store failure', () => { viewerMock .expects('sendMessageAwaitResponse') .withExactArgs( 'saveStore', env.sandbox.match(() => true) ) .returns(Promise.reject('unknown')) .once(); return binding .saveBlob('https://acme.com', 'BLOB1') .then( () => 'SUCCESS', () => 'ERROR' ) .then((res) => { expect(res).to.equal('ERROR'); }); }); });