javascript-solid-server 0.0.11 → 0.0.13

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.
@@ -0,0 +1,427 @@
1
+ /**
2
+ * Identity Provider Tests
3
+ */
4
+
5
+ import { describe, it, before, after, beforeEach } from 'node:test';
6
+ import assert from 'node:assert';
7
+ import { createServer } from '../src/server.js';
8
+ import fs from 'fs-extra';
9
+ import path from 'path';
10
+
11
+ const TEST_PORT = 3099;
12
+ const TEST_HOST = 'localhost';
13
+ const BASE_URL = `http://${TEST_HOST}:${TEST_PORT}`;
14
+ const DATA_DIR = './test-data-idp';
15
+
16
+ describe('Identity Provider', () => {
17
+ let server;
18
+
19
+ before(async () => {
20
+ // Clean up any existing test data
21
+ await fs.remove(DATA_DIR);
22
+ await fs.ensureDir(DATA_DIR);
23
+
24
+ // Create server with IdP enabled
25
+ server = createServer({
26
+ logger: false,
27
+ root: DATA_DIR,
28
+ idp: true,
29
+ idpIssuer: BASE_URL,
30
+ });
31
+
32
+ await server.listen({ port: TEST_PORT, host: TEST_HOST });
33
+ });
34
+
35
+ after(async () => {
36
+ await server.close();
37
+ await fs.remove(DATA_DIR);
38
+ });
39
+
40
+ describe('OIDC Discovery', () => {
41
+ it('should serve /.well-known/openid-configuration', async () => {
42
+ const res = await fetch(`${BASE_URL}/.well-known/openid-configuration`);
43
+ assert.strictEqual(res.status, 200);
44
+
45
+ const config = await res.json();
46
+ assert.strictEqual(config.issuer, BASE_URL);
47
+ assert.ok(config.authorization_endpoint);
48
+ assert.ok(config.token_endpoint);
49
+ assert.ok(config.jwks_uri);
50
+ });
51
+
52
+ it('should include required Solid-OIDC endpoints', async () => {
53
+ const res = await fetch(`${BASE_URL}/.well-known/openid-configuration`);
54
+ const config = await res.json();
55
+
56
+ assert.ok(config.registration_endpoint, 'should have registration endpoint');
57
+ assert.ok(config.scopes_supported.includes('webid'), 'should support webid scope');
58
+ assert.ok(config.dpop_signing_alg_values_supported, 'should support DPoP');
59
+ });
60
+
61
+ it('should serve /.well-known/jwks.json', async () => {
62
+ const res = await fetch(`${BASE_URL}/.well-known/jwks.json`);
63
+ assert.strictEqual(res.status, 200);
64
+
65
+ const jwks = await res.json();
66
+ assert.ok(Array.isArray(jwks.keys));
67
+ assert.ok(jwks.keys.length > 0, 'should have at least one key');
68
+ // Keys should be public (no 'd' component)
69
+ assert.ok(!jwks.keys[0].d, 'should not expose private key component');
70
+ });
71
+ });
72
+
73
+ describe('Pod Creation with IdP', () => {
74
+ it('should require email when IdP is enabled', async () => {
75
+ const res = await fetch(`${BASE_URL}/.pods`, {
76
+ method: 'POST',
77
+ headers: { 'Content-Type': 'application/json' },
78
+ body: JSON.stringify({ name: 'noemail' }),
79
+ });
80
+
81
+ assert.strictEqual(res.status, 400);
82
+ const body = await res.json();
83
+ assert.ok(body.error.includes('Email'));
84
+ });
85
+
86
+ it('should require password when IdP is enabled', async () => {
87
+ const res = await fetch(`${BASE_URL}/.pods`, {
88
+ method: 'POST',
89
+ headers: { 'Content-Type': 'application/json' },
90
+ body: JSON.stringify({ name: 'nopass', email: 'test@example.com' }),
91
+ });
92
+
93
+ assert.strictEqual(res.status, 400);
94
+ const body = await res.json();
95
+ assert.ok(body.error.includes('Password'));
96
+ });
97
+
98
+ it('should require minimum password length', async () => {
99
+ const res = await fetch(`${BASE_URL}/.pods`, {
100
+ method: 'POST',
101
+ headers: { 'Content-Type': 'application/json' },
102
+ body: JSON.stringify({ name: 'shortpass', email: 'test@example.com', password: 'short' }),
103
+ });
104
+
105
+ assert.strictEqual(res.status, 400);
106
+ const body = await res.json();
107
+ assert.ok(body.error.includes('8'));
108
+ });
109
+
110
+ it('should create pod with account', async () => {
111
+ const uniqueId = Date.now();
112
+ const res = await fetch(`${BASE_URL}/.pods`, {
113
+ method: 'POST',
114
+ headers: { 'Content-Type': 'application/json' },
115
+ body: JSON.stringify({
116
+ name: `idpuser${uniqueId}`,
117
+ email: `idpuser${uniqueId}@example.com`,
118
+ password: 'securepassword123',
119
+ }),
120
+ });
121
+
122
+ assert.strictEqual(res.status, 201);
123
+ const body = await res.json();
124
+
125
+ assert.strictEqual(body.name, `idpuser${uniqueId}`);
126
+ assert.ok(body.webId.includes(`idpuser${uniqueId}`));
127
+ assert.ok(body.podUri.includes(`idpuser${uniqueId}`));
128
+ assert.ok(body.idpIssuer, 'should include IdP issuer');
129
+ assert.ok(body.loginUrl, 'should include login URL');
130
+ // Should NOT have simple token when IdP is enabled
131
+ assert.ok(!body.token, 'should not have simple token');
132
+ });
133
+
134
+ it('should reject duplicate email', async () => {
135
+ const uniqueId = Date.now();
136
+ const duplicateEmail = `duplicate${uniqueId}@example.com`;
137
+
138
+ // First user
139
+ await fetch(`${BASE_URL}/.pods`, {
140
+ method: 'POST',
141
+ headers: { 'Content-Type': 'application/json' },
142
+ body: JSON.stringify({
143
+ name: `first${uniqueId}`,
144
+ email: duplicateEmail,
145
+ password: 'password123',
146
+ }),
147
+ });
148
+
149
+ // Second user with same email
150
+ const res = await fetch(`${BASE_URL}/.pods`, {
151
+ method: 'POST',
152
+ headers: { 'Content-Type': 'application/json' },
153
+ body: JSON.stringify({
154
+ name: `second${uniqueId}`,
155
+ email: duplicateEmail,
156
+ password: 'password456',
157
+ }),
158
+ });
159
+
160
+ assert.strictEqual(res.status, 409);
161
+ const body = await res.json();
162
+ assert.ok(body.error.includes('Email'));
163
+ });
164
+ });
165
+
166
+ describe('Login Interaction', () => {
167
+ it('should respond to authorization endpoint', async () => {
168
+ // Start an authorization flow
169
+ // Various responses are acceptable - 302/303 (redirect), 400 (bad request), 404 (no route)
170
+ // This just verifies the server handles the request
171
+ const res = await fetch(`${BASE_URL}/idp/auth?client_id=test&redirect_uri=http://localhost&response_type=code&scope=openid`, {
172
+ redirect: 'manual',
173
+ });
174
+
175
+ // oidc-provider mounted via middie may return different status codes
176
+ // The important thing is it doesn't crash and returns a valid HTTP response
177
+ assert.ok(res.status >= 200 && res.status < 600, `got valid HTTP status ${res.status}`);
178
+ });
179
+ });
180
+ });
181
+
182
+ describe('Identity Provider - Accounts', () => {
183
+ let server;
184
+ const ACCOUNTS_DATA_DIR = './test-data-idp-accounts';
185
+
186
+ before(async () => {
187
+ await fs.remove(ACCOUNTS_DATA_DIR);
188
+ await fs.ensureDir(ACCOUNTS_DATA_DIR);
189
+
190
+ server = createServer({
191
+ logger: false,
192
+ root: ACCOUNTS_DATA_DIR,
193
+ idp: true,
194
+ idpIssuer: `http://${TEST_HOST}:${TEST_PORT + 1}`,
195
+ });
196
+
197
+ await server.listen({ port: TEST_PORT + 1, host: TEST_HOST });
198
+ });
199
+
200
+ after(async () => {
201
+ await server.close();
202
+ await fs.remove(ACCOUNTS_DATA_DIR);
203
+ });
204
+
205
+ it('should store account data in .idp directory', async () => {
206
+ const uniqueName = `stored${Date.now()}`;
207
+ const uniqueEmail = `stored${Date.now()}@example.com`;
208
+
209
+ const res = await fetch(`http://${TEST_HOST}:${TEST_PORT + 1}/.pods`, {
210
+ method: 'POST',
211
+ headers: { 'Content-Type': 'application/json' },
212
+ body: JSON.stringify({
213
+ name: uniqueName,
214
+ email: uniqueEmail,
215
+ password: 'password123',
216
+ }),
217
+ });
218
+
219
+ assert.strictEqual(res.status, 201, 'pod creation should succeed');
220
+
221
+ // Check that account data exists
222
+ const accountsDir = path.join(ACCOUNTS_DATA_DIR, '.idp', 'accounts');
223
+ const exists = await fs.pathExists(accountsDir);
224
+ assert.ok(exists, 'accounts directory should exist');
225
+
226
+ // Check email index
227
+ const emailIndex = await fs.readJson(path.join(accountsDir, '_email_index.json'));
228
+ assert.ok(emailIndex[uniqueEmail], 'email index should contain account');
229
+ });
230
+
231
+ it('should hash passwords', async () => {
232
+ const uniqueName = `hashed${Date.now()}`;
233
+ const uniqueEmail = `hashed${Date.now()}@example.com`;
234
+
235
+ const res = await fetch(`http://${TEST_HOST}:${TEST_PORT + 1}/.pods`, {
236
+ method: 'POST',
237
+ headers: { 'Content-Type': 'application/json' },
238
+ body: JSON.stringify({
239
+ name: uniqueName,
240
+ email: uniqueEmail,
241
+ password: 'mypassword',
242
+ }),
243
+ });
244
+
245
+ assert.strictEqual(res.status, 201, 'pod creation should succeed');
246
+
247
+ // Read account file
248
+ const accountsDir = path.join(ACCOUNTS_DATA_DIR, '.idp', 'accounts');
249
+ const emailIndex = await fs.readJson(path.join(accountsDir, '_email_index.json'));
250
+ const accountId = emailIndex[uniqueEmail];
251
+ const account = await fs.readJson(path.join(accountsDir, `${accountId}.json`));
252
+
253
+ // Password should be hashed, not plain text
254
+ assert.ok(account.passwordHash, 'should have passwordHash');
255
+ assert.ok(account.passwordHash.startsWith('$2'), 'should be bcrypt hash');
256
+ assert.ok(!account.password, 'should not store plain password');
257
+ });
258
+ });
259
+
260
+ describe('Identity Provider - Credentials Endpoint', () => {
261
+ let server;
262
+ const CREDS_DATA_DIR = './test-data-idp-creds';
263
+ const CREDS_PORT = 3101;
264
+ const CREDS_URL = `http://${TEST_HOST}:${CREDS_PORT}`;
265
+
266
+ before(async () => {
267
+ await fs.remove(CREDS_DATA_DIR);
268
+ await fs.ensureDir(CREDS_DATA_DIR);
269
+
270
+ server = createServer({
271
+ logger: false,
272
+ root: CREDS_DATA_DIR,
273
+ idp: true,
274
+ idpIssuer: CREDS_URL,
275
+ });
276
+
277
+ await server.listen({ port: CREDS_PORT, host: TEST_HOST });
278
+
279
+ // Create a test user
280
+ await fetch(`${CREDS_URL}/.pods`, {
281
+ method: 'POST',
282
+ headers: { 'Content-Type': 'application/json' },
283
+ body: JSON.stringify({
284
+ name: 'credtest',
285
+ email: 'credtest@example.com',
286
+ password: 'testpassword123',
287
+ }),
288
+ });
289
+ });
290
+
291
+ after(async () => {
292
+ await server.close();
293
+ await fs.remove(CREDS_DATA_DIR);
294
+ });
295
+
296
+ describe('GET /idp/credentials', () => {
297
+ it('should return endpoint info', async () => {
298
+ const res = await fetch(`${CREDS_URL}/idp/credentials`);
299
+ assert.strictEqual(res.status, 200);
300
+
301
+ const info = await res.json();
302
+ assert.ok(info.endpoint);
303
+ assert.strictEqual(info.method, 'POST');
304
+ assert.ok(info.parameters.email);
305
+ assert.ok(info.parameters.password);
306
+ });
307
+ });
308
+
309
+ describe('POST /idp/credentials', () => {
310
+ it('should return 400 for missing credentials', async () => {
311
+ const res = await fetch(`${CREDS_URL}/idp/credentials`, {
312
+ method: 'POST',
313
+ headers: { 'Content-Type': 'application/json' },
314
+ body: JSON.stringify({}),
315
+ });
316
+
317
+ assert.strictEqual(res.status, 400);
318
+ const body = await res.json();
319
+ assert.strictEqual(body.error, 'invalid_request');
320
+ });
321
+
322
+ it('should return 401 for wrong password', async () => {
323
+ const res = await fetch(`${CREDS_URL}/idp/credentials`, {
324
+ method: 'POST',
325
+ headers: { 'Content-Type': 'application/json' },
326
+ body: JSON.stringify({
327
+ email: 'credtest@example.com',
328
+ password: 'wrongpassword',
329
+ }),
330
+ });
331
+
332
+ assert.strictEqual(res.status, 401);
333
+ const body = await res.json();
334
+ assert.strictEqual(body.error, 'invalid_grant');
335
+ });
336
+
337
+ it('should return 401 for unknown email', async () => {
338
+ const res = await fetch(`${CREDS_URL}/idp/credentials`, {
339
+ method: 'POST',
340
+ headers: { 'Content-Type': 'application/json' },
341
+ body: JSON.stringify({
342
+ email: 'unknown@example.com',
343
+ password: 'anypassword',
344
+ }),
345
+ });
346
+
347
+ assert.strictEqual(res.status, 401);
348
+ });
349
+
350
+ it('should return access token for valid credentials', async () => {
351
+ const res = await fetch(`${CREDS_URL}/idp/credentials`, {
352
+ method: 'POST',
353
+ headers: { 'Content-Type': 'application/json' },
354
+ body: JSON.stringify({
355
+ email: 'credtest@example.com',
356
+ password: 'testpassword123',
357
+ }),
358
+ });
359
+
360
+ assert.strictEqual(res.status, 200);
361
+ const body = await res.json();
362
+
363
+ assert.ok(body.access_token, 'should have access_token');
364
+ assert.strictEqual(body.token_type, 'Bearer');
365
+ assert.ok(body.expires_in > 0, 'should have expires_in');
366
+ assert.ok(body.webid.includes('credtest'), 'should have webid');
367
+ });
368
+
369
+ it('should return simple token with webid for Bearer auth', async () => {
370
+ const res = await fetch(`${CREDS_URL}/idp/credentials`, {
371
+ method: 'POST',
372
+ headers: { 'Content-Type': 'application/json' },
373
+ body: JSON.stringify({
374
+ email: 'credtest@example.com',
375
+ password: 'testpassword123',
376
+ }),
377
+ });
378
+
379
+ const body = await res.json();
380
+
381
+ // Simple tokens have format: base64payload.signature
382
+ const parts = body.access_token.split('.');
383
+ assert.strictEqual(parts.length, 2, 'simple token has 2 parts');
384
+
385
+ // Decode the payload
386
+ const payload = JSON.parse(Buffer.from(parts[0], 'base64url').toString());
387
+
388
+ assert.ok(payload.webId, 'token should have webId');
389
+ assert.ok(payload.webId.includes('credtest'), 'webId should reference user');
390
+ assert.ok(payload.exp > payload.iat, 'should have valid expiry');
391
+ });
392
+
393
+ it('should work with form-encoded body', async () => {
394
+ const res = await fetch(`${CREDS_URL}/idp/credentials`, {
395
+ method: 'POST',
396
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
397
+ body: 'email=credtest%40example.com&password=testpassword123',
398
+ });
399
+
400
+ assert.strictEqual(res.status, 200);
401
+ const body = await res.json();
402
+ assert.ok(body.access_token);
403
+ });
404
+
405
+ it('should allow using token to access protected resource', async () => {
406
+ // Get access token
407
+ const tokenRes = await fetch(`${CREDS_URL}/idp/credentials`, {
408
+ method: 'POST',
409
+ headers: { 'Content-Type': 'application/json' },
410
+ body: JSON.stringify({
411
+ email: 'credtest@example.com',
412
+ password: 'testpassword123',
413
+ }),
414
+ });
415
+
416
+ const { access_token } = await tokenRes.json();
417
+
418
+ // Try to access private resource
419
+ const res = await fetch(`${CREDS_URL}/credtest/private/`, {
420
+ headers: { 'Authorization': `Bearer ${access_token}` },
421
+ });
422
+
423
+ // Should succeed (not 401/403)
424
+ assert.ok([200, 404].includes(res.status), `expected 200 or 404, got ${res.status}`);
425
+ });
426
+ });
427
+ });