javascript-solid-server 0.0.9 → 0.0.11

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,250 @@
1
+ /**
2
+ * Conditional Request Tests
3
+ *
4
+ * Tests If-Match and If-None-Match header support.
5
+ */
6
+
7
+ import { describe, it, before, after } from 'node:test';
8
+ import assert from 'node:assert';
9
+ import {
10
+ startTestServer,
11
+ stopTestServer,
12
+ request,
13
+ createTestPod,
14
+ assertStatus
15
+ } from './helpers.js';
16
+
17
+ describe('Conditional Requests', () => {
18
+ before(async () => {
19
+ await startTestServer();
20
+ await createTestPod('condtest');
21
+ });
22
+
23
+ after(async () => {
24
+ await stopTestServer();
25
+ });
26
+
27
+ describe('If-None-Match on GET', () => {
28
+ it('should return 304 Not Modified when ETag matches', async () => {
29
+ // Create a resource
30
+ await request('/condtest/public/etag-test.json', {
31
+ method: 'PUT',
32
+ headers: { 'Content-Type': 'application/ld+json' },
33
+ body: JSON.stringify({ '@id': '#test', 'http://example.org/value': 1 }),
34
+ auth: 'condtest'
35
+ });
36
+
37
+ // Get the ETag
38
+ const res1 = await request('/condtest/public/etag-test.json');
39
+ const etag = res1.headers.get('ETag');
40
+ assert.ok(etag, 'Response should have ETag');
41
+
42
+ // Request with matching If-None-Match
43
+ const res2 = await request('/condtest/public/etag-test.json', {
44
+ headers: { 'If-None-Match': etag }
45
+ });
46
+ assertStatus(res2, 304);
47
+ });
48
+
49
+ it('should return 200 when ETag does not match', async () => {
50
+ const res = await request('/condtest/public/etag-test.json', {
51
+ headers: { 'If-None-Match': '"different-etag"' }
52
+ });
53
+ assertStatus(res, 200);
54
+ });
55
+
56
+ it('should return 304 with If-None-Match: *', async () => {
57
+ const res = await request('/condtest/public/etag-test.json', {
58
+ headers: { 'If-None-Match': '*' }
59
+ });
60
+ assertStatus(res, 304);
61
+ });
62
+ });
63
+
64
+ describe('If-Match on PUT', () => {
65
+ it('should succeed when ETag matches', async () => {
66
+ // Create resource
67
+ await request('/condtest/public/match-test.json', {
68
+ method: 'PUT',
69
+ headers: { 'Content-Type': 'application/ld+json' },
70
+ body: JSON.stringify({ '@id': '#test', 'http://example.org/v': 1 }),
71
+ auth: 'condtest'
72
+ });
73
+
74
+ // Get ETag
75
+ const res1 = await request('/condtest/public/match-test.json');
76
+ const etag = res1.headers.get('ETag');
77
+
78
+ // Update with matching If-Match
79
+ const res2 = await request('/condtest/public/match-test.json', {
80
+ method: 'PUT',
81
+ headers: {
82
+ 'Content-Type': 'application/ld+json',
83
+ 'If-Match': etag
84
+ },
85
+ body: JSON.stringify({ '@id': '#test', 'http://example.org/v': 2 }),
86
+ auth: 'condtest'
87
+ });
88
+ assertStatus(res2, 204);
89
+ });
90
+
91
+ it('should return 412 when ETag does not match', async () => {
92
+ const res = await request('/condtest/public/match-test.json', {
93
+ method: 'PUT',
94
+ headers: {
95
+ 'Content-Type': 'application/ld+json',
96
+ 'If-Match': '"wrong-etag"'
97
+ },
98
+ body: JSON.stringify({ '@id': '#test', 'http://example.org/v': 3 }),
99
+ auth: 'condtest'
100
+ });
101
+ assertStatus(res, 412);
102
+ });
103
+
104
+ it('should succeed with If-Match: * on existing resource', async () => {
105
+ const res = await request('/condtest/public/match-test.json', {
106
+ method: 'PUT',
107
+ headers: {
108
+ 'Content-Type': 'application/ld+json',
109
+ 'If-Match': '*'
110
+ },
111
+ body: JSON.stringify({ '@id': '#test', 'http://example.org/v': 4 }),
112
+ auth: 'condtest'
113
+ });
114
+ assertStatus(res, 204);
115
+ });
116
+
117
+ it('should return 412 with If-Match: * on non-existent resource', async () => {
118
+ const res = await request('/condtest/public/nonexistent.json', {
119
+ method: 'PUT',
120
+ headers: {
121
+ 'Content-Type': 'application/ld+json',
122
+ 'If-Match': '*'
123
+ },
124
+ body: JSON.stringify({ '@id': '#new' }),
125
+ auth: 'condtest'
126
+ });
127
+ assertStatus(res, 412);
128
+ });
129
+ });
130
+
131
+ describe('If-None-Match on PUT', () => {
132
+ it('should succeed with If-None-Match: * on new resource', async () => {
133
+ const res = await request('/condtest/public/create-only.json', {
134
+ method: 'PUT',
135
+ headers: {
136
+ 'Content-Type': 'application/ld+json',
137
+ 'If-None-Match': '*'
138
+ },
139
+ body: JSON.stringify({ '@id': '#new' }),
140
+ auth: 'condtest'
141
+ });
142
+ assertStatus(res, 201);
143
+ });
144
+
145
+ it('should return 412 with If-None-Match: * on existing resource', async () => {
146
+ const res = await request('/condtest/public/create-only.json', {
147
+ method: 'PUT',
148
+ headers: {
149
+ 'Content-Type': 'application/ld+json',
150
+ 'If-None-Match': '*'
151
+ },
152
+ body: JSON.stringify({ '@id': '#update' }),
153
+ auth: 'condtest'
154
+ });
155
+ assertStatus(res, 412);
156
+ });
157
+ });
158
+
159
+ describe('If-Match on DELETE', () => {
160
+ it('should succeed when ETag matches', async () => {
161
+ // Create resource
162
+ await request('/condtest/public/delete-match.json', {
163
+ method: 'PUT',
164
+ headers: { 'Content-Type': 'application/ld+json' },
165
+ body: JSON.stringify({ '@id': '#delete' }),
166
+ auth: 'condtest'
167
+ });
168
+
169
+ // Get ETag
170
+ const res1 = await request('/condtest/public/delete-match.json');
171
+ const etag = res1.headers.get('ETag');
172
+
173
+ // Delete with matching If-Match
174
+ const res2 = await request('/condtest/public/delete-match.json', {
175
+ method: 'DELETE',
176
+ headers: { 'If-Match': etag },
177
+ auth: 'condtest'
178
+ });
179
+ assertStatus(res2, 204);
180
+ });
181
+
182
+ it('should return 412 when ETag does not match', async () => {
183
+ // Create resource
184
+ await request('/condtest/public/delete-nomatch.json', {
185
+ method: 'PUT',
186
+ headers: { 'Content-Type': 'application/ld+json' },
187
+ body: JSON.stringify({ '@id': '#delete' }),
188
+ auth: 'condtest'
189
+ });
190
+
191
+ const res = await request('/condtest/public/delete-nomatch.json', {
192
+ method: 'DELETE',
193
+ headers: { 'If-Match': '"wrong-etag"' },
194
+ auth: 'condtest'
195
+ });
196
+ assertStatus(res, 412);
197
+ });
198
+ });
199
+
200
+ describe('If-Match on PATCH', () => {
201
+ it('should succeed when ETag matches', async () => {
202
+ // Create resource
203
+ await request('/condtest/public/patch-match.json', {
204
+ method: 'PUT',
205
+ headers: { 'Content-Type': 'application/ld+json' },
206
+ body: JSON.stringify({ '@id': '#test', 'http://example.org/name': 'Original' }),
207
+ auth: 'condtest'
208
+ });
209
+
210
+ // Get ETag
211
+ const res1 = await request('/condtest/public/patch-match.json');
212
+ const etag = res1.headers.get('ETag');
213
+
214
+ // Patch with matching If-Match
215
+ const patch = `
216
+ @prefix solid: <http://www.w3.org/ns/solid/terms#>.
217
+ _:patch a solid:InsertDeletePatch;
218
+ solid:inserts { <#test> <http://example.org/updated> "true" }.
219
+ `;
220
+ const res2 = await request('/condtest/public/patch-match.json', {
221
+ method: 'PATCH',
222
+ headers: {
223
+ 'Content-Type': 'text/n3',
224
+ 'If-Match': etag
225
+ },
226
+ body: patch,
227
+ auth: 'condtest'
228
+ });
229
+ assertStatus(res2, 204);
230
+ });
231
+
232
+ it('should return 412 when ETag does not match', async () => {
233
+ const patch = `
234
+ @prefix solid: <http://www.w3.org/ns/solid/terms#>.
235
+ _:patch a solid:InsertDeletePatch;
236
+ solid:inserts { <#test> <http://example.org/bad> "true" }.
237
+ `;
238
+ const res = await request('/condtest/public/patch-match.json', {
239
+ method: 'PATCH',
240
+ headers: {
241
+ 'Content-Type': 'text/n3',
242
+ 'If-Match': '"wrong-etag"'
243
+ },
244
+ body: patch,
245
+ auth: 'condtest'
246
+ });
247
+ assertStatus(res, 412);
248
+ });
249
+ });
250
+ });
@@ -0,0 +1,349 @@
1
+ /**
2
+ * Solid Conformance Tests (Simplified)
3
+ *
4
+ * Tests based on solid/solid-crud-tests but using Bearer token auth.
5
+ * Covers the same MUST requirements from the Solid Protocol spec.
6
+ */
7
+
8
+ import { describe, it, before, after } from 'node:test';
9
+ import assert from 'node:assert';
10
+ import {
11
+ startTestServer,
12
+ stopTestServer,
13
+ request,
14
+ createTestPod,
15
+ assertStatus
16
+ } from './helpers.js';
17
+
18
+ describe('Solid Protocol Conformance', () => {
19
+ let token;
20
+
21
+ before(async () => {
22
+ // Enable conneg for full Turtle support (required for Solid conformance)
23
+ await startTestServer({ conneg: true });
24
+ const pod = await createTestPod('conformance');
25
+ token = pod.token;
26
+ });
27
+
28
+ after(async () => {
29
+ await stopTestServer();
30
+ });
31
+
32
+ describe('MUST: Create non-container using POST', () => {
33
+ it('creates the resource and returns 201', async () => {
34
+ const res = await request('/conformance/public/', {
35
+ method: 'POST',
36
+ headers: {
37
+ 'Content-Type': 'text/turtle',
38
+ 'Slug': 'post-created.ttl'
39
+ },
40
+ body: '<#hello> <#linked> <#world> .',
41
+ auth: 'conformance'
42
+ });
43
+ assertStatus(res, 201);
44
+ assert.ok(res.headers.get('Location'), 'Should return Location header');
45
+ });
46
+
47
+ it('adds the resource to container listing', async () => {
48
+ const res = await request('/conformance/public/');
49
+ const body = await res.text();
50
+ assert.ok(body.includes('post-created.ttl'), 'Container should list the resource');
51
+ });
52
+ });
53
+
54
+ describe('MUST: Create non-container using PUT', () => {
55
+ it('creates the resource', async () => {
56
+ const res = await request('/conformance/public/put-created.ttl', {
57
+ method: 'PUT',
58
+ headers: { 'Content-Type': 'text/turtle' },
59
+ body: '<#hello> <#linked> <#world> .',
60
+ auth: 'conformance'
61
+ });
62
+ assert.ok([200, 201, 204].includes(res.status), `Expected 2xx, got ${res.status}`);
63
+ });
64
+
65
+ it('adds the resource to container listing', async () => {
66
+ const res = await request('/conformance/public/');
67
+ const body = await res.text();
68
+ assert.ok(body.includes('put-created.ttl'), 'Container should list the resource');
69
+ });
70
+ });
71
+
72
+ describe('MUST: Create container using PUT', () => {
73
+ it('creates container with trailing slash', async () => {
74
+ // First create a resource inside the new container (creates container implicitly)
75
+ const res = await request('/conformance/public/new-container/test.ttl', {
76
+ method: 'PUT',
77
+ headers: { 'Content-Type': 'text/turtle' },
78
+ body: '<#test> <#is> <#here> .',
79
+ auth: 'conformance'
80
+ });
81
+ assert.ok([200, 201, 204].includes(res.status));
82
+
83
+ // Container should exist
84
+ const containerRes = await request('/conformance/public/new-container/');
85
+ assertStatus(containerRes, 200);
86
+ });
87
+ });
88
+
89
+ describe('MUST: Update using PUT', () => {
90
+ it('overwrites existing resource', async () => {
91
+ // Create
92
+ await request('/conformance/public/update-test.ttl', {
93
+ method: 'PUT',
94
+ headers: { 'Content-Type': 'text/turtle' },
95
+ body: '<#v> <#is> "1" .',
96
+ auth: 'conformance'
97
+ });
98
+
99
+ // Update
100
+ const res = await request('/conformance/public/update-test.ttl', {
101
+ method: 'PUT',
102
+ headers: { 'Content-Type': 'text/turtle' },
103
+ body: '<#v> <#is> "2" .',
104
+ auth: 'conformance'
105
+ });
106
+ assertStatus(res, 204);
107
+
108
+ // Verify
109
+ const getRes = await request('/conformance/public/update-test.ttl');
110
+ const body = await getRes.text();
111
+ assert.ok(body.includes('"2"'), 'Resource should be updated');
112
+ });
113
+ });
114
+
115
+ describe('MUST: Update using PATCH (N3)', () => {
116
+ it('adds triple to existing resource', async () => {
117
+ // Create resource with context for cleaner patch matching
118
+ await request('/conformance/public/patch-test.json', {
119
+ method: 'PUT',
120
+ headers: { 'Content-Type': 'application/ld+json' },
121
+ body: JSON.stringify({
122
+ '@context': { 'ex': 'http://example.org/' },
123
+ '@id': '#me',
124
+ 'ex:name': 'Test'
125
+ }),
126
+ auth: 'conformance'
127
+ });
128
+
129
+ const patch = `
130
+ @prefix solid: <http://www.w3.org/ns/solid/terms#>.
131
+ @prefix ex: <http://example.org/>.
132
+ _:patch a solid:InsertDeletePatch;
133
+ solid:inserts { <#me> ex:added "yes" }.
134
+ `;
135
+ const res = await request('/conformance/public/patch-test.json', {
136
+ method: 'PATCH',
137
+ headers: { 'Content-Type': 'text/n3' },
138
+ body: patch,
139
+ auth: 'conformance'
140
+ });
141
+ assertStatus(res, 204);
142
+
143
+ const getRes = await request('/conformance/public/patch-test.json', {
144
+ headers: { 'Accept': 'application/ld+json' }
145
+ });
146
+ const data = await getRes.json();
147
+ // Check for either prefixed or full URI form
148
+ const added = data['ex:added'] || data['http://example.org/added'];
149
+ assert.strictEqual(added, 'yes');
150
+ });
151
+ });
152
+
153
+ describe('MUST: Update using PATCH (SPARQL Update)', () => {
154
+ it('modifies resource with INSERT DATA', async () => {
155
+ await request('/conformance/public/sparql-test.json', {
156
+ method: 'PUT',
157
+ headers: { 'Content-Type': 'application/ld+json' },
158
+ body: JSON.stringify({ '@id': '#item' }),
159
+ auth: 'conformance'
160
+ });
161
+
162
+ const sparql = `
163
+ PREFIX ex: <http://example.org/>
164
+ INSERT DATA { <#item> ex:status "active" }
165
+ `;
166
+ const res = await request('/conformance/public/sparql-test.json', {
167
+ method: 'PATCH',
168
+ headers: { 'Content-Type': 'application/sparql-update' },
169
+ body: sparql,
170
+ auth: 'conformance'
171
+ });
172
+ assertStatus(res, 204);
173
+ });
174
+ });
175
+
176
+ describe('MUST: Delete resource', () => {
177
+ it('deletes and returns 204', async () => {
178
+ await request('/conformance/public/to-delete.ttl', {
179
+ method: 'PUT',
180
+ headers: { 'Content-Type': 'text/turtle' },
181
+ body: '<#x> <#y> <#z> .',
182
+ auth: 'conformance'
183
+ });
184
+
185
+ const res = await request('/conformance/public/to-delete.ttl', {
186
+ method: 'DELETE',
187
+ auth: 'conformance'
188
+ });
189
+ assertStatus(res, 204);
190
+
191
+ const getRes = await request('/conformance/public/to-delete.ttl');
192
+ assertStatus(getRes, 404);
193
+ });
194
+
195
+ it('removes from container listing', async () => {
196
+ const res = await request('/conformance/public/');
197
+ const body = await res.text();
198
+ assert.ok(!body.includes('to-delete.ttl'), 'Should not be in listing');
199
+ });
200
+ });
201
+
202
+ describe('MUST: LDP Headers', () => {
203
+ it('includes Link rel=type for containers', async () => {
204
+ const res = await request('/conformance/public/');
205
+ const link = res.headers.get('Link');
206
+ assert.ok(link.includes('ldp#BasicContainer'), 'Should have BasicContainer type');
207
+ assert.ok(link.includes('ldp#Container'), 'Should have Container type');
208
+ });
209
+
210
+ it('includes Link rel=type for resources', async () => {
211
+ const res = await request('/conformance/public/put-created.ttl');
212
+ const link = res.headers.get('Link');
213
+ assert.ok(link.includes('ldp#Resource'), 'Should have Resource type');
214
+ });
215
+
216
+ it('includes Link rel=acl', async () => {
217
+ const res = await request('/conformance/public/put-created.ttl');
218
+ const link = res.headers.get('Link');
219
+ assert.ok(link.includes('rel="acl"'), 'Should have acl link');
220
+ });
221
+
222
+ it('includes ETag header', async () => {
223
+ const res = await request('/conformance/public/put-created.ttl');
224
+ assert.ok(res.headers.get('ETag'), 'Should have ETag');
225
+ });
226
+
227
+ it('includes Allow header on OPTIONS', async () => {
228
+ const res = await request('/conformance/public/', { method: 'OPTIONS' });
229
+ const allow = res.headers.get('Allow');
230
+ assert.ok(allow.includes('GET'), 'Should allow GET');
231
+ assert.ok(allow.includes('POST'), 'Should allow POST');
232
+ });
233
+
234
+ it('includes Accept-Post for containers', async () => {
235
+ const res = await request('/conformance/public/', { method: 'OPTIONS' });
236
+ assert.ok(res.headers.get('Accept-Post'), 'Should have Accept-Post');
237
+ });
238
+
239
+ it('includes Accept-Put for resources', async () => {
240
+ const res = await request('/conformance/public/put-created.ttl', { method: 'OPTIONS' });
241
+ assert.ok(res.headers.get('Accept-Put'), 'Should have Accept-Put');
242
+ });
243
+
244
+ it('includes Accept-Patch for resources', async () => {
245
+ const res = await request('/conformance/public/put-created.ttl', { method: 'OPTIONS' });
246
+ const acceptPatch = res.headers.get('Accept-Patch');
247
+ assert.ok(acceptPatch, 'Should have Accept-Patch');
248
+ assert.ok(acceptPatch.includes('text/n3'), 'Should accept N3');
249
+ });
250
+ });
251
+
252
+ describe('MUST: WAC Headers', () => {
253
+ it('includes WAC-Allow header', async () => {
254
+ const res = await request('/conformance/public/');
255
+ assert.ok(res.headers.get('WAC-Allow'), 'Should have WAC-Allow');
256
+ });
257
+ });
258
+
259
+ describe('MUST: Conditional Requests', () => {
260
+ it('returns 304 for If-None-Match on GET', async () => {
261
+ const res1 = await request('/conformance/public/put-created.ttl');
262
+ const etag = res1.headers.get('ETag');
263
+
264
+ const res2 = await request('/conformance/public/put-created.ttl', {
265
+ headers: { 'If-None-Match': etag }
266
+ });
267
+ assertStatus(res2, 304);
268
+ });
269
+
270
+ it('returns 412 for If-Match mismatch on PUT', async () => {
271
+ const res = await request('/conformance/public/put-created.ttl', {
272
+ method: 'PUT',
273
+ headers: {
274
+ 'Content-Type': 'text/turtle',
275
+ 'If-Match': '"wrong-etag"'
276
+ },
277
+ body: '<#new> <#data> <#here> .',
278
+ auth: 'conformance'
279
+ });
280
+ assertStatus(res, 412);
281
+ });
282
+
283
+ it('returns 412 for If-None-Match: * on existing resource', async () => {
284
+ const res = await request('/conformance/public/put-created.ttl', {
285
+ method: 'PUT',
286
+ headers: {
287
+ 'Content-Type': 'text/turtle',
288
+ 'If-None-Match': '*'
289
+ },
290
+ body: '<#new> <#data> <#here> .',
291
+ auth: 'conformance'
292
+ });
293
+ assertStatus(res, 412);
294
+ });
295
+ });
296
+
297
+ describe('MUST: CORS Headers', () => {
298
+ it('includes Access-Control-Allow-Origin', async () => {
299
+ const res = await request('/conformance/public/', {
300
+ headers: { 'Origin': 'https://example.com' }
301
+ });
302
+ const acao = res.headers.get('Access-Control-Allow-Origin');
303
+ assert.ok(acao, 'Should have ACAO header');
304
+ });
305
+
306
+ it('includes Access-Control-Expose-Headers', async () => {
307
+ const res = await request('/conformance/public/', {
308
+ headers: { 'Origin': 'https://example.com' }
309
+ });
310
+ const expose = res.headers.get('Access-Control-Expose-Headers');
311
+ assert.ok(expose, 'Should expose headers');
312
+ assert.ok(expose.includes('Location'), 'Should expose Location');
313
+ assert.ok(expose.includes('Link'), 'Should expose Link');
314
+ });
315
+
316
+ it('handles preflight OPTIONS', async () => {
317
+ const res = await request('/conformance/public/', {
318
+ method: 'OPTIONS',
319
+ headers: {
320
+ 'Origin': 'https://example.com',
321
+ 'Access-Control-Request-Method': 'PUT'
322
+ }
323
+ });
324
+ assertStatus(res, 204);
325
+ assert.ok(res.headers.get('Access-Control-Allow-Methods'), 'Should have Allow-Methods');
326
+ });
327
+ });
328
+
329
+ describe('MUST: Content Negotiation', () => {
330
+ it('returns JSON-LD by default for RDF resources', async () => {
331
+ const res = await request('/conformance/public/patch-test.json');
332
+ const ct = res.headers.get('Content-Type');
333
+ assert.ok(ct.includes('application/ld+json') || ct.includes('application/json'));
334
+ });
335
+ });
336
+
337
+ describe('SHOULD: WebSocket Notifications', () => {
338
+ it('includes Updates-Via header when enabled', async () => {
339
+ // Note: requires server started with notifications: true
340
+ // This test documents the expected behavior
341
+ const res = await request('/conformance/', { method: 'OPTIONS' });
342
+ // Updates-Via may or may not be present depending on server config
343
+ const updatesVia = res.headers.get('Updates-Via');
344
+ if (updatesVia) {
345
+ assert.ok(updatesVia.includes('ws'), 'Should be WebSocket URL');
346
+ }
347
+ });
348
+ });
349
+ });