pryv 3.0.1 → 3.0.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pryv",
3
- "version": "3.0.1",
3
+ "version": "3.0.2",
4
4
  "description": "Pryv JavaScript library",
5
5
  "keywords": [
6
6
  "Pryv",
@@ -59,7 +59,7 @@ async function getEventStreamed (conn, queryParam, parser) {
59
59
  }
60
60
 
61
61
  if (errResult) {
62
- throw new Error(errResult);
62
+ throw new Error(errResult?.message + ' ' + errResult?.rawResponse);
63
63
  }
64
64
 
65
65
  // We're done!
@@ -0,0 +1,126 @@
1
+ /**
2
+ * @license
3
+ * [BSD-3-Clause](https://github.com/pryv/lib-js/blob/master/LICENSE)
4
+ */
5
+ /* global describe, it, before, after, expect, pryv, testData */
6
+
7
+ const { createId: cuid } = require('@paralleldrive/cuid2');
8
+
9
+ let conn = null;
10
+ const testStreamId = 'acc-' + cuid().slice(0, 8);
11
+ let createdAccessId = null;
12
+ let createdAccessToken = null;
13
+
14
+ describe('[ACSX] Accesses', () => {
15
+ before(async function () {
16
+ this.timeout(15000);
17
+ await testData.prepare();
18
+ conn = new pryv.Connection(testData.apiEndpointWithToken);
19
+
20
+ // Create a test stream for permissions
21
+ await conn.api([{
22
+ method: 'streams.create',
23
+ params: { id: testStreamId, name: 'Access Test Stream' }
24
+ }]);
25
+ });
26
+
27
+ describe('[ACRX] accesses.create', function () {
28
+ it('[ACRA] create a shared access', async () => {
29
+ const res = await conn.api([{
30
+ method: 'accesses.create',
31
+ params: {
32
+ name: 'test-shared-' + cuid().slice(0, 8),
33
+ type: 'shared',
34
+ permissions: [{ streamId: testStreamId, level: 'read' }]
35
+ }
36
+ }]);
37
+ expect(res[0]).to.exist;
38
+ expect(res[0].access).to.exist;
39
+ expect(res[0].access.token).to.exist;
40
+ expect(res[0].access.type).to.equal('shared');
41
+ createdAccessId = res[0].access.id;
42
+ createdAccessToken = res[0].access.token;
43
+ });
44
+
45
+ it('[ACRB] create an app access', async () => {
46
+ const res = await conn.api([{
47
+ method: 'accesses.create',
48
+ params: {
49
+ name: 'test-app-' + cuid().slice(0, 8),
50
+ type: 'app',
51
+ permissions: [{ streamId: testStreamId, level: 'contribute' }]
52
+ }
53
+ }]);
54
+ expect(res[0]).to.exist;
55
+ expect(res[0].access).to.exist;
56
+ expect(res[0].access.type).to.equal('app');
57
+ });
58
+
59
+ it('[ACRC] reject access with invalid permission level', async () => {
60
+ const res = await conn.api([{
61
+ method: 'accesses.create',
62
+ params: {
63
+ name: 'bad-access',
64
+ type: 'shared',
65
+ permissions: [{ streamId: testStreamId, level: 'bogus' }]
66
+ }
67
+ }]);
68
+ expect(res[0]).to.exist;
69
+ expect(res[0].error).to.exist;
70
+ });
71
+ });
72
+
73
+ describe('[AGTX] accesses.get', function () {
74
+ it('[AGTA] list accesses', async () => {
75
+ const res = await conn.api([{
76
+ method: 'accesses.get',
77
+ params: {}
78
+ }]);
79
+ expect(res[0]).to.exist;
80
+ expect(res[0].accesses).to.exist;
81
+ expect(Array.isArray(res[0].accesses)).to.equal(true);
82
+ expect(res[0].accesses.length).to.be.gt(0);
83
+ });
84
+
85
+ it('[AGTB] created access is in the list', async () => {
86
+ const res = await conn.api([{
87
+ method: 'accesses.get',
88
+ params: {}
89
+ }]);
90
+ const found = res[0].accesses.find(a => a.id === createdAccessId);
91
+ expect(found).to.exist;
92
+ expect(found.token).to.equal(createdAccessToken);
93
+ });
94
+ });
95
+
96
+ describe('[ADLX] accesses.delete', function () {
97
+ it('[ADLA] delete (revoke) an access', async () => {
98
+ expect(createdAccessId).to.exist;
99
+ const res = await conn.api([{
100
+ method: 'accesses.delete',
101
+ params: { id: createdAccessId }
102
+ }]);
103
+ expect(res[0]).to.exist;
104
+ expect(res[0].accessDeletion).to.exist;
105
+ expect(res[0].accessDeletion.id).to.equal(createdAccessId);
106
+ });
107
+
108
+ it('[ADLB] deleted access is no longer in the list', async () => {
109
+ const res = await conn.api([{
110
+ method: 'accesses.get',
111
+ params: {}
112
+ }]);
113
+ const found = res[0].accesses.find(a => a.id === createdAccessId);
114
+ expect(found).to.not.exist;
115
+ });
116
+ });
117
+
118
+ // Cleanup
119
+ after(async () => {
120
+ if (!conn) return;
121
+ await conn.api([
122
+ { method: 'streams.delete', params: { id: testStreamId } },
123
+ { method: 'streams.delete', params: { id: testStreamId } }
124
+ ]);
125
+ });
126
+ });
@@ -0,0 +1,73 @@
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
+ let conn = null;
8
+
9
+ describe('[ACTX] Account', () => {
10
+ before(async function () {
11
+ this.timeout(15000);
12
+ await testData.prepare();
13
+ conn = new pryv.Connection(testData.apiEndpointWithToken);
14
+ });
15
+
16
+ describe('[AINX] account.get', function () {
17
+ it('[AINA] get account info', async () => {
18
+ const res = await conn.api([{
19
+ method: 'account.get',
20
+ params: {}
21
+ }]);
22
+ expect(res[0]).to.exist;
23
+ expect(res[0].account).to.exist;
24
+ expect(res[0].account.username).to.equal(testData.username);
25
+ expect(res[0].account.email).to.exist;
26
+ });
27
+ });
28
+
29
+ describe('[ASUX] account.get storageUsed', function () {
30
+ it('[ASUA] storageUsed has expected fields', async () => {
31
+ const res = await conn.api([{
32
+ method: 'account.get',
33
+ params: {}
34
+ }]);
35
+ const account = res[0].account;
36
+ expect(account.storageUsed).to.exist;
37
+ expect(account.storageUsed.dbDocuments).to.be.a('number');
38
+ expect(account.storageUsed.attachedFiles).to.be.a('number');
39
+ });
40
+ });
41
+
42
+ describe('[ACPX] account.changePassword', function () {
43
+ it('[ACPA] change password and change back', async () => {
44
+ const oldPassword = testData.password;
45
+ const newPassword = oldPassword + '-new';
46
+
47
+ // Change to new password
48
+ const res1 = await conn.api([{
49
+ method: 'account.changePassword',
50
+ params: { oldPassword, newPassword }
51
+ }]);
52
+ expect(res1[0]).to.exist;
53
+ expect(res1[0].error).to.not.exist;
54
+
55
+ // Change back to original
56
+ const res2 = await conn.api([{
57
+ method: 'account.changePassword',
58
+ params: { oldPassword: newPassword, newPassword: oldPassword }
59
+ }]);
60
+ expect(res2[0]).to.exist;
61
+ expect(res2[0].error).to.not.exist;
62
+ });
63
+
64
+ it('[ACPB] reject wrong old password', async () => {
65
+ const res = await conn.api([{
66
+ method: 'account.changePassword',
67
+ params: { oldPassword: 'wrong-password', newPassword: 'new-password' }
68
+ }]);
69
+ expect(res[0]).to.exist;
70
+ expect(res[0].error).to.exist;
71
+ });
72
+ });
73
+ });
@@ -0,0 +1,193 @@
1
+ /**
2
+ * @license
3
+ * [BSD-3-Clause](https://github.com/pryv/lib-js/blob/master/LICENSE)
4
+ */
5
+ /* global describe, it, before, after, expect, JSDOM, testData */
6
+
7
+ const AuthController = require('../src/Auth/AuthController');
8
+ const AuthStates = require('../src/Auth/AuthStates');
9
+ const Service = require('../src/Service');
10
+
11
+ describe('[ACNX] AuthController', function () {
12
+ this.timeout(15000);
13
+
14
+ let service;
15
+ let cleanupDom = false;
16
+
17
+ before(async function () {
18
+ await testData.prepare();
19
+ service = new Service(testData.serviceInfoUrl);
20
+ await service.info();
21
+ });
22
+
23
+ before(async () => {
24
+ if (typeof document !== 'undefined') return;
25
+ cleanupDom = true;
26
+ const dom = new JSDOM('<!DOCTYPE html>', {
27
+ url: 'http://localhost/'
28
+ });
29
+ global.document = dom.window.document;
30
+ global.window = dom.window;
31
+ global.location = dom.window.location;
32
+ global.navigator = { userAgent: 'Safari' };
33
+ });
34
+
35
+ after(async () => {
36
+ if (!cleanupDom) return;
37
+ delete global.document;
38
+ delete global.window;
39
+ delete global.location;
40
+ });
41
+
42
+ describe('[ACVX] Validation', function () {
43
+ it('[ACVA] throws error when settings is null', function () {
44
+ expect(() => new AuthController(null, service)).to.throw('settings cannot be null');
45
+ });
46
+
47
+ it('[ACVB] throws error when authRequest is missing', function () {
48
+ expect(() => new AuthController({}, service)).to.throw('Missing settings.authRequest');
49
+ });
50
+
51
+ it('[ACVC] throws error when requestingAppId is missing', function () {
52
+ expect(() => new AuthController({
53
+ authRequest: { requestedPermissions: [] }
54
+ }, service)).to.throw('Missing settings.authRequest.requestingAppId');
55
+ });
56
+
57
+ it('[ACVD] throws error when requestedPermissions is missing', function () {
58
+ expect(() => new AuthController({
59
+ authRequest: { requestingAppId: 'test-app' }
60
+ }, service)).to.throw('Missing settings.authRequest.requestedPermissions');
61
+ });
62
+ });
63
+
64
+ describe('[ACLX] Listeners', function () {
65
+ it('[ACLA] calls onStateChange listener when state changes', async function () {
66
+ const stateChanges = [];
67
+ const auth = new AuthController({
68
+ authRequest: {
69
+ requestingAppId: 'test-app',
70
+ requestedPermissions: []
71
+ },
72
+ onStateChange: (state) => stateChanges.push(state)
73
+ }, service);
74
+
75
+ await auth.init();
76
+ expect(stateChanges.length).to.be.greaterThan(0);
77
+ expect(stateChanges[0].status).to.equal(AuthStates.LOADING);
78
+ });
79
+
80
+ it('[ACLB] handles listener errors gracefully', async function () {
81
+ const auth = new AuthController({
82
+ authRequest: {
83
+ requestingAppId: 'test-app',
84
+ requestedPermissions: []
85
+ },
86
+ onStateChange: () => { throw new Error('Listener error'); }
87
+ }, service);
88
+
89
+ // Should not throw
90
+ await auth.init();
91
+ expect(auth.state).to.exist;
92
+ });
93
+ });
94
+
95
+ describe('[ACHX] handleClick', function () {
96
+ it('[ACHA] triggers SIGNOUT when authorized', async function () {
97
+ const stateChanges = [];
98
+ const auth = new AuthController({
99
+ authRequest: {
100
+ requestingAppId: 'test-app',
101
+ requestedPermissions: []
102
+ },
103
+ onStateChange: (state) => stateChanges.push(state)
104
+ }, service);
105
+
106
+ await auth.init();
107
+
108
+ // Simulate authorized state
109
+ auth._state = { status: AuthStates.AUTHORIZED };
110
+
111
+ await auth.handleClick();
112
+ const lastState = stateChanges[stateChanges.length - 1];
113
+ expect(lastState.status).to.equal(AuthStates.SIGNOUT);
114
+ });
115
+
116
+ it('[ACHB] handles NEED_SIGNIN click (reopens popup)', async function () {
117
+ const auth = new AuthController({
118
+ authRequest: {
119
+ requestingAppId: 'test-app',
120
+ requestedPermissions: []
121
+ }
122
+ }, service);
123
+
124
+ await auth.init();
125
+
126
+ // Simulate NEED_SIGNIN state
127
+ const needSigninState = { status: AuthStates.NEED_SIGNIN, authUrl: 'http://test.url' };
128
+ auth._state = needSigninState;
129
+
130
+ await auth.handleClick();
131
+ // Should trigger state change with same state
132
+ expect(auth.state.status).to.equal(AuthStates.NEED_SIGNIN);
133
+ });
134
+ });
135
+
136
+ describe('[ACSX] stopAuthRequest', function () {
137
+ it('[ACSA] sets error state with message', async function () {
138
+ const auth = new AuthController({
139
+ authRequest: {
140
+ requestingAppId: 'test-app',
141
+ requestedPermissions: []
142
+ }
143
+ }, service);
144
+
145
+ await auth.init();
146
+ auth.stopAuthRequest('Test error message');
147
+
148
+ expect(auth.state.status).to.equal(AuthStates.ERROR);
149
+ expect(auth.state.message).to.equal('Test error message');
150
+ });
151
+ });
152
+
153
+ describe('[ACRX] returnURL', function () {
154
+ it('[ACRA] throws on invalid returnURL trailer', function () {
155
+ const auth = new AuthController({
156
+ authRequest: {
157
+ requestingAppId: 'test-app',
158
+ requestedPermissions: []
159
+ }
160
+ }, service);
161
+
162
+ expect(() => auth.getReturnURL('http://example.com')).to.throw('Last character');
163
+ });
164
+
165
+ it('[ACRB] handles null/undefined returnURL', function () {
166
+ const auth = new AuthController({
167
+ authRequest: {
168
+ requestingAppId: 'test-app',
169
+ requestedPermissions: []
170
+ }
171
+ }, service);
172
+
173
+ // Desktop browser (Safari) returns false for auto mode
174
+ const result = auth.getReturnURL(undefined, 'http://test.com', { userAgent: 'Safari' });
175
+ expect(result).to.equal(false);
176
+ });
177
+ });
178
+
179
+ describe('[AIST] State', function () {
180
+ it('[AISA] state getter and setter work correctly', async function () {
181
+ const auth = new AuthController({
182
+ authRequest: {
183
+ requestingAppId: 'test-app',
184
+ requestedPermissions: []
185
+ }
186
+ }, service);
187
+
188
+ auth.state = { status: AuthStates.LOADING };
189
+ expect(auth.state.status).to.equal(AuthStates.LOADING);
190
+ expect(auth.state.id).to.equal(AuthStates.LOADING); // retro-compatibility
191
+ });
192
+ });
193
+ });
@@ -0,0 +1,102 @@
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('[CECX] Connection Edge Cases', function () {
8
+ this.timeout(15000);
9
+
10
+ let conn;
11
+
12
+ before(async function () {
13
+ await testData.prepare();
14
+ conn = new pryv.Connection(testData.apiEndpointWithToken);
15
+ });
16
+
17
+ describe('[CERX] Error handling', function () {
18
+ it('[CERA] api() throws on non-array input', async function () {
19
+ let error = null;
20
+ try {
21
+ await conn.api({ method: 'events.get' });
22
+ } catch (e) {
23
+ error = e;
24
+ }
25
+ expect(error).to.exist;
26
+ expect(error.message).to.include('array');
27
+ });
28
+
29
+ it('[CERB] apiOne() throws on error response', async function () {
30
+ let error = null;
31
+ try {
32
+ await conn.apiOne('invalid.method', {});
33
+ } catch (e) {
34
+ error = e;
35
+ }
36
+ expect(error).to.exist;
37
+ });
38
+
39
+ it('[CERC] constructor throws on invalid service param', function () {
40
+ let error = null;
41
+ try {
42
+ const _conn = new pryv.Connection(testData.apiEndpointWithToken, { notAService: true });
43
+ expect(_conn).to.not.exist; // Should not reach here
44
+ } catch (e) {
45
+ error = e;
46
+ }
47
+ expect(error).to.exist;
48
+ expect(error.message).to.include('Invalid service');
49
+ });
50
+
51
+ it('[CERD] _handleMeta throws on missing meta', async function () {
52
+ let error = null;
53
+ try {
54
+ conn._handleMeta({}, Date.now() / 1000);
55
+ } catch (e) {
56
+ error = e;
57
+ }
58
+ expect(error).to.exist;
59
+ expect(error.message).to.include('meta');
60
+ });
61
+
62
+ it('[CERE] _handleMeta throws on missing serverTime', async function () {
63
+ let error = null;
64
+ try {
65
+ conn._handleMeta({ meta: {} }, Date.now() / 1000);
66
+ } catch (e) {
67
+ error = e;
68
+ }
69
+ expect(error).to.exist;
70
+ expect(error.message).to.include('serverTime');
71
+ });
72
+ });
73
+
74
+ describe('[CSCX] Service getter', function () {
75
+ it('[CSCA] creates service when not provided', function () {
76
+ const conn2 = new pryv.Connection(testData.apiEndpoint);
77
+ const service = conn2.service;
78
+ expect(service).to.exist;
79
+ // Second access returns same instance
80
+ expect(conn2.service).to.equal(service);
81
+ });
82
+
83
+ it('[CSCB] uses provided service', async function () {
84
+ const service = new pryv.Service(testData.serviceInfoUrl);
85
+ await service.info();
86
+ const conn2 = new pryv.Connection(testData.apiEndpoint, service);
87
+ expect(conn2.service).to.equal(service);
88
+ });
89
+ });
90
+
91
+ describe('[CGEX] get() method', function () {
92
+ it('[CGEA] get() without query params', async function () {
93
+ const result = await conn.get('access-info');
94
+ expect(result).to.exist;
95
+ });
96
+
97
+ it('[CGEB] get() with null query params', async function () {
98
+ const result = await conn.get('access-info', null);
99
+ expect(result).to.exist;
100
+ });
101
+ });
102
+ });
@@ -5,7 +5,7 @@
5
5
  /* global describe, it, before, after, beforeEach, afterEach, expect, JSDOM, pryv, Blob, FormData */
6
6
 
7
7
  // URL and URLSearchParams are native in Node.js and browsers
8
- const cuid = require('cuid');
8
+ const { createId: cuid } = require('@paralleldrive/cuid2');
9
9
  const testData = require('../../../test/test-data');
10
10
 
11
11
  let conn = null;
@@ -23,9 +23,9 @@ describe('[CONX] Connection', () => {
23
23
  await testData.prepare();
24
24
  conn = new pryv.Connection(testData.apiEndpointWithToken);
25
25
 
26
- // create some events
27
- const toBeDeletedId = cuid();
28
- const toBeTrashed = cuid();
26
+ // create some events (prefix with 'c' for v2 event ID pattern)
27
+ const toBeDeletedId = 'c' + cuid();
28
+ const toBeTrashed = 'c' + cuid();
29
29
  await conn.api([
30
30
  {
31
31
  method: 'events.create',
@@ -359,7 +359,34 @@ describe('[CONX] Connection', () => {
359
359
  expect(eventsCount).to.equal(res.eventsCount);
360
360
  });
361
361
 
362
+ it('[CSNY] streaming with roneous query params', async () => {
363
+ const queryParams = { fromTime: 0, toTime: Date.now() / 1000, limit: 10000, types: ['note/txt'], streams: ['bogus', 'bogus2'] };
364
+ let eventsCount = 0;
365
+ function forEachEvent (event) {
366
+ eventsCount++;
367
+ }
368
+ try {
369
+ const res = await conn.getEventsStreamed(queryParams, forEachEvent);
370
+ expect(eventsCount).to.equal(res.eventsCount);
371
+ } catch (e) {
372
+ return;
373
+ }
374
+ throw new Error('Should fail');
375
+ });
376
+
362
377
  it('[CSNB] streaming includesDeletion', async () => {
378
+ // Create, trash, and delete events to ensure we have all three states
379
+ const trashId = 'c' + cuid();
380
+ const deleteId = 'c' + cuid();
381
+ const setupRes = await conn.api([
382
+ { method: 'events.create', params: { id: trashId, streamIds: ['data'], type: 'note/txt', content: 'to trash' } },
383
+ { method: 'events.create', params: { id: deleteId, streamIds: ['data'], type: 'note/txt', content: 'to delete' } },
384
+ { method: 'events.delete', params: { id: trashId } },
385
+ { method: 'events.delete', params: { id: deleteId } },
386
+ { method: 'events.delete', params: { id: deleteId } }
387
+ ]);
388
+ // Verify setup succeeded
389
+ expect(setupRes[0].event, 'event creation failed: ' + JSON.stringify(setupRes[0].error)).to.exist;
363
390
  const queryParams = { fromTime: 0, toTime: now, limit: 10000, includeDeletions: true, modifiedSince: 0, state: 'all' };
364
391
  let eventsCount = 0;
365
392
  let trashedCount = 0;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * @license
3
+ * [BSD-3-Clause](https://github.com/pryv/lib-js/blob/master/LICENSE)
4
+ */
5
+ /* global describe, it, before, after, expect, JSDOM */
6
+
7
+ const CookieUtils = require('../src/Browser/CookieUtils');
8
+
9
+ describe('[COKX] CookieUtils', function () {
10
+ let cleanupDom = false;
11
+
12
+ before(async () => {
13
+ if (typeof document !== 'undefined') return; // in browser
14
+ cleanupDom = true;
15
+ const dom = new JSDOM('<!DOCTYPE html>', {
16
+ url: 'http://localhost/'
17
+ });
18
+ global.document = dom.window.document;
19
+ global.window = dom.window;
20
+ });
21
+
22
+ after(async () => {
23
+ if (!cleanupDom) return;
24
+ delete global.document;
25
+ delete global.window;
26
+ });
27
+
28
+ it('[COKA] set() and get() cookie', async function () {
29
+ const testKey = 'test-cookie-key';
30
+ const testValue = { foo: 'bar', num: 123 };
31
+ CookieUtils.set(testKey, testValue);
32
+ const retrieved = CookieUtils.get(testKey);
33
+ expect(retrieved).to.deep.equal(testValue);
34
+ });
35
+
36
+ it('[COKB] get() returns undefined for non-existent cookie', async function () {
37
+ const result = CookieUtils.get('non-existent-cookie-key');
38
+ expect(result).to.be.undefined;
39
+ });
40
+
41
+ it('[COKC] del() removes cookie', async function () {
42
+ const testKey = 'cookie-to-delete';
43
+ CookieUtils.set(testKey, { data: 'test' });
44
+ expect(CookieUtils.get(testKey)).to.exist;
45
+ CookieUtils.del(testKey);
46
+ // After deletion, the cookie is set to { deleted: true } with expiration in the past
47
+ // The cookie may still exist but with deleted flag
48
+ const afterDel = CookieUtils.get(testKey);
49
+ if (afterDel) {
50
+ expect(afterDel.deleted).to.be.true;
51
+ }
52
+ });
53
+
54
+ it('[COKD] set() with custom expiration', async function () {
55
+ const testKey = 'cookie-with-expiry';
56
+ const testValue = 'expires-soon';
57
+ CookieUtils.set(testKey, testValue, 30);
58
+ const retrieved = CookieUtils.get(testKey);
59
+ expect(retrieved).to.equal(testValue);
60
+ });
61
+
62
+ it('[COKE] handles special characters in values', async function () {
63
+ const testKey = 'special-chars';
64
+ const testValue = { text: 'hello=world&foo=bar', unicode: '日本語' };
65
+ CookieUtils.set(testKey, testValue);
66
+ const retrieved = CookieUtils.get(testKey);
67
+ expect(retrieved).to.deep.equal(testValue);
68
+ });
69
+ });
@@ -0,0 +1,196 @@
1
+ /**
2
+ * @license
3
+ * [BSD-3-Clause](https://github.com/pryv/lib-js/blob/master/LICENSE)
4
+ */
5
+ /* global describe, it, before, after, expect, JSDOM, testData */
6
+
7
+ const LoginButton = require('../src/Browser/LoginButton');
8
+ const Service = require('../src/Service');
9
+ const AuthStates = require('../src/Auth/AuthStates');
10
+
11
+ describe('[LBTX] LoginButton', function () {
12
+ this.timeout(20000);
13
+
14
+ let service;
15
+ let cleanupDom = false;
16
+ let dom;
17
+
18
+ before(async function () {
19
+ await testData.prepare();
20
+ service = new Service(testData.serviceInfoUrl);
21
+ await service.info();
22
+ });
23
+
24
+ before(async () => {
25
+ if (typeof document !== 'undefined') return;
26
+ cleanupDom = true;
27
+ dom = new JSDOM('<!DOCTYPE html><body><span id="loginButton"></span></body>', {
28
+ url: 'http://localhost/'
29
+ });
30
+ global.document = dom.window.document;
31
+ global.window = dom.window;
32
+ global.location = dom.window.location;
33
+ global.navigator = { userAgent: 'Safari' };
34
+ global.confirm = () => true;
35
+ });
36
+
37
+ after(async () => {
38
+ if (!cleanupDom) return;
39
+ delete global.document;
40
+ delete global.window;
41
+ delete global.location;
42
+ delete global.confirm;
43
+ });
44
+
45
+ describe('[LBCX] Constructor and Init', function () {
46
+ it('[LBCA] creates LoginButton with valid settings', async function () {
47
+ const settings = {
48
+ spanButtonID: 'loginButton',
49
+ authRequest: {
50
+ requestingAppId: 'test-app',
51
+ requestedPermissions: []
52
+ }
53
+ };
54
+ const loginBtn = new LoginButton(settings, service);
55
+ expect(loginBtn.authSettings).to.equal(settings);
56
+ expect(loginBtn.service).to.equal(service);
57
+ });
58
+
59
+ it('[LBCB] init() sets up button and auth controller', async function () {
60
+ const settings = {
61
+ spanButtonID: 'loginButton',
62
+ authRequest: {
63
+ requestingAppId: 'test-app',
64
+ requestedPermissions: []
65
+ }
66
+ };
67
+ const loginBtn = new LoginButton(settings, service);
68
+ await loginBtn.init();
69
+ expect(loginBtn.auth).to.exist;
70
+ expect(loginBtn._cookieKey).to.include('pryv-libjs-');
71
+ });
72
+ });
73
+
74
+ describe('[LBAX] Authorization data', function () {
75
+ let loginBtn;
76
+
77
+ before(async function () {
78
+ const settings = {
79
+ spanButtonID: 'loginButton',
80
+ authRequest: {
81
+ requestingAppId: 'test-app-auth',
82
+ requestedPermissions: []
83
+ }
84
+ };
85
+ loginBtn = new LoginButton(settings, service);
86
+ await loginBtn.init();
87
+ });
88
+
89
+ it('[LBAA] saveAuthorizationData and getAuthorizationData work', function () {
90
+ const authData = { apiEndpoint: 'https://test.pryv.me', username: 'testuser' };
91
+ loginBtn.saveAuthorizationData(authData);
92
+ const retrieved = loginBtn.getAuthorizationData();
93
+ expect(retrieved).to.deep.equal(authData);
94
+ });
95
+
96
+ it('[LBAB] deleteAuthorizationData removes data', async function () {
97
+ loginBtn.saveAuthorizationData({ test: 'data' });
98
+ await loginBtn.deleteAuthorizationData();
99
+ const retrieved = loginBtn.getAuthorizationData();
100
+ // After deletion, either undefined or has deleted flag
101
+ if (retrieved) {
102
+ expect(retrieved.deleted).to.be.true;
103
+ }
104
+ });
105
+ });
106
+
107
+ describe('[LBOX] onClick', function () {
108
+ it('[LBOA] onClick calls auth.handleClick', async function () {
109
+ const settings = {
110
+ spanButtonID: 'loginButton',
111
+ authRequest: {
112
+ requestingAppId: 'test-app-click',
113
+ requestedPermissions: []
114
+ }
115
+ };
116
+ const loginBtn = new LoginButton(settings, service);
117
+ await loginBtn.init();
118
+
119
+ let handleClickCalled = false;
120
+ loginBtn.auth.handleClick = function () { handleClickCalled = true; };
121
+
122
+ loginBtn.onClick();
123
+ expect(handleClickCalled).to.be.true;
124
+ });
125
+ });
126
+
127
+ describe('[LBSX] onStateChange', function () {
128
+ let loginBtn;
129
+
130
+ before(async function () {
131
+ const settings = {
132
+ spanButtonID: 'loginButton',
133
+ authRequest: {
134
+ requestingAppId: 'test-app-state',
135
+ requestedPermissions: []
136
+ }
137
+ };
138
+ loginBtn = new LoginButton(settings, service);
139
+ await loginBtn.init();
140
+ });
141
+
142
+ it('[LBSA] handles LOADING state', async function () {
143
+ await loginBtn.onStateChange({ status: AuthStates.LOADING });
144
+ expect(loginBtn.text).to.equal('...');
145
+ });
146
+
147
+ it('[LBSB] handles INITIALIZED state', async function () {
148
+ await loginBtn.onStateChange({ status: AuthStates.INITIALIZED });
149
+ expect(loginBtn.text).to.include('Signin');
150
+ });
151
+
152
+ it('[LBSC] handles AUTHORIZED state', async function () {
153
+ await loginBtn.onStateChange({
154
+ status: AuthStates.AUTHORIZED,
155
+ username: 'testuser',
156
+ apiEndpoint: 'https://test.pryv.me'
157
+ });
158
+ expect(loginBtn.text).to.equal('testuser');
159
+ });
160
+
161
+ it('[LBSD] handles ERROR state', async function () {
162
+ await loginBtn.onStateChange({
163
+ status: AuthStates.ERROR,
164
+ message: 'Test error'
165
+ });
166
+ expect(loginBtn.text).to.include('Error');
167
+ expect(loginBtn.text).to.include('Test error');
168
+ });
169
+
170
+ it('[LBSE] handles SIGNOUT state when confirmed', async function () {
171
+ // Save some auth data first
172
+ loginBtn.saveAuthorizationData({ test: 'data' });
173
+ await loginBtn.onStateChange({ status: AuthStates.SIGNOUT });
174
+ // Confirm is mocked to return true
175
+ });
176
+
177
+ it('[LBSF] handles unknown state gracefully', async function () {
178
+ // Should log warning but not throw
179
+ await loginBtn.onStateChange({ status: 'UNKNOWN_STATE' });
180
+ });
181
+ });
182
+
183
+ describe('[LBWX] Without spanButtonID', function () {
184
+ it('[LBWA] init works without span (logs warning)', async function () {
185
+ const settings = {
186
+ authRequest: {
187
+ requestingAppId: 'test-app-no-span',
188
+ requestedPermissions: []
189
+ }
190
+ };
191
+ const loginBtn = new LoginButton(settings, service);
192
+ await loginBtn.init();
193
+ expect(loginBtn.loginButtonSpan).to.be.null;
194
+ });
195
+ });
196
+ });
@@ -0,0 +1,48 @@
1
+ /**
2
+ * @license
3
+ * [BSD-3-Clause](https://github.com/pryv/lib-js/blob/master/LICENSE)
4
+ */
5
+ /* global describe, it, expect */
6
+
7
+ const Messages = require('../src/Auth/LoginMessages');
8
+
9
+ describe('[MSGX] LoginMessages', function () {
10
+ it('[MSGA] returns English messages by default', function () {
11
+ const msgs = Messages('en');
12
+ expect(msgs.LOADING).to.equal('...');
13
+ expect(msgs.ERROR).to.equal('Error');
14
+ expect(msgs.LOGIN).to.equal('Signin');
15
+ expect(msgs.SIGNOUT_CONFIRM).to.equal('Logout?');
16
+ });
17
+
18
+ it('[MSGB] returns French messages when available', function () {
19
+ const msgs = Messages('fr');
20
+ expect(msgs.ERROR).to.equal('Erreur');
21
+ expect(msgs.LOGIN).to.equal('Login');
22
+ expect(msgs.SIGNOUT_CONFIRM).to.equal('Se déconnecter ?');
23
+ });
24
+
25
+ it('[MSGC] falls back to English for unavailable language', function () {
26
+ const msgs = Messages('de');
27
+ expect(msgs.LOADING).to.equal('...');
28
+ expect(msgs.ERROR).to.equal('Error');
29
+ });
30
+
31
+ it('[MSGD] uses custom definitions when provided', function () {
32
+ const customDefs = {
33
+ LOADING: { en: 'Loading...', fr: 'Chargement...' },
34
+ CUSTOM: { en: 'Custom Message', fr: 'Message personnalisé' }
35
+ };
36
+ const msgs = Messages('en', customDefs);
37
+ expect(msgs.LOADING).to.equal('Loading...');
38
+ expect(msgs.CUSTOM).to.equal('Custom Message');
39
+ });
40
+
41
+ it('[MSGE] custom definitions fall back to English', function () {
42
+ const customDefs = {
43
+ TEST: { en: 'English only' }
44
+ };
45
+ const msgs = Messages('fr', customDefs);
46
+ expect(msgs.TEST).to.equal('English only');
47
+ });
48
+ });
@@ -0,0 +1,40 @@
1
+ /**
2
+ * @license
3
+ * [BSD-3-Clause](https://github.com/pryv/lib-js/blob/master/LICENSE)
4
+ */
5
+ /* global describe, it, expect */
6
+
7
+ const PryvError = require('../src/lib/PryvError');
8
+
9
+ describe('[PERX] PryvError', function () {
10
+ it('[PERA] creates error with message', function () {
11
+ const error = new PryvError('Test error message');
12
+ expect(error.message).to.equal('Test error message');
13
+ expect(error.name).to.equal('PryvError');
14
+ expect(error).to.be.instanceOf(Error);
15
+ expect(error).to.be.instanceOf(PryvError);
16
+ });
17
+
18
+ it('[PERB] includes innerObject when provided', function () {
19
+ const innerError = new Error('Inner error');
20
+ const error = new PryvError('Outer message', innerError);
21
+ expect(error.innerObject).to.equal(innerError);
22
+ });
23
+
24
+ it('[PERC] accepts object as innerObject', function () {
25
+ const innerObj = { code: 'ERR_001', details: 'some details' };
26
+ const error = new PryvError('Error with object', innerObj);
27
+ expect(error.innerObject).to.deep.equal(innerObj);
28
+ });
29
+
30
+ it('[PERD] has proper stack trace', function () {
31
+ const error = new PryvError('Stack test');
32
+ expect(error.stack).to.exist;
33
+ expect(error.stack).to.include('PryvError');
34
+ });
35
+
36
+ it('[PERE] innerObject is undefined when not provided', function () {
37
+ const error = new PryvError('No inner');
38
+ expect(error.innerObject).to.be.undefined;
39
+ });
40
+ });
@@ -0,0 +1,135 @@
1
+ /**
2
+ * @license
3
+ * [BSD-3-Clause](https://github.com/pryv/lib-js/blob/master/LICENSE)
4
+ */
5
+ /* global describe, it, before, after, expect, pryv, testData */
6
+
7
+ const { createId: cuid } = require('@paralleldrive/cuid2');
8
+
9
+ let conn = null;
10
+ const suffix = cuid().slice(0, 8);
11
+ const testStreamId = 'str-' + suffix;
12
+ const childStreamId = 'str-child-' + suffix;
13
+ const testStreamName = 'Test Stream ' + suffix;
14
+ const childStreamName = 'Child Stream ' + suffix;
15
+
16
+ describe('[STRX] Streams', () => {
17
+ before(async function () {
18
+ this.timeout(15000);
19
+ await testData.prepare();
20
+ conn = new pryv.Connection(testData.apiEndpointWithToken);
21
+ });
22
+
23
+ describe('[SCRX] streams.create', function () {
24
+ it('[SCRA] create a root stream', async () => {
25
+ const res = await conn.api([{
26
+ method: 'streams.create',
27
+ params: { id: testStreamId, name: testStreamName }
28
+ }]);
29
+ expect(res[0]).to.exist;
30
+ expect(res[0].stream).to.exist;
31
+ expect(res[0].stream.id).to.equal(testStreamId);
32
+ expect(res[0].stream.name).to.equal(testStreamName);
33
+ });
34
+
35
+ it('[SCRB] create a child stream', async () => {
36
+ const res = await conn.api([{
37
+ method: 'streams.create',
38
+ params: { id: childStreamId, name: childStreamName, parentId: testStreamId }
39
+ }]);
40
+ expect(res[0]).to.exist;
41
+ expect(res[0].stream).to.exist;
42
+ expect(res[0].stream.parentId).to.equal(testStreamId);
43
+ });
44
+
45
+ it('[SCRC] reject duplicate stream id', async () => {
46
+ const res = await conn.api([{
47
+ method: 'streams.create',
48
+ params: { id: testStreamId, name: 'Duplicate ' + suffix }
49
+ }]);
50
+ expect(res[0]).to.exist;
51
+ expect(res[0].error).to.exist;
52
+ expect(res[0].error.id).to.equal('item-already-exists');
53
+ });
54
+ });
55
+
56
+ describe('[SGTX] streams.get', function () {
57
+ it('[SGTA] get all streams', async () => {
58
+ const res = await conn.api([{
59
+ method: 'streams.get',
60
+ params: {}
61
+ }]);
62
+ expect(res[0]).to.exist;
63
+ expect(res[0].streams).to.exist;
64
+ expect(Array.isArray(res[0].streams)).to.equal(true);
65
+ expect(res[0].streams.length).to.be.gt(0);
66
+ });
67
+
68
+ it('[SGTB] get stream tree includes parent and child', async () => {
69
+ const res = await conn.api([{
70
+ method: 'streams.get',
71
+ params: {}
72
+ }]);
73
+ const streams = res[0].streams;
74
+ const parent = findStream(streams, testStreamId);
75
+ expect(parent).to.exist;
76
+ expect(parent.children).to.exist;
77
+ const child = parent.children.find(s => s.id === childStreamId);
78
+ expect(child).to.exist;
79
+ });
80
+ });
81
+
82
+ describe('[SUPX] streams.update', function () {
83
+ it('[SUPA] rename a stream', async () => {
84
+ const res = await conn.api([{
85
+ method: 'streams.update',
86
+ params: { id: childStreamId, update: { name: 'Renamed ' + suffix } }
87
+ }]);
88
+ expect(res[0]).to.exist;
89
+ expect(res[0].stream).to.exist;
90
+ expect(res[0].stream.name).to.equal('Renamed ' + suffix);
91
+ });
92
+ });
93
+
94
+ describe('[SDLX] streams.delete', function () {
95
+ it('[SDLA] trash a stream (first delete)', async () => {
96
+ const res = await conn.api([{
97
+ method: 'streams.delete',
98
+ params: { id: childStreamId }
99
+ }]);
100
+ expect(res[0]).to.exist;
101
+ expect(res[0].stream).to.exist;
102
+ expect(res[0].stream.trashed).to.equal(true);
103
+ });
104
+
105
+ it('[SDLB] delete a trashed stream (second delete)', async () => {
106
+ const res = await conn.api([{
107
+ method: 'streams.delete',
108
+ params: { id: childStreamId }
109
+ }]);
110
+ expect(res[0]).to.exist;
111
+ // After second delete, stream is gone
112
+ expect(res[0].streamDeletion).to.exist;
113
+ });
114
+ });
115
+
116
+ // Cleanup parent stream
117
+ after(async () => {
118
+ if (!conn) return;
119
+ await conn.api([
120
+ { method: 'streams.delete', params: { id: testStreamId } },
121
+ { method: 'streams.delete', params: { id: testStreamId } }
122
+ ]);
123
+ });
124
+ });
125
+
126
+ function findStream (streams, id) {
127
+ for (const s of streams) {
128
+ if (s.id === id) return s;
129
+ if (s.children) {
130
+ const found = findStream(s.children, id);
131
+ if (found) return found;
132
+ }
133
+ }
134
+ return null;
135
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * @license
3
+ * [BSD-3-Clause](https://github.com/pryv/lib-js/blob/master/LICENSE)
4
+ */
5
+ /* global describe, it, expect */
6
+
7
+ const buildSearchParams = require('../src/lib/buildSearchParams');
8
+
9
+ describe('[BSPX] buildSearchParams', function () {
10
+ it('[BSPA] builds params from simple object', function () {
11
+ const result = buildSearchParams({ foo: 'bar', num: 123 });
12
+ expect(result).to.equal('foo=bar&num=123');
13
+ });
14
+
15
+ it('[BSPB] handles arrays as repeated keys', function () {
16
+ const result = buildSearchParams({ tags: ['a', 'b', 'c'] });
17
+ expect(result).to.equal('tags=a&tags=b&tags=c');
18
+ });
19
+
20
+ it('[BSPC] handles mixed arrays and values', function () {
21
+ const result = buildSearchParams({
22
+ name: 'test',
23
+ ids: ['id1', 'id2'],
24
+ limit: 10
25
+ });
26
+ expect(result).to.include('name=test');
27
+ expect(result).to.include('ids=id1');
28
+ expect(result).to.include('ids=id2');
29
+ expect(result).to.include('limit=10');
30
+ });
31
+
32
+ it('[BSPD] ignores null and undefined values', function () {
33
+ const result = buildSearchParams({
34
+ valid: 'yes',
35
+ nullVal: null,
36
+ undefVal: undefined
37
+ });
38
+ expect(result).to.equal('valid=yes');
39
+ });
40
+
41
+ it('[BSPE] handles empty object', function () {
42
+ const result = buildSearchParams({});
43
+ expect(result).to.equal('');
44
+ });
45
+
46
+ it('[BSPF] encodes special characters', function () {
47
+ const result = buildSearchParams({ query: 'hello world', special: 'a=b&c=d' });
48
+ expect(result).to.include('query=hello+world');
49
+ expect(result).to.include('special=a%3Db%26c%3Dd');
50
+ });
51
+
52
+ it('[BSPG] handles boolean values', function () {
53
+ const result = buildSearchParams({ active: true, disabled: false });
54
+ expect(result).to.equal('active=true&disabled=false');
55
+ });
56
+
57
+ it('[BSPH] handles empty arrays', function () {
58
+ const result = buildSearchParams({ tags: [], name: 'test' });
59
+ expect(result).to.equal('name=test');
60
+ });
61
+ });
@@ -0,0 +1,198 @@
1
+ /**
2
+ * @license
3
+ * [BSD-3-Clause](https://github.com/pryv/lib-js/blob/master/LICENSE)
4
+ */
5
+ /* global describe, it, expect */
6
+
7
+ const jsonParser = require('../src/lib/json-parser');
8
+ const { EventEmitter } = require('events');
9
+
10
+ // Helper to create a mock response stream
11
+ function createMockResponse (chunks) {
12
+ const emitter = new EventEmitter();
13
+ emitter.setEncoding = function () {};
14
+
15
+ // Emit chunks asynchronously
16
+ setTimeout(() => {
17
+ chunks.forEach(chunk => emitter.emit('data', chunk));
18
+ emitter.emit('end');
19
+ }, 0);
20
+
21
+ return emitter;
22
+ }
23
+
24
+ describe('[JPSX] json-parser', function () {
25
+ it('[JPSA] parses single event correctly', function (done) {
26
+ const events = [];
27
+ const parser = jsonParser((event) => events.push(event), false);
28
+
29
+ const jsonData = '{"events":[{"id":"evt1","type":"note/txt"}],"meta":{"serverTime":1234567890}}';
30
+ const mockRes = createMockResponse([jsonData]);
31
+ mockRes.statusCode = 200;
32
+
33
+ parser(mockRes, (err, body) => {
34
+ expect(err).to.be.undefined;
35
+ expect(events).to.have.lengthOf(1);
36
+ expect(events[0].id).to.equal('evt1');
37
+ expect(body.eventsCount).to.equal(1);
38
+ done();
39
+ });
40
+ });
41
+
42
+ it('[JPSB] parses multiple events', function (done) {
43
+ const events = [];
44
+ const parser = jsonParser((event) => events.push(event), false);
45
+
46
+ const jsonData = '{"events":[{"id":"e1"},{"id":"e2"},{"id":"e3"}],"meta":{"serverTime":123}}';
47
+ const mockRes = createMockResponse([jsonData]);
48
+ mockRes.statusCode = 200;
49
+
50
+ parser(mockRes, (err, body) => {
51
+ expect(err).to.be.undefined;
52
+ expect(events).to.have.lengthOf(3);
53
+ expect(body.eventsCount).to.equal(3);
54
+ done();
55
+ });
56
+ });
57
+
58
+ it('[JPSC] handles chunked data', function (done) {
59
+ const events = [];
60
+ const parser = jsonParser((event) => events.push(event), false);
61
+
62
+ // Split JSON across multiple chunks
63
+ const chunks = [
64
+ '{"events":[{"id":"ev',
65
+ '1","type":"test"},{"i',
66
+ 'd":"ev2"}],"meta":{"serverTime":1}}'
67
+ ];
68
+ const mockRes = createMockResponse(chunks);
69
+ mockRes.statusCode = 200;
70
+
71
+ parser(mockRes, (err, body) => {
72
+ expect(err).to.be.undefined;
73
+ expect(events).to.have.lengthOf(2);
74
+ expect(events[0].id).to.equal('ev1');
75
+ expect(events[1].id).to.equal('ev2');
76
+ done();
77
+ });
78
+ });
79
+
80
+ it('[JPSD] handles empty events array', function (done) {
81
+ const events = [];
82
+ const parser = jsonParser((event) => events.push(event), false);
83
+
84
+ const jsonData = '{"events":[],"meta":{"serverTime":123}}';
85
+ const mockRes = createMockResponse([jsonData]);
86
+ mockRes.statusCode = 200;
87
+
88
+ parser(mockRes, (err, body) => {
89
+ expect(err).to.be.undefined;
90
+ expect(events).to.have.lengthOf(0);
91
+ expect(body.eventsCount).to.equal(0);
92
+ done();
93
+ });
94
+ });
95
+
96
+ it('[JPSE] handles events with nested objects', function (done) {
97
+ const events = [];
98
+ const parser = jsonParser((event) => events.push(event), false);
99
+
100
+ const jsonData = '{"events":[{"id":"e1","content":{"nested":{"deep":"value"}}}],"meta":{"serverTime":1}}';
101
+ const mockRes = createMockResponse([jsonData]);
102
+ mockRes.statusCode = 200;
103
+
104
+ parser(mockRes, (err, body) => {
105
+ expect(err).to.be.undefined;
106
+ expect(events).to.have.lengthOf(1);
107
+ expect(events[0].content.nested.deep).to.equal('value');
108
+ done();
109
+ });
110
+ });
111
+
112
+ it('[JPSF] handles events with strings containing special chars', function (done) {
113
+ const events = [];
114
+ const parser = jsonParser((event) => events.push(event), false);
115
+
116
+ const jsonData = '{"events":[{"id":"e1","content":"line1\\nline2\\twith\\\\backslash"}],"meta":{"serverTime":1}}';
117
+ const mockRes = createMockResponse([jsonData]);
118
+ mockRes.statusCode = 200;
119
+
120
+ parser(mockRes, (err, body) => {
121
+ expect(err).to.be.undefined;
122
+ expect(events).to.have.lengthOf(1);
123
+ expect(events[0].content).to.include('line1');
124
+ done();
125
+ });
126
+ });
127
+
128
+ it('[JPSG] includes eventDeletions when includeDeletions is true', function (done) {
129
+ const events = [];
130
+ const parser = jsonParser((event) => events.push(event), true);
131
+
132
+ // Split data into chunks to simulate real HTTP streaming
133
+ // The parser needs multiple data events to process state transitions correctly
134
+ const chunks = [
135
+ '{"events":[{"id":"e1"}]',
136
+ ',"eventDeletions":[{"id":"del1"}]',
137
+ ',"meta":{"serverTime":1}}'
138
+ ];
139
+ const mockRes = createMockResponse(chunks);
140
+ mockRes.statusCode = 200;
141
+
142
+ parser(mockRes, (err, body) => {
143
+ expect(err).to.be.undefined;
144
+ expect(events).to.have.lengthOf(2);
145
+ expect(body.eventsCount).to.equal(1);
146
+ expect(body.eventDeletionsCount).to.equal(1);
147
+ done();
148
+ });
149
+ });
150
+
151
+ it('[JPSH] handles invalid JSON gracefully', function (done) {
152
+ const events = [];
153
+ const parser = jsonParser((event) => events.push(event), false);
154
+
155
+ const jsonData = 'not valid json';
156
+ const mockRes = createMockResponse([jsonData]);
157
+ mockRes.statusCode = 200;
158
+
159
+ parser(mockRes, (err, body) => {
160
+ expect(err).to.exist;
161
+ expect(err.rawResponse).to.exist;
162
+ expect(err.statusCode).to.equal(200);
163
+ done();
164
+ });
165
+ });
166
+
167
+ it('[JPSI] handles events with quoted strings containing brackets', function (done) {
168
+ const events = [];
169
+ const parser = jsonParser((event) => events.push(event), false);
170
+
171
+ const jsonData = '{"events":[{"id":"e1","content":"text with {braces} and [brackets]"}],"meta":{"serverTime":1}}';
172
+ const mockRes = createMockResponse([jsonData]);
173
+ mockRes.statusCode = 200;
174
+
175
+ parser(mockRes, (err, body) => {
176
+ expect(err).to.be.undefined;
177
+ expect(events).to.have.lengthOf(1);
178
+ expect(events[0].content).to.include('{braces}');
179
+ done();
180
+ });
181
+ });
182
+
183
+ it('[JPSJ] preserves meta and other root properties', function (done) {
184
+ const events = [];
185
+ const parser = jsonParser((event) => events.push(event), false);
186
+
187
+ const jsonData = '{"someProperty":"value","events":[{"id":"e1"}],"meta":{"serverTime":123,"apiVersion":"1.2.3"}}';
188
+ const mockRes = createMockResponse([jsonData]);
189
+ mockRes.statusCode = 200;
190
+
191
+ parser(mockRes, (err, body) => {
192
+ expect(err).to.be.undefined;
193
+ expect(body.someProperty).to.equal('value');
194
+ expect(body.meta.apiVersion).to.equal('1.2.3');
195
+ done();
196
+ });
197
+ });
198
+ });