javascript-solid-server 0.0.155 → 0.0.157
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 +1 -1
- package/src/auth/middleware.js +2 -2
- package/src/handlers/container.js +7 -2
- package/src/handlers/resource.js +67 -33
- package/src/rdf/conneg.js +18 -5
- package/test/conneg.test.js +324 -0
package/package.json
CHANGED
package/src/auth/middleware.js
CHANGED
|
@@ -385,8 +385,8 @@ function getErrorPage(statusCode, isAuthenticated, request) {
|
|
|
385
385
|
</div>
|
|
386
386
|
|
|
387
387
|
<p class="footer">
|
|
388
|
-
Powered by <a href="https://
|
|
389
|
-
<a href="https://
|
|
388
|
+
Powered by <a href="https://jss.live/">JSS</a> •
|
|
389
|
+
<a href="https://jss.live/docs/">Docs</a>
|
|
390
390
|
</p>
|
|
391
391
|
</div>
|
|
392
392
|
</body>
|
|
@@ -42,11 +42,16 @@ export async function handlePost(request, reply) {
|
|
|
42
42
|
|
|
43
43
|
// Check if we can accept this input type
|
|
44
44
|
if (!canAcceptInput(contentType, connegEnabled)) {
|
|
45
|
+
const acceptValue = connegEnabled
|
|
46
|
+
? 'application/ld+json, application/json, text/turtle, text/n3'
|
|
47
|
+
: 'application/ld+json, application/json';
|
|
48
|
+
reply.header('Accept', acceptValue);
|
|
49
|
+
reply.header('Accept-Post', acceptValue);
|
|
45
50
|
return reply.code(415).send({
|
|
46
51
|
error: 'Unsupported Media Type',
|
|
47
52
|
message: connegEnabled
|
|
48
|
-
? 'Supported types: application/ld+json, text/turtle, text/n3'
|
|
49
|
-
: 'Supported
|
|
53
|
+
? 'Supported types: application/ld+json, application/json, text/turtle, text/n3'
|
|
54
|
+
: 'Supported types: application/ld+json, application/json (enable conneg for Turtle/N3 support)'
|
|
50
55
|
});
|
|
51
56
|
}
|
|
52
57
|
|
package/src/handlers/resource.js
CHANGED
|
@@ -149,17 +149,18 @@ export async function handleGet(request, reply) {
|
|
|
149
149
|
const content = await storage.read(indexPath);
|
|
150
150
|
const indexStats = await storage.stat(indexPath);
|
|
151
151
|
|
|
152
|
-
//
|
|
152
|
+
// Pick the negotiated RDF type using q-aware Accept parsing. The
|
|
153
|
+
// naive `acceptHeader.includes('text/turtle')` we used to do here
|
|
154
|
+
// ignored q-weights — `Accept: application/ld+json, text/turtle;q=0.1`
|
|
155
|
+
// would still pick Turtle even though JSON-LD was preferred (#325).
|
|
153
156
|
const acceptHeader = request.headers.accept || '';
|
|
154
|
-
const
|
|
155
|
-
acceptHeader
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
acceptHeader.includes('application/json')
|
|
162
|
-
);
|
|
157
|
+
const negotiated = connegEnabled
|
|
158
|
+
? selectContentType(acceptHeader, true)
|
|
159
|
+
: null;
|
|
160
|
+
const wantsTurtle = negotiated === RDF_TYPES.TURTLE
|
|
161
|
+
|| negotiated === RDF_TYPES.N3
|
|
162
|
+
|| negotiated === 'application/n-triples';
|
|
163
|
+
const wantsJsonLd = negotiated === RDF_TYPES.JSON_LD;
|
|
163
164
|
|
|
164
165
|
if (wantsTurtle || wantsJsonLd) {
|
|
165
166
|
// Extract JSON-LD from HTML data island
|
|
@@ -257,13 +258,14 @@ export async function handleGet(request, reply) {
|
|
|
257
258
|
return reply.type('text/html').send(html);
|
|
258
259
|
}
|
|
259
260
|
|
|
260
|
-
//
|
|
261
|
+
// Pick the negotiated RDF type using q-aware Accept parsing (#325).
|
|
261
262
|
const acceptHeader = request.headers.accept || '';
|
|
262
|
-
const
|
|
263
|
-
acceptHeader
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
263
|
+
const negotiated = connegEnabled
|
|
264
|
+
? selectContentType(acceptHeader, true)
|
|
265
|
+
: null;
|
|
266
|
+
const wantsTurtle = negotiated === RDF_TYPES.TURTLE
|
|
267
|
+
|| negotiated === RDF_TYPES.N3
|
|
268
|
+
|| negotiated === 'application/n-triples';
|
|
267
269
|
|
|
268
270
|
if (wantsTurtle) {
|
|
269
271
|
// Convert container JSON-LD to Turtle
|
|
@@ -383,11 +385,14 @@ export async function handleGet(request, reply) {
|
|
|
383
385
|
if (connegEnabled) {
|
|
384
386
|
const contentStr = content.toString();
|
|
385
387
|
const acceptHeader = request.headers.accept || '';
|
|
386
|
-
// Serve Turtle if: URL ends with .ttl OR Accept
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
388
|
+
// Serve Turtle if: URL ends with .ttl OR Accept's q-weighted top
|
|
389
|
+
// RDF type is Turtle/N3 (#325 — naive substring matching ignored
|
|
390
|
+
// q-weights and would pick Turtle whenever it appeared in Accept).
|
|
391
|
+
const negotiated = selectContentType(acceptHeader, true);
|
|
392
|
+
const wantsTurtle = urlPath.endsWith('.ttl')
|
|
393
|
+
|| negotiated === RDF_TYPES.TURTLE
|
|
394
|
+
|| negotiated === RDF_TYPES.N3
|
|
395
|
+
|| negotiated === 'application/n-triples';
|
|
391
396
|
|
|
392
397
|
// Check if this is HTML with JSON-LD data island
|
|
393
398
|
const isHtmlWithDataIsland = contentStr.trimStart().startsWith('<!DOCTYPE') ||
|
|
@@ -505,24 +510,31 @@ export async function handleHead(request, reply) {
|
|
|
505
510
|
let contentType;
|
|
506
511
|
|
|
507
512
|
if (stats.isDirectory) {
|
|
508
|
-
// For directories with index.html, determine content type based on Accept header
|
|
509
513
|
const indexPath = storagePath.endsWith('/') ? `${storagePath}index.html` : `${storagePath}/index.html`;
|
|
510
514
|
const indexExists = await storage.exists(indexPath);
|
|
515
|
+
const acceptHeader = request.headers.accept || '';
|
|
511
516
|
|
|
512
|
-
if (
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
const
|
|
518
|
-
|
|
517
|
+
if (connegEnabled) {
|
|
518
|
+
// HEAD must mirror what GET would emit; otherwise client caches and
|
|
519
|
+
// RDF-aware tooling key off a content-type that doesn't match the
|
|
520
|
+
// body they'll see on the next GET (#325). Use q-aware Accept
|
|
521
|
+
// parsing for both the index.html and listing branches.
|
|
522
|
+
const negotiated = selectContentType(acceptHeader, true);
|
|
523
|
+
const wantsTurtle = negotiated === RDF_TYPES.TURTLE
|
|
524
|
+
|| negotiated === RDF_TYPES.N3
|
|
525
|
+
|| negotiated === 'application/n-triples';
|
|
526
|
+
const wantsJsonLd = negotiated === RDF_TYPES.JSON_LD;
|
|
519
527
|
|
|
520
528
|
if (wantsTurtle) {
|
|
521
529
|
contentType = 'text/turtle';
|
|
522
530
|
} else if (wantsJsonLd) {
|
|
523
|
-
|
|
531
|
+
// For an index.html container, only override to JSON-LD if the
|
|
532
|
+
// Accept header explicitly asked for JSON; otherwise fall back
|
|
533
|
+
// to text/html so HEAD matches the index.html that GET serves.
|
|
534
|
+
const explicitJson = /\b(application\/ld\+json|application\/json)\b/i.test(acceptHeader);
|
|
535
|
+
contentType = (indexExists && !explicitJson) ? 'text/html' : 'application/ld+json';
|
|
524
536
|
} else {
|
|
525
|
-
contentType = 'text/html';
|
|
537
|
+
contentType = indexExists ? 'text/html' : 'application/ld+json';
|
|
526
538
|
}
|
|
527
539
|
} else if (indexExists) {
|
|
528
540
|
contentType = 'text/html';
|
|
@@ -602,13 +614,35 @@ export async function handlePut(request, reply) {
|
|
|
602
614
|
|
|
603
615
|
const contentType = request.headers['content-type'] || '';
|
|
604
616
|
|
|
617
|
+
// ACL resources require a JSON-LD payload (application/ld+json or
|
|
618
|
+
// application/json). Round-trip serialization between JSON-LD and
|
|
619
|
+
// Turtle representations has limitations that can cause data loss
|
|
620
|
+
// when a client PUTs Turtle and later requests Turtle.
|
|
621
|
+
// Other RDF resources are unaffected. The guard fires regardless
|
|
622
|
+
// of conneg setting and also when Content-Type is missing.
|
|
623
|
+
const ctMain = contentType.split(';')[0].trim().toLowerCase();
|
|
624
|
+
const isJsonLd = ctMain === 'application/ld+json' || ctMain === 'application/json';
|
|
625
|
+
if (urlPath.endsWith('.acl') && !isJsonLd) {
|
|
626
|
+
reply.header('Accept', 'application/ld+json, application/json');
|
|
627
|
+
reply.header('Accept-Put', 'application/ld+json, application/json');
|
|
628
|
+
return reply.code(415).send({
|
|
629
|
+
error: 'Unsupported Media Type',
|
|
630
|
+
message: 'ACL resources must be sent as application/ld+json or application/json.'
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
|
|
605
634
|
// Check if we can accept this input type
|
|
606
635
|
if (!canAcceptInput(contentType, connegEnabled)) {
|
|
636
|
+
const acceptValue = connegEnabled
|
|
637
|
+
? 'application/ld+json, application/json, text/turtle, text/n3'
|
|
638
|
+
: 'application/ld+json, application/json';
|
|
639
|
+
reply.header('Accept', acceptValue);
|
|
640
|
+
reply.header('Accept-Put', acceptValue);
|
|
607
641
|
return reply.code(415).send({
|
|
608
642
|
error: 'Unsupported Media Type',
|
|
609
643
|
message: connegEnabled
|
|
610
|
-
? 'Supported types: application/ld+json, text/turtle, text/n3'
|
|
611
|
-
: 'Supported
|
|
644
|
+
? 'Supported types: application/ld+json, application/json, text/turtle, text/n3'
|
|
645
|
+
: 'Supported types: application/ld+json, application/json (enable conneg for Turtle/N3 support)'
|
|
612
646
|
});
|
|
613
647
|
}
|
|
614
648
|
|
package/src/rdf/conneg.js
CHANGED
|
@@ -205,20 +205,33 @@ export function getVaryHeader(connegEnabled, mashlibEnabled = false) {
|
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
/**
|
|
208
|
-
* Get Accept-* headers for responses
|
|
208
|
+
* Get Accept-* headers for responses.
|
|
209
|
+
*
|
|
210
|
+
* The explicitly listed RDF types are aligned with the formats this
|
|
211
|
+
* module accepts so clients can discover support consistently:
|
|
212
|
+
* - JSON-LD (application/ld+json) and JSON (application/json alias)
|
|
213
|
+
* are advertised in all conneg modes.
|
|
214
|
+
* - Turtle (text/turtle) and N3 (text/n3) are advertised only when
|
|
215
|
+
* conneg is enabled (SUPPORTED_INPUT in this file).
|
|
216
|
+
*
|
|
217
|
+
* Note: a wildcard (asterisk-slash-asterisk) is included as a broad
|
|
218
|
+
* interoperability hint for generic clients and proxies. It is not a
|
|
219
|
+
* strict contract that every media type matching the wildcard will be
|
|
220
|
+
* accepted by canAcceptInput() (e.g., application/n-triples and
|
|
221
|
+
* application/rdf+xml are not accepted).
|
|
209
222
|
*/
|
|
210
223
|
export function getAcceptHeaders(connegEnabled, isContainer = false) {
|
|
211
224
|
const headers = {};
|
|
212
225
|
|
|
213
226
|
if (isContainer) {
|
|
214
227
|
headers['Accept-Post'] = connegEnabled
|
|
215
|
-
? `${RDF_TYPES.JSON_LD}, ${RDF_TYPES.TURTLE}, */*`
|
|
216
|
-
: `${RDF_TYPES.JSON_LD}, */*`;
|
|
228
|
+
? `${RDF_TYPES.JSON_LD}, application/json, ${RDF_TYPES.TURTLE}, ${RDF_TYPES.N3}, */*`
|
|
229
|
+
: `${RDF_TYPES.JSON_LD}, application/json, */*`;
|
|
217
230
|
}
|
|
218
231
|
|
|
219
232
|
headers['Accept-Put'] = connegEnabled
|
|
220
|
-
? `${RDF_TYPES.JSON_LD}, ${RDF_TYPES.TURTLE}, */*`
|
|
221
|
-
: `${RDF_TYPES.JSON_LD}, */*`;
|
|
233
|
+
? `${RDF_TYPES.JSON_LD}, application/json, ${RDF_TYPES.TURTLE}, ${RDF_TYPES.N3}, */*`
|
|
234
|
+
: `${RDF_TYPES.JSON_LD}, application/json, */*`;
|
|
222
235
|
|
|
223
236
|
headers['Accept-Patch'] = 'text/n3, application/sparql-update';
|
|
224
237
|
|
package/test/conneg.test.js
CHANGED
|
@@ -193,6 +193,34 @@ describe('Content Negotiation (conneg enabled)', () => {
|
|
|
193
193
|
assert.ok(acceptPost && acceptPost.includes('text/turtle'),
|
|
194
194
|
'Accept-Post should include text/turtle');
|
|
195
195
|
});
|
|
196
|
+
|
|
197
|
+
it('should advertise N3 support in Accept-Put when conneg enabled', async () => {
|
|
198
|
+
const res = await request('/connegtest/public/alice.json');
|
|
199
|
+
const acceptPut = res.headers.get('Accept-Put');
|
|
200
|
+
assert.ok(acceptPut && acceptPut.includes('text/n3'),
|
|
201
|
+
'Accept-Put should include text/n3 (canAcceptInput accepts it under conneg)');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('should advertise N3 support in Accept-Post for containers when conneg enabled', async () => {
|
|
205
|
+
const res = await request('/connegtest/public/');
|
|
206
|
+
const acceptPost = res.headers.get('Accept-Post');
|
|
207
|
+
assert.ok(acceptPost && acceptPost.includes('text/n3'),
|
|
208
|
+
'Accept-Post should include text/n3 (canAcceptInput accepts it under conneg)');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should advertise application/json in Accept-Put', async () => {
|
|
212
|
+
const res = await request('/connegtest/public/alice.json');
|
|
213
|
+
const acceptPut = res.headers.get('Accept-Put');
|
|
214
|
+
assert.ok(acceptPut && acceptPut.includes('application/json'),
|
|
215
|
+
'Accept-Put should include application/json (canAcceptInput treats it as a JSON-LD alias)');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('should advertise application/json in Accept-Post for containers', async () => {
|
|
219
|
+
const res = await request('/connegtest/public/');
|
|
220
|
+
const acceptPost = res.headers.get('Accept-Post');
|
|
221
|
+
assert.ok(acceptPost && acceptPost.includes('application/json'),
|
|
222
|
+
'Accept-Post should include application/json (canAcceptInput treats it as a JSON-LD alias)');
|
|
223
|
+
});
|
|
196
224
|
});
|
|
197
225
|
|
|
198
226
|
// Regression coverage for #294 — Solid convention dotfiles (.acl, .meta)
|
|
@@ -261,6 +289,107 @@ describe('Content Negotiation (conneg enabled)', () => {
|
|
|
261
289
|
'round-tripped JSON-LD should have at least one @-keyword');
|
|
262
290
|
});
|
|
263
291
|
});
|
|
292
|
+
|
|
293
|
+
// ACL resources require a JSON-LD payload (application/ld+json or
|
|
294
|
+
// application/json) on PUT regardless of conneg setting: round-trip
|
|
295
|
+
// serialization between JSON-LD and Turtle has known limitations
|
|
296
|
+
// that can cause data loss. See #295.
|
|
297
|
+
describe('ACL content-type guard (#295)', () => {
|
|
298
|
+
const aclJsonLd = {
|
|
299
|
+
'@context': { acl: 'http://www.w3.org/ns/auth/acl#' },
|
|
300
|
+
'@graph': [
|
|
301
|
+
{
|
|
302
|
+
'@id': '#owner',
|
|
303
|
+
'@type': 'acl:Authorization',
|
|
304
|
+
'acl:agent': { '@id': '#me' },
|
|
305
|
+
'acl:accessTo': { '@id': './' },
|
|
306
|
+
'acl:mode': [{ '@id': 'acl:Read' }, { '@id': 'acl:Write' }, { '@id': 'acl:Control' }]
|
|
307
|
+
}
|
|
308
|
+
]
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
it('rejects text/turtle PUT to .acl with 415', async () => {
|
|
312
|
+
const turtle = `
|
|
313
|
+
@prefix acl: <http://www.w3.org/ns/auth/acl#>.
|
|
314
|
+
<#owner> a acl:Authorization;
|
|
315
|
+
acl:mode acl:Read.
|
|
316
|
+
`;
|
|
317
|
+
const res = await request('/connegtest/public/turtle-reject.acl', {
|
|
318
|
+
method: 'PUT',
|
|
319
|
+
headers: { 'Content-Type': 'text/turtle' },
|
|
320
|
+
body: turtle,
|
|
321
|
+
auth: 'connegtest'
|
|
322
|
+
});
|
|
323
|
+
assertStatus(res, 415);
|
|
324
|
+
assertHeaderContains(res, 'Accept', 'application/ld+json');
|
|
325
|
+
assertHeaderContains(res, 'Accept', 'application/json');
|
|
326
|
+
assertHeaderContains(res, 'Accept-Put', 'application/ld+json');
|
|
327
|
+
assertHeaderContains(res, 'Accept-Put', 'application/json');
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('rejects text/n3 PUT to .acl with 415', async () => {
|
|
331
|
+
const res = await request('/connegtest/public/n3-reject.acl', {
|
|
332
|
+
method: 'PUT',
|
|
333
|
+
headers: { 'Content-Type': 'text/n3' },
|
|
334
|
+
body: '@prefix acl: <http://www.w3.org/ns/auth/acl#>. <#x> a acl:Authorization.',
|
|
335
|
+
auth: 'connegtest'
|
|
336
|
+
});
|
|
337
|
+
assertStatus(res, 415);
|
|
338
|
+
assertHeaderContains(res, 'Accept', 'application/ld+json');
|
|
339
|
+
assertHeaderContains(res, 'Accept', 'application/json');
|
|
340
|
+
assertHeaderContains(res, 'Accept-Put', 'application/ld+json');
|
|
341
|
+
assertHeaderContains(res, 'Accept-Put', 'application/json');
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('rejects text/plain PUT to .acl with 415 (URL-extension protection)', async () => {
|
|
345
|
+
const res = await request('/connegtest/public/plain-reject.acl', {
|
|
346
|
+
method: 'PUT',
|
|
347
|
+
headers: { 'Content-Type': 'text/plain' },
|
|
348
|
+
body: 'arbitrary text',
|
|
349
|
+
auth: 'connegtest'
|
|
350
|
+
});
|
|
351
|
+
assertStatus(res, 415);
|
|
352
|
+
assertHeaderContains(res, 'Accept', 'application/ld+json');
|
|
353
|
+
assertHeaderContains(res, 'Accept', 'application/json');
|
|
354
|
+
assertHeaderContains(res, 'Accept-Put', 'application/ld+json');
|
|
355
|
+
assertHeaderContains(res, 'Accept-Put', 'application/json');
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('rejects PUT to .acl with no Content-Type with 415', async () => {
|
|
359
|
+
// Use Uint8Array body so fetch() doesn't auto-set Content-Type
|
|
360
|
+
// (which it does for string bodies: text/plain;charset=UTF-8).
|
|
361
|
+
const res = await request('/connegtest/public/no-ct-reject.acl', {
|
|
362
|
+
method: 'PUT',
|
|
363
|
+
body: new Uint8Array([1, 2, 3, 4]),
|
|
364
|
+
auth: 'connegtest'
|
|
365
|
+
});
|
|
366
|
+
assertStatus(res, 415);
|
|
367
|
+
assertHeaderContains(res, 'Accept', 'application/ld+json');
|
|
368
|
+
assertHeaderContains(res, 'Accept', 'application/json');
|
|
369
|
+
assertHeaderContains(res, 'Accept-Put', 'application/ld+json');
|
|
370
|
+
assertHeaderContains(res, 'Accept-Put', 'application/json');
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('accepts application/ld+json PUT to .acl', async () => {
|
|
374
|
+
const res = await request('/connegtest/public/jsonld-accept.acl', {
|
|
375
|
+
method: 'PUT',
|
|
376
|
+
headers: { 'Content-Type': 'application/ld+json' },
|
|
377
|
+
body: JSON.stringify(aclJsonLd),
|
|
378
|
+
auth: 'connegtest'
|
|
379
|
+
});
|
|
380
|
+
assert.ok(res.status < 300, `JSON-LD PUT to .acl should succeed, got ${res.status}`);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('accepts application/json PUT to .acl', async () => {
|
|
384
|
+
const res = await request('/connegtest/public/json-accept.acl', {
|
|
385
|
+
method: 'PUT',
|
|
386
|
+
headers: { 'Content-Type': 'application/json' },
|
|
387
|
+
body: JSON.stringify(aclJsonLd),
|
|
388
|
+
auth: 'connegtest'
|
|
389
|
+
});
|
|
390
|
+
assert.ok(res.status < 300, `application/json PUT to .acl should succeed, got ${res.status}`);
|
|
391
|
+
});
|
|
392
|
+
});
|
|
264
393
|
});
|
|
265
394
|
|
|
266
395
|
describe('Content Negotiation (conneg disabled - default)', () => {
|
|
@@ -352,5 +481,200 @@ describe('Content Negotiation (conneg disabled - default)', () => {
|
|
|
352
481
|
assert.ok(!acceptPut || !acceptPut.includes('text/turtle'),
|
|
353
482
|
'Accept-Put should NOT include text/turtle when conneg disabled');
|
|
354
483
|
});
|
|
484
|
+
|
|
485
|
+
it('should advertise application/json in Accept-Put when conneg disabled', async () => {
|
|
486
|
+
const res = await request('/noconneg/public/');
|
|
487
|
+
const acceptPut = res.headers.get('Accept-Put');
|
|
488
|
+
assert.ok(acceptPut && acceptPut.includes('application/json'),
|
|
489
|
+
'Accept-Put should include application/json (canAcceptInput treats it as a JSON-LD alias)');
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
it('should advertise application/json in Accept-Post when conneg disabled', async () => {
|
|
493
|
+
const res = await request('/noconneg/public/');
|
|
494
|
+
const acceptPost = res.headers.get('Accept-Post');
|
|
495
|
+
assert.ok(acceptPost && acceptPost.includes('application/json'),
|
|
496
|
+
'Accept-Post should include application/json (canAcceptInput treats it as a JSON-LD alias)');
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it('should not advertise text/n3 in Accept-Put when conneg disabled', async () => {
|
|
500
|
+
const res = await request('/noconneg/public/');
|
|
501
|
+
const acceptPut = res.headers.get('Accept-Put');
|
|
502
|
+
assert.ok(!acceptPut || !acceptPut.includes('text/n3'),
|
|
503
|
+
'Accept-Put should NOT include text/n3 when conneg disabled');
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
// The .acl content-type guard applies regardless of conneg setting (#295).
|
|
508
|
+
// The default deployment configuration is conneg disabled, so ensure the
|
|
509
|
+
// guard fires there too.
|
|
510
|
+
describe('ACL content-type guard (#295) — conneg disabled', () => {
|
|
511
|
+
const aclJsonLd = {
|
|
512
|
+
'@context': { acl: 'http://www.w3.org/ns/auth/acl#' },
|
|
513
|
+
'@graph': [
|
|
514
|
+
{
|
|
515
|
+
'@id': '#owner',
|
|
516
|
+
'@type': 'acl:Authorization',
|
|
517
|
+
'acl:agent': { '@id': '#me' },
|
|
518
|
+
'acl:accessTo': { '@id': './' },
|
|
519
|
+
'acl:mode': [{ '@id': 'acl:Read' }, { '@id': 'acl:Write' }, { '@id': 'acl:Control' }]
|
|
520
|
+
}
|
|
521
|
+
]
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
it('rejects text/turtle PUT to .acl with 415', async () => {
|
|
525
|
+
const turtle = `@prefix acl: <http://www.w3.org/ns/auth/acl#>. <#x> a acl:Authorization.`;
|
|
526
|
+
const res = await request('/noconneg/public/turtle-reject.acl', {
|
|
527
|
+
method: 'PUT',
|
|
528
|
+
headers: { 'Content-Type': 'text/turtle' },
|
|
529
|
+
body: turtle,
|
|
530
|
+
auth: 'noconneg'
|
|
531
|
+
});
|
|
532
|
+
assertStatus(res, 415);
|
|
533
|
+
assertHeaderContains(res, 'Accept', 'application/ld+json');
|
|
534
|
+
assertHeaderContains(res, 'Accept', 'application/json');
|
|
535
|
+
assertHeaderContains(res, 'Accept-Put', 'application/ld+json');
|
|
536
|
+
assertHeaderContains(res, 'Accept-Put', 'application/json');
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
it('rejects text/plain PUT to .acl with 415', async () => {
|
|
540
|
+
const res = await request('/noconneg/public/plain-reject.acl', {
|
|
541
|
+
method: 'PUT',
|
|
542
|
+
headers: { 'Content-Type': 'text/plain' },
|
|
543
|
+
body: 'arbitrary text',
|
|
544
|
+
auth: 'noconneg'
|
|
545
|
+
});
|
|
546
|
+
assertStatus(res, 415);
|
|
547
|
+
assertHeaderContains(res, 'Accept', 'application/ld+json');
|
|
548
|
+
assertHeaderContains(res, 'Accept', 'application/json');
|
|
549
|
+
assertHeaderContains(res, 'Accept-Put', 'application/ld+json');
|
|
550
|
+
assertHeaderContains(res, 'Accept-Put', 'application/json');
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
it('rejects PUT to .acl with no Content-Type with 415', async () => {
|
|
554
|
+
// Use Uint8Array body so fetch() doesn't auto-set Content-Type
|
|
555
|
+
// (which it does for string bodies: text/plain;charset=UTF-8).
|
|
556
|
+
const res = await request('/noconneg/public/no-ct-reject.acl', {
|
|
557
|
+
method: 'PUT',
|
|
558
|
+
body: new Uint8Array([1, 2, 3, 4]),
|
|
559
|
+
auth: 'noconneg'
|
|
560
|
+
});
|
|
561
|
+
assertStatus(res, 415);
|
|
562
|
+
assertHeaderContains(res, 'Accept', 'application/ld+json');
|
|
563
|
+
assertHeaderContains(res, 'Accept', 'application/json');
|
|
564
|
+
assertHeaderContains(res, 'Accept-Put', 'application/ld+json');
|
|
565
|
+
assertHeaderContains(res, 'Accept-Put', 'application/json');
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
it('accepts application/ld+json PUT to .acl', async () => {
|
|
569
|
+
const res = await request('/noconneg/public/jsonld-accept.acl', {
|
|
570
|
+
method: 'PUT',
|
|
571
|
+
headers: { 'Content-Type': 'application/ld+json' },
|
|
572
|
+
body: JSON.stringify(aclJsonLd),
|
|
573
|
+
auth: 'noconneg'
|
|
574
|
+
});
|
|
575
|
+
assert.ok(res.status < 300, `JSON-LD PUT to .acl should succeed, got ${res.status}`);
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
it('accepts application/json PUT to .acl', async () => {
|
|
579
|
+
const res = await request('/noconneg/public/json-accept.acl', {
|
|
580
|
+
method: 'PUT',
|
|
581
|
+
headers: { 'Content-Type': 'application/json' },
|
|
582
|
+
body: JSON.stringify(aclJsonLd),
|
|
583
|
+
auth: 'noconneg'
|
|
584
|
+
});
|
|
585
|
+
assert.ok(res.status < 300, `application/json PUT to .acl should succeed, got ${res.status}`);
|
|
586
|
+
});
|
|
587
|
+
});
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
// Regression coverage for #325 — q-weighted Accept and HEAD/GET parity.
|
|
591
|
+
// Previously the conneg dispatcher used naive substring matching on the
|
|
592
|
+
// Accept header, so any Accept that mentioned text/turtle (even at q=0.1
|
|
593
|
+
// alongside q=1.0 application/ld+json) returned Turtle. Separately, HEAD
|
|
594
|
+
// on a container without an index.html hard-coded application/ld+json,
|
|
595
|
+
// so HEAD and GET disagreed on content-type for the same URL.
|
|
596
|
+
describe('Content Negotiation — q-weights and HEAD/GET parity (#325)', () => {
|
|
597
|
+
before(async () => {
|
|
598
|
+
await startTestServer({ conneg: true });
|
|
599
|
+
await createTestPod('qwtest');
|
|
600
|
+
});
|
|
601
|
+
after(async () => { await stopTestServer(); });
|
|
602
|
+
|
|
603
|
+
function ct(res) {
|
|
604
|
+
return (res.headers.get('content-type') || '').split(';')[0].trim();
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
describe('container — q-weight respected', () => {
|
|
608
|
+
it('Accept: jsonld q=1.0, turtle q=0.1 → JSON-LD', async () => {
|
|
609
|
+
const res = await request('/qwtest/', {
|
|
610
|
+
headers: { Accept: 'application/ld+json;q=1.0, text/turtle;q=0.1' }
|
|
611
|
+
});
|
|
612
|
+
assertStatus(res, 200);
|
|
613
|
+
assert.strictEqual(ct(res), 'application/ld+json');
|
|
614
|
+
const body = await res.text();
|
|
615
|
+
assert.ok(body.trimStart().startsWith('{'),
|
|
616
|
+
`body should be JSON, got: ${body.slice(0, 80)}`);
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
it('Accept: jsonld, turtle;q=0.5 → JSON-LD wins (downstream repro)', async () => {
|
|
620
|
+
const res = await request('/qwtest/', {
|
|
621
|
+
headers: { Accept: 'application/ld+json, text/turtle;q=0.5' }
|
|
622
|
+
});
|
|
623
|
+
assert.strictEqual(ct(res), 'application/ld+json');
|
|
624
|
+
const body = await res.text();
|
|
625
|
+
assert.ok(body.trimStart().startsWith('{'),
|
|
626
|
+
`body should be JSON, got: ${body.slice(0, 80)}`);
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
it('Accept: turtle (explicit) → Turtle', async () => {
|
|
630
|
+
const res = await request('/qwtest/', { headers: { Accept: 'text/turtle' } });
|
|
631
|
+
assert.strictEqual(ct(res), 'text/turtle');
|
|
632
|
+
const body = await res.text();
|
|
633
|
+
assert.ok(body.trimStart().startsWith('@prefix'),
|
|
634
|
+
`body should be Turtle, got: ${body.slice(0, 80)}`);
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
it('no Accept → JSON-LD (native default)', async () => {
|
|
638
|
+
const res = await request('/qwtest/');
|
|
639
|
+
assert.strictEqual(ct(res), 'application/ld+json');
|
|
640
|
+
});
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
describe('container — HEAD content-type matches GET', () => {
|
|
644
|
+
const cases = [
|
|
645
|
+
['no Accept', {}],
|
|
646
|
+
['jsonld preferred', { Accept: 'application/ld+json;q=1.0, text/turtle;q=0.1' }],
|
|
647
|
+
['turtle preferred', { Accept: 'text/turtle' }],
|
|
648
|
+
['mixed (q=0.5)', { Accept: 'application/ld+json, text/turtle;q=0.5' }]
|
|
649
|
+
];
|
|
650
|
+
for (const [label, headers] of cases) {
|
|
651
|
+
it(`HEAD === GET content-type — ${label}`, async () => {
|
|
652
|
+
const get = await request('/qwtest/', { headers });
|
|
653
|
+
const head = await request('/qwtest/', { method: 'HEAD', headers });
|
|
654
|
+
assert.strictEqual(get.status, 200);
|
|
655
|
+
assert.strictEqual(head.status, 200);
|
|
656
|
+
assert.strictEqual(ct(head), ct(get),
|
|
657
|
+
`HEAD ct (${ct(head)}) must equal GET ct (${ct(get)}) for ${label}`);
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
describe('container — auth path matches anonymous', () => {
|
|
663
|
+
it('GET with auth returns same content-type as without auth (turtle case)', async () => {
|
|
664
|
+
const headers = { Accept: 'text/turtle' };
|
|
665
|
+
const anon = await request('/qwtest/', { headers });
|
|
666
|
+
const authed = await request('/qwtest/', { headers, auth: 'qwtest' });
|
|
667
|
+
assert.strictEqual(ct(anon), 'text/turtle');
|
|
668
|
+
assert.strictEqual(ct(authed), ct(anon),
|
|
669
|
+
'authenticated GET must report the same content-type as anonymous');
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
it('GET with auth returns same content-type as without auth (jsonld case)', async () => {
|
|
673
|
+
const headers = { Accept: 'application/ld+json;q=1.0, text/turtle;q=0.1' };
|
|
674
|
+
const anon = await request('/qwtest/', { headers });
|
|
675
|
+
const authed = await request('/qwtest/', { headers, auth: 'qwtest' });
|
|
676
|
+
assert.strictEqual(ct(anon), 'application/ld+json');
|
|
677
|
+
assert.strictEqual(ct(authed), ct(anon));
|
|
678
|
+
});
|
|
355
679
|
});
|
|
356
680
|
});
|