Skip to content

Commit 269d082

Browse files
committed
TS-first: import .ts directly, tests run from source (Node 24)
- All 103 internal TS imports changed from .js to .ts extensions - tsconfig: rewriteRelativeImportExtensions for tsc .ts→.js output - Tests import from ts/ source, no longer need compiled js/ - webpack builds directly from ts/ (transpileOnly) - eslint.config.js renamed to .cjs, .cjs excluded from lint - Fixed import type for Node 24 type stripping (interfaces.ts) - Added HDSLibError to index.ts exports - Added HDSProfile singleton (Plan 11 Phase 2) - 227 tests pass on Node 24
1 parent 5fd6c0f commit 269d082

57 files changed

Lines changed: 794 additions & 139 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

eslint.config.js renamed to eslint.config.cjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ module.exports = [
1515
'docs/**',
1616
'tests-browser/**',
1717
'js/**',
18-
'dist/**'
18+
'dist/**',
19+
'**/*.cjs'
1920
]
2021
},
2122

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"test": "NODE_ENV=test mocha tests --test-reporter=spec",
1212
"test:coverage": "NODE_ENV=test c8 mocha tests --test-reporter=spec",
1313
"test:coverage:ci": "NODE_ENV=test c8 mocha tests --test-reporter=spec --timeout=20000",
14-
"build": "tsc; webpack --config webpack.config.cjs",
14+
"build": "webpack --config webpack.config.cjs",
1515
"setup": "sh scripts/setup.sh",
1616
"deploy": "bash scripts/deploy.sh",
1717
"build:ts": "tsc",

