javascript-solid-server 0.0.3 → 0.0.6

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,175 @@
1
+ /**
2
+ * Authentication and Authorization tests
3
+ */
4
+
5
+ import { describe, it, before, after } from 'node:test';
6
+ import assert from 'node:assert';
7
+ import {
8
+ startTestServer,
9
+ stopTestServer,
10
+ request,
11
+ createTestPod,
12
+ getPodToken,
13
+ getBaseUrl,
14
+ assertStatus,
15
+ assertHeader
16
+ } from './helpers.js';
17
+
18
+ describe('Authentication', () => {
19
+ before(async () => {
20
+ await startTestServer();
21
+ });
22
+
23
+ after(async () => {
24
+ await stopTestServer();
25
+ });
26
+
27
+ describe('Token Authentication', () => {
28
+ it('should return token on pod creation', async () => {
29
+ const result = await createTestPod('authtest');
30
+
31
+ assert.ok(result.token, 'Should return a token');
32
+ assert.ok(result.token.includes('.'), 'Token should have signature');
33
+ });
34
+
35
+ it('should allow authenticated access to private resources', async () => {
36
+ await createTestPod('privatetest');
37
+
38
+ // Should succeed with auth
39
+ const res = await request('/privatetest/private/', { auth: 'privatetest' });
40
+ assertStatus(res, 200);
41
+ });
42
+
43
+ it('should deny unauthenticated access to private resources', async () => {
44
+ await createTestPod('denytest');
45
+
46
+ // Should fail without auth
47
+ const res = await request('/denytest/private/');
48
+ assertStatus(res, 401);
49
+ });
50
+
51
+ it('should return 403 for wrong user accessing private resources', async () => {
52
+ await createTestPod('user1');
53
+ await createTestPod('user2');
54
+
55
+ // User2 trying to access User1's private folder
56
+ const res = await request('/user1/private/', { auth: 'user2' });
57
+ assertStatus(res, 403);
58
+ });
59
+
60
+ it('should accept Bearer token format', async () => {
61
+ await createTestPod('bearertest');
62
+ const token = getPodToken('bearertest');
63
+
64
+ const res = await fetch(`${getBaseUrl()}/bearertest/private/`, {
65
+ headers: { 'Authorization': `Bearer ${token}` }
66
+ });
67
+ assertStatus(res, 200);
68
+ });
69
+
70
+ it('should reject invalid tokens', async () => {
71
+ await createTestPod('invalidtest');
72
+
73
+ const res = await fetch(`${getBaseUrl()}/invalidtest/private/`, {
74
+ headers: { 'Authorization': 'Bearer invalid.token' }
75
+ });
76
+ assertStatus(res, 401);
77
+ });
78
+ });
79
+
80
+ describe('WAC Enforcement', () => {
81
+ it('should allow public read on pod root', async () => {
82
+ await createTestPod('publicread');
83
+
84
+ // Public folder should be readable without auth
85
+ const res = await request('/publicread/public/');
86
+ assertStatus(res, 200);
87
+ });
88
+
89
+ it('should allow public read on explicit public folders', async () => {
90
+ await createTestPod('explicitpublic');
91
+
92
+ // Root ACL has public read default
93
+ const res = await request('/explicitpublic/');
94
+ assertStatus(res, 200);
95
+ });
96
+
97
+ it('should allow authenticated write to owned resources', async () => {
98
+ await createTestPod('writetest');
99
+
100
+ const res = await request('/writetest/public/test.txt', {
101
+ method: 'PUT',
102
+ body: 'test content',
103
+ auth: 'writetest'
104
+ });
105
+ assertStatus(res, 201);
106
+ });
107
+
108
+ it('should deny unauthenticated write', async () => {
109
+ await createTestPod('nowrite');
110
+
111
+ const res = await request('/nowrite/public/test.txt', {
112
+ method: 'PUT',
113
+ body: 'test content'
114
+ });
115
+ assertStatus(res, 401);
116
+ });
117
+
118
+ it('should deny other user write to owned resources', async () => {
119
+ await createTestPod('owner1');
120
+ await createTestPod('attacker');
121
+
122
+ const res = await request('/owner1/public/test.txt', {
123
+ method: 'PUT',
124
+ body: 'malicious content',
125
+ auth: 'attacker'
126
+ });
127
+ assertStatus(res, 403);
128
+ });
129
+
130
+ it('should allow public append to inbox', async () => {
131
+ await createTestPod('inboxtest');
132
+
133
+ // POST to inbox should work for anyone (public append)
134
+ const res = await request('/inboxtest/inbox/', {
135
+ method: 'POST',
136
+ headers: {
137
+ 'Content-Type': 'application/json',
138
+ 'Slug': 'notification'
139
+ },
140
+ body: JSON.stringify({ type: 'notification' })
141
+ });
142
+ assertStatus(res, 201);
143
+ });
144
+
145
+ it('should deny public read on inbox', async () => {
146
+ await createTestPod('inboxread');
147
+
148
+ // GET inbox should fail for unauthenticated
149
+ const res = await request('/inboxread/inbox/');
150
+ assertStatus(res, 401);
151
+ });
152
+ });
153
+
154
+ describe('WAC-Allow Header', () => {
155
+ it('should include user permissions for authenticated requests', async () => {
156
+ await createTestPod('wacallow');
157
+
158
+ const res = await request('/wacallow/public/', { auth: 'wacallow' });
159
+ const wacAllow = res.headers.get('WAC-Allow');
160
+
161
+ assert.ok(wacAllow, 'Should have WAC-Allow header');
162
+ assert.ok(wacAllow.includes('user='), 'Should include user permissions');
163
+ });
164
+
165
+ it('should include public permissions', async () => {
166
+ await createTestPod('wacpublic');
167
+
168
+ const res = await request('/wacpublic/public/');
169
+ const wacAllow = res.headers.get('WAC-Allow');
170
+
171
+ assert.ok(wacAllow, 'Should have WAC-Allow header');
172
+ assert.ok(wacAllow.includes('public='), 'Should include public permissions');
173
+ });
174
+ });
175
+ });
package/test/helpers.js CHANGED
@@ -11,6 +11,9 @@ const TEST_DATA_DIR = './data';
11
11
  let server = null;
