Skip to content

Commit 0e0a460

Browse files
Add CTH compatibility test script
- npm run test:cth verifies IdP setup for conformance testing - Tests OIDC discovery, JWKS, user creation, credentials endpoint - Generates cth.env file for running Solid conformance test harness - All 11 compatibility checks pass
1 parent 69bce65 commit 0e0a460

2 files changed

Lines changed: 370 additions & 0 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"start": "node bin/jss.js start",
2020
"dev": "node --watch bin/jss.js start",
2121
"test": "node --test --test-concurrency=1",
22+
"test:cth": "node scripts/test-cth-compat.js",
2223
"benchmark": "node benchmark.js"
2324
},
2425
"dependencies": {

scripts/test-cth-compat.js

Lines changed: 369 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,369 @@
1+
#!/usr/bin/env node
2+
/**
3+
* CTH Compatibility Test Script
4+
*
5+
* Tests that JSS is configured correctly for the Solid Conformance Test Harness.
6+
*
7+
* Usage:
8+
* node scripts/test-cth-compat.js [--port 3000] [--run-cth]
9+
*/
10+
11+
import { createServer } from '../src/server.js';
12+
import fs from 'fs-extra';
13+
import path from 'path';
14+
import { fileURLToPath } from 'url';
15+
16+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
17+
18+
// Configuration
19+
const PORT = parseInt(process.argv.find(a => a.startsWith('--port='))?.split('=')[1] || '3456');
20+
const RUN_CTH = process.argv.includes('--run-cth');
21+
const BASE_URL = `http://localhost:${PORT}`;
22+
const DATA_DIR = path.join(__dirname, '../.test-cth-data');
23+
24+
// Test users
25+
const ALICE = {
26+
name: 'alice',
27+
email: 'alice@test.local',
28+
password: 'alicepassword123',
29+
};
30+
31+
const BOB = {
32+
name: 'bob',
33+
email: 'bob@test.local',
34+
password: 'bobpassword123',
35+
};
36+
37+
// Colors for output
38+
const GREEN = '\x1b[32m';
39+
const RED = '\x1b[31m';
40+
const YELLOW = '\x1b[33m';
41+
const CYAN = '\x1b[36m';
42+
const RESET = '\x1b[0m';
43+
44+
function log(msg, color = RESET) {
45+
console.log(`${color}${msg}${RESET}`);
46+
}
47+
48+
function pass(msg) {
49+
log(` ✓ ${msg}`, GREEN);
50+
}
51+
52+
function fail(msg) {
53+
log(` ✗ ${msg}`, RED);
54+
}
55+
56+
function info(msg) {
57+
log(` ℹ ${msg}`, CYAN);
58+
}
59+
60+
async function main() {
61+
log('\n=== JSS CTH Compatibility Test ===\n', CYAN);
62+
63+
let server;
64+
let passed = 0;
65+
let failed = 0;
66+
67+
try {
68+
// Clean up and prepare
69+
await fs.remove(DATA_DIR);
70+
await fs.ensureDir(DATA_DIR);
71+
72+
// Start server with IdP
73+
log('Starting server with IdP enabled...', YELLOW);
74+
server = createServer({
75+
logger: false,
76+
root: DATA_DIR,
77+
idp: true,
78+
idpIssuer: BASE_URL,
79+
});
80+
81+
await server.listen({ port: PORT, host: 'localhost' });
82+
pass(`Server running at ${BASE_URL}`);
83+
passed++;
84+
85+
// Test 1: OIDC Discovery
86+
log('\n1. OIDC Discovery', YELLOW);
87+
{
88+
const res = await fetch(`${BASE_URL}/.well-known/openid-configuration`);
89+
if (res.status === 200) {
90+
const config = await res.json();
91+
if (config.issuer === BASE_URL) {
92+
pass('/.well-known/openid-configuration returns valid config');
93+
passed++;
94+
} else {
95+
fail(`Issuer mismatch: expected ${BASE_URL}, got ${config.issuer}`);
96+
failed++;
97+
}
98+
} else {
99+
fail(`OIDC discovery returned ${res.status}`);
100+
failed++;
101+
}
102+
}
103+
104+
// Test 2: JWKS
105+
log('\n2. JWKS Endpoint', YELLOW);
106+
{
107+
const res = await fetch(`${BASE_URL}/.well-known/jwks.json`);
108+
if (res.status === 200) {
109+
const jwks = await res.json();
110+
if (jwks.keys?.length > 0) {
111+
pass('/.well-known/jwks.json returns keys');
112+
passed++;
113+
} else {
114+
fail('JWKS has no keys');
115+
failed++;
116+
}
117+
} else {
118+
fail(`JWKS returned ${res.status}`);
119+
failed++;
120+
}
121+
}
122+
123+
// Test 3: Create Alice
124+
log('\n3. Create Test User: Alice', YELLOW);
125+
let aliceWebId, aliceToken;
126+
{
127+
const res = await fetch(`${BASE_URL}/.pods`, {
128+
method: 'POST',
129+
headers: { 'Content-Type': 'application/json' },
130+
body: JSON.stringify(ALICE),
131+
});
132+
133+
if (res.status === 201) {
134+
const data = await res.json();
135+
aliceWebId = data.webId;
136+
pass(`Created Alice: ${aliceWebId}`);
137+
passed++;
138+
} else {
139+
const err = await res.text();
140+
fail(`Failed to create Alice: ${res.status} - ${err}`);
141+
failed++;
142+
}
143+
}
144+
145+
// Test 4: Create Bob
146+
log('\n4. Create Test User: Bob', YELLOW);
147+
let bobWebId;
148+
{
149+
const res = await fetch(`${BASE_URL}/.pods`, {
150+
method: 'POST',
151+
headers: { 'Content-Type': 'application/json' },
152+
body: JSON.stringify(BOB),
153+
});
154+
155+
if (res.status === 201) {
156+
const data = await res.json();
157+
bobWebId = data.webId;
158+
pass(`Created Bob: ${bobWebId}`);
159+
passed++;
160+
} else {
161+
const err = await res.text();
162+
fail(`Failed to create Bob: ${res.status} - ${err}`);
163+
failed++;
164+
}
165+
}
166+
167+
// Test 5: Credentials endpoint - JSON
168+
log('\n5. Credentials Endpoint (JSON)', YELLOW);
169+
{
170+
const res = await fetch(`${BASE_URL}/idp/credentials`, {
171+
method: 'POST',
172+
headers: { 'Content-Type': 'application/json' },
173+
body: JSON.stringify({ email: ALICE.email, password: ALICE.password }),
174+
});
175+
176+
if (res.status === 200) {
177+
const data = await res.json();
178+
if (data.access_token && data.webid) {
179+
aliceToken = data.access_token;
180+
pass(`Got token for Alice (type: ${data.token_type})`);
181+
passed++;
182+
} else {
183+
fail('Missing access_token or webid in response');
184+
failed++;
185+
}
186+
} else {
187+
const err = await res.text();
188+
fail(`Credentials endpoint failed: ${res.status} - ${err}`);
189+
failed++;
190+
}
191+
}
192+
193+
// Test 6: Credentials endpoint - Form encoded
194+
log('\n6. Credentials Endpoint (Form-encoded)', YELLOW);
195+
{
196+
const res = await fetch(`${BASE_URL}/idp/credentials`, {
197+
method: 'POST',
198+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
199+
body: `email=${encodeURIComponent(BOB.email)}&password=${encodeURIComponent(BOB.password)}`,
200+
});
201+
202+
if (res.status === 200) {
203+
const data = await res.json();
204+
if (data.access_token) {
205+
pass('Form-encoded credentials work');
206+
passed++;
207+
} else {
208+
fail('Missing access_token in response');
209+
failed++;
210+
}
211+
} else {
212+
const err = await res.text();
213+
fail(`Form-encoded credentials failed: ${res.status} - ${err}`);
214+
failed++;
215+
}
216+
}
217+
218+
// Test 7: Invalid credentials
219+
log('\n7. Invalid Credentials Handling', YELLOW);
220+
{
221+
const res = await fetch(`${BASE_URL}/idp/credentials`, {
222+
method: 'POST',
223+
headers: { 'Content-Type': 'application/json' },
224+
body: JSON.stringify({ email: ALICE.email, password: 'wrongpassword' }),
225+
});
226+
227+
if (res.status === 401) {
228+
pass('Returns 401 for invalid password');
229+
passed++;
230+
} else {
231+
fail(`Expected 401, got ${res.status}`);
232+
failed++;
233+
}
234+
}
235+
236+
// Test 8: Token authentication
237+
log('\n8. Token Authentication', YELLOW);
238+
{
239+
if (aliceToken) {
240+
const res = await fetch(`${BASE_URL}/alice/`, {
241+
headers: { 'Authorization': `Bearer ${aliceToken}` },
242+
});
243+
244+
if (res.status === 200) {
245+
pass('Token grants access to own pod');
246+
passed++;
247+
} else {
248+
fail(`Token auth failed: ${res.status}`);
249+
failed++;
250+
}
251+
} else {
252+
fail('No token available to test');
253+
failed++;
254+
}
255+
}
256+
257+
// Test 9: Write with token
258+
log('\n9. Write with Token', YELLOW);
259+
{
260+
if (aliceToken) {
261+
const res = await fetch(`${BASE_URL}/alice/public/test.json`, {
262+
method: 'PUT',
263+
headers: {
264+
'Authorization': `Bearer ${aliceToken}`,
265+
'Content-Type': 'application/ld+json',
266+
},
267+
body: JSON.stringify({ '@id': '#test', 'http://example.org/value': 42 }),
268+
});
269+
270+
if ([200, 201, 204].includes(res.status)) {
271+
pass('Can write to pod with token');
272+
passed++;
273+
} else {
274+
fail(`Write failed: ${res.status}`);
275+
failed++;
276+
}
277+
} else {
278+
fail('No token available to test');
279+
failed++;
280+
}
281+
}
282+
283+
// Test 10: Public read
284+
log('\n10. Public Read Access', YELLOW);
285+
{
286+
const res = await fetch(`${BASE_URL}/alice/public/test.json`);
287+
288+
if (res.status === 200) {
289+
pass('Public resources are readable');
290+
passed++;
291+
} else {
292+
fail(`Public read failed: ${res.status}`);
293+
failed++;
294+
}
295+
}
296+
297+
// Summary
298+
log('\n=== Summary ===\n', CYAN);
299+
log(`Passed: ${passed}`, GREEN);
300+
if (failed > 0) {
301+
log(`Failed: ${failed}`, RED);
302+
}
303+
304+
// Generate CTH env file
305+
log('\n=== CTH Configuration ===\n', CYAN);
306+
307+
const envContent = `# CTH Environment for JSS
308+
# Generated by test-cth-compat.js
309+
310+
SOLID_IDENTITY_PROVIDER=${BASE_URL}
311+
RESOURCE_SERVER_ROOT=${BASE_URL}
312+
TEST_CONTAINER=alice/public/
313+
314+
USERS_ALICE_WEBID=${aliceWebId || `${BASE_URL}/alice/#me`}
315+
USERS_ALICE_USERNAME=${ALICE.email}
316+
USERS_ALICE_PASSWORD=${ALICE.password}
317+
318+
USERS_BOB_WEBID=${bobWebId || `${BASE_URL}/bob/#me`}
319+
USERS_BOB_USERNAME=${BOB.email}
320+
USERS_BOB_PASSWORD=${BOB.password}
321+
322+
LOGIN_ENDPOINT=${BASE_URL}/idp/credentials
323+
324+
# For self-signed certs
325+
ALLOW_SELF_SIGNED_CERTS=true
326+
`;
327+
328+
const envPath = path.join(__dirname, '../cth.env');
329+
await fs.writeFile(envPath, envContent);
330+
info(`CTH env file written to: ${envPath}`);
331+
332+
log('\nTo run CTH:', YELLOW);
333+
log(`
334+
# Keep JSS running with IdP:
335+
JSS_PORT=${PORT} jss start --idp
336+
337+
# In another terminal, run CTH:
338+
docker run -i --rm \\
339+
--network host \\
340+
-v "$(pwd)"/reports:/reports \\
341+
--env-file=cth.env \\
342+
solidproject/conformance-test-harness \\
343+
--output=/reports \\
344+
--target=jss
345+
`);
346+
347+
if (RUN_CTH) {
348+
log('\nRunning CTH (this may take a while)...', YELLOW);
349+
// Would spawn docker here, but keeping server running is complex
350+
info('CTH auto-run not implemented yet. Run manually with commands above.');
351+
}
352+
353+
} catch (err) {
354+
fail(`Error: ${err.message}`);
355+
console.error(err);
356+
failed++;
357+
} finally {
358+
// Cleanup
359+
if (server) {
360+
await server.close();
361+
}
362+
await fs.remove(DATA_DIR);
363+
}
364+
365+
// Exit with error code if any tests failed
366+
process.exit(failed > 0 ? 1 : 0);
367+
}
368+
369+
main();

0 commit comments

Comments
 (0)