tests/HDSProfile.test.js

Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
import { assert } from './test-utils/deps-node.js';
2+
import { HDSProfile, PROFILE_FIELDS, pryv } from '../ts/index.ts';
3+
import { createUser, init as initPryvService } from './test-utils/pryvService.js';
4+
5+
describe('[PRFL] PROFILE_FIELDS', () => {
6+
it('[PRFL1] has expected keys', () => {
7+
assert.strictEqual(PROFILE_FIELDS.displayName.eventType, 'contact/display-name');
8+
assert.strictEqual(PROFILE_FIELDS.displayName.streamId, 'profile-display-name');
9+
assert.strictEqual(PROFILE_FIELDS.avatar.eventType, 'picture/*');
10+
assert.strictEqual(PROFILE_FIELDS.avatar.streamId, 'profile-avatar');
11+
assert.strictEqual(PROFILE_FIELDS.name.eventType, 'contact/name');
12+
assert.strictEqual(PROFILE_FIELDS.surname.eventType, 'contact/surname');
13+
assert.strictEqual(PROFILE_FIELDS.dateOfBirth.eventType, 'date/iso-8601');
14+
assert.strictEqual(PROFILE_FIELDS.sex.eventType, 'attributes/biological-sex');
15+
assert.strictEqual(PROFILE_FIELDS.country.eventType, 'contact/country');
16+
});
17+
});
18+
19+
describe('[HDSP] HDSProfile (dev API)', function () {
20+
this.timeout(15000);
21+
22+
let connection;
23+
24+
before(async () => {
25+
await initPryvService();
26+
const user = await createUser();
27+
connection = new pryv.Connection(user.apiEndpoint);
28+
29+
// Create profile stream tree
30+
await connection.api([
31+
{ method: 'streams.create', params: { id: 'profile', name: 'Profile' } },
32+
{ method: 'streams.create', params: { id: 'profile-display-name', name: 'Display Name', parentId: 'profile' } },
33+
{ method: 'streams.create', params: { id: 'profile-avatar', name: 'Avatar', parentId: 'profile' } },
34+
{ method: 'streams.create', params: { id: 'profile-name', name: 'Name', parentId: 'profile' } },
35+
{ method: 'streams.create', params: { id: 'profile-date-of-birth', name: 'Date of Birth', parentId: 'profile' } },
36+
{ method: 'streams.create', params: { id: 'profile-sex', name: 'Sex', parentId: 'profile' } },
37+
{ method: 'streams.create', params: { id: 'profile-address', name: 'Address', parentId: 'profile' } },
38+
]);
39+
});
40+
41+
afterEach(() => {
42+
HDSProfile.unhook();
43+
});
44+
45+
describe('[HDSP-U] unhook / defaults', () => {
46+
it('[HDSP-U1] isHooked is false by default', () => {
47+
assert.strictEqual(HDSProfile.isHooked, false);
48+
});
49+
50+
it('[HDSP-U2] get returns null for all fields when not hooked', () => {
51+
assert.strictEqual(HDSProfile.get('displayName'), null);
52+
assert.strictEqual(HDSProfile.get('avatar'), null);
53+
assert.strictEqual(HDSProfile.get('name'), null);
54+
assert.strictEqual(HDSProfile.get('surname'), null);
55+
assert.strictEqual(HDSProfile.get('dateOfBirth'), null);
56+
assert.strictEqual(HDSProfile.get('sex'), null);
57+
assert.strictEqual(HDSProfile.get('country'), null);
58+
});
59+
60+
it('[HDSP-U3] getAll returns all defaults', () => {
61+
const all = HDSProfile.getAll();
62+
for (const key of Object.keys(PROFILE_FIELDS)) {
63+
assert.strictEqual(all[key], null, `${key} should be null`);
64+
}
65+
});
66+
67+
it('[HDSP-U4] unhook resets after hook', async () => {
68+
await HDSProfile.hookToConnection(connection);
69+
assert.strictEqual(HDSProfile.isHooked, true);
70+
HDSProfile.unhook();
71+
assert.strictEqual(HDSProfile.isHooked, false);
72+
assert.strictEqual(HDSProfile.get('displayName'), null);
73+
});
74+
});
75+
76+
describe('[HDSP-H] hookToConnection', () => {
77+
it('[HDSP-H1] hooks to empty profile', async () => {
78+
await HDSProfile.hookToConnection(connection);
79+
assert.strictEqual(HDSProfile.isHooked, true);
80+
assert.strictEqual(HDSProfile.get('displayName'), null);
81+
assert.strictEqual(HDSProfile.get('name'), null);
82+
assert.strictEqual(HDSProfile.get('avatar'), null);
83+
});
84+
85+
it('[HDSP-H2] ignores events outside profile streams', async () => {
86+
// Create a contact/display-name event in a non-profile stream
87+
await connection.api([
88+
{ method: 'streams.create', params: { id: 'other-stream', name: 'Other' } },
89+
{ method: 'events.create', params: { streamIds: ['other-stream'], type: 'contact/display-name', content: 'Wrong' } },
90+
]);
91+
92+
await HDSProfile.hookToConnection(connection);
93+
// Should not pick up the event from other-stream
94+
assert.strictEqual(HDSProfile.get('displayName'), null);
95+
});
96+
});
97+
98+
describe('[HDSP-S] set profile fields', () => {
99+
it('[HDSP-S1] throws when not hooked', async () => {
100+
await assert.rejects(
101+
() => HDSProfile.set('displayName', 'Alice'),
102+
/hookToConnection/
103+
);
104+
});
105+
106+
it('[HDSP-S2] set displayName creates and persists', async () => {
107+
await HDSProfile.hookToConnection(connection);
108+
109+
await HDSProfile.set('displayName', 'Dr. Smith');
110+
assert.strictEqual(HDSProfile.get('displayName'), 'Dr. Smith');
111+
112+
// Verify persistence
113+
await HDSProfile.reload();
114+
assert.strictEqual(HDSProfile.get('displayName'), 'Dr. Smith');
115+
});
116+
117+
it('[HDSP-S3] update displayName modifies existing', async () => {
118+
await HDSProfile.hookToConnection(connection);
119+
assert.strictEqual(HDSProfile.get('displayName'), 'Dr. Smith');
120+
121+
await HDSProfile.set('displayName', 'Professor Smith');
122+
assert.strictEqual(HDSProfile.get('displayName'), 'Professor Smith');
123+
124+
await HDSProfile.reload();
125+
assert.strictEqual(HDSProfile.get('displayName'), 'Professor Smith');
126+
});
127+
128+
it('[HDSP-S4] set all profile fields', async () => {
129+
await HDSProfile.hookToConnection(connection);
130+
131+
await HDSProfile.set('name', 'John');
132+
await HDSProfile.set('surname', 'Smith');
133+
await HDSProfile.set('dateOfBirth', '1985-03-15');
134+
await HDSProfile.set('sex', 'male');
135+
await HDSProfile.set('country', 'Switzerland');
136+
137+
assert.strictEqual(HDSProfile.get('name'), 'John');
138+
assert.strictEqual(HDSProfile.get('surname'), 'Smith');
139+
assert.strictEqual(HDSProfile.get('dateOfBirth'), '1985-03-15');
140+
assert.strictEqual(HDSProfile.get('sex'), 'male');
141+
assert.strictEqual(HDSProfile.get('country'), 'Switzerland');
142+
143+
// Verify all persist
144+
await HDSProfile.reload();
145+
assert.strictEqual(HDSProfile.get('name'), 'John');
146+
assert.strictEqual(HDSProfile.get('surname'), 'Smith');
147+
assert.strictEqual(HDSProfile.get('dateOfBirth'), '1985-03-15');
148+
assert.strictEqual(HDSProfile.get('sex'), 'male');
149+
assert.strictEqual(HDSProfile.get('country'), 'Switzerland');
150+
});
151+
152+
it('[HDSP-S5] getAll returns all set values', async () => {
153+
await HDSProfile.hookToConnection(connection);
154+
const all = HDSProfile.getAll();
155+
assert.strictEqual(all.displayName, 'Professor Smith');
156+
assert.strictEqual(all.name, 'John');
157+
assert.strictEqual(all.surname, 'Smith');
158+
assert.strictEqual(all.dateOfBirth, '1985-03-15');
159+
assert.strictEqual(all.sex, 'male');
160+
assert.strictEqual(all.country, 'Switzerland');
161+
});
162+
});
163+
164+
describe('[HDSP-A] avatar', () => {
165+
it('[HDSP-A1] setAvatarFromFile throws when not hooked', async () => {
166+
const blob = new Blob(['test'], { type: 'image/png' });
167+
await assert.rejects(
168+
() => HDSProfile.setAvatarFromFile(blob, 'test.png'),
169+
/hookToConnection/
170+
);
171+
});
172+
173+
it('[HDSP-A2] setAvatarFromDataUrl throws when not hooked', async () => {
174+
await assert.rejects(
175+
() => HDSProfile.setAvatarFromDataUrl('data:image/png;base64,abc'),
176+
/hookToConnection/
177+
);
178+
});
179+
180+
it('[HDSP-A3] setAvatarFromDataUrl creates and reads back', async () => {
181+
await HDSProfile.hookToConnection(connection);
182+
assert.strictEqual(HDSProfile.get('avatar'), null);
183+
184+
const dataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
185+
await HDSProfile.setAvatarFromDataUrl(dataUrl);
186+
assert.strictEqual(HDSProfile.get('avatar'), dataUrl);
187+
assert.strictEqual(HDSProfile.getAvatarUrl(), dataUrl);
188+
189+
// Verify persistence
190+
await HDSProfile.reload();
191+
assert.strictEqual(HDSProfile.get('avatar'), dataUrl);
192+
});
193+
194+
it('[HDSP-A4] setAvatarFromDataUrl replaces existing', async () => {
195+
await HDSProfile.hookToConnection(connection);
196+
assert.ok(HDSProfile.get('avatar') !== null, 'Should have avatar from previous test');
197+
198+
const newDataUrl = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
199+
await HDSProfile.setAvatarFromDataUrl(newDataUrl);
200+
assert.strictEqual(HDSProfile.get('avatar'), newDataUrl);
201+
202+
await HDSProfile.reload();
203+
assert.strictEqual(HDSProfile.get('avatar'), newDataUrl);
204+
});
205+
206+
it('[HDSP-A5] setAvatarFromFile creates event with attachment', async () => {
207+
await HDSProfile.hookToConnection(connection);
208+
209+
// Create a small PNG blob (1x1 red pixel)
210+
const pngBytes = Uint8Array.from(atob(
211+
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg=='
212+
), c => c.charCodeAt(0));
213+
const blob = new Blob([pngBytes], { type: 'image/png' });
214+
215+
await HDSProfile.setAvatarFromFile(blob, 'red-pixel.png');
216+
217+
const avatarUrl = HDSProfile.getAvatarUrl();
218+
assert.ok(avatarUrl, 'Should have avatar URL');
219+
assert.ok(avatarUrl.includes('/events/'), 'Should be an attachment URL');
220+
assert.ok(avatarUrl.includes('red-pixel.png'), 'Should contain filename');
221+
assert.ok(avatarUrl.includes('auth='), 'Should contain auth token');
222+
223+
// Verify persistence
224+
await HDSProfile.reload();
225+
const reloadedUrl = HDSProfile.getAvatarUrl();
226+
assert.ok(reloadedUrl, 'Should still have avatar after reload');
227+
assert.ok(reloadedUrl.includes('/events/'), 'Should still be attachment URL');
228+
});
229+
230+
it('[HDSP-A6] setAvatarFromFile replaces previous avatar', async () => {
231+
await HDSProfile.hookToConnection(connection);
232+
const previousUrl = HDSProfile.getAvatarUrl();
233+
assert.ok(previousUrl, 'Should have avatar from previous test');
234+
235+
const blob = new Blob([new Uint8Array([0x89, 0x50, 0x4E, 0x47])], { type: 'image/png' });
236+
await HDSProfile.setAvatarFromFile(blob, 'new-avatar.png');
237+
238+
const newUrl = HDSProfile.getAvatarUrl();
239+
assert.ok(newUrl, 'Should have new avatar URL');
240+
assert.ok(newUrl.includes('new-avatar.png'), 'Should contain new filename');
241+
assert.notStrictEqual(newUrl, previousUrl, 'URL should differ from previous');
242+
});
243+
});
244+
245+
describe('[HDSP-R] reload', () => {
246+
it('[HDSP-R1] reload reflects server state', async () => {
247+
await HDSProfile.hookToConnection(connection);
248+
const before = HDSProfile.get('displayName');
249+
250+
// Modify directly via API (bypassing HDSProfile)
251+
const events = await connection.apiOne('events.get', {
252+
streams: ['profile-display-name'],
253+
types: ['contact/display-name'],
254+
limit: 1
255+
}, 'events');
256+
assert.ok(events.length > 0, 'Should have displayName event');
257+
await connection.apiOne('events.update', {
258+
id: events[0].id,
259+
update: { content: 'Changed Externally' }
260+
}, 'event');
261+
262+
// HDSProfile still has old value
263+
assert.strictEqual(HDSProfile.get('displayName'), before);
264+
265+
// After reload, picks up the change
266+
await HDSProfile.reload();
267+
assert.strictEqual(HDSProfile.get('displayName'), 'Changed Externally');
268+
});
269+
});
270+
271+
describe('[HDSP-X] readFromConnection', () => {
272+
let sharedConnection;
273+
274+
before(async () => {
275+
// Create a shared access with profile read permission
276+
const access = await connection.apiOne('accesses.create', {
277+
name: 'profile-reader',
278+
type: 'shared',
279+
permissions: [{ streamId: 'profile', level: 'read' }]
280+
}, 'access');
281+
sharedConnection = new pryv.Connection(access.apiEndpoint);
282+
});
283+
284+
it('[HDSP-X1] reads profile via shared connection', async () => {
285+
const profile = await HDSProfile.readFromConnection(sharedConnection);
286+
assert.strictEqual(profile.displayName, 'Changed Externally');
287+
assert.strictEqual(profile.name, 'John');
288+
assert.strictEqual(profile.surname, 'Smith');
289+
assert.strictEqual(profile.sex, 'male');
290+
});
291+
292+
it('[HDSP-X2] reads avatar via shared connection', async () => {
293+
const profile = await HDSProfile.readFromConnection(sharedConnection);
294+
assert.ok(profile.avatar, 'Should have avatar URL');
295+
assert.ok(profile.avatar.includes('/events/'), 'Avatar should be an attachment URL');
296+
assert.ok(profile.avatar.includes('auth='), 'Avatar URL should contain auth token');
297+
});
298+
299+
it('[HDSP-X3] does not affect singleton state', async () => {
300+
HDSProfile.unhook();
301+
assert.strictEqual(HDSProfile.isHooked, false);
302+
303+
const profile = await HDSProfile.readFromConnection(sharedConnection);
304+
assert.ok(profile.displayName, 'Should read profile');
305+
assert.strictEqual(HDSProfile.isHooked, false, 'Singleton should remain unhooked');
306+
assert.strictEqual(HDSProfile.get('displayName'), null, 'Singleton values should be unchanged');
307+
});
308+
309+
it('[HDSP-X4] returns defaults when connection has no profile access', async () => {
310+
// Create a restricted access without any profile permission
311+
await connection.api([
312+
{ method: 'streams.create', params: { id: 'unrelated-stream', name: 'Unrelated' } },
313+
]);
314+
const restrictedAccess = await connection.apiOne('accesses.create', {
315+
name: 'no-profile',
316+
type: 'shared',
317+
permissions: [{ streamId: 'unrelated-stream', level: 'read' }]
318+
}, 'access');
319+
const restrictedConn = new pryv.Connection(restrictedAccess.apiEndpoint);
320+
321+
const profile = await HDSProfile.readFromConnection(restrictedConn);
322+
assert.strictEqual(profile.displayName, null, 'Should not see displayName');
323+
assert.strictEqual(profile.name, null, 'Should not see name');
324+
assert.strictEqual(profile.surname, null, 'Should not see surname');
325+
assert.strictEqual(profile.avatar, null, 'Should not see avatar');
326+
});
327+
});
328+
});

