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.
- package/.claude/settings.local.json +5 -1
- package/package.json +2 -2
- package/src/handlers/container.js +51 -7
- package/src/handlers/resource.js +21 -0
- package/src/server.js +3 -1
- package/src/webid/profile.js +161 -0
- package/test/helpers.js +124 -0
- package/test/ldp.test.js +322 -0
- package/test/pod.test.js +126 -0
- package/test/webid.test.js +152 -0
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "javascript-solid-server",
|
|
3
|
-
"version": "0.0.
|
|
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
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
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'] =
|
|
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
|
-
|
|
151
|
+
webId,
|
|
152
|
+
podUri
|
|
109
153
|
});
|
|
110
154
|
}
|
package/src/handlers/resource.js
CHANGED
|
@@ -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, '&')
|
|
111
|
+
.replace(/</g, '<')
|
|
112
|
+
.replace(/>/g, '>')
|
|
113
|
+
.replace(/"/g, '"');
|
|
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
|
+
}
|
package/test/helpers.js
ADDED
|
@@ -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
|
+
}
|
package/test/ldp.test.js
ADDED
|
@@ -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
|
+
});
|
package/test/pod.test.js
ADDED
|
@@ -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
|
+
});
|