Skip to content

Commit fb68aea

Browse files
committed
Fix tests
1 parent adae01d commit fb68aea

16 files changed

Lines changed: 444 additions & 188 deletions

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,4 @@ USER deno
2121

2222
EXPOSE 8417
2323

24-
CMD ["deno", "run", "--cached-only", "--no-prompt", "--allow-read=examples,var", "--allow-write=var", "--allow-env=PORT,DATABASE_URL,LTI_TOOL_PRIVATE_JWK,APP_ORIGIN,APP_RUNTIME_ORIGIN,LANTERN_OPERATOR_NAME,USER,LOGNAME", "--allow-net", "main.ts"]
24+
CMD ["deno", "run", "--cached-only", "--no-prompt", "--allow-read=examples,var", "--allow-write=var", "--allow-env=PORT,DATABASE_URL,DATABASE_CA_CERT,LTI_TOOL_PRIVATE_JWK,APP_ORIGIN,APP_RUNTIME_ORIGIN,LANTERN_OPERATOR_NAME,USER,LOGNAME", "--allow-net", "main.ts"]

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@ another proxy.
124124
Minimum environment:
125125

126126
- `DATABASE_URL`: Postgres connection string
127+
- `DATABASE_CA_CERT`: optional PEM CA certificate for managed Postgres providers
128+
such as DigitalOcean
127129
- `LTI_TOOL_PRIVATE_JWK`: one RSA private JWK used to publish Lantern's public
128130
JWKS and sign LTI assertions
129131
- `APP_ORIGIN`: recommended for stable local config and required anywhere

deno.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app_lti_config_login_test.ts

Lines changed: 70 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -320,77 +320,84 @@ Deno.test('GET /lti/login tolerates one extra percent-encoding layer on opaque L
320320
});
321321

