@dhis2/app-service-offline 3.0.0-beta.1 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/cjs/__tests__/integration.test.js +8 -6
- package/build/cjs/index.js +9 -1
- package/build/cjs/lib/__tests__/clear-sensitive-caches.test.js +140 -0
- package/build/cjs/lib/__tests__/offline-provider.test.js +14 -12
- package/build/cjs/lib/__tests__/use-cacheable-section.test.js +14 -12
- package/build/cjs/lib/clear-sensitive-caches.js +89 -0
- package/build/es/__tests__/integration.test.js +7 -6
- package/build/es/index.js +2 -1
- package/build/es/lib/__tests__/clear-sensitive-caches.test.js +132 -0
- package/build/es/lib/__tests__/offline-provider.test.js +13 -12
- package/build/es/lib/__tests__/use-cacheable-section.test.js +13 -12
- package/build/es/lib/clear-sensitive-caches.js +80 -0
- package/build/types/index.d.ts +1 -0
- package/build/types/lib/clear-sensitive-caches.d.ts +16 -0
- package/package.json +2 -2
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
var _appServiceAlerts = require("@dhis2/app-service-alerts");
|
|
4
|
+
|
|
3
5
|
var _react = require("@testing-library/react");
|
|
4
6
|
|
|
5
7
|
var _react2 = _interopRequireDefault(require("react"));
|
|
@@ -66,13 +68,13 @@ const TestSection = ({
|
|
|
66
68
|
|
|
67
69
|
const TestSingleSection = props => {
|
|
68
70
|
// Props are spread so they can be overwritten
|
|
69
|
-
return /*#__PURE__*/_react2.default.createElement(_offlineProvider.OfflineProvider, _extends({
|
|
71
|
+
return /*#__PURE__*/_react2.default.createElement(_appServiceAlerts.AlertsProvider, null, /*#__PURE__*/_react2.default.createElement(_offlineProvider.OfflineProvider, _extends({
|
|
70
72
|
offlineInterface: _testMocks.mockOfflineInterface
|
|
71
73
|
}, props), /*#__PURE__*/_react2.default.createElement(TestControls, _extends({
|
|
72
74
|
id: '1'
|
|
73
75
|
}, props)), /*#__PURE__*/_react2.default.createElement(TestSection, _extends({
|
|
74
76
|
id: '1'
|
|
75
|
-
}, props)));
|
|
77
|
+
}, props))));
|
|
76
78
|
}; // Suppress 'act' warning for these tests
|
|
77
79
|
|
|
78
80
|
|
|
@@ -227,7 +229,7 @@ describe('Coordination between useCacheableSection and CacheableSection', () =>
|
|
|
227
229
|
const TwoTestSections = props =>
|
|
228
230
|
/*#__PURE__*/
|
|
229
231
|
// Props are spread so they can be overwritten (but only on one section)
|
|
230
|
-
_react2.default.createElement(_offlineProvider.OfflineProvider, _extends({
|
|
232
|
+
_react2.default.createElement(_appServiceAlerts.AlertsProvider, null, /*#__PURE__*/_react2.default.createElement(_offlineProvider.OfflineProvider, _extends({
|
|
231
233
|
offlineInterface: _testMocks.mockOfflineInterface
|
|
232
234
|
}, props), /*#__PURE__*/_react2.default.createElement(TestControls, _extends({
|
|
233
235
|
id: '1'
|
|
@@ -237,7 +239,7 @@ _react2.default.createElement(_offlineProvider.OfflineProvider, _extends({
|
|
|
237
239
|
id: '2'
|
|
238
240
|
}), /*#__PURE__*/_react2.default.createElement(TestSection, {
|
|
239
241
|
id: '2'
|
|
240
|
-
})); // test that other sections don't rerender when one section does
|
|
242
|
+
}))); // test that other sections don't rerender when one section does
|
|
241
243
|
|
|
242
244
|
|
|
243
245
|
describe('Performant state management', () => {
|
|
@@ -286,13 +288,13 @@ describe('Performant state management', () => {
|
|
|
286
288
|
describe('useCacheableSection can be used inside a child of CacheableSection', () => {
|
|
287
289
|
const ChildTest = props => {
|
|
288
290
|
// Props are spread so they can be overwritten
|
|
289
|
-
return /*#__PURE__*/_react2.default.createElement(_offlineProvider.OfflineProvider, _extends({
|
|
291
|
+
return /*#__PURE__*/_react2.default.createElement(_appServiceAlerts.AlertsProvider, null, /*#__PURE__*/_react2.default.createElement(_offlineProvider.OfflineProvider, _extends({
|
|
290
292
|
offlineInterface: _testMocks.mockOfflineInterface
|
|
291
293
|
}, props), /*#__PURE__*/_react2.default.createElement(TestSection, _extends({
|
|
292
294
|
id: '1'
|
|
293
295
|
}, props), /*#__PURE__*/_react2.default.createElement(TestControls, _extends({
|
|
294
296
|
id: '1'
|
|
295
|
-
}, props))));
|
|
297
|
+
}, props)))));
|
|
296
298
|
};
|
|
297
299
|
|
|
298
300
|
it('handles a successful recording', async done => {
|
package/build/cjs/index.js
CHANGED
|
@@ -33,6 +33,12 @@ Object.defineProperty(exports, "useOnlineStatus", {
|
|
|
33
33
|
return _onlineStatus.useOnlineStatus;
|
|
34
34
|
}
|
|
35
35
|
});
|
|
36
|
+
Object.defineProperty(exports, "clearSensitiveCaches", {
|
|
37
|
+
enumerable: true,
|
|
38
|
+
get: function () {
|
|
39
|
+
return _clearSensitiveCaches.clearSensitiveCaches;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
36
42
|
|
|
37
43
|
var _offlineProvider = require("./lib/offline-provider");
|
|
38
44
|
|
|
@@ -40,4 +46,6 @@ var _cacheableSection = require("./lib/cacheable-section");
|
|
|
40
46
|
|
|
41
47
|
var _cacheableSectionState = require("./lib/cacheable-section-state");
|
|
42
48
|
|
|
43
|
-
var _onlineStatus = require("./lib/online-status");
|
|
49
|
+
var _onlineStatus = require("./lib/online-status");
|
|
50
|
+
|
|
51
|
+
var _clearSensitiveCaches = require("./lib/clear-sensitive-caches");
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _FDBFactory = _interopRequireDefault(require("fake-indexeddb/lib/FDBFactory"));
|
|
4
|
+
|
|
5
|
+
var _idb = require("idb");
|
|
6
|
+
|
|
7
|
+
require("fake-indexeddb/auto");
|
|
8
|
+
|
|
9
|
+
var _clearSensitiveCaches = require("../clear-sensitive-caches");
|
|
10
|
+
|
|
11
|
+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
12
|
+
|
|
13
|
+
// Mocks for CacheStorage API
|
|
14
|
+
// Returns true if an existing cache is deleted
|
|
15
|
+
const makeCachesDeleteMock = keys => {
|
|
16
|
+
return jest.fn().mockImplementation(key => Promise.resolve(keys.includes(key)));
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const keysMockDefault = jest.fn().mockImplementation(async () => []);
|
|
20
|
+
const deleteMockDefault = makeCachesDeleteMock([]);
|
|
21
|
+
const cachesDefault = {
|
|
22
|
+
keys: keysMockDefault,
|
|
23
|
+
delete: deleteMockDefault
|
|
24
|
+
};
|
|
25
|
+
window.caches = cachesDefault;
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
window.caches = cachesDefault;
|
|
28
|
+
jest.clearAllMocks();
|
|
29
|
+
}); // silence debug logs for these tests
|
|
30
|
+
|
|
31
|
+
const originalDebug = console.debug;
|
|
32
|
+
beforeAll(() => {
|
|
33
|
+
jest.spyOn(console, 'debug').mockImplementation((...args) => {
|
|
34
|
+
const pattern = /Clearing sensitive caches/;
|
|
35
|
+
|
|
36
|
+
if (typeof args[0] === 'string' && pattern.test(args[0])) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return originalDebug.call(console, ...args);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
afterAll(() => {
|
|
44
|
+
;
|
|
45
|
+
console.debug.mockRestore();
|
|
46
|
+
});
|
|
47
|
+
it('does not fail if there are no caches or no sections-db', () => {
|
|
48
|
+
return expect((0, _clearSensitiveCaches.clearSensitiveCaches)()).resolves.toBe(false);
|
|
49
|
+
});
|
|
50
|
+
it('clears potentially sensitive caches', async () => {
|
|
51
|
+
const testKeys = ['cache1', 'cache2', 'app-shell'];
|
|
52
|
+
const keysMock = jest.fn().mockImplementation(() => Promise.resolve(testKeys));
|
|
53
|
+
const deleteMock = makeCachesDeleteMock(testKeys);
|
|
54
|
+
window.caches = {
|
|
55
|
+
keys: keysMock,
|
|
56
|
+
delete: deleteMock
|
|
57
|
+
};
|
|
58
|
+
const cachesDeleted = await (0, _clearSensitiveCaches.clearSensitiveCaches)();
|
|
59
|
+
expect(cachesDeleted).toBe(true);
|
|
60
|
+
expect(deleteMock).toHaveBeenCalledTimes(3);
|
|
61
|
+
expect(deleteMock.mock.calls[0][0]).toBe('cache1');
|
|
62
|
+
expect(deleteMock.mock.calls[1][0]).toBe('cache2');
|
|
63
|
+
expect(deleteMock.mock.calls[2][0]).toBe('app-shell');
|
|
64
|
+
});
|
|
65
|
+
it('preserves keepable caches', async () => {
|
|
66
|
+
const keysMock = jest.fn().mockImplementation(async () => ['cache1', 'cache2', 'app-shell', 'other-assets', 'workbox-precache-v2-https://hey.howareya.now/']);
|
|
67
|
+
window.caches = { ...cachesDefault,
|
|
68
|
+
keys: keysMock
|
|
69
|
+
};
|
|
70
|
+
await (0, _clearSensitiveCaches.clearSensitiveCaches)();
|
|
71
|
+
expect(deleteMockDefault).toHaveBeenCalledTimes(3);
|
|
72
|
+
expect(deleteMockDefault.mock.calls[0][0]).toBe('cache1');
|
|
73
|
+
expect(deleteMockDefault.mock.calls[1][0]).toBe('cache2');
|
|
74
|
+
expect(deleteMockDefault.mock.calls[2][0]).toBe('app-shell');
|
|
75
|
+
expect(deleteMockDefault).not.toHaveBeenCalledWith('other-assets');
|
|
76
|
+
expect(deleteMockDefault).not.toHaveBeenCalledWith('workbox-precache-v2-https://hey.howareya.now/');
|
|
77
|
+
});
|
|
78
|
+
describe('clears sections-db', () => {
|
|
79
|
+
// Test DB
|
|
80
|
+
function openTestDB(dbName) {
|
|
81
|
+
// simplified version of app platform openDB logic
|
|
82
|
+
return (0, _idb.openDB)(dbName, 1, {
|
|
83
|
+
upgrade(db) {
|
|
84
|
+
db.createObjectStore(_clearSensitiveCaches.SECTIONS_STORE, {
|
|
85
|
+
keyPath: 'sectionId'
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
afterEach(() => {
|
|
93
|
+
// reset indexedDB state
|
|
94
|
+
window.indexedDB = new _FDBFactory.default();
|
|
95
|
+
});
|
|
96
|
+
it('clears sections-db if it exists', async () => {
|
|
97
|
+
// Open and populate test DB
|
|
98
|
+
const db = await openTestDB(_clearSensitiveCaches.SECTIONS_DB);
|
|
99
|
+
await db.put(_clearSensitiveCaches.SECTIONS_STORE, {
|
|
100
|
+
sectionId: 'id-1',
|
|
101
|
+
lastUpdated: new Date(),
|
|
102
|
+
requests: 3
|
|
103
|
+
});
|
|
104
|
+
await db.put(_clearSensitiveCaches.SECTIONS_STORE, {
|
|
105
|
+
sectionId: 'id-2',
|
|
106
|
+
lastUpdated: new Date(),
|
|
107
|
+
requests: 3
|
|
108
|
+
});
|
|
109
|
+
await (0, _clearSensitiveCaches.clearSensitiveCaches)(); // Sections-db should be cleared
|
|
110
|
+
|
|
111
|
+
const allSections = await db.getAll(_clearSensitiveCaches.SECTIONS_STORE);
|
|
112
|
+
expect(allSections).toHaveLength(0);
|
|
113
|
+
});
|
|
114
|
+
it("doesn't clear sections-db if it doesn't exist and doesn't open a new one", async () => {
|
|
115
|
+
const openMock = jest.fn();
|
|
116
|
+
window.indexedDB.open = openMock;
|
|
117
|
+
expect(await indexedDB.databases()).not.toContain(_clearSensitiveCaches.SECTIONS_DB);
|
|
118
|
+
await (0, _clearSensitiveCaches.clearSensitiveCaches)();
|
|
119
|
+
expect(openMock).not.toHaveBeenCalled();
|
|
120
|
+
return expect(await indexedDB.databases()).not.toContain(_clearSensitiveCaches.SECTIONS_DB);
|
|
121
|
+
});
|
|
122
|
+
it("doesn't handle IDB if 'databases' property is not on window.indexedDB", async () => {
|
|
123
|
+
// Open DB -- 'indexedDB.open' _would_ get called in this test
|
|
124
|
+
// if 'databases' property exists
|
|
125
|
+
await openTestDB(_clearSensitiveCaches.SECTIONS_DB);
|
|
126
|
+
const openMock = jest.fn();
|
|
127
|
+
window.indexedDB.open = openMock; // Remove 'databases' from indexedDB prototype for this test
|
|
128
|
+
// (simulates Firefox environment)
|
|
129
|
+
|
|
130
|
+
const idbProto = Object.getPrototypeOf(window.indexedDB);
|
|
131
|
+
const databases = idbProto.databases;
|
|
132
|
+
delete idbProto.databases;
|
|
133
|
+
expect('databases' in window.indexedDB).toBe(false);
|
|
134
|
+
await expect((0, _clearSensitiveCaches.clearSensitiveCaches)()).resolves.toBeDefined();
|
|
135
|
+
expect(openMock).not.toHaveBeenCalled(); // Restore indexedDB prototype for later tests
|
|
136
|
+
|
|
137
|
+
idbProto.databases = databases;
|
|
138
|
+
expect('databases' in window.indexedDB).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
var _appServiceAlerts = require("@dhis2/app-service-alerts");
|
|
4
|
+
|
|
3
5
|
var _react = require("@testing-library/react");
|
|
4
6
|
|
|
5
7
|
var _react2 = _interopRequireDefault(require("react"));
|
|
@@ -34,17 +36,17 @@ afterEach(() => {
|
|
|
34
36
|
});
|
|
35
37
|
describe('Testing offline provider', () => {
|
|
36
38
|
it('Should render without failing', () => {
|
|
37
|
-
(0, _react.render)( /*#__PURE__*/_react2.default.createElement(_offlineProvider.OfflineProvider, {
|
|
39
|
+
(0, _react.render)( /*#__PURE__*/_react2.default.createElement(_appServiceAlerts.AlertsProvider, null, /*#__PURE__*/_react2.default.createElement(_offlineProvider.OfflineProvider, {
|
|
38
40
|
offlineInterface: _testMocks.mockOfflineInterface
|
|
39
41
|
}, /*#__PURE__*/_react2.default.createElement("div", {
|
|
40
42
|
"data-testid": "test-div"
|
|
41
|
-
})));
|
|
43
|
+
}))));
|
|
42
44
|
expect(_react.screen.getByTestId('test-div')).toBeInTheDocument();
|
|
43
45
|
});
|
|
44
46
|
it('Should initialize the offline interface with an update prompt', () => {
|
|
45
|
-
(0, _react.render)( /*#__PURE__*/_react2.default.createElement(_offlineProvider.OfflineProvider, {
|
|
47
|
+
(0, _react.render)( /*#__PURE__*/_react2.default.createElement(_appServiceAlerts.AlertsProvider, null, /*#__PURE__*/_react2.default.createElement(_offlineProvider.OfflineProvider, {
|
|
46
48
|
offlineInterface: _testMocks.mockOfflineInterface
|
|
47
|
-
}));
|
|
49
|
+
})));
|
|
48
50
|
expect(_testMocks.mockOfflineInterface.init).toHaveBeenCalledTimes(1); // Expect to have been called with a 'promptUpdate' function
|
|
49
51
|
|
|
50
52
|
const arg = _testMocks.mockOfflineInterface.init.mock.calls[0][0];
|
|
@@ -71,9 +73,9 @@ describe('Testing offline provider', () => {
|
|
|
71
73
|
}, JSON.stringify(cachedSections));
|
|
72
74
|
};
|
|
73
75
|
|
|
74
|
-
(0, _react.render)( /*#__PURE__*/_react2.default.createElement(_offlineProvider.OfflineProvider, {
|
|
76
|
+
(0, _react.render)( /*#__PURE__*/_react2.default.createElement(_appServiceAlerts.AlertsProvider, null, /*#__PURE__*/_react2.default.createElement(_offlineProvider.OfflineProvider, {
|
|
75
77
|
offlineInterface: testOfflineInterface
|
|
76
|
-
}, /*#__PURE__*/_react2.default.createElement(CachedSections, null)));
|
|
78
|
+
}, /*#__PURE__*/_react2.default.createElement(CachedSections, null))));
|
|
77
79
|
const {
|
|
78
80
|
getByTestId
|
|
79
81
|
} = _react.screen;
|
|
@@ -100,26 +102,26 @@ describe('Testing offline provider', () => {
|
|
|
100
102
|
}));
|
|
101
103
|
};
|
|
102
104
|
|
|
103
|
-
(0, _react.render)( /*#__PURE__*/_react2.default.createElement(_offlineProvider.OfflineProvider, {
|
|
105
|
+
(0, _react.render)( /*#__PURE__*/_react2.default.createElement(_appServiceAlerts.AlertsProvider, null, /*#__PURE__*/_react2.default.createElement(_offlineProvider.OfflineProvider, {
|
|
104
106
|
offlineInterface: _testMocks.mockOfflineInterface
|
|
105
|
-
}, /*#__PURE__*/_react2.default.createElement(TestConsumer, null)));
|
|
107
|
+
}, /*#__PURE__*/_react2.default.createElement(TestConsumer, null))));
|
|
106
108
|
expect(_react.screen.getByTestId('test-div')).toBeInTheDocument();
|
|
107
109
|
});
|
|
108
110
|
it('Should render without failing when no offlineInterface is provided', () => {
|
|
109
|
-
(0, _react.render)( /*#__PURE__*/_react2.default.createElement(_offlineProvider.OfflineProvider, null, /*#__PURE__*/_react2.default.createElement("div", {
|
|
111
|
+
(0, _react.render)( /*#__PURE__*/_react2.default.createElement(_appServiceAlerts.AlertsProvider, null, /*#__PURE__*/_react2.default.createElement(_offlineProvider.OfflineProvider, null, /*#__PURE__*/_react2.default.createElement("div", {
|
|
110
112
|
"data-testid": "test-div"
|
|
111
|
-
})));
|
|
113
|
+
}))));
|
|
112
114
|
expect(_react.screen.getByTestId('test-div')).toBeInTheDocument();
|
|
113
115
|
});
|
|
114
116
|
it('Should render without failing if PWA is not enabled', () => {
|
|
115
117
|
const testOfflineInterface = { ..._testMocks.mockOfflineInterface,
|
|
116
118
|
pwaEnabled: false
|
|
117
119
|
};
|
|
118
|
-
(0, _react.render)( /*#__PURE__*/_react2.default.createElement(_offlineProvider.OfflineProvider, {
|
|
120
|
+
(0, _react.render)( /*#__PURE__*/_react2.default.createElement(_appServiceAlerts.AlertsProvider, null, /*#__PURE__*/_react2.default.createElement(_offlineProvider.OfflineProvider, {
|
|
119
121
|
offlineInterface: testOfflineInterface
|
|
120
122
|
}, /*#__PURE__*/_react2.default.createElement("div", {
|
|
121
123
|
"data-testid": "test-div"
|
|
122
|
-
}))); // Init should still be called - see comments in offline-provider.js
|
|
124
|
+
})))); // Init should still be called - see comments in offline-provider.js
|
|
123
125
|
|
|
124
126
|
expect(testOfflineInterface.init).toHaveBeenCalled();
|
|
125
127
|
expect(_react.screen.getByTestId('test-div')).toBeInTheDocument();
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
var _appServiceAlerts = require("@dhis2/app-service-alerts");
|
|
4
|
+
|
|
3
5
|
var _reactHooks = require("@testing-library/react-hooks");
|
|
4
6
|
|
|
5
7
|
var _react = _interopRequireDefault(require("react"));
|
|
@@ -37,9 +39,9 @@ it('renders in the default state initially', () => {
|
|
|
37
39
|
} = (0, _reactHooks.renderHook)(() => (0, _cacheableSection.useCacheableSection)('one'), {
|
|
38
40
|
wrapper: ({
|
|
39
41
|
children
|
|
40
|
-
}) => /*#__PURE__*/_react.default.createElement(_offlineProvider.OfflineProvider, {
|
|
42
|
+
}) => /*#__PURE__*/_react.default.createElement(_appServiceAlerts.AlertsProvider, null, /*#__PURE__*/_react.default.createElement(_offlineProvider.OfflineProvider, {
|
|
41
43
|
offlineInterface: _testMocks.mockOfflineInterface
|
|
42
|
-
}, children)
|
|
44
|
+
}, children))
|
|
43
45
|
});
|
|
44
46
|
expect(result.current.recordingState).toBe('default');
|
|
45
47
|
expect(result.current.isCached).toBe(false);
|
|
@@ -59,9 +61,9 @@ it('handles a successful recording', async done => {
|
|
|
59
61
|
} = (0, _reactHooks.renderHook)(() => (0, _cacheableSection.useCacheableSection)(sectionId), {
|
|
60
62
|
wrapper: ({
|
|
61
63
|
children
|
|
62
|
-
}) => /*#__PURE__*/_react.default.createElement(_offlineProvider.OfflineProvider, {
|
|
64
|
+
}) => /*#__PURE__*/_react.default.createElement(_appServiceAlerts.AlertsProvider, null, /*#__PURE__*/_react.default.createElement(_offlineProvider.OfflineProvider, {
|
|
63
65
|
offlineInterface: testOfflineInterface
|
|
64
|
-
}, children)
|
|
66
|
+
}, children))
|
|
65
67
|
});
|
|
66
68
|
|
|
67
69
|
const assertRecordingStarted = () => {
|
|
@@ -119,9 +121,9 @@ it('handles a recording that encounters an error', async done => {
|
|
|
119
121
|
} = (0, _reactHooks.renderHook)(() => (0, _cacheableSection.useCacheableSection)('one'), {
|
|
120
122
|
wrapper: ({
|
|
121
123
|
children
|
|
122
|
-
}) => /*#__PURE__*/_react.default.createElement(_offlineProvider.OfflineProvider, {
|
|
124
|
+
}) => /*#__PURE__*/_react.default.createElement(_appServiceAlerts.AlertsProvider, null, /*#__PURE__*/_react.default.createElement(_offlineProvider.OfflineProvider, {
|
|
123
125
|
offlineInterface: testOfflineInterface
|
|
124
|
-
}, children)
|
|
126
|
+
}, children))
|
|
125
127
|
});
|
|
126
128
|
|
|
127
129
|
const assertRecordingStarted = () => {
|
|
@@ -159,9 +161,9 @@ it('handles an error starting the recording', async () => {
|
|
|
159
161
|
} = (0, _reactHooks.renderHook)(() => (0, _cacheableSection.useCacheableSection)('err'), {
|
|
160
162
|
wrapper: ({
|
|
161
163
|
children
|
|
162
|
-
}) => /*#__PURE__*/_react.default.createElement(_offlineProvider.OfflineProvider, {
|
|
164
|
+
}) => /*#__PURE__*/_react.default.createElement(_appServiceAlerts.AlertsProvider, null, /*#__PURE__*/_react.default.createElement(_offlineProvider.OfflineProvider, {
|
|
163
165
|
offlineInterface: testOfflineInterface
|
|
164
|
-
}, children)
|
|
166
|
+
}, children))
|
|
165
167
|
});
|
|
166
168
|
await expect(result.current.startRecording()).rejects.toThrow('Failed message' // from failedMessageRecordingMock
|
|
167
169
|
);
|
|
@@ -180,9 +182,9 @@ it('handles remove and updates sections', async () => {
|
|
|
180
182
|
} = (0, _reactHooks.renderHook)(() => (0, _cacheableSection.useCacheableSection)(sectionId), {
|
|
181
183
|
wrapper: ({
|
|
182
184
|
children
|
|
183
|
-
}) => /*#__PURE__*/_react.default.createElement(_offlineProvider.OfflineProvider, {
|
|
185
|
+
}) => /*#__PURE__*/_react.default.createElement(_appServiceAlerts.AlertsProvider, null, /*#__PURE__*/_react.default.createElement(_offlineProvider.OfflineProvider, {
|
|
184
186
|
offlineInterface: testOfflineInterface
|
|
185
|
-
}, children)
|
|
187
|
+
}, children))
|
|
186
188
|
}); // Wait for state to sync with indexedDB
|
|
187
189
|
|
|
188
190
|
await waitFor(() => result.current.isCached === true);
|
|
@@ -211,9 +213,9 @@ it('handles a change in ID', async () => {
|
|
|
211
213
|
} = (0, _reactHooks.renderHook)((...args) => (0, _cacheableSection.useCacheableSection)(...args), {
|
|
212
214
|
wrapper: ({
|
|
213
215
|
children
|
|
214
|
-
}) => /*#__PURE__*/_react.default.createElement(_offlineProvider.OfflineProvider, {
|
|
216
|
+
}) => /*#__PURE__*/_react.default.createElement(_appServiceAlerts.AlertsProvider, null, /*#__PURE__*/_react.default.createElement(_offlineProvider.OfflineProvider, {
|
|
215
217
|
offlineInterface: testOfflineInterface
|
|
216
|
-
}, children),
|
|
218
|
+
}, children)),
|
|
217
219
|
initialProps: 'id-one'
|
|
218
220
|
}); // Wait for state to sync with indexedDB
|
|
219
221
|
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.clearSensitiveCaches = clearSensitiveCaches;
|
|
7
|
+
exports.SECTIONS_STORE = exports.SECTIONS_DB = void 0;
|
|
8
|
+
// IndexedDB names; should be the same as in @dhis2/pwa
|
|
9
|
+
const SECTIONS_DB = 'sections-db';
|
|
10
|
+
exports.SECTIONS_DB = SECTIONS_DB;
|
|
11
|
+
const SECTIONS_STORE = 'sections-store'; // Non-sensitive caches that can be kept:
|
|
12
|
+
|
|
13
|
+
exports.SECTIONS_STORE = SECTIONS_STORE;
|
|
14
|
+
const KEEPABLE_CACHES = [/^workbox-precache/, // precached static assets
|
|
15
|
+
/^other-assets/ // static assets cached at runtime - shouldn't be sensitive
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
/*
|
|
19
|
+
* Clears the 'sections-db' IndexedDB if it exists. Designed to avoid opening
|
|
20
|
+
* a new DB if it doesn't exist yet. Firefox can't check if 'sections-db'
|
|
21
|
+
* exists, in which circumstance the IndexedDB is unaffected. It's inelegant
|
|
22
|
+
* but acceptable because the IndexedDB has no sensitive data (only metadata
|
|
23
|
+
* of recorded sections), and the OfflineInterface handles discrepancies
|
|
24
|
+
* between CacheStorage and IndexedDB.
|
|
25
|
+
*/
|
|
26
|
+
const clearDB = async dbName => {
|
|
27
|
+
if (!('databases' in indexedDB)) {
|
|
28
|
+
// FF does not have indexedDB.databases. For that, just clear caches,
|
|
29
|
+
// and offline interface will handle discrepancies in PWA apps.
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const dbs = await window.indexedDB.databases();
|
|
34
|
+
|
|
35
|
+
if (!dbs.some(({
|
|
36
|
+
name
|
|
37
|
+
}) => name === dbName)) {
|
|
38
|
+
// Sections-db is not created; nothing to do here
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
// IndexedDB fun:
|
|
44
|
+
const openDBRequest = indexedDB.open(dbName);
|
|
45
|
+
|
|
46
|
+
openDBRequest.onsuccess = e => {
|
|
47
|
+
const db = e.target.result;
|
|
48
|
+
const tx = db.transaction(SECTIONS_STORE, 'readwrite'); // When the transaction completes is when the operation is done:
|
|
49
|
+
|
|
50
|
+
tx.oncomplete = () => resolve();
|
|
51
|
+
|
|
52
|
+
tx.onerror = e => reject(e.target.error);
|
|
53
|
+
|
|
54
|
+
const os = tx.objectStore(SECTIONS_STORE);
|
|
55
|
+
const clearReq = os.clear();
|
|
56
|
+
|
|
57
|
+
clearReq.onerror = e => reject(e.target.error);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
openDBRequest.onerror = e => {
|
|
61
|
+
reject(e.target.error);
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Used to clear caches and 'sections-db' IndexedDB when a user logs out or a
|
|
67
|
+
* different user logs in to prevent someone from accessing a different user's
|
|
68
|
+
* caches. Should be able to be used in a non-PWA app.
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
async function clearSensitiveCaches(dbName = SECTIONS_DB) {
|
|
73
|
+
console.debug('Clearing sensitive caches');
|
|
74
|
+
const cacheKeys = await caches.keys();
|
|
75
|
+
return Promise.all([// (Resolves to 'false' because this can't detect if anything was deleted):
|
|
76
|
+
clearDB(dbName).then(() => false), // Remove caches if not in keepable list
|
|
77
|
+
...cacheKeys.map(key => {
|
|
78
|
+
if (!KEEPABLE_CACHES.some(pattern => pattern.test(key))) {
|
|
79
|
+
return caches.delete(key);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return false;
|
|
83
|
+
})]).then(responses => {
|
|
84
|
+
// Return true if any caches have been cleared
|
|
85
|
+
// (caches.delete() returns true if a cache is deleted successfully)
|
|
86
|
+
// PWA apps can reload to restore their app shell cache
|
|
87
|
+
return responses.some(response => response);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
|
|
2
2
|
|
|
3
|
+
import { AlertsProvider } from '@dhis2/app-service-alerts';
|
|
3
4
|
import { act, fireEvent, render, screen } from '@testing-library/react';
|
|
4
5
|
import React from 'react';
|
|
5
6
|
import { useCacheableSection, CacheableSection } from '../lib/cacheable-section';
|
|
@@ -56,13 +57,13 @@ const TestSection = ({
|
|
|
56
57
|
|
|
57
58
|
const TestSingleSection = props => {
|
|
58
59
|
// Props are spread so they can be overwritten
|
|
59
|
-
return /*#__PURE__*/React.createElement(OfflineProvider, _extends({
|
|
60
|
+
return /*#__PURE__*/React.createElement(AlertsProvider, null, /*#__PURE__*/React.createElement(OfflineProvider, _extends({
|
|
60
61
|
offlineInterface: mockOfflineInterface
|
|
61
62
|
}, props), /*#__PURE__*/React.createElement(TestControls, _extends({
|
|
62
63
|
id: '1'
|
|
63
64
|
}, props)), /*#__PURE__*/React.createElement(TestSection, _extends({
|
|
64
65
|
id: '1'
|
|
65
|
-
}, props)));
|
|
66
|
+
}, props))));
|
|
66
67
|
}; // Suppress 'act' warning for these tests
|
|
67
68
|
|
|
68
69
|
|
|
@@ -217,7 +218,7 @@ describe('Coordination between useCacheableSection and CacheableSection', () =>
|
|
|
217
218
|
const TwoTestSections = props =>
|
|
218
219
|
/*#__PURE__*/
|
|
219
220
|
// Props are spread so they can be overwritten (but only on one section)
|
|
220
|
-
React.createElement(OfflineProvider, _extends({
|
|
221
|
+
React.createElement(AlertsProvider, null, /*#__PURE__*/React.createElement(OfflineProvider, _extends({
|
|
221
222
|
offlineInterface: mockOfflineInterface
|
|
222
223
|
}, props), /*#__PURE__*/React.createElement(TestControls, _extends({
|
|
223
224
|
id: '1'
|
|
@@ -227,7 +228,7 @@ React.createElement(OfflineProvider, _extends({
|
|
|
227
228
|
id: '2'
|
|
228
229
|
}), /*#__PURE__*/React.createElement(TestSection, {
|
|
229
230
|
id: '2'
|
|
230
|
-
})); // test that other sections don't rerender when one section does
|
|
231
|
+
}))); // test that other sections don't rerender when one section does
|
|
231
232
|
|
|
232
233
|
|
|
233
234
|
describe('Performant state management', () => {
|
|
@@ -276,13 +277,13 @@ describe('Performant state management', () => {
|
|
|
276
277
|
describe('useCacheableSection can be used inside a child of CacheableSection', () => {
|
|
277
278
|
const ChildTest = props => {
|
|
278
279
|
// Props are spread so they can be overwritten
|
|
279
|
-
return /*#__PURE__*/React.createElement(OfflineProvider, _extends({
|
|
280
|
+
return /*#__PURE__*/React.createElement(AlertsProvider, null, /*#__PURE__*/React.createElement(OfflineProvider, _extends({
|
|
280
281
|
offlineInterface: mockOfflineInterface
|
|
281
282
|
}, props), /*#__PURE__*/React.createElement(TestSection, _extends({
|
|
282
283
|
id: '1'
|
|
283
284
|
}, props), /*#__PURE__*/React.createElement(TestControls, _extends({
|
|
284
285
|
id: '1'
|
|
285
|
-
}, props))));
|
|
286
|
+
}, props)))));
|
|
286
287
|
};
|
|
287
288
|
|
|
288
289
|
it('handles a successful recording', async done => {
|
package/build/es/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { OfflineProvider } from './lib/offline-provider';
|
|
2
2
|
export { CacheableSection, useCacheableSection } from './lib/cacheable-section';
|
|
3
3
|
export { useCachedSections } from './lib/cacheable-section-state';
|
|
4
|
-
export { useOnlineStatus } from './lib/online-status';
|
|
4
|
+
export { useOnlineStatus } from './lib/online-status';
|
|
5
|
+
export { clearSensitiveCaches } from './lib/clear-sensitive-caches';
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import FDBFactory from 'fake-indexeddb/lib/FDBFactory';
|
|
2
|
+
import { openDB } from 'idb';
|
|
3
|
+
import 'fake-indexeddb/auto';
|
|
4
|
+
import { clearSensitiveCaches, SECTIONS_DB, SECTIONS_STORE } from '../clear-sensitive-caches'; // Mocks for CacheStorage API
|
|
5
|
+
// Returns true if an existing cache is deleted
|
|
6
|
+
|
|
7
|
+
const makeCachesDeleteMock = keys => {
|
|
8
|
+
return jest.fn().mockImplementation(key => Promise.resolve(keys.includes(key)));
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const keysMockDefault = jest.fn().mockImplementation(async () => []);
|
|
12
|
+
const deleteMockDefault = makeCachesDeleteMock([]);
|
|
13
|
+
const cachesDefault = {
|
|
14
|
+
keys: keysMockDefault,
|
|
15
|
+
delete: deleteMockDefault
|
|
16
|
+
};
|
|
17
|
+
window.caches = cachesDefault;
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
window.caches = cachesDefault;
|
|
20
|
+
jest.clearAllMocks();
|
|
21
|
+
}); // silence debug logs for these tests
|
|
22
|
+
|
|
23
|
+
const originalDebug = console.debug;
|
|
24
|
+
beforeAll(() => {
|
|
25
|
+
jest.spyOn(console, 'debug').mockImplementation((...args) => {
|
|
26
|
+
const pattern = /Clearing sensitive caches/;
|
|
27
|
+
|
|
28
|
+
if (typeof args[0] === 'string' && pattern.test(args[0])) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return originalDebug.call(console, ...args);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
afterAll(() => {
|
|
36
|
+
;
|
|
37
|
+
console.debug.mockRestore();
|
|
38
|
+
});
|
|
39
|
+
it('does not fail if there are no caches or no sections-db', () => {
|
|
40
|
+
return expect(clearSensitiveCaches()).resolves.toBe(false);
|
|
41
|
+
});
|
|
42
|
+
it('clears potentially sensitive caches', async () => {
|
|
43
|
+
const testKeys = ['cache1', 'cache2', 'app-shell'];
|
|
44
|
+
const keysMock = jest.fn().mockImplementation(() => Promise.resolve(testKeys));
|
|
45
|
+
const deleteMock = makeCachesDeleteMock(testKeys);
|
|
46
|
+
window.caches = {
|
|
47
|
+
keys: keysMock,
|
|
48
|
+
delete: deleteMock
|
|
49
|
+
};
|
|
50
|
+
const cachesDeleted = await clearSensitiveCaches();
|
|
51
|
+
expect(cachesDeleted).toBe(true);
|
|
52
|
+
expect(deleteMock).toHaveBeenCalledTimes(3);
|
|
53
|
+
expect(deleteMock.mock.calls[0][0]).toBe('cache1');
|
|
54
|
+
expect(deleteMock.mock.calls[1][0]).toBe('cache2');
|
|
55
|
+
expect(deleteMock.mock.calls[2][0]).toBe('app-shell');
|
|
56
|
+
});
|
|
57
|
+
it('preserves keepable caches', async () => {
|
|
58
|
+
const keysMock = jest.fn().mockImplementation(async () => ['cache1', 'cache2', 'app-shell', 'other-assets', 'workbox-precache-v2-https://hey.howareya.now/']);
|
|
59
|
+
window.caches = { ...cachesDefault,
|
|
60
|
+
keys: keysMock
|
|
61
|
+
};
|
|
62
|
+
await clearSensitiveCaches();
|
|
63
|
+
expect(deleteMockDefault).toHaveBeenCalledTimes(3);
|
|
64
|
+
expect(deleteMockDefault.mock.calls[0][0]).toBe('cache1');
|
|
65
|
+
expect(deleteMockDefault.mock.calls[1][0]).toBe('cache2');
|
|
66
|
+
expect(deleteMockDefault.mock.calls[2][0]).toBe('app-shell');
|
|
67
|
+
expect(deleteMockDefault).not.toHaveBeenCalledWith('other-assets');
|
|
68
|
+
expect(deleteMockDefault).not.toHaveBeenCalledWith('workbox-precache-v2-https://hey.howareya.now/');
|
|
69
|
+
});
|
|
70
|
+
describe('clears sections-db', () => {
|
|
71
|
+
// Test DB
|
|
72
|
+
function openTestDB(dbName) {
|
|
73
|
+
// simplified version of app platform openDB logic
|
|
74
|
+
return openDB(dbName, 1, {
|
|
75
|
+
upgrade(db) {
|
|
76
|
+
db.createObjectStore(SECTIONS_STORE, {
|
|
77
|
+
keyPath: 'sectionId'
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
afterEach(() => {
|
|
85
|
+
// reset indexedDB state
|
|
86
|
+
window.indexedDB = new FDBFactory();
|
|
87
|
+
});
|
|
88
|
+
it('clears sections-db if it exists', async () => {
|
|
89
|
+
// Open and populate test DB
|
|
90
|
+
const db = await openTestDB(SECTIONS_DB);
|
|
91
|
+
await db.put(SECTIONS_STORE, {
|
|
92
|
+
sectionId: 'id-1',
|
|
93
|
+
lastUpdated: new Date(),
|
|
94
|
+
requests: 3
|
|
95
|
+
});
|
|
96
|
+
await db.put(SECTIONS_STORE, {
|
|
97
|
+
sectionId: 'id-2',
|
|
98
|
+
lastUpdated: new Date(),
|
|
99
|
+
requests: 3
|
|
100
|
+
});
|
|
101
|
+
await clearSensitiveCaches(); // Sections-db should be cleared
|
|
102
|
+
|
|
103
|
+
const allSections = await db.getAll(SECTIONS_STORE);
|
|
104
|
+
expect(allSections).toHaveLength(0);
|
|
105
|
+
});
|
|
106
|
+
it("doesn't clear sections-db if it doesn't exist and doesn't open a new one", async () => {
|
|
107
|
+
const openMock = jest.fn();
|
|
108
|
+
window.indexedDB.open = openMock;
|
|
109
|
+
expect(await indexedDB.databases()).not.toContain(SECTIONS_DB);
|
|
110
|
+
await clearSensitiveCaches();
|
|
111
|
+
expect(openMock).not.toHaveBeenCalled();
|
|
112
|
+
return expect(await indexedDB.databases()).not.toContain(SECTIONS_DB);
|
|
113
|
+
});
|
|
114
|
+
it("doesn't handle IDB if 'databases' property is not on window.indexedDB", async () => {
|
|
115
|
+
// Open DB -- 'indexedDB.open' _would_ get called in this test
|
|
116
|
+
// if 'databases' property exists
|
|
117
|
+
await openTestDB(SECTIONS_DB);
|
|
118
|
+
const openMock = jest.fn();
|
|
119
|
+
window.indexedDB.open = openMock; // Remove 'databases' from indexedDB prototype for this test
|
|
120
|
+
// (simulates Firefox environment)
|
|
121
|
+
|
|
122
|
+
const idbProto = Object.getPrototypeOf(window.indexedDB);
|
|
123
|
+
const databases = idbProto.databases;
|
|
124
|
+
delete idbProto.databases;
|
|
125
|
+
expect('databases' in window.indexedDB).toBe(false);
|
|
126
|
+
await expect(clearSensitiveCaches()).resolves.toBeDefined();
|
|
127
|
+
expect(openMock).not.toHaveBeenCalled(); // Restore indexedDB prototype for later tests
|
|
128
|
+
|
|
129
|
+
idbProto.databases = databases;
|
|
130
|
+
expect('databases' in window.indexedDB).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AlertsProvider } from '@dhis2/app-service-alerts';
|
|
1
2
|
import { render, screen, waitFor } from '@testing-library/react';
|
|
2
3
|
import React from 'react';
|
|
3
4
|
import { mockOfflineInterface } from '../../utils/test-mocks';
|
|
@@ -24,17 +25,17 @@ afterEach(() => {
|
|
|
24
25
|
});
|
|
25
26
|
describe('Testing offline provider', () => {
|
|
26
27
|
it('Should render without failing', () => {
|
|
27
|
-
render( /*#__PURE__*/React.createElement(OfflineProvider, {
|
|
28
|
+
render( /*#__PURE__*/React.createElement(AlertsProvider, null, /*#__PURE__*/React.createElement(OfflineProvider, {
|
|
28
29
|
offlineInterface: mockOfflineInterface
|
|
29
30
|
}, /*#__PURE__*/React.createElement("div", {
|
|
30
31
|
"data-testid": "test-div"
|
|
31
|
-
})));
|
|
32
|
+
}))));
|
|
32
33
|
expect(screen.getByTestId('test-div')).toBeInTheDocument();
|
|
33
34
|
});
|
|
34
35
|
it('Should initialize the offline interface with an update prompt', () => {
|
|
35
|
-
render( /*#__PURE__*/React.createElement(OfflineProvider, {
|
|
36
|
+
render( /*#__PURE__*/React.createElement(AlertsProvider, null, /*#__PURE__*/React.createElement(OfflineProvider, {
|
|
36
37
|
offlineInterface: mockOfflineInterface
|
|
37
|
-
}));
|
|
38
|
+
})));
|
|
38
39
|
expect(mockOfflineInterface.init).toHaveBeenCalledTimes(1); // Expect to have been called with a 'promptUpdate' function
|
|
39
40
|
|
|
40
41
|
const arg = mockOfflineInterface.init.mock.calls[0][0];
|
|
@@ -61,9 +62,9 @@ describe('Testing offline provider', () => {
|
|
|
61
62
|
}, JSON.stringify(cachedSections));
|
|
62
63
|
};
|
|
63
64
|
|
|
64
|
-
render( /*#__PURE__*/React.createElement(OfflineProvider, {
|
|
65
|
+
render( /*#__PURE__*/React.createElement(AlertsProvider, null, /*#__PURE__*/React.createElement(OfflineProvider, {
|
|
65
66
|
offlineInterface: testOfflineInterface
|
|
66
|
-
}, /*#__PURE__*/React.createElement(CachedSections, null)));
|
|
67
|
+
}, /*#__PURE__*/React.createElement(CachedSections, null))));
|
|
67
68
|
const {
|
|
68
69
|
getByTestId
|
|
69
70
|
} = screen;
|
|
@@ -90,26 +91,26 @@ describe('Testing offline provider', () => {
|
|
|
90
91
|
}));
|
|
91
92
|
};
|
|
92
93
|
|
|
93
|
-
render( /*#__PURE__*/React.createElement(OfflineProvider, {
|
|
94
|
+
render( /*#__PURE__*/React.createElement(AlertsProvider, null, /*#__PURE__*/React.createElement(OfflineProvider, {
|
|
94
95
|
offlineInterface: mockOfflineInterface
|
|
95
|
-
}, /*#__PURE__*/React.createElement(TestConsumer, null)));
|
|
96
|
+
}, /*#__PURE__*/React.createElement(TestConsumer, null))));
|
|
96
97
|
expect(screen.getByTestId('test-div')).toBeInTheDocument();
|
|
97
98
|
});
|
|
98
99
|
it('Should render without failing when no offlineInterface is provided', () => {
|
|
99
|
-
render( /*#__PURE__*/React.createElement(OfflineProvider, null, /*#__PURE__*/React.createElement("div", {
|
|
100
|
+
render( /*#__PURE__*/React.createElement(AlertsProvider, null, /*#__PURE__*/React.createElement(OfflineProvider, null, /*#__PURE__*/React.createElement("div", {
|
|
100
101
|
"data-testid": "test-div"
|
|
101
|
-
})));
|
|
102
|
+
}))));
|
|
102
103
|
expect(screen.getByTestId('test-div')).toBeInTheDocument();
|
|
103
104
|
});
|
|
104
105
|
it('Should render without failing if PWA is not enabled', () => {
|
|
105
106
|
const testOfflineInterface = { ...mockOfflineInterface,
|
|
106
107
|
pwaEnabled: false
|
|
107
108
|
};
|
|
108
|
-
render( /*#__PURE__*/React.createElement(OfflineProvider, {
|
|
109
|
+
render( /*#__PURE__*/React.createElement(AlertsProvider, null, /*#__PURE__*/React.createElement(OfflineProvider, {
|
|
109
110
|
offlineInterface: testOfflineInterface
|
|
110
111
|
}, /*#__PURE__*/React.createElement("div", {
|
|
111
112
|
"data-testid": "test-div"
|
|
112
|
-
}))); // Init should still be called - see comments in offline-provider.js
|
|
113
|
+
})))); // Init should still be called - see comments in offline-provider.js
|
|
113
114
|
|
|
114
115
|
expect(testOfflineInterface.init).toHaveBeenCalled();
|
|
115
116
|
expect(screen.getByTestId('test-div')).toBeInTheDocument();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/* eslint-disable react/display-name, react/prop-types */
|
|
2
|
+
import { AlertsProvider } from '@dhis2/app-service-alerts';
|
|
2
3
|
import { renderHook, act } from '@testing-library/react-hooks';
|
|
3
4
|
import React from 'react';
|
|
4
5
|
import { errorRecordingMock, failedMessageRecordingMock, mockOfflineInterface } from '../../utils/test-mocks';
|
|
@@ -28,9 +29,9 @@ it('renders in the default state initially', () => {
|
|
|
28
29
|
} = renderHook(() => useCacheableSection('one'), {
|
|
29
30
|
wrapper: ({
|
|
30
31
|
children
|
|
31
|
-
}) => /*#__PURE__*/React.createElement(OfflineProvider, {
|
|
32
|
+
}) => /*#__PURE__*/React.createElement(AlertsProvider, null, /*#__PURE__*/React.createElement(OfflineProvider, {
|
|
32
33
|
offlineInterface: mockOfflineInterface
|
|
33
|
-
}, children)
|
|
34
|
+
}, children))
|
|
34
35
|
});
|
|
35
36
|
expect(result.current.recordingState).toBe('default');
|
|
36
37
|
expect(result.current.isCached).toBe(false);
|
|
@@ -50,9 +51,9 @@ it('handles a successful recording', async done => {
|
|
|
50
51
|
} = renderHook(() => useCacheableSection(sectionId), {
|
|
51
52
|
wrapper: ({
|
|
52
53
|
children
|
|
53
|
-
}) => /*#__PURE__*/React.createElement(OfflineProvider, {
|
|
54
|
+
}) => /*#__PURE__*/React.createElement(AlertsProvider, null, /*#__PURE__*/React.createElement(OfflineProvider, {
|
|
54
55
|
offlineInterface: testOfflineInterface
|
|
55
|
-
}, children)
|
|
56
|
+
}, children))
|
|
56
57
|
});
|
|
57
58
|
|
|
58
59
|
const assertRecordingStarted = () => {
|
|
@@ -110,9 +111,9 @@ it('handles a recording that encounters an error', async done => {
|
|
|
110
111
|
} = renderHook(() => useCacheableSection('one'), {
|
|
111
112
|
wrapper: ({
|
|
112
113
|
children
|
|
113
|
-
}) => /*#__PURE__*/React.createElement(OfflineProvider, {
|
|
114
|
+
}) => /*#__PURE__*/React.createElement(AlertsProvider, null, /*#__PURE__*/React.createElement(OfflineProvider, {
|
|
114
115
|
offlineInterface: testOfflineInterface
|
|
115
|
-
}, children)
|
|
116
|
+
}, children))
|
|
116
117
|
});
|
|
117
118
|
|
|
118
119
|
const assertRecordingStarted = () => {
|
|
@@ -150,9 +151,9 @@ it('handles an error starting the recording', async () => {
|
|
|
150
151
|
} = renderHook(() => useCacheableSection('err'), {
|
|
151
152
|
wrapper: ({
|
|
152
153
|
children
|
|
153
|
-
}) => /*#__PURE__*/React.createElement(OfflineProvider, {
|
|
154
|
+
}) => /*#__PURE__*/React.createElement(AlertsProvider, null, /*#__PURE__*/React.createElement(OfflineProvider, {
|
|
154
155
|
offlineInterface: testOfflineInterface
|
|
155
|
-
}, children)
|
|
156
|
+
}, children))
|
|
156
157
|
});
|
|
157
158
|
await expect(result.current.startRecording()).rejects.toThrow('Failed message' // from failedMessageRecordingMock
|
|
158
159
|
);
|
|
@@ -171,9 +172,9 @@ it('handles remove and updates sections', async () => {
|
|
|
171
172
|
} = renderHook(() => useCacheableSection(sectionId), {
|
|
172
173
|
wrapper: ({
|
|
173
174
|
children
|
|
174
|
-
}) => /*#__PURE__*/React.createElement(OfflineProvider, {
|
|
175
|
+
}) => /*#__PURE__*/React.createElement(AlertsProvider, null, /*#__PURE__*/React.createElement(OfflineProvider, {
|
|
175
176
|
offlineInterface: testOfflineInterface
|
|
176
|
-
}, children)
|
|
177
|
+
}, children))
|
|
177
178
|
}); // Wait for state to sync with indexedDB
|
|
178
179
|
|
|
179
180
|
await waitFor(() => result.current.isCached === true);
|
|
@@ -202,9 +203,9 @@ it('handles a change in ID', async () => {
|
|
|
202
203
|
} = renderHook((...args) => useCacheableSection(...args), {
|
|
203
204
|
wrapper: ({
|
|
204
205
|
children
|
|
205
|
-
}) => /*#__PURE__*/React.createElement(OfflineProvider, {
|
|
206
|
+
}) => /*#__PURE__*/React.createElement(AlertsProvider, null, /*#__PURE__*/React.createElement(OfflineProvider, {
|
|
206
207
|
offlineInterface: testOfflineInterface
|
|
207
|
-
}, children),
|
|
208
|
+
}, children)),
|
|
208
209
|
initialProps: 'id-one'
|
|
209
210
|
}); // Wait for state to sync with indexedDB
|
|
210
211
|
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// IndexedDB names; should be the same as in @dhis2/pwa
|
|
2
|
+
export const SECTIONS_DB = 'sections-db';
|
|
3
|
+
export const SECTIONS_STORE = 'sections-store'; // Non-sensitive caches that can be kept:
|
|
4
|
+
|
|
5
|
+
const KEEPABLE_CACHES = [/^workbox-precache/, // precached static assets
|
|
6
|
+
/^other-assets/ // static assets cached at runtime - shouldn't be sensitive
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
* Clears the 'sections-db' IndexedDB if it exists. Designed to avoid opening
|
|
11
|
+
* a new DB if it doesn't exist yet. Firefox can't check if 'sections-db'
|
|
12
|
+
* exists, in which circumstance the IndexedDB is unaffected. It's inelegant
|
|
13
|
+
* but acceptable because the IndexedDB has no sensitive data (only metadata
|
|
14
|
+
* of recorded sections), and the OfflineInterface handles discrepancies
|
|
15
|
+
* between CacheStorage and IndexedDB.
|
|
16
|
+
*/
|
|
17
|
+
const clearDB = async dbName => {
|
|
18
|
+
if (!('databases' in indexedDB)) {
|
|
19
|
+
// FF does not have indexedDB.databases. For that, just clear caches,
|
|
20
|
+
// and offline interface will handle discrepancies in PWA apps.
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const dbs = await window.indexedDB.databases();
|
|
25
|
+
|
|
26
|
+
if (!dbs.some(({
|
|
27
|
+
name
|
|
28
|
+
}) => name === dbName)) {
|
|
29
|
+
// Sections-db is not created; nothing to do here
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
// IndexedDB fun:
|
|
35
|
+
const openDBRequest = indexedDB.open(dbName);
|
|
36
|
+
|
|
37
|
+
openDBRequest.onsuccess = e => {
|
|
38
|
+
const db = e.target.result;
|
|
39
|
+
const tx = db.transaction(SECTIONS_STORE, 'readwrite'); // When the transaction completes is when the operation is done:
|
|
40
|
+
|
|
41
|
+
tx.oncomplete = () => resolve();
|
|
42
|
+
|
|
43
|
+
tx.onerror = e => reject(e.target.error);
|
|
44
|
+
|
|
45
|
+
const os = tx.objectStore(SECTIONS_STORE);
|
|
46
|
+
const clearReq = os.clear();
|
|
47
|
+
|
|
48
|
+
clearReq.onerror = e => reject(e.target.error);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
openDBRequest.onerror = e => {
|
|
52
|
+
reject(e.target.error);
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Used to clear caches and 'sections-db' IndexedDB when a user logs out or a
|
|
58
|
+
* different user logs in to prevent someone from accessing a different user's
|
|
59
|
+
* caches. Should be able to be used in a non-PWA app.
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
export async function clearSensitiveCaches(dbName = SECTIONS_DB) {
|
|
64
|
+
console.debug('Clearing sensitive caches');
|
|
65
|
+
const cacheKeys = await caches.keys();
|
|
66
|
+
return Promise.all([// (Resolves to 'false' because this can't detect if anything was deleted):
|
|
67
|
+
clearDB(dbName).then(() => false), // Remove caches if not in keepable list
|
|
68
|
+
...cacheKeys.map(key => {
|
|
69
|
+
if (!KEEPABLE_CACHES.some(pattern => pattern.test(key))) {
|
|
70
|
+
return caches.delete(key);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return false;
|
|
74
|
+
})]).then(responses => {
|
|
75
|
+
// Return true if any caches have been cleared
|
|
76
|
+
// (caches.delete() returns true if a cache is deleted successfully)
|
|
77
|
+
// PWA apps can reload to restore their app shell cache
|
|
78
|
+
return responses.some(response => response);
|
|
79
|
+
});
|
|
80
|
+
}
|
package/build/types/index.d.ts
CHANGED
|
@@ -2,3 +2,4 @@ export { OfflineProvider } from './lib/offline-provider';
|
|
|
2
2
|
export { CacheableSection, useCacheableSection } from './lib/cacheable-section';
|
|
3
3
|
export { useCachedSections } from './lib/cacheable-section-state';
|
|
4
4
|
export { useOnlineStatus } from './lib/online-status';
|
|
5
|
+
export { clearSensitiveCaches } from './lib/clear-sensitive-caches';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare const SECTIONS_DB = "sections-db";
|
|
2
|
+
export declare const SECTIONS_STORE = "sections-store";
|
|
3
|
+
declare global {
|
|
4
|
+
interface IDBFactory {
|
|
5
|
+
databases(): Promise<[{
|
|
6
|
+
name: string;
|
|
7
|
+
version: number;
|
|
8
|
+
}]>;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Used to clear caches and 'sections-db' IndexedDB when a user logs out or a
|
|
13
|
+
* different user logs in to prevent someone from accessing a different user's
|
|
14
|
+
* caches. Should be able to be used in a non-PWA app.
|
|
15
|
+
*/
|
|
16
|
+
export declare function clearSensitiveCaches(dbName?: string): Promise<any>;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dhis2/app-service-offline",
|
|
3
3
|
"description": "A runtime service for online/offline detection and offline caching",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.2.0",
|
|
5
5
|
"main": "./build/cjs/index.js",
|
|
6
6
|
"module": "./build/es/index.js",
|
|
7
7
|
"types": "build/types/index.d.ts",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"coverage": "yarn test --coverage"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
|
-
"@dhis2/app-service-alerts": "3.
|
|
36
|
+
"@dhis2/app-service-alerts": "3.2.0",
|
|
37
37
|
"prop-types": "^15.7.2",
|
|
38
38
|
"react": "^16.8.6",
|
|
39
39
|
"react-dom": "^16.8.6"
|