-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathwebid.test.js
More file actions
326 lines (284 loc) · 13.2 KB
/
webid.test.js
File metadata and controls
326 lines (284 loc) · 13.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
/**
* WebID Profile tests
*/
import { describe, it, before, after } from 'node:test';
import assert from 'node:assert';
import {
startTestServer,
stopTestServer,
request,
createTestPod,
assertStatus,
assertHeader,
assertHeaderContains,
} from './helpers.js';
describe('WebID Profile', () => {
let baseUrl;
let podInfo;
before(async () => {
const result = await startTestServer();
baseUrl = result.baseUrl;
podInfo = await createTestPod('webidtest');
});
after(async () => {
await stopTestServer();
});
describe('Profile Document', () => {
// Profile is now a plain JSON-LD doc at /pod/profile/card.jsonld.
const profilePath = '/webidtest/profile/card.jsonld';
it('should serve profile as JSON-LD', async () => {
const res = await request(profilePath);
assertStatus(res, 200);
assertHeaderContains(res, 'Content-Type', 'application/ld+json');
});
it('should be valid JSON-LD with @context and @id', async () => {
const res = await request(profilePath);
const jsonLd = await res.json();
assert.ok(jsonLd['@context'], 'Should have @context');
assert.ok(jsonLd['@id'], 'Should have @id');
});
// LWS-CID document conformance, Phase A of #386. The profile must be
// structurally a W3C Controlled Identifier document so a future
// PATCH-in-keys app (or server migration) can drop verificationMethod
// entries in without further plumbing. CID v1 vocabulary is declared
// inline rather than via context URL so JSS's conneg layer can
// expand every term without fetching external contexts — the IRIs
// are the same either way.
it('declares all six CID v1 terms in @context (#386 Phase A)', async () => {
const res = await request(profilePath);
const jsonLd = await res.json();
const ctx = jsonLd['@context'];
assert.ok(ctx, '@context required');
// All six CID terms must be declared and expand to the CID v1
// namespace. Accept either prefixed (cid:term) or full-URI
// (https://www.w3.org/ns/cid/v1#term) form.
const cidTerms = ['controller', 'verificationMethod', 'authentication', 'assertionMethod', 'publicKeyJwk', 'publicKeyMultibase'];
for (const term of cidTerms) {
const mapping = ctx[term];
assert.ok(mapping, `@context must define \`${term}\``);
const id = typeof mapping === 'string' ? mapping : mapping['@id'];
assert.match(id, new RegExp(`^(cid:${term}|https://www\\.w3\\.org/ns/cid/v1#${term})$`),
`${term} must map to the CID v1 namespace`);
}
// Container/type flags Phase B relies on:
// verificationMethod values are inline objects, NOT IRIs — must
// NOT have @type:@id (would force string-only) and SHOULD have
// @container:@set so a single entry is still an array.
assert.notStrictEqual(ctx.verificationMethod['@type'], '@id',
'verificationMethod values are objects, not IRIs');
assert.strictEqual(ctx.verificationMethod['@container'], '@set');
// authentication / assertionMethod reference verificationMethod
// entries by IRI, so @type:@id is correct.
assert.strictEqual(ctx.authentication['@type'], '@id');
assert.strictEqual(ctx.assertionMethod['@type'], '@id');
// JWK is a literal JSON value (rdf:JSON datatype) per JSON-LD 1.1.
assert.strictEqual(ctx.publicKeyJwk['@type'], '@json');
});
it('declares CID v1 class names (Multikey, JsonWebKey) as flat aliases (#417)', async () => {
// Without these mappings, an app PATCHing in a VM with the
// spec-example shape `{type: "Multikey", ...}` produces a bare
// relative-IRI `<Multikey>` in the Turtle conneg output, which
// resolves to a fictional class on the pod's own host (e.g.
// `<pod>/profile/Multikey` instead of `cid:Multikey`).
//
// The flat-alias shape (`"Multikey": "cid:Multikey"`) makes
// bare-term emission work correctly through both JSON-LD
// expansion AND our Turtle conneg layer — and matches the
// "JSON-LD with flat context aliases" pattern consumers like
// LOSOS / LION rely on.
const res = await request(profilePath);
const jsonLd = await res.json();
const ctx = jsonLd['@context'];
for (const cls of ['Multikey', 'JsonWebKey']) {
const mapping = ctx[cls];
assert.ok(mapping, `@context must define class alias \`${cls}\``);
const id = typeof mapping === 'string' ? mapping : mapping['@id'];
assert.match(id, new RegExp(`^(cid:${cls}|https://www\\.w3\\.org/ns/cid/v1#${cls})$`),
`${cls} must map to the CID v1 namespace`);
}
});
it('declares self-control via controller === @id (#386 Phase A)', async () => {
const res = await request(profilePath);
const jsonLd = await res.json();
assert.strictEqual(jsonLd.controller, jsonLd['@id'],
'profile must declare itself as its own controller per CID v1');
});
it('should have correct WebID URI', async () => {
const res = await request(profilePath);
const jsonLd = await res.json();
assert.ok(jsonLd['@id'].endsWith('/webidtest/profile/card.jsonld#me'),
`WebID should end with /profile/card.jsonld#me, got ${jsonLd['@id']}`);
});
it('should have foaf:name', async () => {
const res = await request(profilePath);
const jsonLd = await res.json();
assert.strictEqual(jsonLd['foaf:name'], 'webidtest');
});
it('should have solid:oidcIssuer', async () => {
const res = await request(profilePath);
const jsonLd = await res.json();
assert.ok(jsonLd['oidcIssuer'], 'Should have oidcIssuer');
});
it('should have pim:storage pointing to pod', async () => {
const res = await request(profilePath);
const jsonLd = await res.json();
assert.ok(jsonLd['storage'].endsWith('/webidtest/'), 'Storage should point to pod');
});
it('should have ldp:inbox', async () => {
const res = await request(profilePath);
const jsonLd = await res.json();
assert.ok(jsonLd['inbox'].endsWith('/webidtest/inbox/'), 'Should have inbox');
});
it('should have mainEntityOfPage pointing to the document', async () => {
const res = await request(profilePath);
const jsonLd = await res.json();
// Empty string is a relative URI reference to the document itself (JSON-LD)
assert.strictEqual(jsonLd['mainEntityOfPage'], '', 'mainEntityOfPage should be "" (self)');
});
it('should have isPrimaryTopicOf pointing to the document', async () => {
const res = await request(profilePath);
const jsonLd = await res.json();
// Empty string is a relative URI reference to the document itself (JSON-LD)
assert.strictEqual(jsonLd['isPrimaryTopicOf'], '', 'isPrimaryTopicOf should be "" (self)');
});
// LWS 1.0 Controlled Identifier alignment (#320).
// These assertions live alongside the WebID predicate assertions — both
// must continue to hold since the profile is dual-write.
it('should emit a CID service[] with an lws:OpenIdProvider entry', async () => {
const res = await request(profilePath);
const jsonLd = await res.json();
assert.ok(Array.isArray(jsonLd.service), 'profile should have a service array');
const oidc = jsonLd.service.find((s) => s['@type'] === 'lws:OpenIdProvider');
assert.ok(oidc, 'service[] must include an lws:OpenIdProvider entry');
});
it('lws:OpenIdProvider service.serviceEndpoint mirrors oidcIssuer', async () => {
const res = await request(profilePath);
const jsonLd = await res.json();
assert.ok(Array.isArray(jsonLd.service), 'profile should have a service array');
const oidc = jsonLd.service.find((s) => s['@type'] === 'lws:OpenIdProvider');
assert.ok(oidc, 'service[] must include an lws:OpenIdProvider entry');
assert.strictEqual(
oidc.serviceEndpoint,
jsonLd.oidcIssuer,
'serviceEndpoint must equal the existing oidcIssuer value'
);
});
it('lws:OpenIdProvider service.id is a fragment on the profile document', async () => {
const res = await request(profilePath);
const jsonLd = await res.json();
assert.ok(Array.isArray(jsonLd.service), 'profile should have a service array');
const oidc = jsonLd.service.find((s) => s['@type'] === 'lws:OpenIdProvider');
assert.ok(oidc, 'service[] must include an lws:OpenIdProvider entry');
const docUrl = jsonLd['@id'].split('#')[0];
assert.strictEqual(oidc['@id'], `${docUrl}#oidc`,
'service entry @id should be `<profile-doc>#oidc`');
});
});
describe('WebID Resolution', () => {
const profilePath = '/webidtest/profile/card.jsonld';
it('should return LDP headers', async () => {
const res = await request(profilePath);
assertHeaderContains(res, 'Link', 'ldp#Resource');
assertHeader(res, 'WAC-Allow');
});
it('should return CORS headers', async () => {
const res = await request(profilePath, {
headers: { 'Origin': 'https://example.com' }
});
assertHeader(res, 'Access-Control-Allow-Origin');
});
});
});
// With conneg enabled the profile is converted to Turtle on demand. The
// CID service[] must survive that conversion — LWS verifiers that ask for
// Turtle need to see the nested service node's type and serviceEndpoint,
// not just a bare URI reference to it.
describe('WebID Profile — Turtle conneg (#320)', () => {
before(async () => {
await startTestServer({ conneg: true });
await createTestPod('webidturtletest');
});
after(async () => {
await stopTestServer();
});
it('Turtle conneg: generated profile @context expands bare Multikey/JsonWebKey terms (#417)', async () => {
// Combine the production profile generator's @context with a
// synthetic VM (the spec-example shape `{type: "Multikey", ...}`)
// and run it through the same conneg path the live profile
// would. Asserts: the bare-term type expands to the CID v1
// namespace, not to a relative IRI that resolves to a fake
// class on the pod's host.
const { generateProfile } = await import('../src/webid/profile.js');
const { fromJsonLd } = await import('../src/rdf/conneg.js');
const webId = 'https://example.test/profile/card.jsonld#me';
const profile = generateProfile({
webId,
name: 'mk-test',
podUri: 'https://example.test/',
issuer: 'https://example.test/',
});
// Inject a Multikey VM authored with the spec-example bare-term
// type — this is the shape the bug surfaces on.
const vmId = webId.replace('#me', '#nostr-key-1');
profile.verificationMethod = [{
id: vmId,
type: 'Multikey',
controller: webId,
publicKeyMultibase: 'fe70102de7ec0123456789abcdef0123456789abcdef0123456789abcdef0123456789ab',
}];
profile.authentication = [vmId];
const { content: ttl } = await fromJsonLd(profile, 'text/turtle', 'https://example.test/', true);
// Pre-#417: would emit `a <Multikey>` (relative — resolves to
// `https://example.test/profile/Multikey`).
// After #417: the @context maps `Multikey -> cid:Multikey`, so
// the converter expands the bare term to the CID v1 IRI.
assert.ok(
ttl.includes('cid:Multikey') || ttl.includes('cid/v1#Multikey'),
`Turtle must emit a CID-namespaced Multikey class, got:\n${ttl}`,
);
assert.ok(
!/\ba\s+<Multikey>\s*[;.]/.test(ttl),
`Turtle must NOT emit bare <Multikey> (resolves to fictional class), got:\n${ttl}`,
);
// Don't regress #416: the VM block + publicKeyMultibase must
// still survive the conversion.
assert.ok(
ttl.includes('publicKeyMultibase') || ttl.includes('cid/v1#publicKeyMultibase'),
`Turtle must include cid:publicKeyMultibase, got:\n${ttl}`,
);
assert.ok(
ttl.includes('fe70102de7ec'),
`Turtle must include the publicKeyMultibase value, got:\n${ttl}`,
);
});
it('Turtle variant includes cid:service with lws:OpenIdProvider and serviceEndpoint', async () => {
const res = await request('/webidturtletest/profile/card.jsonld', {
headers: { Accept: 'text/turtle' }
});
assertStatus(res, 200);
assertHeaderContains(res, 'Content-Type', 'text/turtle');
const ttl = await res.text();
// Accept either prefixed (cid:service) or expanded full-URI form. The
// critical property is that the nested service node's data survived the
// JSON-LD → Turtle conversion — i.e. the type and endpoint are present
// as their own triples, not dropped.
assert.ok(
ttl.includes('cid:service') || ttl.includes('cid/v1#service'),
`Turtle should reference the CID service predicate, got:\n${ttl}`
);
assert.ok(
ttl.includes('OpenIdProvider'),
`Turtle should declare the lws:OpenIdProvider type, got:\n${ttl}`
);
assert.ok(
ttl.includes('cid:serviceEndpoint') || ttl.includes('cid/v1#serviceEndpoint'),
`Turtle should include the cid:serviceEndpoint predicate, got:\n${ttl}`
);
// The service entry URI appears as a subject (its own line), proving it
// was emitted as a first-class node rather than a bare URI reference.
assert.ok(
/#oidc>\s+(?:a|<[^>]*#type>)/.test(ttl),
`Turtle should emit the service entry as a subject, got:\n${ttl}`
);
});
});