322322
Deno.test('GET /lti/login applies deployment override compatibility behavior only after resolving the saved deployment', async () => {
323-
const repository = createInMemoryPackageReviewRepository({
324-
deployments: [
325-
buildDeploymentRecord({
326-
binding: buildMoodleDeploymentBinding({
327-
issuer: 'https://moodle.example',
328-
clientId: 'moodle-client-123',
329-
deploymentId: 'moodle-deployment-123',
323+
const previousOrigin = Deno.env.get('APP_ORIGIN');
324+
Deno.env.delete('APP_ORIGIN');
325+
326+
try {
327+
const repository = createInMemoryPackageReviewRepository({
328+
deployments: [
329+
buildDeploymentRecord({
330+
binding: buildMoodleDeploymentBinding({
331+
issuer: 'https://moodle.example',
332+
clientId: 'moodle-client-123',
333+
deploymentId: 'moodle-deployment-123',
334+
}),
335+
ltiProfileOverride: 'governedCompatibility',
330336
}),
331-
ltiProfileOverride: 'governedCompatibility',
332-
}),
333-
],
334-
lanternLtiProfileSettings: {
335-
defaultLtiProfile: 'certification',
336-
updatedAt: TEST_NOW,
337-
},
338-
});
339-
const response = await createApp({
340-
getRepository: () => repository,
341-
}).request(
342-
'http://localhost/lti/login?iss=https%3A%2F%2Fmoodle.example&login_hint=opaque%252Flogin%253Fhint&client_id=moodle-client-123&deployment_id=moodle-deployment-123&lti_message_hint=context%2523value',
343-
);
337+
],
338+
lanternLtiProfileSettings: {
339+
defaultLtiProfile: 'certification',
340+
updatedAt: TEST_NOW,
341+
},
342+
});
343+
const response = await createApp({
344+
getRepository: () => repository,
345+
}).request(
346+
'http://localhost/lti/login?iss=https%3A%2F%2Fmoodle.example&login_hint=opaque%252Flogin%253Fhint&client_id=moodle-client-123&deployment_id=moodle-deployment-123&lti_message_hint=context%2523value',
347+
);
344348

345-
assertEquals(response.status, 302);
349+
assertEquals(response.status, 302);
346350

347-
const location = response.headers.get('location');
351+
const location = response.headers.get('location');
348352

349-
if (!location) {
350-
throw new Error('Expected Moodle authorization redirect location.');
351-
}
353+
if (!location) {
354+
throw new Error('Expected Moodle authorization redirect location.');
355+
}
352356

353-
const redirected = new URL(location);
354-
const state = redirected.searchParams.get('state');
357+
const redirected = new URL(location);
358+
const state = redirected.searchParams.get('state');
355359

356-
if (!state) {
357-
throw new Error('Expected saved login state in the Moodle redirect.');
358-
}
360+
if (!state) {
361+
throw new Error('Expected saved login state in the Moodle redirect.');
362+
}
359363

360-
const saved = await repository.getLoginStateByState(state);
361-
const interopEvents = await repository.listAuditEventsByEventType('interop.path_used');
364+
const saved = await repository.getLoginStateByState(state);
365+
const interopEvents = await repository.listAuditEventsByEventType('interop.path_used');
362366

363-
assertEquals(redirected.searchParams.get('login_hint'), 'opaque/login?hint');
364-
assertEquals(saved?.loginHint, 'opaque/login?hint');
365-
assertEquals(saved?.ltiMessageHint, 'context#value');
366-
assertEquals(saved?.targetLinkUri, 'http://localhost/lti/launch');
367-
assertEquals(
368-
interopEvents.some(
369-
(event) =>
370-
event.detail.path === 'opaque_login_hint_decode' &&
371-
event.detail.ltiProfileId === 'governedCompatibility' &&
372-
event.detail.ltiProfileSource === 'deploymentOverride',
373-
),
374-
true,
375-
);
376-
assertEquals(
377-
interopEvents.some(
378-
(event) =>
379-
event.detail.path === 'opaque_lti_message_hint_decode' &&
380-
event.detail.ltiProfileId === 'governedCompatibility' &&
381-
event.detail.ltiProfileSource === 'deploymentOverride',
382-
),
383-
true,
384-
);
385-
assertEquals(
386-
interopEvents.some(
387-
(event) =>
388-
event.detail.path === 'platform_default_launch_target' &&
389-
event.detail.ltiProfileId === 'governedCompatibility' &&
390-
event.detail.ltiProfileSource === 'deploymentOverride',
391-
),
392-
true,
393-
);
367+
assertEquals(redirected.searchParams.get('login_hint'), 'opaque/login?hint');
368+
assertEquals(saved?.loginHint, 'opaque/login?hint');
369+
assertEquals(saved?.ltiMessageHint, 'context#value');
370+
assertEquals(saved?.targetLinkUri, 'http://localhost/lti/launch');
371+
assertEquals(
372+
interopEvents.some(
373+
(event) =>
374+
event.detail.path === 'opaque_login_hint_decode' &&
375+
event.detail.ltiProfileId === 'governedCompatibility' &&
376+
event.detail.ltiProfileSource === 'deploymentOverride',
377+
),
378+
true,
379+
);
380+
assertEquals(
381+
interopEvents.some(
382+
(event) =>
383+
event.detail.path === 'opaque_lti_message_hint_decode' &&
384+
event.detail.ltiProfileId === 'governedCompatibility' &&
385+
event.detail.ltiProfileSource === 'deploymentOverride',
386+
),
387+
true,
388+
);
389+
assertEquals(
390+
interopEvents.some(
391+
(event) =>
392+
event.detail.path === 'platform_default_launch_target' &&
393+
event.detail.ltiProfileId === 'governedCompatibility' &&
394+
event.detail.ltiProfileSource === 'deploymentOverride',
395+
),
396+
true,
397+
);
398+
} finally {
399+
restoreEnv('APP_ORIGIN', previousOrigin);
400+
}
394401
});
395402

396403
Deno.test('GET /lti/login rejects opaque hint compatibility decoding when the resolved profile is certification', async () => {

src/app_lti_launch_multilms_test.ts

Lines changed: 91 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
getTestCanvasJwks,
1313
signCanvasIdToken,
1414
} from './test_helpers/lti.ts';
15-
import { withFetchStub } from './app_test_support.ts';
15+
import { withFetchStub, withRuntimeOriginEnv } from './app_test_support.ts';
1616

1717
Deno.test('POST /lti/launch accepts a signed Moodle resource-link launch and records shared audit evidence', async () => {
1818
const repository = createInMemoryPackageReviewRepository({
@@ -72,49 +72,51 @@ Deno.test('POST /lti/launch accepts a signed Moodle resource-link launch and rec
7272
}),
7373
);
7474

75-
await withFetchStub(
76-
() =>
77-
Promise.resolve(
78-
new Response(JSON.stringify(getTestCanvasJwks()), {
79-
status: 200,
80-
headers: { 'content-type': 'application/json' },
81-
}),
82-
),
83-
async () => {
84-
const response = await createApp({
85-
getRepository: () => repository,
86-
}).request('http://localhost/lti/launch', {
87-
method: 'POST',
88-
body: formData,
89-
});
90-
91-
assertEquals(response.status, 303);
92-
const location = response.headers.get('location');
93-
94-
if (!location) {
95-
throw new Error('Expected runtime-session handoff redirect.');
96-
}
97-
98-
const sessionId = location.match(/\/runtime\/sessions\/([^?]+)/)?.[1];
99-
100-
if (!sessionId) {
101-
throw new Error('Expected runtime session id in redirect.');
102-
}
103-
104-
const saved = await repository.getRuntimeSessionById(sessionId);
105-
const auditEvents = await repository.listAuditEventsByEventType('launch.accepted');
106-
107-
assertEquals(saved?.deploymentRecordId, 13);
108-
assertEquals(saved?.launch.courseId, 'moodle-course-42');
109-
assertEquals(auditEvents.length, 1);
110-
assertEquals(auditEvents[0]?.detail.lms, 'moodle');
111-
assertEquals(auditEvents[0]?.detail.issuer, 'https://moodle.example');
112-
assertEquals(auditEvents[0]?.detail.clientId, 'moodle-client-123');
113-
assertEquals(auditEvents[0]?.detail.deploymentId, 'moodle-deployment-123');
114-
assertEquals(auditEvents[0]?.detail.resourceLinkId, 'moodle-resource-link');
115-
assertEquals(auditEvents[0]?.detail.contextId, 'moodle-course-42');
116-
},
117-
);
75+
await withRuntimeOriginEnv(async () => {
76+
await withFetchStub(
77+
() =>
78+
Promise.resolve(
79+
new Response(JSON.stringify(getTestCanvasJwks()), {
80+
status: 200,
81+
headers: { 'content-type': 'application/json' },
82+
}),
83+
),
84+
async () => {
85+
const response = await createApp({
86+
getRepository: () => repository,
87+
}).request('http://localhost/lti/launch', {
88+
method: 'POST',
89+
body: formData,
90+
});
91+
92+
assertEquals(response.status, 303);
93+
const location = response.headers.get('location');
94+
95+
if (!location) {
96+
throw new Error('Expected runtime-session handoff redirect.');
97+
}
98+
99+
const sessionId = location.match(/\/runtime\/sessions\/([^?]+)/)?.[1];
100+
101+
if (!sessionId) {
102+
throw new Error('Expected runtime session id in redirect.');
103+
}
104+
105+
const saved = await repository.getRuntimeSessionById(sessionId);
106+
const auditEvents = await repository.listAuditEventsByEventType('launch.accepted');
107+
108+
assertEquals(saved?.deploymentRecordId, 13);
109+
assertEquals(saved?.launch.courseId, 'moodle-course-42');
110+
assertEquals(auditEvents.length, 1);
111+
assertEquals(auditEvents[0]?.detail.lms, 'moodle');
112+
assertEquals(auditEvents[0]?.detail.issuer, 'https://moodle.example');
113+
assertEquals(auditEvents[0]?.detail.clientId, 'moodle-client-123');
114+
assertEquals(auditEvents[0]?.detail.deploymentId, 'moodle-deployment-123');
115+
assertEquals(auditEvents[0]?.detail.resourceLinkId, 'moodle-resource-link');
116+
assertEquals(auditEvents[0]?.detail.contextId, 'moodle-course-42');
117+
},
118+
);
119+
});
118120
});
119121

120122
Deno.test('POST /lti/launch accepts a signed Sakai resource-link launch and records shared audit evidence', async () => {
@@ -175,47 +177,49 @@ Deno.test('POST /lti/launch accepts a signed Sakai resource-link launch and reco
175177
}),
176178
);
177179

178-
await withFetchStub(
179-
() =>
180-
Promise.resolve(
181-
new Response(JSON.stringify(getTestCanvasJwks()), {
182-
status: 200,
183-
headers: { 'content-type': 'application/json' },
184-
}),
185-
),
186-
async () => {
187-
const response = await createApp({
188-
getRepository: () => repository,
189-
}).request('http://localhost/lti/launch', {
190-
method: 'POST',
191-
body: formData,
192-
});
193-
194-
assertEquals(response.status, 303);
195-
const location = response.headers.get('location');
196-
197-
if (!location) {
198-
throw new Error('Expected runtime-session handoff redirect.');
199-
}
200-
201-
const sessionId = location.match(/\/runtime\/sessions\/([^?]+)/)?.[1];
202-
203-
if (!sessionId) {
204-
throw new Error('Expected runtime session id in redirect.');
205-
}
206-
207-
const saved = await repository.getRuntimeSessionById(sessionId);
208-
const auditEvents = await repository.listAuditEventsByEventType('launch.accepted');
209-
210-
assertEquals(saved?.deploymentRecordId, 17);
211-
assertEquals(saved?.launch.courseId, 'sakai-course-42');
212-
assertEquals(auditEvents.length, 1);
213-
assertEquals(auditEvents[0]?.detail.lms, 'sakai');
214-
assertEquals(auditEvents[0]?.detail.issuer, 'https://sakai.example');
215-
assertEquals(auditEvents[0]?.detail.clientId, 'sakai-client-123');
216-
assertEquals(auditEvents[0]?.detail.deploymentId, 'sakai-deployment-123');
217-
assertEquals(auditEvents[0]?.detail.resourceLinkId, 'sakai-resource-link');
218-
assertEquals(auditEvents[0]?.detail.contextId, 'sakai-course-42');
219-
},
220-
);
180+
await withRuntimeOriginEnv(async () => {
181+
await withFetchStub(
182+
() =>
183+
Promise.resolve(
184+
new Response(JSON.stringify(getTestCanvasJwks()), {
185+
status: 200,
186+
headers: { 'content-type': 'application/json' },
187+
}),
188+
),
189+
async () => {
190+
const response = await createApp({
191+
getRepository: () => repository,
192+
}).request('http://localhost/lti/launch', {
193+
method: 'POST',
194+
body: formData,
195+
});
196+
197+
assertEquals(response.status, 303);
198+
const location = response.headers.get('location');
199+
200+
if (!location) {
201+
throw new Error('Expected runtime-session handoff redirect.');
202+
}
203+
204+
const sessionId = location.match(/\/runtime\/sessions\/([^?]+)/)?.[1];
205+
206+
if (!sessionId) {
207+
throw new Error('Expected runtime session id in redirect.');
208+
}
209+
210+
const saved = await repository.getRuntimeSessionById(sessionId);
211+
const auditEvents = await repository.listAuditEventsByEventType('launch.accepted');
212+
213+
assertEquals(saved?.deploymentRecordId, 17);
214+
assertEquals(saved?.launch.courseId, 'sakai-course-42');
215+
assertEquals(auditEvents.length, 1);
216+
assertEquals(auditEvents[0]?.detail.lms, 'sakai');
217+
assertEquals(auditEvents[0]?.detail.issuer, 'https://sakai.example');
218+
assertEquals(auditEvents[0]?.detail.clientId, 'sakai-client-123');
219+
assertEquals(auditEvents[0]?.detail.deploymentId, 'sakai-deployment-123');
220+
assertEquals(auditEvents[0]?.detail.resourceLinkId, 'sakai-resource-link');
221+
assertEquals(auditEvents[0]?.detail.contextId, 'sakai-course-42');
222+
},
223+
);
224+
});
221225
});

0 commit comments

Comments
 (0)