cozy-pouch-link 48.25.0 → 49.0.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.
Files changed (41) hide show
  1. package/dist/CozyPouchLink.js +593 -237
  2. package/dist/CozyPouchLink.spec.js +67 -42
  3. package/dist/PouchManager.js +317 -254
  4. package/dist/PouchManager.spec.js +91 -58
  5. package/dist/helpers.js +79 -0
  6. package/dist/helpers.spec.js +85 -1
  7. package/dist/jsonapi.js +54 -7
  8. package/dist/jsonapi.spec.js +57 -14
  9. package/dist/localStorage.js +646 -207
  10. package/dist/localStorage.spec.js +48 -0
  11. package/dist/mango.js +72 -20
  12. package/dist/mango.spec.js +1 -1
  13. package/dist/migrations/adapter.js +1 -1
  14. package/dist/platformWeb.js +120 -0
  15. package/dist/remote.js +39 -5
  16. package/dist/remote.spec.js +214 -0
  17. package/dist/replicateOnce.js +337 -0
  18. package/dist/startReplication.js +70 -45
  19. package/dist/startReplication.spec.js +374 -39
  20. package/dist/types.js +80 -0
  21. package/dist/utils.js +11 -2
  22. package/package.json +9 -5
  23. package/types/AccessToken.d.ts +16 -0
  24. package/types/CozyPouchLink.d.ts +228 -0
  25. package/types/PouchManager.d.ts +86 -0
  26. package/types/__tests__/fixtures.d.ts +48 -0
  27. package/types/__tests__/mocks.d.ts +4 -0
  28. package/types/helpers.d.ts +17 -0
  29. package/types/index.d.ts +1 -0
  30. package/types/jsonapi.d.ts +19 -0
  31. package/types/localStorage.d.ts +124 -0
  32. package/types/logger.d.ts +2 -0
  33. package/types/loop.d.ts +60 -0
  34. package/types/mango.d.ts +3 -0
  35. package/types/migrations/adapter.d.ts +18 -0
  36. package/types/platformWeb.d.ts +17 -0
  37. package/types/remote.d.ts +6 -0
  38. package/types/replicateOnce.d.ts +29 -0
  39. package/types/startReplication.d.ts +12 -0
  40. package/types/types.d.ts +104 -0
  41. package/types/utils.d.ts +3 -0
@@ -0,0 +1,48 @@
1
+ import { PouchLocalStorage } from './localStorage'
2
+
3
+ describe('LocalStorage', () => {
4
+ describe('Type assertion', () => {
5
+ it('should throw if setItem method is missing', () => {
6
+ expect(() => {
7
+ new PouchLocalStorage({
8
+ getItem: jest.fn(),
9
+ removeItem: jest.fn()
10
+ })
11
+ }).toThrow(
12
+ 'Provided storageEngine is missing the following methods: setItem'
13
+ )
14
+ })
15
+
16
+ it('should throw if getItem method is missing', () => {
17
+ expect(() => {
18
+ new PouchLocalStorage({
19
+ setItem: jest.fn(),
20
+ removeItem: jest.fn()
21
+ })
22
+ }).toThrow(
23
+ 'Provided storageEngine is missing the following methods: getItem'
24
+ )
25
+ })
26
+
27
+ it('should throw if removeItem method is missing', () => {
28
+ expect(() => {
29
+ new PouchLocalStorage({
30
+ getItem: jest.fn(),
31
+ setItem: jest.fn()
32
+ })
33
+ }).toThrow(
34
+ 'Provided storageEngine is missing the following methods: removeItem'
35
+ )
36
+ })
37
+
38
+ it('should throw if multiple methods are missing', () => {
39
+ expect(() => {
40
+ new PouchLocalStorage({
41
+ getItem: jest.fn()
42
+ })
43
+ }).toThrow(
44
+ 'Provided storageEngine is missing the following methods: setItem, removeItem'
45
+ )
46
+ })
47
+ })
48
+ })
package/dist/mango.js CHANGED
@@ -5,28 +5,74 @@ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefau
5
5
  Object.defineProperty(exports, "__esModule", {
6
6
  value: true
7
7
  });
