pryv 3.0.3 → 3.1.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/package.json +1 -1
- package/src/Connection.js +57 -3
- package/src/Service.js +376 -6
- package/src/index.d.ts +114 -2
- package/src/index.js +7 -1
- package/src/lib/MfaRequiredError.js +46 -0
- package/src/lib/PryvError.js +38 -4
- package/src/lib/StaleAccessIdError.js +40 -0
- package/src/lib/errorIds.js +67 -0
- package/src/utils.js +50 -0
- package/test/Connection.test.js +26 -0
- package/test/PryvError.test.js +34 -0
- package/test/Service.accessRequest.test.js +78 -0
- package/test/Service.createUser.test.js +104 -0
- package/test/Service.hostings.test.js +53 -0
- package/test/Service.mfa.test.js +94 -0
- package/test/Service.passwordReset.test.js +87 -0
- package/test/Service.userExists.test.js +29 -0
- package/test/Service.userIdForEmail.test.js +30 -0
- package/test/utils.test.js +44 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* [BSD-3-Clause](https://github.com/pryv/lib-js/blob/master/LICENSE)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const PryvError = require('./PryvError');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Thrown by `Service.login` when the platform replied with `{ mfaToken }`
|
|
10
|
+
* instead of `{ token }`. Consumers catch this, prompt the user for the
|
|
11
|
+
* SMS code, then call `Service.mfaVerify(userId, err.mfaToken, code)`.
|
|
12
|
+
*
|
|
13
|
+
* try { conn = await service.login(u, p, app) }
|
|
14
|
+
* catch (err) {
|
|
15
|
+
* if (err instanceof MfaRequiredError) {
|
|
16
|
+
* const code = await prompt()
|
|
17
|
+
* conn = await service.mfaVerify(u, err.mfaToken, code)
|
|
18
|
+
* } else { throw err }
|
|
19
|
+
* }
|
|
20
|
+
*
|
|
21
|
+
* @extends PryvError
|
|
22
|
+
*/
|
|
23
|
+
class MfaRequiredError extends PryvError {
|
|
24
|
+
/**
|
|
25
|
+
* @param {string} mfaToken - The token returned by the API (use with mfa.challenge / mfa.verify)
|
|
26
|
+
* @param {Response} response - The fetch Response object
|
|
27
|
+
* @param {Object} [body] - Parsed JSON body
|
|
28
|
+
*/
|
|
29
|
+
constructor (mfaToken, response, body) {
|
|
30
|
+
const apiErr = body && body.error;
|
|
31
|
+
const message = (apiErr && apiErr.message) || 'MFA required';
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = 'MfaRequiredError';
|
|
34
|
+
/** @type {string} */
|
|
35
|
+
this.mfaToken = mfaToken;
|
|
36
|
+
this.id = (apiErr && apiErr.id) || 'mfa-required';
|
|
37
|
+
this.status = response && response.status;
|
|
38
|
+
this.response = { body, status: response && response.status };
|
|
39
|
+
|
|
40
|
+
if (Error.captureStackTrace) {
|
|
41
|
+
Error.captureStackTrace(this, MfaRequiredError);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = MfaRequiredError;
|
package/src/lib/PryvError.js
CHANGED
|
@@ -5,26 +5,60 @@
|
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Custom error class for Pryv library errors.
|
|
8
|
-
*
|
|
8
|
+
*
|
|
9
|
+
* Two construction patterns are supported (additive — both stay valid):
|
|
10
|
+
*
|
|
11
|
+
* // Legacy: wrap an underlying error or value
|
|
12
|
+
* throw new PryvError('Failed to do X', innerError)
|
|
13
|
+
*
|
|
14
|
+
* // Structured: carry the API error id, HTTP status, and raw response
|
|
15
|
+
* throw PryvError.fromApiResponse(response, body)
|
|
16
|
+
*
|
|
17
|
+
* Structured fields (`id`, `status`, `response`) are `undefined` unless set
|
|
18
|
+
* via the static factory or assigned post-hoc.
|
|
19
|
+
*
|
|
9
20
|
* @extends Error
|
|
10
21
|
*/
|
|
11
22
|
class PryvError extends Error {
|
|
12
23
|
/**
|
|
13
|
-
* Create a PryvError
|
|
14
24
|
* @param {string} message - Error message
|
|
15
|
-
* @param {Error|Object} [innerObject] -
|
|
25
|
+
* @param {Error|Object} [innerObject] - Underlying error or value
|
|
16
26
|
*/
|
|
17
27
|
constructor (message, innerObject) {
|
|
18
28
|
super(message);
|
|
19
29
|
this.name = 'PryvError';
|
|
20
30
|
/** @type {Error|Object|undefined} */
|
|
21
31
|
this.innerObject = innerObject;
|
|
32
|
+
/** @type {string|undefined} Pryv API error id, e.g. `'unknown-user'` */
|
|
33
|
+
this.id = undefined;
|
|
34
|
+
/** @type {number|undefined} HTTP status that produced this error */
|
|
35
|
+
this.status = undefined;
|
|
36
|
+
/** @type {{ body: any, status: number }|undefined} Raw response */
|
|
37
|
+
this.response = undefined;
|
|
22
38
|
|
|
23
|
-
// Maintains proper stack trace for where error was thrown (only in V8)
|
|
24
39
|
if (Error.captureStackTrace) {
|
|
25
40
|
Error.captureStackTrace(this, PryvError);
|
|
26
41
|
}
|
|
27
42
|
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Build a PryvError from a fetch Response and its parsed JSON body.
|
|
46
|
+
* Pulls `id` and `message` from `body.error` when present (Pryv API shape).
|
|
47
|
+
*
|
|
48
|
+
* @param {Response} response - The fetch Response object
|
|
49
|
+
* @param {Object} [body] - Parsed JSON body
|
|
50
|
+
* @returns {PryvError}
|
|
51
|
+
*/
|
|
52
|
+
static fromApiResponse (response, body) {
|
|
53
|
+
const apiErr = body && body.error;
|
|
54
|
+
const message = (apiErr && apiErr.message) ||
|
|
55
|
+
`Pryv API error (HTTP ${response.status})`;
|
|
56
|
+
const err = new PryvError(message);
|
|
57
|
+
err.id = apiErr && apiErr.id;
|
|
58
|
+
err.status = response.status;
|
|
59
|
+
err.response = { body, status: response.status };
|
|
60
|
+
return err;
|
|
61
|
+
}
|
|
28
62
|
}
|
|
29
63
|
|
|
30
64
|
module.exports = PryvError;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* [BSD-3-Clause](https://github.com/pryv/lib-js/blob/master/LICENSE)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const PryvError = require('./PryvError');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Plan 66: typed error surfaced when a Pryv.io server (≥ 2.0.0-pre.X)
|
|
10
|
+
* rejects an `accesses.update` or `accesses.delete` call with a
|
|
11
|
+
* 409 `stale-resource` response.
|
|
12
|
+
*
|
|
13
|
+
* The composite access id `<base>:<serial>` carries the version the
|
|
14
|
+
* caller last observed. If the access has since been updated, the
|
|
15
|
+
* server rejects the call so the caller refetches the current head
|
|
16
|
+
* (`connection.api('accesses.getOne', { id: base })`) and retries
|
|
17
|
+
* with the fresh composite id.
|
|
18
|
+
*
|
|
19
|
+
* Reach for `.data.provided` to see what the caller sent and
|
|
20
|
+
* `.data.currentSerial` to see what the server currently has.
|
|
21
|
+
*
|
|
22
|
+
* @extends PryvError
|
|
23
|
+
*/
|
|
24
|
+
class StaleAccessIdError extends PryvError {
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} message
|
|
27
|
+
* @param {{ provided?: string, currentSerial?: number | null }} data
|
|
28
|
+
*/
|
|
29
|
+
constructor (message, data) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.name = 'StaleAccessIdError';
|
|
32
|
+
/** @type {{ provided?: string, currentSerial?: number | null }} */
|
|
33
|
+
this.data = data || {};
|
|
34
|
+
if (Error.captureStackTrace) {
|
|
35
|
+
Error.captureStackTrace(this, StaleAccessIdError);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = StaleAccessIdError;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* [BSD-3-Clause](https://github.com/pryv/lib-js/blob/master/LICENSE)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Catalogue of Pryv API error ids.
|
|
8
|
+
*
|
|
9
|
+
* Mirrors `open-pryv.io/components/errors/src/ErrorIds.js`. Use these
|
|
10
|
+
* constants instead of hardcoding error-id strings:
|
|
11
|
+
*
|
|
12
|
+
* if (err instanceof PryvError && err.id === pryv.ERRORS.UNKNOWN_USER) { … }
|
|
13
|
+
*
|
|
14
|
+
* Adding a new id here is safe; renaming or removing one is a breaking
|
|
15
|
+
* change for consumers using the constant.
|
|
16
|
+
*/
|
|
17
|
+
const ERRORS = Object.freeze({
|
|
18
|
+
API_UNAVAILABLE: 'api-unavailable',
|
|
19
|
+
CORRUPTED_DATA: 'corrupted-data',
|
|
20
|
+
FORBIDDEN: 'forbidden',
|
|
21
|
+
INVALID_ACCESS_TOKEN: 'invalid-access-token',
|
|
22
|
+
INVALID_CREDENTIALS: 'invalid-credentials',
|
|
23
|
+
UNSUPPORTED_OPERATION: 'unsupported-operation',
|
|
24
|
+
INVALID_EVENT_TYPE: 'invalid-event-type',
|
|
25
|
+
INVALID_ITEM_ID: 'invalid-item-id',
|
|
26
|
+
INVALID_METHOD: 'invalid-method',
|
|
27
|
+
INVALID_OPERATION: 'invalid-operation',
|
|
28
|
+
INVALID_PARAMETERS_FORMAT: 'invalid-parameters-format',
|
|
29
|
+
INVALID_REQUEST_STRUCTURE: 'invalid-request-structure',
|
|
30
|
+
ITEM_ALREADY_EXISTS: 'item-already-exists',
|
|
31
|
+
MISSING_HEADER: 'missing-header',
|
|
32
|
+
UNEXPECTED_ERROR: 'unexpected-error',
|
|
33
|
+
UNKNOWN_REFERENCED_RESOURCE: 'unknown-referenced-resource',
|
|
34
|
+
UNKNOWN_RESOURCE: 'unknown-resource',
|
|
35
|
+
UNSUPPORTED_CONTENT_TYPE: 'unsupported-content-type',
|
|
36
|
+
TOO_MANY_RESULTS: 'too-many-results',
|
|
37
|
+
GONE: 'removed-method',
|
|
38
|
+
UNAVAILABLE_METHOD: 'unavailable-method',
|
|
39
|
+
|
|
40
|
+
// Registration / unique-field validation
|
|
41
|
+
INVALID_INVITATION_TOKEN: 'invitationToken-invalid',
|
|
42
|
+
INVALID_USERNAME: 'username-invalid',
|
|
43
|
+
USERNAME_REQUIRED: 'username-required',
|
|
44
|
+
INVALID_EMAIL: 'email-invalid',
|
|
45
|
+
INVALID_LANGUAGE: 'language-invalid',
|
|
46
|
+
INVALID_APP_ID: 'appid-invalid',
|
|
47
|
+
INVALID_PASSWORD: 'password-invalid',
|
|
48
|
+
INVALID_REFERER: 'referer-invalid',
|
|
49
|
+
EMAIL_REQUIRED: 'email-required',
|
|
50
|
+
PASSWORD_REQUIRED: 'password-required',
|
|
51
|
+
MISSING_REQUIRED_FIELD: 'missing-required-field',
|
|
52
|
+
NEW_PASSWORD_FIELD_IS_REQUIRED: 'newPassword-required',
|
|
53
|
+
|
|
54
|
+
// Account-stream / system-stream protections
|
|
55
|
+
DENIED_STREAM_ACCESS: 'denied-stream-access',
|
|
56
|
+
TOO_HIGH_ACCESS_FOR_SYSTEM_STREAMS: 'too-high-access-for-account-stream',
|
|
57
|
+
FORBIDDEN_MULTIPLE_ACCOUNT_STREAMS: 'forbidden-multiple-account-streams-events',
|
|
58
|
+
FORBIDDEN_ACCOUNT_EVENT_MODIFICATION: 'forbidden-none-editable-account-streams',
|
|
59
|
+
FORBIDDEN_TO_CHANGE_ACCOUNT_STREAM_ID: 'forbidden-change-account-streams-id',
|
|
60
|
+
FORBIDDEN_TO_EDIT_NONEDITABLE_ACCOUNT_FIELDS: 'forbidden-to-edit-noneditable-account-fields',
|
|
61
|
+
|
|
62
|
+
// Pre-auth lookups (returned by `/reg/:email/uid` and similar)
|
|
63
|
+
UNKNOWN_USER: 'unknown-user',
|
|
64
|
+
UNKNOWN_EMAIL: 'unknown-email'
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
module.exports = ERRORS;
|
package/src/utils.js
CHANGED
|
@@ -155,6 +155,56 @@ const utils = module.exports = {
|
|
|
155
155
|
return url.replace(PRYV_REGEXP, '');
|
|
156
156
|
},
|
|
157
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Plan 66 (open-pryv.io ≥ 2.0.0-pre.X): parse a wire-format access
|
|
160
|
+
* reference into `{ base, serial }`. Accepts both bare cuid
|
|
161
|
+
* (`"abc123"` → `{ base: 'abc123', serial: null }`) and composite
|
|
162
|
+
* (`"abc123:3"` → `{ base: 'abc123', serial: 3 }`). Throws on
|
|
163
|
+
* malformed input. Apply this to `access.id`, `access.createdBy`,
|
|
164
|
+
* `access.modifiedBy`, and `streamIds` entries of the form
|
|
165
|
+
* `access-<base>:<serial>` from audit events.
|
|
166
|
+
* @memberof pryv.utils
|
|
167
|
+
* @param {string} ref - Access reference string
|
|
168
|
+
* @returns {{ base: string, serial: number | null }}
|
|
169
|
+
*/
|
|
170
|
+
parseAccessRef: function (ref) {
|
|
171
|
+
if (typeof ref !== 'string' || ref.length === 0) {
|
|
172
|
+
throw new Error('parseAccessRef: expected a non-empty string, got ' + JSON.stringify(ref));
|
|
173
|
+
}
|
|
174
|
+
const colonIdx = ref.indexOf(':');
|
|
175
|
+
if (colonIdx === -1) return { base: ref, serial: null };
|
|
176
|
+
const base = ref.slice(0, colonIdx);
|
|
177
|
+
const tail = ref.slice(colonIdx + 1);
|
|
178
|
+
if (base.length === 0) {
|
|
179
|
+
throw new Error('parseAccessRef: empty base in ' + JSON.stringify(ref));
|
|
180
|
+
}
|
|
181
|
+
const serial = Number(tail);
|
|
182
|
+
if (!Number.isInteger(serial) || serial < 1) {
|
|
183
|
+
throw new Error('parseAccessRef: serial must be a positive integer, got ' + JSON.stringify(tail));
|
|
184
|
+
}
|
|
185
|
+
return { base, serial };
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Plan 66: render an `{ base, serial }` pair back to the wire
|
|
190
|
+
* format. Bare cuid when serial is null/undefined; `<base>:<serial>`
|
|
191
|
+
* otherwise. Mostly used to construct the composite id when calling
|
|
192
|
+
* `connection.api()` for `accesses.update` / `accesses.delete`.
|
|
193
|
+
* @memberof pryv.utils
|
|
194
|
+
* @param {{ base: string, serial?: number | null }} ref
|
|
195
|
+
* @returns {string}
|
|
196
|
+
*/
|
|
197
|
+
serializeAccessRef: function (ref) {
|
|
198
|
+
if (ref == null || typeof ref.base !== 'string' || ref.base.length === 0) {
|
|
199
|
+
throw new Error('serializeAccessRef: ref.base must be a non-empty string');
|
|
200
|
+
}
|
|
201
|
+
if (ref.serial == null) return ref.base;
|
|
202
|
+
if (!Number.isInteger(ref.serial) || ref.serial < 1) {
|
|
203
|
+
throw new Error('serializeAccessRef: serial must be a positive integer, got ' + JSON.stringify(ref.serial));
|
|
204
|
+
}
|
|
205
|
+
return ref.base + ':' + ref.serial;
|
|
206
|
+
},
|
|
207
|
+
|
|
158
208
|
/**
|
|
159
209
|
* Extract query parameters from a URL
|
|
160
210
|
* @memberof pryv.utils
|
package/test/Connection.test.js
CHANGED
|
@@ -499,5 +499,31 @@ describe('[CONX] Connection', () => {
|
|
|
499
499
|
expect(accessInfoUser.token).to.exist;
|
|
500
500
|
expect(newUser.access.token).to.equal(accessInfoUser.token);
|
|
501
501
|
});
|
|
502
|
+
|
|
503
|
+
// Plan 66 — accessInfo caching + forceRefresh.
|
|
504
|
+
|
|
505
|
+
it('[CAIC] accessInfo() memoizes — second call returns the same object reference', async () => {
|
|
506
|
+
const regexAPIandToken = /(.+):\/\/(.+)/gm;
|
|
507
|
+
const res = regexAPIandToken.exec(testData.apiEndpoint);
|
|
508
|
+
const apiEndpointWithToken = res[1] + '://' + newUser.access.token + '@' + res[2];
|
|
509
|
+
const cachingConn = new pryv.Connection(apiEndpointWithToken);
|
|
510
|
+
const first = await cachingConn.accessInfo();
|
|
511
|
+
const second = await cachingConn.accessInfo();
|
|
512
|
+
expect(second).to.equal(first); // same reference — served from cache
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
it('[CAID] accessInfo(true) forces a refresh and replaces the cached object', async () => {
|
|
516
|
+
const regexAPIandToken = /(.+):\/\/(.+)/gm;
|
|
517
|
+
const res = regexAPIandToken.exec(testData.apiEndpoint);
|
|
518
|
+
const apiEndpointWithToken = res[1] + '://' + newUser.access.token + '@' + res[2];
|
|
519
|
+
const cachingConn = new pryv.Connection(apiEndpointWithToken);
|
|
520
|
+
const first = await cachingConn.accessInfo();
|
|
521
|
+
const refreshed = await cachingConn.accessInfo(true);
|
|
522
|
+
expect(refreshed).to.not.equal(first); // distinct object — re-fetched
|
|
523
|
+
expect(refreshed.token).to.equal(first.token); // same content
|
|
524
|
+
// Next non-forced call returns the refreshed copy from cache.
|
|
525
|
+
const cached = await cachingConn.accessInfo();
|
|
526
|
+
expect(cached).to.equal(refreshed);
|
|
527
|
+
});
|
|
502
528
|
});
|
|
503
529
|
});
|
package/test/PryvError.test.js
CHANGED
|
@@ -37,4 +37,38 @@ describe('[PERX] PryvError', function () {
|
|
|
37
37
|
const error = new PryvError('No inner');
|
|
38
38
|
expect(error.innerObject).to.be.undefined;
|
|
39
39
|
});
|
|
40
|
+
|
|
41
|
+
it('[PERF] structured fields default to undefined for legacy constructor', function () {
|
|
42
|
+
const error = new PryvError('Test');
|
|
43
|
+
expect(error.id).to.be.undefined;
|
|
44
|
+
expect(error.status).to.be.undefined;
|
|
45
|
+
expect(error.response).to.be.undefined;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('[PERG] fromApiResponse populates id/status/response from API body', function () {
|
|
49
|
+
const fakeResponse = { status: 404 };
|
|
50
|
+
const body = { error: { id: 'unknown-user', message: 'Unknown user' } };
|
|
51
|
+
const error = PryvError.fromApiResponse(fakeResponse, body);
|
|
52
|
+
expect(error).to.be.instanceOf(PryvError);
|
|
53
|
+
expect(error.id).to.equal('unknown-user');
|
|
54
|
+
expect(error.status).to.equal(404);
|
|
55
|
+
expect(error.response).to.deep.equal({ body, status: 404 });
|
|
56
|
+
expect(error.message).to.equal('Unknown user');
|
|
57
|
+
expect(error.innerObject).to.be.undefined;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('[PERH] fromApiResponse falls back to a generic message when body has no error', function () {
|
|
61
|
+
const fakeResponse = { status: 500 };
|
|
62
|
+
const error = PryvError.fromApiResponse(fakeResponse, { foo: 'bar' });
|
|
63
|
+
expect(error.id).to.be.undefined;
|
|
64
|
+
expect(error.status).to.equal(500);
|
|
65
|
+
expect(error.message).to.match(/HTTP 500/);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('[PERI] fromApiResponse handles null/undefined body without throwing', function () {
|
|
69
|
+
const fakeResponse = { status: 502 };
|
|
70
|
+
const error = PryvError.fromApiResponse(fakeResponse, undefined);
|
|
71
|
+
expect(error.status).to.equal(502);
|
|
72
|
+
expect(error.response).to.deep.equal({ body: undefined, status: 502 });
|
|
73
|
+
});
|
|
40
74
|
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* [BSD-3-Clause](https://github.com/pryv/lib-js/blob/master/LICENSE)
|
|
4
|
+
*/
|
|
5
|
+
/* global describe, it, before, expect, pryv, testData */
|
|
6
|
+
|
|
7
|
+
describe('[ARQX] Service access-request init', function () {
|
|
8
|
+
let service;
|
|
9
|
+
|
|
10
|
+
before(async function () {
|
|
11
|
+
this.timeout(15000);
|
|
12
|
+
await testData.prepare();
|
|
13
|
+
service = new pryv.Service(testData.serviceInfoUrl);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe('[ASTX] Service.startAccessRequest', function () {
|
|
17
|
+
it('[ASTA] rejects when requestingAppId is missing', async function () {
|
|
18
|
+
let caught;
|
|
19
|
+
try { await service.startAccessRequest({}); } catch (e) { caught = e; }
|
|
20
|
+
expect(caught).to.be.instanceOf(pryv.PryvError);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('[ASTB] returns { key, authUrl, poll, pollRateMs }', async function () {
|
|
24
|
+
this.timeout(15000);
|
|
25
|
+
const env = await service.startAccessRequest({
|
|
26
|
+
requestingAppId: 'jslib-test',
|
|
27
|
+
requestedPermissions: [{
|
|
28
|
+
streamId: 'data',
|
|
29
|
+
level: 'read',
|
|
30
|
+
defaultName: 'Test'
|
|
31
|
+
}]
|
|
32
|
+
});
|
|
33
|
+
expect(env.key).to.be.a('string');
|
|
34
|
+
expect(env.authUrl).to.be.a('string');
|
|
35
|
+
expect(env.poll).to.be.a('string');
|
|
36
|
+
expect(env.poll).to.match(/^https?:\/\//);
|
|
37
|
+
expect(env.pollRateMs).to.be.a('number');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('[APRX] Service.pollAccessRequest', function () {
|
|
42
|
+
it('[APRA] rejects when key is missing', async function () {
|
|
43
|
+
let caught;
|
|
44
|
+
try { await service.pollAccessRequest(); } catch (e) { caught = e; }
|
|
45
|
+
expect(caught).to.be.instanceOf(pryv.PryvError);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('[APRB] polling a fresh request returns NEED_SIGNIN', async function () {
|
|
49
|
+
this.timeout(15000);
|
|
50
|
+
const env = await service.startAccessRequest({
|
|
51
|
+
requestingAppId: 'jslib-test',
|
|
52
|
+
requestedPermissions: [{
|
|
53
|
+
streamId: 'data',
|
|
54
|
+
level: 'read',
|
|
55
|
+
defaultName: 'Test'
|
|
56
|
+
}]
|
|
57
|
+
});
|
|
58
|
+
const state = await service.pollAccessRequest(env.poll);
|
|
59
|
+
expect(state).to.exist;
|
|
60
|
+
expect(state.status).to.equal('NEED_SIGNIN');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('[APRC] accepts a bare key (no scheme) and builds the URL', async function () {
|
|
64
|
+
this.timeout(15000);
|
|
65
|
+
const env = await service.startAccessRequest({
|
|
66
|
+
requestingAppId: 'jslib-test',
|
|
67
|
+
requestedPermissions: [{
|
|
68
|
+
streamId: 'data',
|
|
69
|
+
level: 'read',
|
|
70
|
+
defaultName: 'Test'
|
|
71
|
+
}]
|
|
72
|
+
});
|
|
73
|
+
const state = await service.pollAccessRequest(env.key);
|
|
74
|
+
expect(state).to.exist;
|
|
75
|
+
expect(state.status).to.equal('NEED_SIGNIN');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* [BSD-3-Clause](https://github.com/pryv/lib-js/blob/master/LICENSE)
|
|
4
|
+
*/
|
|
5
|
+
/* global describe, it, before, expect, pryv, testData */
|
|
6
|
+
|
|
7
|
+
const { createId: cuid } = require('@paralleldrive/cuid2');
|
|
8
|
+
|
|
9
|
+
describe('[CRUX] Service.createUser', function () {
|
|
10
|
+
let service;
|
|
11
|
+
let hostingKey;
|
|
12
|
+
|
|
13
|
+
before(async function () {
|
|
14
|
+
this.timeout(15000);
|
|
15
|
+
await testData.prepare();
|
|
16
|
+
service = new pryv.Service(testData.serviceInfoUrl);
|
|
17
|
+
const serviceInfo = await service.info();
|
|
18
|
+
// Discover any available hosting key — equivalent to what the deferred
|
|
19
|
+
// 1.4 `flatHostings()` will eventually do.
|
|
20
|
+
const res = await fetch(serviceInfo.register + 'hostings');
|
|
21
|
+
const tree = await res.json();
|
|
22
|
+
hostingKey = findFirstAvailableHostingKey(tree);
|
|
23
|
+
if (!hostingKey) this.skip();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('[CRUA] rejects when required fields are missing', async function () {
|
|
27
|
+
let caught;
|
|
28
|
+
try {
|
|
29
|
+
await service.createUser({ username: 'foo' });
|
|
30
|
+
} catch (e) {
|
|
31
|
+
caught = e;
|
|
32
|
+
}
|
|
33
|
+
expect(caught).to.be.instanceOf(pryv.PryvError);
|
|
34
|
+
expect(caught.message).to.match(/createUser requires/);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('[CRUB] creates a fresh user and returns username + apiEndpoint', async function () {
|
|
38
|
+
this.timeout(20000);
|
|
39
|
+
const username = 'jslibcr' + cuid().slice(0, 8);
|
|
40
|
+
const result = await service.createUser({
|
|
41
|
+
username,
|
|
42
|
+
password: username + 'PASS!1',
|
|
43
|
+
email: username + '@example.com',
|
|
44
|
+
hosting: hostingKey,
|
|
45
|
+
appId: 'jslib-test',
|
|
46
|
+
language: 'en'
|
|
47
|
+
});
|
|
48
|
+
expect(result.username).to.equal(username);
|
|
49
|
+
expect(result.apiEndpoint).to.be.a('string');
|
|
50
|
+
expect(result.apiEndpoint).to.include(username);
|
|
51
|
+
|
|
52
|
+
// Sanity: the new user can be looked up
|
|
53
|
+
const exists = await service.userExists(username);
|
|
54
|
+
expect(exists).to.equal(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('[CRUC] throws PryvError when re-creating an existing user', async function () {
|
|
58
|
+
this.timeout(20000);
|
|
59
|
+
let caught;
|
|
60
|
+
try {
|
|
61
|
+
await service.createUser({
|
|
62
|
+
username: testData.username,
|
|
63
|
+
password: testData.password,
|
|
64
|
+
email: testData.username + '@pryv.io',
|
|
65
|
+
hosting: hostingKey,
|
|
66
|
+
appId: 'jslib-test'
|
|
67
|
+
});
|
|
68
|
+
} catch (e) {
|
|
69
|
+
caught = e;
|
|
70
|
+
}
|
|
71
|
+
expect(caught).to.be.instanceOf(pryv.PryvError);
|
|
72
|
+
expect(caught.status).to.be.gte(400);
|
|
73
|
+
// Server-side validation order varies; we don't pin the exact id.
|
|
74
|
+
expect(caught.response).to.have.property('status');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('[CRUD] hosting: "auto" picks the first available hosting', async function () {
|
|
78
|
+
this.timeout(20000);
|
|
79
|
+
const username = 'jsliba' + cuid().slice(0, 8);
|
|
80
|
+
const result = await service.createUser({
|
|
81
|
+
username,
|
|
82
|
+
password: username + 'PASS!1',
|
|
83
|
+
email: username + '@example.com',
|
|
84
|
+
hosting: 'auto',
|
|
85
|
+
appId: 'jslib-test'
|
|
86
|
+
});
|
|
87
|
+
expect(result.username).to.equal(username);
|
|
88
|
+
expect(result.apiEndpoint).to.be.a('string');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
function findFirstAvailableHostingKey (tree) {
|
|
93
|
+
const regions = (tree && tree.regions) || {};
|
|
94
|
+
for (const region of Object.values(regions)) {
|
|
95
|
+
const zones = region.zones || {};
|
|
96
|
+
for (const zone of Object.values(zones)) {
|
|
97
|
+
const hostings = zone.hostings || {};
|
|
98
|
+
for (const [key, h] of Object.entries(hostings)) {
|
|
99
|
+
if (h && h.available) return key;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* [BSD-3-Clause](https://github.com/pryv/lib-js/blob/master/LICENSE)
|
|
4
|
+
*/
|
|
5
|
+
/* global describe, it, before, expect, pryv, testData */
|
|
6
|
+
|
|
7
|
+
describe('[HSTX] Service.availableHostings + flatHostings', function () {
|
|
8
|
+
let service;
|
|
9
|
+
|
|
10
|
+
before(async function () {
|
|
11
|
+
this.timeout(15000);
|
|
12
|
+
await testData.prepare();
|
|
13
|
+
service = new pryv.Service(testData.serviceInfoUrl);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('[HSTA] availableHostings() returns the raw regions tree', async function () {
|
|
17
|
+
this.timeout(15000);
|
|
18
|
+
const tree = await service.availableHostings();
|
|
19
|
+
expect(tree).to.be.an('object');
|
|
20
|
+
expect(tree.regions).to.be.an('object');
|
|
21
|
+
// At least one region with at least one zone with at least one hosting
|
|
22
|
+
const regions = Object.values(tree.regions);
|
|
23
|
+
expect(regions.length).to.be.gte(1);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('[HSTB] flatHostings() returns a list with key/name/region/zone fields', async function () {
|
|
27
|
+
this.timeout(15000);
|
|
28
|
+
const list = await service.flatHostings();
|
|
29
|
+
expect(list).to.be.an('array');
|
|
30
|
+
expect(list.length).to.be.gte(1);
|
|
31
|
+
for (const item of list) {
|
|
32
|
+
expect(item.key).to.be.a('string');
|
|
33
|
+
expect(item.name).to.be.a('string');
|
|
34
|
+
expect(item.region).to.be.a('string');
|
|
35
|
+
expect(item.zone).to.be.a('string');
|
|
36
|
+
expect(item.availableCore).to.be.a('string');
|
|
37
|
+
expect(item.available).to.be.a('boolean');
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('[HSTC] flatHostings() shape matches availableHostings() leaf nodes', async function () {
|
|
42
|
+
this.timeout(15000);
|
|
43
|
+
const tree = await service.availableHostings();
|
|
44
|
+
const flat = await service.flatHostings();
|
|
45
|
+
let leafCount = 0;
|
|
46
|
+
for (const region of Object.values(tree.regions || {})) {
|
|
47
|
+
for (const zone of Object.values(region.zones || {})) {
|
|
48
|
+
leafCount += Object.keys(zone.hostings || {}).length;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
expect(flat.length).to.equal(leafCount);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* [BSD-3-Clause](https://github.com/pryv/lib-js/blob/master/LICENSE)
|
|
4
|
+
*/
|
|
5
|
+
/* global describe, it, before, expect, pryv, testData */
|
|
6
|
+
|
|
7
|
+
const { createId: cuid } = require('@paralleldrive/cuid2');
|
|
8
|
+
|
|
9
|
+
// Note: testData.username is a non-MFA-protected test user, so we cannot
|
|
10
|
+
// exercise the success paths of mfaChallenge / mfaVerify against pryv.me.
|
|
11
|
+
// What we can verify:
|
|
12
|
+
// - argument validation
|
|
13
|
+
// - bogus mfaToken → PryvError on the wire
|
|
14
|
+
// - MfaRequiredError type + export shape
|
|
15
|
+
// The happy path is exercised end-to-end by open-pryv.io's MFA test suite
|
|
16
|
+
// (which spins up a fake SMS provider); replicating that here would require
|
|
17
|
+
// a mock layer the project doesn't ship.
|
|
18
|
+
|
|
19
|
+
describe('[MFLX] Service MFA', function () {
|
|
20
|
+
let service;
|
|
21
|
+
|
|
22
|
+
before(async function () {
|
|
23
|
+
this.timeout(15000);
|
|
24
|
+
await testData.prepare();
|
|
25
|
+
service = new pryv.Service(testData.serviceInfoUrl);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('[MCHX] Service.mfaChallenge', function () {
|
|
29
|
+
it('[MCHA] rejects when args are missing', async function () {
|
|
30
|
+
let caught;
|
|
31
|
+
try { await service.mfaChallenge(testData.username); } catch (e) { caught = e; }
|
|
32
|
+
expect(caught).to.be.instanceOf(pryv.PryvError);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('[MCHB] throws PryvError on bogus mfaToken', async function () {
|
|
36
|
+
this.timeout(15000);
|
|
37
|
+
let caught;
|
|
38
|
+
try {
|
|
39
|
+
await service.mfaChallenge(testData.username, 'bogus-' + cuid().slice(0, 8));
|
|
40
|
+
} catch (e) { caught = e; }
|
|
41
|
+
expect(caught).to.be.instanceOf(pryv.PryvError);
|
|
42
|
+
expect(caught.status).to.be.gte(400);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('[MVRX] Service.mfaVerify', function () {
|
|
47
|
+
it('[MVRA] rejects when args are missing', async function () {
|
|
48
|
+
let caught;
|
|
49
|
+
try { await service.mfaVerify(testData.username, 'token'); } catch (e) { caught = e; }
|
|
50
|
+
expect(caught).to.be.instanceOf(pryv.PryvError);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('[MVRB] throws PryvError on bogus mfaToken', async function () {
|
|
54
|
+
this.timeout(15000);
|
|
55
|
+
let caught;
|
|
56
|
+
try {
|
|
57
|
+
await service.mfaVerify(
|
|
58
|
+
testData.username,
|
|
59
|
+
'bogus-' + cuid().slice(0, 8),
|
|
60
|
+
'123456'
|
|
61
|
+
);
|
|
62
|
+
} catch (e) { caught = e; }
|
|
63
|
+
expect(caught).to.be.instanceOf(pryv.PryvError);
|
|
64
|
+
expect(caught.status).to.be.gte(400);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('[MERX] MfaRequiredError', function () {
|
|
69
|
+
it('[MERA] is exported on the package root and extends PryvError', function () {
|
|
70
|
+
expect(pryv.MfaRequiredError).to.be.a('function');
|
|
71
|
+
const err = new pryv.MfaRequiredError(
|
|
72
|
+
'tok-abc',
|
|
73
|
+
{ status: 200 },
|
|
74
|
+
{ mfaToken: 'tok-abc' }
|
|
75
|
+
);
|
|
76
|
+
expect(err).to.be.instanceOf(pryv.MfaRequiredError);
|
|
77
|
+
expect(err).to.be.instanceOf(pryv.PryvError);
|
|
78
|
+
expect(err.mfaToken).to.equal('tok-abc');
|
|
79
|
+
expect(err.id).to.equal('mfa-required');
|
|
80
|
+
expect(err.status).to.equal(200);
|
|
81
|
+
expect(err.name).to.equal('MfaRequiredError');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('[MERB] picks up id/message from API error body when provided', function () {
|
|
85
|
+
const err = new pryv.MfaRequiredError(
|
|
86
|
+
'tok-xyz',
|
|
87
|
+
{ status: 401 },
|
|
88
|
+
{ error: { id: 'custom-id', message: 'custom msg' } }
|
|
89
|
+
);
|
|
90
|
+
expect(err.id).to.equal('custom-id');
|
|
91
|
+
expect(err.message).to.equal('custom msg');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
});
|