javascript-solid-server 0.0.2 → 0.0.3

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.
@@ -9,7 +9,11 @@
9
9
  "Bash(npm install:*)",
10
10
  "Bash(timeout 3 node:*)",
11
11
  "Bash(PORT=3030 timeout 3 node:*)",
12
- "Bash(git commit:*)"
12
+ "Bash(git commit:*)",
13
+ "Bash(pkill:*)",
14
+ "Bash(curl:*)",
15
+ "Bash(npm test:*)",
16
+ "Bash(git add:*)"
13
17
  ]
14
18
  }
15
19
  }
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "javascript-solid-server",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "A minimal, fast Solid server",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
7
7
  "scripts": {
8
8
  "start": "node src/index.js",
9
9
  "dev": "node --watch src/index.js",
10
- "test": "node --test"
10
+ "test": "node --test --test-concurrency=1"
11
11
  },
12
12
  "dependencies": {
13
13
  "fastify": "^4.25.2",
@@ -1,6 +1,10 @@
1
1
  import * as storage from '../storage/filesystem.js';
2
2
  import { getAllHeaders } from '../ldp/headers.js';
3
3
  import { isContainer } from '../utils/url.js';
4
+ import { generateProfile, generatePreferences, generateTypeIndex, serialize } from '../webid/profile.js';
5
+
6
+ // Content type for profile card
7
+ const PROFILE_CONTENT_TYPE = 'text/html';
4
8
 
5
9
  /**
6
10
  * Handle POST request to container (create new resource)
@@ -69,6 +73,16 @@ export async function handlePost(request, reply) {
69
73
  /**
70
74
  * Create a pod (container) for a user
71
75
  * POST /.pods with { "name": "alice" }
76
+ *
77
+ * Creates the following structure:
78
+ * /{name}/
79
+ * /{name}/profile/card - WebID profile
80
+ * /{name}/inbox/ - Notifications
81
+ * /{name}/public/ - Public files
82
+ * /{name}/private/ - Private files
83
+ * /{name}/settings/prefs - Preferences
84
+ * /{name}/settings/publicTypeIndex
85
+ * /{name}/settings/privateTypeIndex
72
86
  */
73
87
  export async function handleCreatePod(request, reply) {
74
88
  const { name } = request.body || {};
@@ -89,22 +103,52 @@ export async function handleCreatePod(request, reply) {
89
103
  return reply.code(409).send({ error: 'Pod already exists' });
90
104
  }
91
105
 
92
- // Create pod container
93
- const success = await storage.createContainer(podPath);
94
- if (!success) {
106
+ // Build URIs
107
+ // WebID is at pod root: /alice/#me
108
+ const baseUri = `${request.protocol}://${request.hostname}`;
109
+ const podUri = `${baseUri}${podPath}`;
110
+ const webId = `${podUri}#me`;
111
+ const issuer = baseUri;
112
+
113
+ try {
114
+ // Create pod directory structure
115
+ await storage.createContainer(podPath);
116
+ await storage.createContainer(`${podPath}inbox/`);
117
+ await storage.createContainer(`${podPath}public/`);
118
+ await storage.createContainer(`${podPath}private/`);
119
+ await storage.createContainer(`${podPath}settings/`);
120
+
121
+ // Generate and write WebID profile as index.html at pod root
122
+ const profileHtml = generateProfile({ webId, name, podUri, issuer });
123
+ await storage.write(`${podPath}index.html`, profileHtml);
124
+
125
+ // Generate and write preferences
126
+ const prefs = generatePreferences({ webId, podUri });
127
+ await storage.write(`${podPath}settings/prefs`, serialize(prefs));
128
+
129
+ // Generate and write type indexes
130
+ const publicTypeIndex = generateTypeIndex(`${podUri}settings/publicTypeIndex`);
131
+ await storage.write(`${podPath}settings/publicTypeIndex`, serialize(publicTypeIndex));
132
+
133
+ const privateTypeIndex = generateTypeIndex(`${podUri}settings/privateTypeIndex`);
134
+ await storage.write(`${podPath}settings/privateTypeIndex`, serialize(privateTypeIndex));
135
+
136
+ } catch (err) {
137
+ console.error('Pod creation error:', err);
138
+ // Cleanup on failure
139
+ await storage.remove(podPath);
95
140
  return reply.code(500).send({ error: 'Failed to create pod' });
96
141
  }
97
142
 
98
- const location = `${request.protocol}://${request.hostname}${podPath}`;
99
143
  const origin = request.headers.origin;
100
-
101
144
  const headers = getAllHeaders({ isContainer: true, origin });
102
- headers['Location'] = location;
145
+ headers['Location'] = podUri;
103
146
 
104
147
  Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
105
148
 
106
149
  return reply.code(201).send({
107
150
  name,
108
- url: location
151
+ webId,
152
+ podUri
109
153
  });
110
154
  }
@@ -18,6 +18,27 @@ export async function handleGet(request, reply) {
18
18
 
19
19
  // Handle container
20
20
  if (stats.isDirectory) {
21
+ // Check for index.html (serves as both profile and container representation)
22
+ const indexPath = urlPath.endsWith('/') ? `${urlPath}index.html` : `${urlPath}/index.html`;
23
+ const indexExists = await storage.exists(indexPath);
24
+
25
+ if (indexExists) {
26
+ // Serve index.html (contains JSON-LD structured data)
27
+ const content = await storage.read(indexPath);
28
+ const indexStats = await storage.stat(indexPath);
29
+
30
+ const headers = getAllHeaders({
31
+ isContainer: true,
32
+ etag: indexStats?.etag || stats.etag,
33
+ contentType: 'text/html',
34
+ origin
35
+ });
36
+
37
+ Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
38
+ return reply.send(content);
39
+ }
40
+
41
+ // No index.html, return JSON-LD container listing
21
42
  const entries = await storage.listContainer(urlPath);
22
43
  const baseUrl = `${request.protocol}://${request.hostname}${urlPath}`;
23
44
  const jsonLd = generateContainerJsonLd(baseUrl, entries || []);
package/src/server.js CHANGED
@@ -25,8 +25,10 @@ export function createServer(options = {}) {
25
25
  const corsHeaders = getCorsHeaders(request.headers.origin);
26
26
  Object.entries(corsHeaders).forEach(([k, v]) => reply.header(k, v));
27
27
 
28
- // Handle preflight
28
+ // Handle preflight OPTIONS
29
29
  if (request.method === 'OPTIONS') {
30
+ // Add Allow header for LDP compliance
31
+ reply.header('Allow', 'GET, HEAD, POST, PUT, DELETE, PATCH, OPTIONS');
30
32
  reply.code(204).send();
31
33
  return reply;
32
34
  }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * WebID Profile generation
3
+ * Creates profile documents following Solid conventions
4
+ * Profile is HTML with embedded JSON-LD structured data
5
+ */
6
+
7
+ const FOAF = 'http://xmlns.com/foaf/0.1/';
8
+ const SOLID = 'http://www.w3.org/ns/solid/terms#';
9
+ const SCHEMA = 'http://schema.org/';
10
+ const LDP = 'http://www.w3.org/ns/ldp#';
11
+ const PIM = 'http://www.w3.org/ns/pim/space#';
12
+
13
+ /**
14
+ * Generate JSON-LD data for a WebID profile
15
+ * @param {object} options
16
+ * @param {string} options.webId - Full WebID URI (e.g., https://example.com/alice/profile/card#me)
17
+ * @param {string} options.name - Display name
18
+ * @param {string} options.podUri - Pod root URI (e.g., https://example.com/alice/)
19
+ * @param {string} options.issuer - OIDC issuer URI
20
+ * @returns {object} JSON-LD profile data
21
+ */
22
+ export function generateProfileJsonLd({ webId, name, podUri, issuer }) {
23
+ const pod = podUri.endsWith('/') ? podUri : podUri + '/';
24
+ const profileDoc = webId.split('#')[0];
25
+
26
+ return {
27
+ '@context': {
28
+ 'foaf': FOAF,
29
+ 'solid': SOLID,
30
+ 'schema': SCHEMA,
31
+ 'pim': PIM,
32
+ 'ldp': LDP,
33
+ 'inbox': { '@id': 'ldp:inbox', '@type': '@id' },
34
+ 'storage': { '@id': 'pim:storage', '@type': '@id' },
35
+ 'oidcIssuer': { '@id': 'solid:oidcIssuer', '@type': '@id' },
36
+ 'preferencesFile': { '@id': 'pim:preferencesFile', '@type': '@id' }
37
+ },
38
+ '@graph': [
39
+ {
40
+ '@id': profileDoc,
41
+ '@type': 'foaf:PersonalProfileDocument',
42
+ 'foaf:maker': { '@id': webId },
43
+ 'foaf:primaryTopic': { '@id': webId }
44
+ },
45
+ {
46
+ '@id': webId,
47
+ '@type': ['foaf:Person', 'schema:Person'],
48
+ 'foaf:name': name,
49
+ 'inbox': `${pod}inbox/`,
50
+ 'storage': pod,
51
+ 'oidcIssuer': issuer,
52
+ 'preferencesFile': `${pod}settings/prefs`
53
+ }
54
+ ]
55
+ };
56
+ }
57
+
58
+ /**
59
+ * Generate HTML profile with embedded JSON-LD
60
+ * @param {object} options
61
+ * @param {string} options.webId - Full WebID URI
62
+ * @param {string} options.name - Display name
63
+ * @param {string} options.podUri - Pod root URI
64
+ * @param {string} options.issuer - OIDC issuer URI
65
+ * @returns {string} HTML document with JSON-LD
66
+ */
67
+ export function generateProfile({ webId, name, podUri, issuer }) {
68
+ const jsonLd = generateProfileJsonLd({ webId, name, podUri, issuer });
69
+ const pod = podUri.endsWith('/') ? podUri : podUri + '/';
70
+
71
+ return `<!DOCTYPE html>
72
+ <html lang="en">
73
+ <head>
74
+ <meta charset="utf-8">
75
+ <meta name="viewport" content="width=device-width, initial-scale=1">
76
+ <title>${escapeHtml(name)}'s Profile</title>
77
+ <script type="application/ld+json">
78
+ ${JSON.stringify(jsonLd, null, 2)}
79
+ </script>
80
+ <style>
81
+ body { font-family: system-ui, sans-serif; max-width: 600px; margin: 2rem auto; padding: 0 1rem; }
82
+ h1 { color: #333; }
83
+ .card { background: #f5f5f5; padding: 1.5rem; border-radius: 8px; }
84
+ dt { font-weight: bold; margin-top: 1rem; }
85
+ dd { margin-left: 0; color: #666; }
86
+ a { color: #7c4dff; }
87
+ </style>
88
+ </head>
89
+ <body>
90
+ <div class="card">
91
+ <h1>${escapeHtml(name)}</h1>
92
+ <dl>
93
+ <dt>WebID</dt>
94
+ <dd><a href="${escapeHtml(webId)}">${escapeHtml(webId)}</a></dd>
95
+ <dt>Storage</dt>
96
+ <dd><a href="${escapeHtml(pod)}">${escapeHtml(pod)}</a></dd>
97
+ <dt>Inbox</dt>
98
+ <dd><a href="${escapeHtml(pod)}inbox/">${escapeHtml(pod)}inbox/</a></dd>
99
+ </dl>
100
+ </div>
101
+ </body>
102
+ </html>`;
103
+ }
104
+
105
+ /**
106
+ * Escape HTML entities
107
+ */
108
+ function escapeHtml(str) {
109
+ return str
110
+ .replace(/&/g, '&amp;')
111
+ .replace(/</g, '&lt;')
112
+ .replace(/>/g, '&gt;')
113
+ .replace(/"/g, '&quot;');
114
+ }
115
+
116
+ /**
117
+ * Generate preferences file as JSON-LD
118
+ * @param {object} options
119
+ * @param {string} options.webId - Full WebID URI
120
+ * @param {string} options.podUri - Pod root URI
121
+ * @returns {object} JSON-LD preferences document
122
+ */
123
+ export function generatePreferences({ webId, podUri }) {
124
+ const pod = podUri.endsWith('/') ? podUri : podUri + '/';
125
+
126
+ return {
127
+ '@context': {
128
+ 'solid': SOLID,
129
+ 'pim': PIM,
130
+ 'publicTypeIndex': { '@id': 'solid:publicTypeIndex', '@type': '@id' },
131
+ 'privateTypeIndex': { '@id': 'solid:privateTypeIndex', '@type': '@id' }
132
+ },
133
+ '@id': `${pod}settings/prefs`,
134
+ 'publicTypeIndex': `${pod}settings/publicTypeIndex`,
135
+ 'privateTypeIndex': `${pod}settings/privateTypeIndex`
136
+ };
137
+ }
138
+
139
+ /**
140
+ * Generate an empty type index
141
+ * @param {string} uri - URI of the type index
142
+ * @returns {object} JSON-LD type index document
143
+ */
144
+ export function generateTypeIndex(uri) {
145
+ return {
146
+ '@context': {
147
+ 'solid': SOLID
148
+ },
149
+ '@id': uri,
150
+ '@type': 'solid:TypeIndex'
151
+ };
152
+ }
153
+
154
+ /**
155
+ * Serialize JSON-LD to string
156
+ * @param {object} jsonLd
157
+ * @returns {string}
158
+ */
159
+ export function serialize(jsonLd) {
160
+ return JSON.stringify(jsonLd, null, 2);
161
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Test helpers for JavaScript Solid Server
3
+ */
4
+
5
+ import { createServer } from '../src/server.js';
6
+ import fs from 'fs-extra';
7
+ import path from 'path';
8
+
9
+ const TEST_DATA_DIR = './data';
10
+
11
+ let server = null;
12
+ let baseUrl = null;
13
+
14
+ /**
15
+ * Start a test server on a random available port
16
+ * @returns {Promise<{server: object, baseUrl: string}>}
17
+ */
18
+ export async function startTestServer() {
19
+ // Clean up any existing test data
20
+ await fs.emptyDir(TEST_DATA_DIR);
21
+
22
+ server = createServer({ logger: false });
23
+ // Use port 0 to let OS assign available port
24
+ await server.listen({ port: 0, host: '127.0.0.1' });
25
+
26
+ const address = server.server.address();
27
+ baseUrl = `http://127.0.0.1:${address.port}`;
28
+
29
+ return { server, baseUrl };
30
+ }
31
+
32
+ /**
33
+ * Stop the test server
34
+ */
35
+ export async function stopTestServer() {
36
+ if (server) {
37
+ await server.close();
38
+ server = null;
39
+ }
40
+ // Clean up test data
41
+ await fs.emptyDir(TEST_DATA_DIR);
42
+ }
43
+
44
+ /**
45
+ * Get the base URL
46
+ */
47
+ export function getBaseUrl() {
48
+ return baseUrl;
49
+ }
50
+
51
+ /**
52
+ * Create a pod for testing
53
+ * @param {string} name - Pod name
54
+ * @returns {Promise<{webId: string, podUri: string}>}
55
+ */
56
+ export async function createTestPod(name) {
57
+ const res = await fetch(`${baseUrl}/.pods`, {
58
+ method: 'POST',
59
+ headers: { 'Content-Type': 'application/json' },
60
+ body: JSON.stringify({ name })
61
+ });
62
+
63
+ if (!res.ok) {
64
+ throw new Error(`Failed to create pod: ${res.status}`);
65
+ }
66
+
67
+ return res.json();
68
+ }
69
+
70
+ /**
71
+ * Make a request to the test server
72
+ * @param {string} path - URL path
73
+ * @param {object} options - fetch options
74
+ * @returns {Promise<Response>}
75
+ */
76
+ export async function request(urlPath, options = {}) {
77
+ const url = urlPath.startsWith('http') ? urlPath : `${baseUrl}${urlPath}`;
78
+ return fetch(url, options);
79
+ }
80
+
81
+ /**
82
+ * Assert response status
83
+ */
84
+ export function assertStatus(res, expected, message = '') {
85
+ if (res.status !== expected) {
86
+ throw new Error(`Expected status ${expected}, got ${res.status}. ${message}`);
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Assert response header exists
92
+ */
93
+ export function assertHeader(res, header, expected = undefined) {
94
+ const value = res.headers.get(header);
95
+ if (value === null) {
96
+ throw new Error(`Expected header ${header} to exist`);
97
+ }
98
+ if (expected !== undefined && value !== expected) {
99
+ throw new Error(`Expected header ${header} to be "${expected}", got "${value}"`);
100
+ }
101
+ return value;
102
+ }
103
+
104
+ /**
105
+ * Assert response header contains value
106
+ */
107
+ export function assertHeaderContains(res, header, substring) {
108
+ const value = res.headers.get(header);
109
+ if (value === null || !value.includes(substring)) {
110
+ throw new Error(`Expected header ${header} to contain "${substring}", got "${value}"`);
111
+ }
112
+ return value;
113
+ }
114
+
115
+ /**
116
+ * Parse JSON-LD from HTML (extracts from script tag)
117
+ */
118
+ export function extractJsonLdFromHtml(html) {
119
+ const match = html.match(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/);
120
+ if (!match) {
121
+ throw new Error('No JSON-LD found in HTML');
122
+ }
123
+ return JSON.parse(match[1]);
124
+ }
@@ -0,0 +1,322 @@
1
+ /**
2
+ * LDP (Linked Data Platform) CRUD tests
3
+ */
4
+
5
+ import { describe, it, before, after, beforeEach } from 'node:test';
6
+ import assert from 'node:assert';
7
+ import {
8
+ startTestServer,
9
+ stopTestServer,
10
+ request,
11
+ createTestPod,
12
+ assertStatus,
13
+ assertHeader,
14
+ assertHeaderContains
15
+ } from './helpers.js';
16
+
17
+ describe('LDP CRUD Operations', () => {
18
+ let baseUrl;
19
+
20
+ before(async () => {
21
+ const result = await startTestServer();
22
+ baseUrl = result.baseUrl;
23
+ await createTestPod('ldptest');
24
+ });
25
+
26
+ after(async () => {
27
+ await stopTestServer();
28
+ });
29
+
30
+ describe('GET', () => {
31
+ it('should return 404 for non-existent resource', async () => {
32
+ const res = await request('/ldptest/nonexistent.json');
33
+ assertStatus(res, 404);
34
+ });
35
+
36
+ it('should return container listing for empty container', async () => {
37
+ const res = await request('/ldptest/public/');
38
+
39
+ assertStatus(res, 200);
40
+ assertHeaderContains(res, 'Link', 'Container');
41
+ });
42
+
43
+ it('should return resource content', async () => {
44
+ // Create resource first
45
+ await request('/ldptest/public/test.json', {
46
+ method: 'PUT',
47
+ headers: { 'Content-Type': 'application/json' },
48
+ body: JSON.stringify({ hello: 'world' })
49
+ });
50
+
51
+ const res = await request('/ldptest/public/test.json');
52
+
53
+ assertStatus(res, 200);
54
+ const data = await res.json();
55
+ assert.strictEqual(data.hello, 'world');
56
+ });
57
+
58
+ it('should return ETag header', async () => {
59
+ await request('/ldptest/public/etag-test.txt', {
60
+ method: 'PUT',
61
+ body: 'test content'
62
+ });
63
+
64
+ const res = await request('/ldptest/public/etag-test.txt');
65
+
66
+ assertStatus(res, 200);
67
+ assertHeader(res, 'ETag');
68
+ });
69
+ });
70
+
71
+ describe('HEAD', () => {
72
+ it('should return headers without body', async () => {
73
+ await request('/ldptest/public/head-test.txt', {
74
+ method: 'PUT',
75
+ body: 'test content'
76
+ });
77
+
78
+ const res = await request('/ldptest/public/head-test.txt', {
79
+ method: 'HEAD'
80
+ });
81
+
82
+ assertStatus(res, 200);
83
+ assertHeader(res, 'Content-Type');
84
+ assertHeader(res, 'ETag');
85
+
86
+ const body = await res.text();
87
+ assert.strictEqual(body, '');
88
+ });
89
+
90
+ it('should return 404 for non-existent', async () => {
91
+ const res = await request('/ldptest/public/no-such-file.txt', {
92
+ method: 'HEAD'
93
+ });
94
+
95
+ assertStatus(res, 404);
96
+ });
97
+ });
98
+
99
+ describe('PUT', () => {
100
+ it('should create new resource', async () => {
101
+ const res = await request('/ldptest/public/new-resource.json', {
102
+ method: 'PUT',
103
+ headers: { 'Content-Type': 'application/json' },
104
+ body: JSON.stringify({ created: true })
105
+ });
106
+
107
+ assertStatus(res, 201);
108
+ assertHeader(res, 'Location');
109
+ });
110
+
111
+ it('should update existing resource', async () => {
112
+ // Create
113
+ await request('/ldptest/public/update-me.txt', {
114
+ method: 'PUT',
115
+ body: 'original'
116
+ });
117
+
118
+ // Update
119
+ const res = await request('/ldptest/public/update-me.txt', {
120
+ method: 'PUT',
121
+ body: 'updated'
122
+ });
123
+
124
+ assertStatus(res, 204);
125
+
126
+ // Verify
127
+ const verify = await request('/ldptest/public/update-me.txt');
128
+ const content = await verify.text();
129
+ assert.strictEqual(content, 'updated');
130
+ });
131
+
132
+ it('should create parent containers', async () => {
133
+ const res = await request('/ldptest/public/nested/deep/file.txt', {
134
+ method: 'PUT',
135
+ body: 'nested content'
136
+ });
137
+
138
+ assertStatus(res, 201);
139
+
140
+ // Verify parent exists
141
+ const parent = await request('/ldptest/public/nested/deep/');
142
+ assertStatus(parent, 200);
143
+ });
144
+
145
+ it('should reject PUT to container path', async () => {
146
+ const res = await request('/ldptest/public/invalid/', {
147
+ method: 'PUT',
148
+ body: 'cannot put to container'
149
+ });
150
+
151
+ assertStatus(res, 409);
152
+ });
153
+ });
154
+
155
+ describe('POST', () => {
156
+ it('should create resource in container', async () => {
157
+ const res = await request('/ldptest/public/', {
158
+ method: 'POST',
159
+ headers: {
160
+ 'Content-Type': 'application/json',
161
+ 'Slug': 'posted-resource'
162
+ },
163
+ body: JSON.stringify({ posted: true })
164
+ });
165
+
166
+ assertStatus(res, 201);
167
+ assertHeader(res, 'Location');
168
+
169
+ // Verify created
170
+ const location = res.headers.get('Location');
171
+ const verify = await request(location);
172
+ assertStatus(verify, 200);
173
+ });
174
+
175
+ it('should use Slug header for filename', async () => {
176
+ const res = await request('/ldptest/public/', {
177
+ method: 'POST',
178
+ headers: {
179
+ 'Content-Type': 'text/plain',
180
+ 'Slug': 'my-custom-name.txt'
181
+ },
182
+ body: 'slug test'
183
+ });
184
+
185
+ const location = res.headers.get('Location');
186
+ assert.ok(location.includes('my-custom-name'), 'Should use slug in filename');
187
+ });
188
+
189
+ it('should create container with Link header', async () => {
190
+ const res = await request('/ldptest/public/', {
191
+ method: 'POST',
192
+ headers: {
193
+ 'Slug': 'new-container',
194
+ 'Link': '<http://www.w3.org/ns/ldp#BasicContainer>; rel="type"'
195
+ }
196
+ });
197
+
198
+ assertStatus(res, 201);
199
+
200
+ const location = res.headers.get('Location');
201
+ assert.ok(location.endsWith('/'), 'Container location should end with /');
202
+
203
+ // Verify it's a container
204
+ const verify = await request(location);
205
+ assertHeaderContains(verify, 'Link', 'Container');
206
+ });
207
+
208
+ it('should reject POST to non-container', async () => {
209
+ await request('/ldptest/public/file-not-container.txt', {
210
+ method: 'PUT',
211
+ body: 'just a file'
212
+ });
213
+
214
+ const res = await request('/ldptest/public/file-not-container.txt', {
215
+ method: 'POST',
216
+ body: 'trying to post'
217
+ });
218
+
219
+ assertStatus(res, 405);
220
+ });
221
+ });
222
+
223
+ describe('DELETE', () => {
224
+ it('should delete resource', async () => {
225
+ await request('/ldptest/public/to-delete.txt', {
226
+ method: 'PUT',
227
+ body: 'delete me'
228
+ });
229
+
230
+ const res = await request('/ldptest/public/to-delete.txt', {
231
+ method: 'DELETE'
232
+ });
233
+
234
+ assertStatus(res, 204);
235
+
236
+ // Verify deleted
237
+ const verify = await request('/ldptest/public/to-delete.txt');
238
+ assertStatus(verify, 404);
239
+ });
240
+
241
+ it('should return 404 for non-existent', async () => {
242
+ const res = await request('/ldptest/public/never-existed.txt', {
243
+ method: 'DELETE'
244
+ });
245
+
246
+ assertStatus(res, 404);
247
+ });
248
+
249
+ it('should delete container', async () => {
250
+ // Create container
251
+ await request('/ldptest/public/', {
252
+ method: 'POST',
253
+ headers: {
254
+ 'Slug': 'container-to-delete',
255
+ 'Link': '<http://www.w3.org/ns/ldp#BasicContainer>; rel="type"'
256
+ }
257
+ });
258
+
259
+ const res = await request('/ldptest/public/container-to-delete/', {
260
+ method: 'DELETE'
261
+ });
262
+
263
+ assertStatus(res, 204);
264
+ });
265
+ });
266
+
267
+ describe('OPTIONS', () => {
268
+ it('should return allowed methods', async () => {
269
+ const res = await request('/ldptest/public/', {
270
+ method: 'OPTIONS'
271
+ });
272
+
273
+ assertStatus(res, 204);
274
+ const allow = assertHeader(res, 'Allow');
275
+ assert.ok(allow.includes('GET'), 'Should allow GET');
276
+ assert.ok(allow.includes('POST'), 'Should allow POST');
277
+ });
278
+
279
+ it('should return CORS headers', async () => {
280
+ const res = await request('/ldptest/public/', {
281
+ method: 'OPTIONS',
282
+ headers: { 'Origin': 'https://app.example.com' }
283
+ });
284
+
285
+ assertHeader(res, 'Access-Control-Allow-Origin');
286
+ assertHeader(res, 'Access-Control-Allow-Methods');
287
+ });
288
+ });
289
+
290
+ describe('LDP Headers', () => {
291
+ it('should return Link type header for resource', async () => {
292
+ await request('/ldptest/public/resource-link.txt', {
293
+ method: 'PUT',
294
+ body: 'test'
295
+ });
296
+
297
+ const res = await request('/ldptest/public/resource-link.txt');
298
+
299
+ assertHeaderContains(res, 'Link', 'ldp#Resource');
300
+ });
301
+
302
+ it('should return Link type headers for container', async () => {
303
+ const res = await request('/ldptest/public/');
304
+
305
+ const link = res.headers.get('Link');
306
+ assert.ok(link.includes('ldp#Resource'), 'Should be LDP Resource');
307
+ assert.ok(link.includes('ldp#Container'), 'Should be LDP Container');
308
+ });
309
+
310
+ it('should return WAC-Allow header', async () => {
311
+ const res = await request('/ldptest/public/');
312
+
313
+ assertHeader(res, 'WAC-Allow');
314
+ });
315
+
316
+ it('should return Accept-Post for containers', async () => {
317
+ const res = await request('/ldptest/public/');
318
+
319
+ assertHeader(res, 'Accept-Post');
320
+ });
321
+ });
322
+ });
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Pod lifecycle 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
+ assertStatus,
12
+ assertHeader,
13
+ assertHeaderContains
14
+ } from './helpers.js';
15
+
16
+ describe('Pod Lifecycle', () => {
17
+ before(async () => {
18
+ await startTestServer();
19
+ });
20
+
21
+ after(async () => {
22
+ await stopTestServer();
23
+ });
24
+
25
+ describe('POST /.pods', () => {
26
+ it('should create a new pod', async () => {
27
+ const res = await request('/.pods', {
28
+ method: 'POST',
29
+ headers: { 'Content-Type': 'application/json' },
30
+ body: JSON.stringify({ name: 'alice' })
31
+ });
32
+
33
+ assertStatus(res, 201);
34
+ assertHeader(res, 'Location');
35
+
36
+ const data = await res.json();
37
+ assert.strictEqual(data.name, 'alice');
38
+ assert.ok(data.webId.endsWith('/alice/#me'));
39
+ assert.ok(data.podUri.endsWith('/alice/'));
40
+ });
41
+
42
+ it('should reject duplicate pod names', async () => {
43
+ // First create
44
+ await request('/.pods', {
45
+ method: 'POST',
46
+ headers: { 'Content-Type': 'application/json' },
47
+ body: JSON.stringify({ name: 'bob' })
48
+ });
49
+
50
+ // Duplicate attempt
51
+ const res = await request('/.pods', {
52
+ method: 'POST',
53
+ headers: { 'Content-Type': 'application/json' },
54
+ body: JSON.stringify({ name: 'bob' })
55
+ });
56
+
57
+ assertStatus(res, 409);
58
+ });
59
+
60
+ it('should reject invalid pod names', async () => {
61
+ const res = await request('/.pods', {
62
+ method: 'POST',
63
+ headers: { 'Content-Type': 'application/json' },
64
+ body: JSON.stringify({ name: '../evil' })
65
+ });
66
+
67
+ assertStatus(res, 400);
68
+ });
69
+
70
+ it('should reject empty pod name', async () => {
71
+ const res = await request('/.pods', {
72
+ method: 'POST',
73
+ headers: { 'Content-Type': 'application/json' },
74
+ body: JSON.stringify({})
75
+ });
76
+
77
+ assertStatus(res, 400);
78
+ });
79
+ });
80
+
81
+ describe('Pod Structure', () => {
82
+ 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
+ });
88
+
89
+ // Check inbox exists
90
+ const inbox = await request('/carol/inbox/');
91
+ assertStatus(inbox, 200);
92
+
93
+ // Check public exists
94
+ const pub = await request('/carol/public/');
95
+ assertStatus(pub, 200);
96
+
97
+ // Check private exists
98
+ const priv = await request('/carol/private/');
99
+ assertStatus(priv, 200);
100
+
101
+ // Check settings exists
102
+ const settings = await request('/carol/settings/');
103
+ assertStatus(settings, 200);
104
+ });
105
+
106
+ 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
+ });
112
+
113
+ // Check prefs
114
+ const prefs = await request('/dan/settings/prefs');
115
+ assertStatus(prefs, 200);
116
+
117
+ // Check public type index
118
+ const pubIndex = await request('/dan/settings/publicTypeIndex');
119
+ assertStatus(pubIndex, 200);
120
+
121
+ // Check private type index
122
+ const privIndex = await request('/dan/settings/privateTypeIndex');
123
+ assertStatus(privIndex, 200);
124
+ });
125
+ });
126
+ });
@@ -0,0 +1,152 @@
1
+ /**
2
+ * WebID Profile 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
+ assertStatus,
13
+ assertHeader,
14
+ assertHeaderContains,
15
+ extractJsonLdFromHtml
16
+ } from './helpers.js';
17
+
18
+ describe('WebID Profile', () => {
19
+ let baseUrl;
20
+ let podInfo;
21
+
22
+ before(async () => {
23
+ const result = await startTestServer();
24
+ baseUrl = result.baseUrl;
25
+ podInfo = await createTestPod('webidtest');
26
+ });
27
+
28
+ after(async () => {
29
+ await stopTestServer();
30
+ });
31
+
32
+ describe('Profile Document', () => {
33
+ it('should serve profile as HTML at pod root', async () => {
34
+ const res = await request('/webidtest/');
35
+
36
+ assertStatus(res, 200);
37
+ assertHeaderContains(res, 'Content-Type', 'text/html');
38
+ });
39
+
40
+ it('should contain JSON-LD structured data', async () => {
41
+ const res = await request('/webidtest/');
42
+ const html = await res.text();
43
+
44
+ const jsonLd = extractJsonLdFromHtml(html);
45
+ assert.ok(jsonLd['@context'], 'Should have @context');
46
+ assert.ok(jsonLd['@graph'], 'Should have @graph');
47
+ });
48
+
49
+ it('should have correct WebID URI', async () => {
50
+ const res = await request('/webidtest/');
51
+ const html = await res.text();
52
+ const jsonLd = extractJsonLdFromHtml(html);
53
+
54
+ // Find the Person in the graph
55
+ const person = jsonLd['@graph'].find(node =>
56
+ Array.isArray(node['@type'])
57
+ ? node['@type'].includes('foaf:Person')
58
+ : node['@type'] === 'foaf:Person'
59
+ );
60
+
61
+ assert.ok(person, 'Should have a foaf:Person');
62
+ assert.ok(person['@id'].endsWith('/webidtest/#me'), 'WebID should end with /#me');
63
+ });
64
+
65
+ it('should have foaf:name', async () => {
66
+ const res = await request('/webidtest/');
67
+ const html = await res.text();
68
+ const jsonLd = extractJsonLdFromHtml(html);
69
+
70
+ const person = jsonLd['@graph'].find(node =>
71
+ Array.isArray(node['@type'])
72
+ ? node['@type'].includes('foaf:Person')
73
+ : node['@type'] === 'foaf:Person'
74
+ );
75
+
76
+ assert.strictEqual(person['foaf:name'], 'webidtest');
77
+ });
78
+
79
+ it('should have solid:oidcIssuer', async () => {
80
+ const res = await request('/webidtest/');
81
+ const html = await res.text();
82
+ const jsonLd = extractJsonLdFromHtml(html);
83
+
84
+ const person = jsonLd['@graph'].find(node =>
85
+ Array.isArray(node['@type'])
86
+ ? node['@type'].includes('foaf:Person')
87
+ : node['@type'] === 'foaf:Person'
88
+ );
89
+
90
+ assert.ok(person['oidcIssuer'], 'Should have oidcIssuer');
91
+ });
92
+
93
+ it('should have pim:storage pointing to pod', async () => {
94
+ const res = await request('/webidtest/');
95
+ const html = await res.text();
96
+ const jsonLd = extractJsonLdFromHtml(html);
97
+
98
+ const person = jsonLd['@graph'].find(node =>
99
+ Array.isArray(node['@type'])
100
+ ? node['@type'].includes('foaf:Person')
101
+ : node['@type'] === 'foaf:Person'
102
+ );
103
+
104
+ assert.ok(person['storage'].endsWith('/webidtest/'), 'Storage should point to pod');
105
+ });
106
+
107
+ it('should have ldp:inbox', async () => {
108
+ const res = await request('/webidtest/');
109
+ const html = await res.text();
110
+ const jsonLd = extractJsonLdFromHtml(html);
111
+
112
+ const person = jsonLd['@graph'].find(node =>
113
+ Array.isArray(node['@type'])
114
+ ? node['@type'].includes('foaf:Person')
115
+ : node['@type'] === 'foaf:Person'
116
+ );
117
+
118
+ assert.ok(person['inbox'].endsWith('/webidtest/inbox/'), 'Should have inbox');
119
+ });
120
+
121
+ it('should have PersonalProfileDocument', async () => {
122
+ const res = await request('/webidtest/');
123
+ const html = await res.text();
124
+ const jsonLd = extractJsonLdFromHtml(html);
125
+
126
+ const doc = jsonLd['@graph'].find(node =>
127
+ node['@type'] === 'foaf:PersonalProfileDocument'
128
+ );
129
+
130
+ assert.ok(doc, 'Should have PersonalProfileDocument');
131
+ assert.ok(doc['foaf:maker'], 'Should have foaf:maker');
132
+ assert.ok(doc['foaf:primaryTopic'], 'Should have foaf:primaryTopic');
133
+ });
134
+ });
135
+
136
+ describe('WebID Resolution', () => {
137
+ it('should return LDP headers', async () => {
138
+ const res = await request('/webidtest/');
139
+
140
+ assertHeaderContains(res, 'Link', 'ldp#Resource');
141
+ assertHeader(res, 'WAC-Allow');
142
+ });
143
+
144
+ it('should return CORS headers', async () => {
145
+ const res = await request('/webidtest/', {
146
+ headers: { 'Origin': 'https://example.com' }
147
+ });
148
+
149
+ assertHeader(res, 'Access-Control-Allow-Origin');
150
+ });
151
+ });
152
+ });