8
- exports.getIndexFields = exports.getIndexNameFromFields = void 0;
8
+ exports.getIndexFields = exports.getIndexNameFromFields = exports.makeKeyFromPartialFilter = void 0;
9
9
 
10
10
  var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
11
11
 
12
- var _flatten = _interopRequireDefault(require("lodash/flatten"));
12
+ var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
13
13
 
14
- var _isObject = _interopRequireDefault(require("lodash/isObject"));
14
+ var _head = _interopRequireDefault(require("lodash/head"));
15
15
 
16
- var getIndexNameFromFields = function getIndexNameFromFields(fields) {
17
- return "by_".concat(fields.join('_and_'));
16
+ /**
17
+ * Process a partial filter to generate a string key
18
+ *
19
+ * /!\ Warning: this method is similar to cozy-stack-client mangoIndex.makeKeyFromPartialFilter()
20
+ * If you edit this method, please check if the change is also needed in mangoIndex file
21
+ *
22
+ * @param {object} condition - An object representing the partial filter or a sub-condition of the partial filter
23
+ * @returns {string} - The string key of the processed partial filter
24
+ */
25
+ var makeKeyFromPartialFilter = function makeKeyFromPartialFilter(condition) {
26
+ if (typeof condition !== 'object' || condition === null) {
27
+ return String(condition);
28
+ }
29
+
30
+ var conditions = Object.entries(condition).map(function (_ref) {
31
+ var _ref2 = (0, _slicedToArray2.default)(_ref, 2),
32
+ key = _ref2[0],
33
+ value = _ref2[1];
34
+
35
+ if (Array.isArray(value) && value.every(function (subObj) {
36
+ return typeof subObj === 'string';
37
+ })) {
38
+ return "".concat(key, "_(").concat(value.join('_'), ")");
39
+ } else if (Array.isArray(value)) {
40
+ return "(".concat(value.map(function (subCondition) {
41
+ return "".concat(makeKeyFromPartialFilter(subCondition));
42
+ }).join(")_".concat(key, "_(")), ")");
43
+ } else if (typeof value === 'object') {
44
+ return "".concat(key, "_").concat(makeKeyFromPartialFilter(value));
45
+ } else {
46
+ return "".concat(key, "_").concat(value);
47
+ }
48
+ });
49
+ return conditions.join(')_and_(');
18
50
  };
51
+ /**
52
+ * Name an index, based on its indexed fields and partial filter.
53
+ *
54
+ * It follows this naming convention:
55
+ * `by_{indexed_field1}_and_{indexed_field2}_filter_({partial_filter.key1}_{partial_filter.value1})_and_({partial_filter.key2}_{partial_filter.value2})`
56
+ *
57
+ * /!\ Warning: this method is similar to cozy-stack-client mangoIndex.getIndexNameFromFields()
58
+ * If you edit this method, please check if the change is also needed in mangoIndex file
59
+ *
60
+ * @param {Array<string>} fields - The indexed fields
61
+ * @param {object} [partialFilter] - The partial filter
62
+ * @returns {string} The index name, built from the fields
63
+ */
19
64
 
20
- exports.getIndexNameFromFields = getIndexNameFromFields;
21
65
 
22
- var getSortKeys = function getSortKeys(sort) {
23
- if (Array.isArray(sort)) {
24
- return (0, _flatten.default)(sort.map(function (x) {
25
- return Object.keys(x);
26
- }));
27
- } else if ((0, _isObject.default)(sort)) {
28
- return Object.keys(sort);
66
+ exports.makeKeyFromPartialFilter = makeKeyFromPartialFilter;
67
+
68
+ var getIndexNameFromFields = function getIndexNameFromFields(fields, partialFilter) {
69
+ var indexName = "by_".concat(fields.join('_and_'));
70
+
71
+ if (partialFilter) {
72
+ return "".concat(indexName, "_filter_(").concat(makeKeyFromPartialFilter(partialFilter), ")");
29
73
  }
74
+
75
+ return indexName;
30
76
  };
31
77
  /**
32
78
  * @function
@@ -34,23 +80,29 @@ var getSortKeys = function getSortKeys(sort) {
34
80
  * query to work
35
81
  *
36
82
  * @private
37
- * @param {object} options - Mango query options
83
+ * @param {import('./types').MangoQueryOptions} options - Mango query options
38
84
  * @returns {Array} - Fields to index
39
85
  */
40
86
 
41
87
 
88
+ exports.getIndexNameFromFields = getIndexNameFromFields;
42
89
  var defaultSelector = {
43
90
  _id: {
44
91
  $gt: null
45
92
  }
46
93
  };
47
94
 
48
- var getIndexFields = function getIndexFields(_ref) {
49
- var _ref$selector = _ref.selector,
50
- selector = _ref$selector === void 0 ? defaultSelector : _ref$selector,
51
- _ref$sort = _ref.sort,
52
- sort = _ref$sort === void 0 ? {} : _ref$sort;
53
- return Array.from(new Set([].concat((0, _toConsumableArray2.default)(Object.keys(selector)), (0, _toConsumableArray2.default)(getSortKeys(sort)))));
95
+ var getIndexFields = function getIndexFields(
96
+ /** @type {import('./types').MangoQueryOptions} */
97
+ _ref3) {
98
+ var _ref3$selector = _ref3.selector,
99
+ selector = _ref3$selector === void 0 ? defaultSelector : _ref3$selector,
100
+ _ref3$sort = _ref3.sort,
101
+ sort = _ref3$sort === void 0 ? [] : _ref3$sort,
102
+ partialFilter = _ref3.partialFilter;
103
+ return Array.from(new Set([].concat((0, _toConsumableArray2.default)(sort.map(function (sortOption) {
104
+ return (0, _head.default)(Object.keys(sortOption));
105
+ })), (0, _toConsumableArray2.default)(selector ? Object.keys(selector) : []), (0, _toConsumableArray2.default)(partialFilter ? Object.keys(partialFilter) : []))));
54
106
  };
55
107
 
56
108
  exports.getIndexFields = getIndexFields;
@@ -6,6 +6,6 @@ describe('mango utils', () => {
6
6
  it('should be able to get the fields from the selector', () => {
7
7
  const query = Q('io.cozy.rockets').sortBy([{ label: true }, { _id: true }])
8
8
  const fields = getIndexFields(query)
9
- expect(fields).toEqual(['_id', 'label'])
9
+ expect(fields).toEqual(['label', '_id'])
10
10
  })
11
11
  })
@@ -25,7 +25,7 @@ var getNewIndexedDBDatabaseName = function getNewIndexedDBDatabaseName(dbName) {
25
25
  * @property {string} [toAdapter] - The new adapter type, e.g. 'indexeddb'
26
26
  *
27
27
  * @param {MigrationParams} params - The migration params
28
- * @returns {object} - The migrated pouch
28
+ * @returns {Promise<object>} - The migrated pouch
29
29
  */
30
30
 
31
31
 
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+
5
+ Object.defineProperty(exports, "__esModule", {
6
+ value: true
7
+ });
8
+ exports.platformWeb = void 0;
9
+
10
+ var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
11
+
12
+ var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
13
+
14
+ var _pouchdbBrowser = _interopRequireDefault(require("pouchdb-browser"));
15
+
16
+ var events = {
17
+ addEventListener: function addEventListener(eventName, handler) {
18
+ document.addEventListener(eventName, handler);
19
+ },
20
+ removeEventListener: function removeEventListener(eventName, handler) {
21
+ document.removeEventListener(eventName, handler);
22
+ }
23
+ };
24
+ var storage = {
25
+ getItem: function () {
26
+ var _getItem = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee(key) {
27
+ return _regenerator.default.wrap(function _callee$(_context) {
28
+ while (1) {
29
+ switch (_context.prev = _context.next) {
30
+ case 0:
31
+ return _context.abrupt("return", window.localStorage.getItem(key));
32
+
33
+ case 1:
34
+ case "end":
35
+ return _context.stop();
36
+ }
37
+ }
38
+ }, _callee);
39
+ }));
40
+
41
+ function getItem(_x) {
42
+ return _getItem.apply(this, arguments);
43
+ }
44
+
45
+ return getItem;
46
+ }(),
47
+ setItem: function () {
48
+ var _setItem = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee2(key, value) {
49
+ return _regenerator.default.wrap(function _callee2$(_context2) {
50
+ while (1) {
51
+ switch (_context2.prev = _context2.next) {
52
+ case 0:
53
+ return _context2.abrupt("return", window.localStorage.setItem(key, value));
54
+
55
+ case 1:
56
+ case "end":
57
+ return _context2.stop();
58
+ }
59
+ }
60
+ }, _callee2);
61
+ }));
62
+
63
+ function setItem(_x2, _x3) {
64
+ return _setItem.apply(this, arguments);
65
+ }
66
+
67
+ return setItem;
68
+ }(),
69
+ removeItem: function () {
70
+ var _removeItem = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee3(key) {
71
+ return _regenerator.default.wrap(function _callee3$(_context3) {
72
+ while (1) {
73
+ switch (_context3.prev = _context3.next) {
74
+ case 0:
75
+ return _context3.abrupt("return", window.localStorage.removeItem(key));
76
+
77
+ case 1:
78
+ case "end":
79
+ return _context3.stop();
80
+ }
81
+ }
82
+ }, _callee3);
83
+ }));
84
+
85
+ function removeItem(_x4) {
86
+ return _removeItem.apply(this, arguments);
87
+ }
88
+
89
+ return removeItem;
90
+ }()
91
+ };
92
+
93
+ var isOnline = /*#__PURE__*/function () {
94
+ var _ref = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee4() {
95
+ return _regenerator.default.wrap(function _callee4$(_context4) {
96
+ while (1) {
97
+ switch (_context4.prev = _context4.next) {
98
+ case 0:
99
+ return _context4.abrupt("return", window.navigator.onLine);
100
+
101
+ case 1:
102
+ case "end":
103
+ return _context4.stop();
104
+ }
105
+ }
106
+ }, _callee4);
107
+ }));
108
+
109
+ return function isOnline() {
110
+ return _ref.apply(this, arguments);
111
+ };
112
+ }();
113
+
114
+ var platformWeb = {
115
+ storage: storage,
116
+ events: events,
117
+ pouchAdapter: _pouchdbBrowser.default,
118
+ isOnline: isOnline
119
+ };
120
+ exports.platformWeb = platformWeb;
package/dist/remote.js CHANGED
@@ -5,7 +5,7 @@ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefau
5
5
  Object.defineProperty(exports, "__esModule", {
6
6
  value: true
7
7
  });