tests/HDSSettings.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { assert } from './test-utils/deps-node.js';
2-
import { HDSSettings, SETTING_TYPES } from '../js/index.js';
2+
import { HDSSettings, SETTING_TYPES } from '../ts/index.ts';
33

44
// Mock connection that captures API calls
55
function createMockConnection (responses = {}) {

tests/MonitorScope.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { assert } from './test-utils/deps-node.js';
2-
import { MonitorScope } from '../js/index.js';
2+
import { MonitorScope } from '../ts/index.ts';
33

44
// Mock connection that simulates Pryv API responses
55
function createMockConnection (responses = {}) {

tests/applicationClass.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { assert } from './test-utils/deps-node.js';
22
import { createUserAndPermissions } from './test-utils/pryvService.js';
3-
import HDSLib from '../js/index.js';
4-
const Application = HDSLib.appTemplates.Application;
3+
import HDSLib from '../ts/index.ts';
54
import { helperNewAppManaging } from './test-utils/helpersAppTemplate.js';
5+
const Application = HDSLib.appTemplates.Application;
66

77
describe('[APAX] Application class', function () {
88
this.timeout(5000);

tests/apptemplates.test.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import { assert } from './test-utils/deps-node.js';
22
import { pryv, createUserPermissions } from './test-utils/pryvService.js';
3-
import HDSLib from '../js/index.js';
4-
const { AppManagingAccount, AppClientAccount, Collector, CollectorClient } = HDSLib.appTemplates;
5-
import { HDSLibError } from '../js/errors.js';
6-
import { initHDSModel } from '../js/index.js';
3+
import HDSLib, { HDSLibError, initHDSModel } from '../ts/index.ts';
74
import { helperNewAppAndUsers, helperNewInvite, helperNewAppManaging } from './test-utils/helpersAppTemplate.js';
5+
const { AppManagingAccount, AppClientAccount, Collector, CollectorClient } = HDSLib.appTemplates;
86

97
describe('[APTX] appTemplates', function () {
108
this.timeout(10000);

tests/apptemplatesRequest.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { initHDSModel } from '../js/index.js';
1+
import { initHDSModel } from '../ts/index.ts';
22
import { helperNewAppManaging } from './test-utils/helpersAppTemplate.js';
33
import { assert } from './test-utils/deps-node.js';
4-
import { CollectorRequest } from '../js/appTemplates/CollectorRequest.js';
4+
import { CollectorRequest } from '../ts/appTemplates/CollectorRequest.ts';
55

66
describe('[APRX] appTemplates Requests', function () {
77
this.timeout(8000);

0 commit comments

Comments
 (0)