12
12
  let baseUrl = null;
13
13
 
14
+ // Store tokens for pods by name
15
+ const podTokens = new Map();
16
+
14
17
  /**
15
18
  * Start a test server on a random available port
16
19
  * @returns {Promise<{server: object, baseUrl: string}>}
@@ -39,6 +42,8 @@ export async function stopTestServer() {
39
42
  }
40
43
  // Clean up test data
41
44
  await fs.emptyDir(TEST_DATA_DIR);
45
+ // Clear tokens
46
+ podTokens.clear();
42
47
  }
43
48
 
44
49
  /**
@@ -51,7 +56,7 @@ export function getBaseUrl() {
51
56
  /**
52
57
  * Create a pod for testing
53
58
  * @param {string} name - Pod name
54
- * @returns {Promise<{webId: string, podUri: string}>}
59
+ * @returns {Promise<{webId: string, podUri: string, token: string}>}
55
60
  */
56
61
  export async function createTestPod(name) {
57
62
  const res = await fetch(`${baseUrl}/.pods`, {
@@ -64,18 +69,47 @@ export async function createTestPod(name) {
64
69
  throw new Error(`Failed to create pod: ${res.status}`);
65
70
  }
66
71
 
67
- return res.json();
72
+ const result = await res.json();
73
+
74
+ // Store the token for this pod
75
+ if (result.token) {
76
+ podTokens.set(name, result.token);
77
+ }
78
+
79
+ return result;
80
+ }
81
+
82
+ /**
83
+ * Get token for a pod
84
+ * @param {string} name - Pod name
85
+ * @returns {string|null}
86
+ */
87
+ export function getPodToken(name) {
88
+ return podTokens.get(name) || null;
68
89
  }
69
90
 
70
91
  /**
71
92
  * Make a request to the test server
72
93
  * @param {string} path - URL path
73
- * @param {object} options - fetch options
94
+ * @param {object} options - fetch options (can include `auth: 'podname'` for authenticated requests)
74
95
  * @returns {Promise<Response>}
75
96
  */
76
97
  export async function request(urlPath, options = {}) {
77
98
  const url = urlPath.startsWith('http') ? urlPath : `${baseUrl}${urlPath}`;
78
- return fetch(url, options);
99
+
100
+ // Handle authentication
101
+ const { auth, ...fetchOptions } = options;
102
+ if (auth) {
103
+ const token = podTokens.get(auth);
104
+ if (token) {
105
+ fetchOptions.headers = {
106
+ ...fetchOptions.headers,
107
+ 'Authorization': `Bearer ${token}`
108
+ };
109
+ }
110
+ }
111
+
112
+ return fetch(url, fetchOptions);
79
113
  }
80
114
 
81
115
  /**
package/test/ldp.test.js CHANGED
@@ -41,11 +41,12 @@ describe('LDP CRUD Operations', () => {
41
41
  });
42
42
 
43
43
  it('should return resource content', async () => {
44
- // Create resource first
44
+ // Create resource first (authenticated)
45
45
  await request('/ldptest/public/test.json', {
46
46
  method: 'PUT',
47
47
  headers: { 'Content-Type': 'application/json' },
48
- body: JSON.stringify({ hello: 'world' })
48
+ body: JSON.stringify({ hello: 'world' }),
49
+ auth: 'ldptest'
49
50
  });
50
51
 
51
52
  const res = await request('/ldptest/public/test.json');
@@ -58,7 +59,8 @@ describe('LDP CRUD Operations', () => {
58
59
  it('should return ETag header', async () => {
59
60
  await request('/ldptest/public/etag-test.txt', {
60
61
  method: 'PUT',
61
- body: 'test content'
62
+ body: 'test content',
63
+ auth: 'ldptest'
62
64
  });
63
65
 
64
66
  const res = await request('/ldptest/public/etag-test.txt');
@@ -72,7 +74,8 @@ describe('LDP CRUD Operations', () => {
72
74
  it('should return headers without body', async () => {
73
75
  await request('/ldptest/public/head-test.txt', {
74
76
  method: 'PUT',
75
- body: 'test content'
77
+ body: 'test content',
78
+ auth: 'ldptest'
76
79
  });
77
80
 
78
81
  const res = await request('/ldptest/public/head-test.txt', {
@@ -101,7 +104,8 @@ describe('LDP CRUD Operations', () => {
101
104
  const res = await request('/ldptest/public/new-resource.json', {
102
105
  method: 'PUT',
103
106
  headers: { 'Content-Type': 'application/json' },
104
- body: JSON.stringify({ created: true })
107
+ body: JSON.stringify({ created: true }),
108
+ auth: 'ldptest'
105
109
  });
106
110
 
107
111
  assertStatus(res, 201);
@@ -112,13 +116,15 @@ describe('LDP CRUD Operations', () => {
112
116
  // Create
113
117
  await request('/ldptest/public/update-me.txt', {
114
118
  method: 'PUT',
115
- body: 'original'
119
+ body: 'original',
120
+ auth: 'ldptest'
116
121
  });
117
122
 
118
123
  // Update
119
124
  const res = await request('/ldptest/public/update-me.txt', {
120
125
  method: 'PUT',
121
- body: 'updated'
126
+ body: 'updated',
127
+ auth: 'ldptest'
122
128
  });
123
129
 
124
130
  assertStatus(res, 204);
@@ -132,7 +138,8 @@ describe('LDP CRUD Operations', () => {
132
138
  it('should create parent containers', async () => {
133
139
  const res = await request('/ldptest/public/nested/deep/file.txt', {
134
140
  method: 'PUT',
135
- body: 'nested content'
141
+ body: 'nested content',
142
+ auth: 'ldptest'
136
143
  });
137
144
 
138
145
  assertStatus(res, 201);
@@ -145,7 +152,8 @@ describe('LDP CRUD Operations', () => {
145
152
  it('should reject PUT to container path', async () => {
146
153
  const res = await request('/ldptest/public/invalid/', {
147
154
  method: 'PUT',
148
- body: 'cannot put to container'
155
+ body: 'cannot put to container',
156
+ auth: 'ldptest'
149
157
  });
150
158
 
151
159
  assertStatus(res, 409);
@@ -160,7 +168,8 @@ describe('LDP CRUD Operations', () => {
160
168
  'Content-Type': 'application/json',
161
169
  'Slug': 'posted-resource'
162
170
  },
163
- body: JSON.stringify({ posted: true })
171
+ body: JSON.stringify({ posted: true }),
172
+ auth: 'ldptest'
164
173
  });
165
174
 
166
175
  assertStatus(res, 201);
@@ -179,7 +188,8 @@ describe('LDP CRUD Operations', () => {
179
188
  'Content-Type': 'text/plain',
180
189
  'Slug': 'my-custom-name.txt'
181
190
  },
182
- body: 'slug test'
191
+ body: 'slug test',
192
+ auth: 'ldptest'
183
193
  });
184
194
 
185
195
  const location = res.headers.get('Location');
@@ -192,7 +202,8 @@ describe('LDP CRUD Operations', () => {
192
202
  headers: {
193
203
  'Slug': 'new-container',
194
204
  'Link': '<http://www.w3.org/ns/ldp#BasicContainer>; rel="type"'
195
- }
205
+ },
206
+ auth: 'ldptest'
196
207
  });
197
208
 
198
209
  assertStatus(res, 201);
@@ -208,12 +219,14 @@ describe('LDP CRUD Operations', () => {
208
219
  it('should reject POST to non-container', async () => {
209
220
  await request('/ldptest/public/file-not-container.txt', {
210
221
  method: 'PUT',
211
- body: 'just a file'
222
+ body: 'just a file',
223
+ auth: 'ldptest'
212
224
  });
213
225
 
214
226
  const res = await request('/ldptest/public/file-not-container.txt', {
215
227
  method: 'POST',
216
- body: 'trying to post'
228
+ body: 'trying to post',
229
+ auth: 'ldptest'
217
230
  });
218
231
 
219
232
  assertStatus(res, 405);
@@ -224,11 +237,13 @@ describe('LDP CRUD Operations', () => {
224
237
  it('should delete resource', async () => {
225
238
  await request('/ldptest/public/to-delete.txt', {
226
239
  method: 'PUT',
227
- body: 'delete me'
240
+ body: 'delete me',
241
+ auth: 'ldptest'
228
242
  });
229
243
 
230
244
  const res = await request('/ldptest/public/to-delete.txt', {
231
- method: 'DELETE'
245
+ method: 'DELETE',
246
+ auth: 'ldptest'
232
247
  });
233
248
 
234
249
  assertStatus(res, 204);
@@ -240,7 +255,8 @@ describe('LDP CRUD Operations', () => {
240
255
 
241
256
  it('should return 404 for non-existent', async () => {
242
257
  const res = await request('/ldptest/public/never-existed.txt', {
243
- method: 'DELETE'
258
+ method: 'DELETE',
259
+ auth: 'ldptest'
244
260
  });
245
261
 
246
262
  assertStatus(res, 404);
@@ -253,11 +269,13 @@ describe('LDP CRUD Operations', () => {
253
269
  headers: {
254
270
  'Slug': 'container-to-delete',
255
271
  'Link': '<http://www.w3.org/ns/ldp#BasicContainer>; rel="type"'
256
- }
272
+ },
273
+ auth: 'ldptest'
257
274
  });
258
275
 
259
276
  const res = await request('/ldptest/public/container-to-delete/', {
260
- method: 'DELETE'
277
+ method: 'DELETE',
278
+ auth: 'ldptest'
261
279
  });
262
280
 
263
281
  assertStatus(res, 204);
@@ -291,7 +309,8 @@ describe('LDP CRUD Operations', () => {
291
309
  it('should return Link type header for resource', async () => {
292
310
  await request('/ldptest/public/resource-link.txt', {
293
311
  method: 'PUT',
294
- body: 'test'
312
+ body: 'test',
313
+ auth: 'ldptest'
295
314
  });
296
315
 
297
316
  const res = await request('/ldptest/public/resource-link.txt');
@@ -318,5 +337,27 @@ describe('LDP CRUD Operations', () => {
318
337
 
319
338
  assertHeader(res, 'Accept-Post');
320
339
  });
340
+
341
+ it('should return acl Link header for resource', async () => {
342
+ await request('/ldptest/public/acl-test.txt', {
343
+ method: 'PUT',
344
+ body: 'test',
345
+ auth: 'ldptest'
346
+ });
347
+
348
+ const res = await request('/ldptest/public/acl-test.txt');
349
+ const link = res.headers.get('Link');
350
+
351
+ assert.ok(link.includes('rel="acl"'), 'Should have acl link relation');
352
+ assert.ok(link.includes('acl-test.txt.acl'), 'ACL should be resource.acl');
353
+ });
354
+
355
+ it('should return acl Link header for container', async () => {
356
+ const res = await request('/ldptest/public/');
357
+ const link = res.headers.get('Link');
358
+
359
+ assert.ok(link.includes('rel="acl"'), 'Should have acl link relation');
360
+ assert.ok(link.includes('.acl'), 'Should link to .acl');
361
+ });
321
362
  });
322
363
  });
package/test/pod.test.js CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  startTestServer,
9
9
  stopTestServer,
10
10
  request,
11
+ createTestPod,
11
12
  assertStatus,
12
13
  assertHeader,
13
14
  assertHeaderContains
@@ -80,46 +81,38 @@ describe('Pod Lifecycle', () => {
80
81
 
81
82
  describe('Pod Structure', () => {
82
83
  it('should create standard folders', async () => {
83
- await request('/.pods', {
84
- method: 'POST',
85
- headers: { 'Content-Type': 'application/json' },
86
- body: JSON.stringify({ name: 'carol' })
87
- });
84
+ await createTestPod('carol');
88
85
 
89
- // Check inbox exists
90
- const inbox = await request('/carol/inbox/');
86
+ // Check inbox exists (needs auth - inbox only allows public append, not read)
87
+ const inbox = await request('/carol/inbox/', { auth: 'carol' });
91
88
  assertStatus(inbox, 200);
92
89
 
93
- // Check public exists
90
+ // Check public exists (public read via root ACL default)
94
91
  const pub = await request('/carol/public/');
95
92
  assertStatus(pub, 200);
96
93
 
97
- // Check private exists
98
- const priv = await request('/carol/private/');
94
+ // Check private exists (needs auth)
95
+ const priv = await request('/carol/private/', { auth: 'carol' });
99
96
  assertStatus(priv, 200);
100
97
 
101
- // Check settings exists
102
- const settings = await request('/carol/settings/');
98
+ // Check settings exists (needs auth)
99
+ const settings = await request('/carol/settings/', { auth: 'carol' });
103
100
  assertStatus(settings, 200);
104
101
  });
105
102
 
106
103
  it('should create settings files', async () => {
107
- await request('/.pods', {
108
- method: 'POST',
109
- headers: { 'Content-Type': 'application/json' },
110
- body: JSON.stringify({ name: 'dan' })
111
- });
104
+ await createTestPod('dan');
112
105
 
113
- // Check prefs
114
- const prefs = await request('/dan/settings/prefs');
106
+ // Check prefs (needs auth - settings is private)
107
+ const prefs = await request('/dan/settings/prefs', { auth: 'dan' });
115
108
  assertStatus(prefs, 200);
116
109
 
117
- // Check public type index
118
- const pubIndex = await request('/dan/settings/publicTypeIndex');
110
+ // Check public type index (needs auth)
111
+ const pubIndex = await request('/dan/settings/publicTypeIndex', { auth: 'dan' });
119
112
  assertStatus(pubIndex, 200);
120
113
 
121
- // Check private type index
122
- const privIndex = await request('/dan/settings/privateTypeIndex');
114
+ // Check private type index (needs auth)
115
+ const privIndex = await request('/dan/settings/privateTypeIndex', { auth: 'dan' });
123
116
  assertStatus(privIndex, 200);
124
117
  });
125
118
  });