8
- exports.fetchRemoteLastSequence = exports.fetchRemoteInstance = void 0;
8
+ exports.fetchRemoteLastSequence = exports.fetchRemoteInstance = exports.isDatabaseUnradableError = exports.isDatabaseNotFoundError = exports.DATABASE_RESERVED_DOCTYPE_ERROR = exports.DATABASE_NOT_FOUND_ERROR = void 0;
9
9
 
10
10
  var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
11
11
 
@@ -13,13 +13,31 @@ var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/
13
13
 
14
14
  var _AccessToken = _interopRequireDefault(require("./AccessToken"));
15
15
 
16
+ var DATABASE_NOT_FOUND_ERROR = 'Database does not exist';
17
+ exports.DATABASE_NOT_FOUND_ERROR = DATABASE_NOT_FOUND_ERROR;
18
+ var DATABASE_RESERVED_DOCTYPE_ERROR = 'Reserved doctype';
19
+ exports.DATABASE_RESERVED_DOCTYPE_ERROR = DATABASE_RESERVED_DOCTYPE_ERROR;
20
+
21
+ var isDatabaseNotFoundError = function isDatabaseNotFoundError(error) {
22
+ return error.message === DATABASE_NOT_FOUND_ERROR;
23
+ };
24
+
25
+ exports.isDatabaseNotFoundError = isDatabaseNotFoundError;
26
+
27
+ var isDatabaseUnradableError = function isDatabaseUnradableError(error) {
28
+ return error.message === DATABASE_RESERVED_DOCTYPE_ERROR;
29
+ };
16
30
  /**
17
31
  * Fetch remote instance
18
32
  *
19
33
  * @param {URL} url - The remote instance URL, including the credentials
20
34
  * @param {object} params - The params to query the remote instance
21
- * @returns {object} The instance response
35
+ * @returns {Promise<object>} The instance response
22
36
  */
