javascript-solid-server 0.0.7 → 0.0.8
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/README.md +167 -146
- package/package.json +3 -2
- package/src/handlers/container.js +34 -3
- package/src/handlers/resource.js +87 -7
- package/src/ldp/headers.js +11 -9
- package/src/rdf/conneg.js +215 -0
- package/src/rdf/turtle.js +411 -0
- package/src/server.js +12 -0
- package/test/conneg.test.js +289 -0
- package/test/helpers.js +4 -2
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content Negotiation Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests Turtle <-> JSON-LD conversion with conneg enabled.
|
|
5
|
+
* Note: Content negotiation is OFF by default (JSON-LD native server).
|
|
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
|
+
assertHeader,
|
|
17
|
+
assertHeaderContains
|
|
18
|
+
} from './helpers.js';
|
|
19
|
+
|
|
20
|
+
describe('Content Negotiation (conneg enabled)', () => {
|
|
21
|
+
before(async () => {
|
|
22
|
+
// Start server with conneg ENABLED
|
|
23
|
+
await startTestServer({ conneg: true });
|
|
24
|
+
await createTestPod('connegtest');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
after(async () => {
|
|
28
|
+
await stopTestServer();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('GET with Accept header', () => {
|
|
32
|
+
it('should return JSON-LD when Accept: application/ld+json', async () => {
|
|
33
|
+
// Create a JSON-LD resource
|
|
34
|
+
const data = {
|
|
35
|
+
'@context': { 'foaf': 'http://xmlns.com/foaf/0.1/' },
|
|
36
|
+
'@id': '#me',
|
|
37
|
+
'foaf:name': 'Alice'
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
await request('/connegtest/public/alice.json', {
|
|
41
|
+
method: 'PUT',
|
|
42
|
+
headers: { 'Content-Type': 'application/ld+json' },
|
|
43
|
+
body: JSON.stringify(data),
|
|
44
|
+
auth: 'connegtest'
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const res = await request('/connegtest/public/alice.json', {
|
|
48
|
+
headers: { 'Accept': 'application/ld+json' }
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
assertStatus(res, 200);
|
|
52
|
+
assertHeaderContains(res, 'Content-Type', 'application/ld+json');
|
|
53
|
+
|
|
54
|
+
const body = await res.json();
|
|
55
|
+
assert.strictEqual(body['foaf:name'], 'Alice');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should return Turtle when Accept: text/turtle', async () => {
|
|
59
|
+
// Create a JSON-LD resource
|
|
60
|
+
const data = {
|
|
61
|
+
'@context': { 'foaf': 'http://xmlns.com/foaf/0.1/' },
|
|
62
|
+
'@id': '#me',
|
|
63
|
+
'foaf:name': 'Bob'
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
await request('/connegtest/public/bob.json', {
|
|
67
|
+
method: 'PUT',
|
|
68
|
+
headers: { 'Content-Type': 'application/ld+json' },
|
|
69
|
+
body: JSON.stringify(data),
|
|
70
|
+
auth: 'connegtest'
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const res = await request('/connegtest/public/bob.json', {
|
|
74
|
+
headers: { 'Accept': 'text/turtle' }
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
assertStatus(res, 200);
|
|
78
|
+
assertHeaderContains(res, 'Content-Type', 'text/turtle');
|
|
79
|
+
|
|
80
|
+
const turtle = await res.text();
|
|
81
|
+
// Should contain foaf prefix and name
|
|
82
|
+
assert.ok(turtle.includes('foaf:') || turtle.includes('http://xmlns.com/foaf/0.1/'),
|
|
83
|
+
'Turtle should contain foaf prefix or URI');
|
|
84
|
+
assert.ok(turtle.includes('Bob'), 'Turtle should contain the name');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should default to JSON-LD for */* Accept', async () => {
|
|
88
|
+
const res = await request('/connegtest/public/alice.json', {
|
|
89
|
+
headers: { 'Accept': '*/*' }
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
assertStatus(res, 200);
|
|
93
|
+
assertHeaderContains(res, 'Content-Type', 'application/ld+json');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should include Vary header with Accept', async () => {
|
|
97
|
+
const res = await request('/connegtest/public/alice.json');
|
|
98
|
+
const vary = res.headers.get('Vary');
|
|
99
|
+
assert.ok(vary && vary.includes('Accept'), 'Should have Vary: Accept');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('PUT with Content-Type', () => {
|
|
104
|
+
it('should accept Turtle input and store as JSON-LD', async () => {
|
|
105
|
+
const turtle = `
|
|
106
|
+
@prefix foaf: <http://xmlns.com/foaf/0.1/>.
|
|
107
|
+
<#me> foaf:name "Charlie".
|
|
108
|
+
`;
|
|
109
|
+
|
|
110
|
+
const res = await request('/connegtest/public/charlie.json', {
|
|
111
|
+
method: 'PUT',
|
|
112
|
+
headers: { 'Content-Type': 'text/turtle' },
|
|
113
|
+
body: turtle,
|
|
114
|
+
auth: 'connegtest'
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
assertStatus(res, 201);
|
|
118
|
+
|
|
119
|
+
// Verify it's stored as JSON-LD
|
|
120
|
+
const getRes = await request('/connegtest/public/charlie.json', {
|
|
121
|
+
headers: { 'Accept': 'application/ld+json' }
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
assertStatus(getRes, 200);
|
|
125
|
+
const data = await getRes.json();
|
|
126
|
+
assert.ok(data['@context'], 'Should have @context');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should accept N3 input', async () => {
|
|
130
|
+
const n3 = `
|
|
131
|
+
@prefix schema: <http://schema.org/>.
|
|
132
|
+
<#item> schema:name "Widget".
|
|
133
|
+
`;
|
|
134
|
+
|
|
135
|
+
const res = await request('/connegtest/public/widget.json', {
|
|
136
|
+
method: 'PUT',
|
|
137
|
+
headers: { 'Content-Type': 'text/n3' },
|
|
138
|
+
body: n3,
|
|
139
|
+
auth: 'connegtest'
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
assertStatus(res, 201);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should return 400 for invalid Turtle', async () => {
|
|
146
|
+
const invalidTurtle = 'this is not valid turtle {{{';
|
|
147
|
+
|
|
148
|
+
const res = await request('/connegtest/public/invalid.json', {
|
|
149
|
+
method: 'PUT',
|
|
150
|
+
headers: { 'Content-Type': 'text/turtle' },
|
|
151
|
+
body: invalidTurtle,
|
|
152
|
+
auth: 'connegtest'
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
assertStatus(res, 400);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe('POST with Content-Type', () => {
|
|
160
|
+
it('should accept Turtle input in POST', async () => {
|
|
161
|
+
const turtle = `
|
|
162
|
+
@prefix dc: <http://purl.org/dc/terms/>.
|
|
163
|
+
<#doc> dc:title "My Document".
|
|
164
|
+
`;
|
|
165
|
+
|
|
166
|
+
const res = await request('/connegtest/public/', {
|
|
167
|
+
method: 'POST',
|
|
168
|
+
headers: {
|
|
169
|
+
'Content-Type': 'text/turtle',
|
|
170
|
+
'Slug': 'turtle-doc.json'
|
|
171
|
+
},
|
|
172
|
+
body: turtle,
|
|
173
|
+
auth: 'connegtest'
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
assertStatus(res, 201);
|
|
177
|
+
const location = res.headers.get('Location');
|
|
178
|
+
assert.ok(location, 'Should have Location header');
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe('Accept-* Headers', () => {
|
|
183
|
+
it('should advertise Turtle support in Accept-Put', async () => {
|
|
184
|
+
const res = await request('/connegtest/public/alice.json');
|
|
185
|
+
const acceptPut = res.headers.get('Accept-Put');
|
|
186
|
+
assert.ok(acceptPut && acceptPut.includes('text/turtle'),
|
|
187
|
+
'Accept-Put should include text/turtle');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should advertise Turtle support in Accept-Post for containers', async () => {
|
|
191
|
+
const res = await request('/connegtest/public/');
|
|
192
|
+
const acceptPost = res.headers.get('Accept-Post');
|
|
193
|
+
assert.ok(acceptPost && acceptPost.includes('text/turtle'),
|
|
194
|
+
'Accept-Post should include text/turtle');
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe('Content Negotiation (conneg disabled - default)', () => {
|
|
200
|
+
before(async () => {
|
|
201
|
+
// Start server with conneg DISABLED (default)
|
|
202
|
+
await startTestServer({ conneg: false });
|
|
203
|
+
await createTestPod('noconneg');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
after(async () => {
|
|
207
|
+
await stopTestServer();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe('Default JSON-LD behavior', () => {
|
|
211
|
+
it('should always return JSON-LD regardless of Accept header', async () => {
|
|
212
|
+
// Create resource
|
|
213
|
+
const data = {
|
|
214
|
+
'@context': { 'foaf': 'http://xmlns.com/foaf/0.1/' },
|
|
215
|
+
'@id': '#me',
|
|
216
|
+
'foaf:name': 'DefaultUser'
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
await request('/noconneg/public/user.json', {
|
|
220
|
+
method: 'PUT',
|
|
221
|
+
headers: { 'Content-Type': 'application/ld+json' },
|
|
222
|
+
body: JSON.stringify(data),
|
|
223
|
+
auth: 'noconneg'
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Request Turtle
|
|
227
|
+
const res = await request('/noconneg/public/user.json', {
|
|
228
|
+
headers: { 'Accept': 'text/turtle' }
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
assertStatus(res, 200);
|
|
232
|
+
// Should still return JSON-LD when conneg disabled
|
|
233
|
+
const body = await res.json();
|
|
234
|
+
assert.strictEqual(body['foaf:name'], 'DefaultUser');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should accept JSON-LD input', async () => {
|
|
238
|
+
const data = { '@id': '#test', 'http://example.org/p': 'value' };
|
|
239
|
+
|
|
240
|
+
const res = await request('/noconneg/public/test.json', {
|
|
241
|
+
method: 'PUT',
|
|
242
|
+
headers: { 'Content-Type': 'application/ld+json' },
|
|
243
|
+
body: JSON.stringify(data),
|
|
244
|
+
auth: 'noconneg'
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
assertStatus(res, 201);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should accept plain JSON input', async () => {
|
|
251
|
+
const data = { foo: 'bar' };
|
|
252
|
+
|
|
253
|
+
const res = await request('/noconneg/public/plain.json', {
|
|
254
|
+
method: 'PUT',
|
|
255
|
+
headers: { 'Content-Type': 'application/json' },
|
|
256
|
+
body: JSON.stringify(data),
|
|
257
|
+
auth: 'noconneg'
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
assertStatus(res, 201);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('should accept non-RDF content types', async () => {
|
|
264
|
+
const res = await request('/noconneg/public/readme.txt', {
|
|
265
|
+
method: 'PUT',
|
|
266
|
+
headers: { 'Content-Type': 'text/plain' },
|
|
267
|
+
body: 'Hello World',
|
|
268
|
+
auth: 'noconneg'
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
assertStatus(res, 201);
|
|
272
|
+
|
|
273
|
+
const getRes = await request('/noconneg/public/readme.txt');
|
|
274
|
+
assertStatus(getRes, 200);
|
|
275
|
+
const text = await getRes.text();
|
|
276
|
+
assert.strictEqual(text, 'Hello World');
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('should not advertise Turtle in Accept-Put when conneg disabled', async () => {
|
|
280
|
+
const res = await request('/noconneg/public/');
|
|
281
|
+
const acceptPut = res.headers.get('Accept-Put');
|
|
282
|
+
// Should only advertise JSON-LD, not Turtle
|
|
283
|
+
assert.ok(acceptPut && acceptPut.includes('application/ld+json'),
|
|
284
|
+
'Accept-Put should include application/ld+json');
|
|
285
|
+
assert.ok(!acceptPut || !acceptPut.includes('text/turtle'),
|
|
286
|
+
'Accept-Put should NOT include text/turtle when conneg disabled');
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
});
|
package/test/helpers.js
CHANGED
|
@@ -16,13 +16,15 @@ const podTokens = new Map();
|
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Start a test server on a random available port
|
|
19
|
+
* @param {object} options - Server options
|
|
20
|
+
* @param {boolean} options.conneg - Enable content negotiation (default false)
|
|
19
21
|
* @returns {Promise<{server: object, baseUrl: string}>}
|
|
20
22
|
*/
|
|
21
|
-
export async function startTestServer() {
|
|
23
|
+
export async function startTestServer(options = {}) {
|
|
22
24
|
// Clean up any existing test data
|
|
23
25
|
await fs.emptyDir(TEST_DATA_DIR);
|
|
24
26
|
|
|
25
|
-
server = createServer({ logger: false });
|
|
27
|
+
server = createServer({ logger: false, ...options });
|
|
26
28
|
// Use port 0 to let OS assign available port
|
|
27
29
|
await server.listen({ port: 0, host: '127.0.0.1' });
|
|
28
30
|
|