37
+
38
+
39
+ exports.isDatabaseUnradableError = isDatabaseUnradableError;
40
+
23
41
  var fetchRemoteInstance = /*#__PURE__*/function () {
24
42
  var _ref = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee(url) {
25
43
  var params,
@@ -66,9 +84,25 @@ var fetchRemoteInstance = /*#__PURE__*/function () {
66
84
  return _context.abrupt("return", data);
67
85
 
68
86
  case 16:
69
- return _context.abrupt("return", null);
87
+ if (!(resp.status === 404)) {
88
+ _context.next = 18;
89
+ break;
90
+ }
91
+
92
+ throw new Error(DATABASE_NOT_FOUND_ERROR);
93
+
94
+ case 18:
95
+ if (!(resp.status === 403 && data.error.includes('message=reserved doctype'))) {
96
+ _context.next = 20;
97
+ break;
98
+ }
99
+
100
+ throw new Error(DATABASE_RESERVED_DOCTYPE_ERROR);
101
+
102
+ case 20:
103
+ throw new Error("Error (".concat(resp.status, ") while fetching remote instance: ").concat(JSON.stringify(data)));
70
104
 
71
- case 17:
105
+ case 21:
72
106
  case "end":
73
107
  return _context.stop();
74
108
  }
@@ -84,7 +118,7 @@ var fetchRemoteInstance = /*#__PURE__*/function () {
84
118
  * Fetch last sequence from remote instance
85
119
  *
86
120
  * @param {string} baseUrl - The base URL of the remote instance
87
- * @returns {string} The last sequence
121
+ * @returns {Promise<string>} The last sequence
88
122
  */
89
123
 
90
124
 
@@ -0,0 +1,214 @@
1
+ import { enableFetchMocks, disableFetchMocks } from 'jest-fetch-mock'
2
+
3
+ import {
4
+ DATABASE_NOT_FOUND_ERROR,
5
+ DATABASE_RESERVED_DOCTYPE_ERROR,
6
+ fetchRemoteInstance,
7
+ fetchRemoteLastSequence
8
+ } from './remote'
9
+
10
+ describe('remote', () => {
11
+ beforeAll(() => {
12
+ enableFetchMocks()
13
+ })
14
+
15
+ beforeEach(() => {
16
+ fetch.resetMocks()
17
+ })
18
+
19
+ afterAll(() => {
20
+ disableFetchMocks()
21
+ })
22
+
23
+ describe('fetchRemoteInstance', () => {
24
+ it(`Should add Authorization header based on URL's password`, async () => {
25
+ const remoteUrl =
26
+ 'https://user:SOME_TOKEN@claude.mycozy.cloud/data/io.cozy.accounts/_changes'
27
+
28
+ mockDatabaseOn(
29
+ 'https://claude.mycozy.cloud/data/io.cozy.accounts/_changes'
30
+ )
31
+
32
+ await fetchRemoteInstance(new URL(remoteUrl))
33
+
34
+ const expectedHeaders = new Headers()
35
+ expectedHeaders.append('Accept', 'application/json')
36
+ expectedHeaders.append('Content-Type', 'application/json')
37
+ expectedHeaders.append('Authorization', 'Bearer SOME_TOKEN')
38
+
39
+ expect(fetch).toHaveBeenCalledWith(
40
+ 'https://claude.mycozy.cloud/data/io.cozy.accounts/_changes',
41
+ {
42
+ headers: expectedHeaders
43
+ }
44
+ )
45
+ })
46
+
47
+ it('Should return data when found', async () => {
48
+ const remoteUrl =
49
+ 'https://user:SOME_TOKEN@claude.mycozy.cloud/data/io.cozy.accounts/_changes?limit=1&descending=true'
50
+ mockDatabaseOn(
51
+ 'https://claude.mycozy.cloud/data/io.cozy.accounts/_changes?limit=1&descending=true'
52
+ )
53
+
54
+ const result = await fetchRemoteInstance(new URL(remoteUrl))
55
+
56
+ expect(result).toStrictEqual({
57
+ last_seq: '97-SOME_SEQ_VALUE',
58
+ pending: -1,
59
+ results: [
60
+ {
61
+ id: 'SOME_ID',
62
+ seq: '97-SOME_SEQ_VALUE',
63
+ doc: null,
64
+ changes: [{ rev: '3-SOME_REV' }]
65
+ }
66
+ ]
67
+ })
68
+ })
69
+
70
+ it('Should add parameters when given', async () => {
71
+ const remoteUrl =
72
+ 'https://user:SOME_TOKEN@claude.mycozy.cloud/data/io.cozy.accounts/_changes'
73
+
74
+ mockDatabaseOn(
75
+ 'https://claude.mycozy.cloud/data/io.cozy.accounts/_changes?limit=1&descending=true'
76
+ )
77
+
78
+ await fetchRemoteInstance(new URL(remoteUrl), {
79
+ limit: 1,
80
+ descending: true
81
+ })
82
+
83
+ const expectedHeaders = new Headers()
84
+ expectedHeaders.append('Accept', 'application/json')
85
+ expectedHeaders.append('Content-Type', 'application/json')
86
+ expectedHeaders.append('Authorization', 'Bearer SOME_TOKEN')
87
+
88
+ expect(fetch).toHaveBeenCalledWith(
89
+ 'https://claude.mycozy.cloud/data/io.cozy.accounts/_changes?limit=1&descending=true',
90
+ expect.anything()
91
+ )
92
+ })
93
+
94
+ it('Should throw when 404 error', async () => {
95
+ const remoteUrl =
96
+ 'https://user:SOME_TOKEN@claude.mycozy.cloud/data/io.cozy.accounts/_changes?limit=1&descending=true'
97
+
98
+ mockDatabaseNotFoundOn(
99
+ 'https://claude.mycozy.cloud/data/io.cozy.accounts/_changes?limit=1&descending=true'
100
+ )
101
+
102
+ await expect(fetchRemoteInstance(new URL(remoteUrl))).rejects.toThrow(
103
+ DATABASE_NOT_FOUND_ERROR
104
+ )
105
+ })
106
+ })
107
+
108
+ describe('fetchRemoteLastSequence', () => {
109
+ it('Should return data when found', async () => {
110
+ const remoteUrl =
111
+ 'https://user:SOME_TOKEN@claude.mycozy.cloud/data/io.cozy.accounts'
112
+ mockDatabaseOn(
113
+ 'https://claude.mycozy.cloud/data/io.cozy.accounts/_changes?limit=1&descending=true'
114
+ )
115
+
116
+ const result = await fetchRemoteLastSequence(remoteUrl)
117
+
118
+ expect(result).toBe('97-SOME_SEQ_VALUE')
119
+ })
120
+
121
+ it('Shoud throw when HTTP error', async () => {
122
+ const remoteUrl =
123
+ 'https://user:SOME_TOKEN@claude.mycozy.cloud/data/io.cozy.accounts'
124
+ mockUnknownErrorOn(
125
+ 'https://claude.mycozy.cloud/data/io.cozy.accounts/_changes?limit=1&descending=true'
126
+ )
127
+
128
+ await expect(fetchRemoteLastSequence(remoteUrl)).rejects.toThrow(
129
+ 'Error (503) while fetching remote instance: {"error":"code=503, message=SOME UNKNOWN ERROR"}'
130
+ )
131
+ })
132
+
133
+ it('Shoud throw dedicated error when 404 error', async () => {
134
+ const remoteUrl =
135
+ 'https://user:SOME_TOKEN@claude.mycozy.cloud/data/io.cozy.accounts'
136
+ mockDatabaseNotFoundOn(
137
+ 'https://claude.mycozy.cloud/data/io.cozy.accounts/_changes?limit=1&descending=true'
138
+ )
139
+
140
+ await expect(fetchRemoteLastSequence(remoteUrl)).rejects.toThrow(
141
+ DATABASE_NOT_FOUND_ERROR
142
+ )
143
+ })
144
+
145
+ it('Shoud throw dedicated error when Reserved Doctype error', async () => {
146
+ const remoteUrl =
147
+ 'https://user:SOME_TOKEN@claude.mycozy.cloud/data/io.cozy.accounts'
148
+ mockDatabaseReservedDoctypeOn(
149
+ 'https://claude.mycozy.cloud/data/io.cozy.accounts/_changes?limit=1&descending=true'
150
+ )
151
+
152
+ await expect(fetchRemoteLastSequence(remoteUrl)).rejects.toThrow(
153
+ DATABASE_RESERVED_DOCTYPE_ERROR
154
+ )
155
+ })
156
+ })
157
+ })
158
+
159
+ const mockDatabaseNotFoundOn = url => {
160
+ fetch.mockOnceIf(url, JSON.stringify({}), {
161
+ error: 'not_found',
162
+ ok: false,
163
+ reason: 'Database does not exist.',
164
+ status: 404
165
+ })
166
+ }
167
+
168
+ const mockDatabaseReservedDoctypeOn = url => {
169
+ fetch.mockOnceIf(
170
+ url,
171
+ JSON.stringify({
172
+ error: 'code=403, message=reserved doctype io.cozy.sharings unreadable'
173
+ }),
174
+ {
175
+ ok: false,
176
+ status: 403
177
+ }
178
+ )
179
+ }
180
+
181
+ const mockUnknownErrorOn = url => {
182
+ fetch.mockOnceIf(
183
+ url,
184
+ JSON.stringify({
185
+ error: 'code=503, message=SOME UNKNOWN ERROR'
186
+ }),
187
+ {
188
+ ok: false,
189
+ status: 503
190
+ }
191
+ )
192
+ }
193
+
194
+ const mockDatabaseOn = url => {
195
+ fetch.mockOnceIf(
196
+ url,
197
+ JSON.stringify({
198
+ last_seq: '97-SOME_SEQ_VALUE',
199
+ pending: -1,
200
+ results: [
201
+ {
202
+ id: 'SOME_ID',
203
+ seq: '97-SOME_SEQ_VALUE',
204
+ doc: null,
205
+ changes: [{ rev: '3-SOME_REV' }]
206
+ }
207
+ ]
208
+ }),
209
+ {
210
+ ok: true,
211
+ status: 200
212
+ }
213
+ )
214
+ }