• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

MarkUsProject / Markus / 20143075828

11 Dec 2025 06:18PM UTC coverage: 91.513%. Remained the same
20143075828

Pull #7763

github

web-flow
Merge 9f55e660a into 3421ef3b2
Pull Request #7763: Release 2.9.0

914 of 1805 branches covered (50.64%)

Branch coverage included in aggregate %.

1584 of 1666 new or added lines in 108 files covered. (95.08%)

573 existing lines in 35 files now uncovered.

43650 of 46892 relevant lines covered (93.09%)

121.63 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.48
/spec/support/lti_controller_examples.rb
1
shared_examples 'lti deployment controller' do
2✔
2
  let(:instructor) { create(:instructor) }
21✔
3
  let!(:client_id) { 'LMS defined ID' }
32✔
4
  let(:target_link_uri) { 'https://example.com/authorize_redirect' }
6✔
5
  let(:host) { 'https://test.host' }
30✔
6
  let(:state) { 'state_param' }
2✔
7
  let(:launch_params) do
1✔
8
    { client_id: 'LMS defined ID',
2✔
9
      login_hint: 'another opque string',
10
      lti_message_hint: 'opaque string',
11
      prompt: 'none',
12
      redirect_uri: 'https://example.com/authorize_redirect',
13
      response_mode: 'form_post',
14
      response_type: 'id_token',
15
      scope: 'openid' }
16
  end
17
  let(:redirect_uri) do
1✔
18
    root_uri = URI(root_url)
2✔
19
    root_uri.query = launch_params.to_query
2✔
20
    root_uri.to_s
2✔
21
  end
22
  let(:mock_roles) { [LtiDeployment::LTI_ROLES[:instructor]] }
14✔
23

24
  def create_pub_jwk
1✔
25
    @create_pub_jwk ||= JWT::JWK.new(OpenSSL::PKey::RSA.new(1024))
32✔
26
  end
27

28
  def generate_payload(roles, nonce)
1✔
29
    { aud: client_id,
10✔
30
      iss: host,
31
      nonce: nonce,
32
      LtiDeployment::LTI_CLAIMS[:deployment_id] => 'some_deployment_id',
33
      LtiDeployment::LTI_CLAIMS[:context] => { label: 'csc108', title: 'test' },
34
      LtiDeployment::LTI_CLAIMS[:custom] => { course_id: 1, user_id: 1 },
35
      LtiDeployment::LTI_CLAIMS[:user_id] => 'some_user_id',
36
      LtiDeployment::LTI_CLAIMS[:roles] => roles }
37
  end
38

39
  def generate_lti_jwt(roles, nonce)
1✔
40
    payload = generate_payload(roles, nonce)
10✔
41
    pub_jwk = create_pub_jwk
10✔
42
    JWT.encode(payload, pub_jwk.keypair, 'RS256', { kid: pub_jwk.kid })
10✔
43
  end
44

45
  describe '#launch' do
1✔
46
    context 'when launching with invalid parameters' do
1✔
47
      let(:lti_message_hint) { 'opaque string' }
6✔
48
      let(:login_hint) { 'another opque string' }
6✔
49

50
      it 'responds with unprocessable_entity if no parameters are passed' do
1✔
51
        request.headers['Referer'] = host
1✔
52
        post :launch, params: {}
1✔
53
        expect(subject).to respond_with(:unprocessable_content)
1✔
54
      end
55

56
      it 'responds with unprocessable_entity if lti_message_hint is not passed' do
1✔
57
        request.headers['Referer'] = host
1✔
58
        post :launch, params: { client_id: client_id, target_link_uri: target_link_uri, login_hint: login_hint }
1✔
59
        expect(subject).to respond_with(:unprocessable_content)
1✔
60
      end
61

62
      it 'responds with unprocessable_entity if client_id is not passed' do
1✔
63
        request.headers['Referer'] = host
1✔
64
        post :launch,
1✔
65
             params: { lti_message_hint: lti_message_hint, target_link_uri: target_link_uri, login_hint: login_hint }
66
        expect(subject).to respond_with(:unprocessable_content)
1✔
67
      end
68

69
      it 'responds with unprocessable_entity if target_link_uri is not passed' do
1✔
70
        request.headers['Referer'] = host
1✔
71
        post :launch, params: { lti_message_hint: lti_message_hint, client_id: client_id, login_hint: login_hint }
1✔
72
        expect(subject).to respond_with(:unprocessable_content)
1✔
73
      end
74

75
      it 'responds with unprocessable_entity if login_hint is not passed' do
1✔
76
        request.headers['Referer'] = host
1✔
77
        post :launch,
1✔
78
             params: { lti_message_hint: lti_message_hint, client_id: client_id, target_link_uri: target_link_uri }
79
        expect(subject).to respond_with(:unprocessable_content)
1✔
80
      end
81

82
      context 'when all required params exist' do
1✔
83
        before do
1✔
84
          stub_request(:post, "https://test.host:443#{self.described_class::LMS_REDIRECT_ENDPOINT}")
2✔
85
            .with(
86
              body: hash_including(launch_params),
87
              headers: {
88
                'Accept' => '*/*',
89
                'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
90
                'Content-Type' => 'application/x-www-form-urlencoded',
91
                'Host' => 'test.host',
92
                'User-Agent' => 'Ruby'
93
              }
94
            )
95
            .to_return(status: 302, body: 'stubbed response', headers: { location: redirect_uri })
96
        end
97

98
        context 'with correct parameters' do
1✔
99
          it 'redirects to the host auth url' do
1✔
100
            request.headers['Referer'] = host
1✔
101
            post :launch, params: { lti_message_hint: lti_message_hint,
1✔
102
                                    login_hint: login_hint,
103
                                    client_id: 'LMS defined ID', target_link_uri: target_link_uri }
104
            expect(response).to have_http_status(:found)
1✔
105
          end
106

107
          it 'sets the lti_launch cookie' do
1✔
108
            request.headers['Referer'] = host
1✔
109
            post :launch, params: { lti_message_hint: lti_message_hint,
1✔
110
                                    login_hint: login_hint,
111
                                    client_id: 'LMS defined ID', target_link_uri: target_link_uri }
112
            expect(cookies.encrypted[:lti_launch_data]).not_to be_nil
1✔
113
          end
114
        end
115
      end
116
    end
117
  end
118

119
  describe '#redirect_login' do
1✔
120
    let(:jwk_url) { "https://test.host:443#{self.class.described_class::LMS_JWK_ENDPOINT}" }
23✔
121
    let(:nonce) { rand(10 ** 30).to_s.rjust(30, '0') }
23✔
122

123
    before do
1✔
124
      stub_request(:get, jwk_url).to_return(status: 200, body: { keys: [create_pub_jwk.export] }.to_json)
22✔
125

126
      lti_launch_data = {}
22✔
127
      lti_launch_data[:client_id] = client_id
22✔
128
      lti_launch_data[:iss] = host
22✔
129
      lti_launch_data[:nonce] = nonce
22✔
130
      lti_launch_data[:state] = session.id
22✔
131
      cookies.permanent.encrypted[:lti_launch_data] =
22✔
132
        { value: JSON.generate(lti_launch_data), expires: 1.hour.from_now }
133
    end
134

135
    it 'deletes the lti_launch_cookie' do
1✔
136
      request.headers['Referer'] = host
1✔
137
      post :redirect_login, params: {}
1✔
138
      expect(response.cookies).to include('lti_launch_data' => nil)
1✔
139
    end
140

141
    context 'post' do
1✔
142
      context 'with incorrect or missing parameters' do
1✔
143
        it 'redirects to an error page with no params' do
1✔
144
          request.headers['Referer'] = host
1✔
145
          post :redirect_login, params: {}
1✔
146
          expect(subject).to render_template('shared/http_status')
1✔
147
        end
148

149
        it 'redirects to an error page with a mismatched state' do
1✔
150
          request.headers['Referer'] = host
1✔
151
          post :redirect_login, params: { state: state, id_token: 'token' }
1✔
152
          expect(subject).to render_template('shared/http_status')
1✔
153
        end
154
      end
155

156
      context 'with correct parameters' do
1✔
157
        let(:lti_jwt) { generate_lti_jwt(mock_roles, nonce) }
7✔
158

159
        before do
1✔
160
          session[:client_id] = client_id
6✔
161
        end
162

163
        it 'successfully decodes the jwt and redirects' do
1✔
164
          request.headers['Referer'] = host
1✔
165
          post_as instructor, :redirect_login, params: { state: session.id.to_s, id_token: lti_jwt }
1✔
166
          expect(response).to redirect_to(choose_course_lti_deployment_path(LtiDeployment.first))
1✔
167
        end
168

169
        it 'successfully decodes the jwt and sets lti_course_id in the session' do
1✔
170
          request.headers['Referer'] = host
1✔
171
          post_as instructor, :redirect_login, params: { state: session.id.to_s, id_token: lti_jwt }
1✔
172
          expect(LtiDeployment.first.lms_course_id).to eq(1)
1✔
173
        end
174

175
        it 'successfully decodes the jwt and sets lti_course_name in the session' do
1✔
176
          request.headers['Referer'] = host
1✔
177
          post_as instructor, :redirect_login, params: { state: session.id.to_s, id_token: lti_jwt }
1✔
178
          expect(LtiDeployment.first.lms_course_name).to eq('test')
1✔
179
        end
180

181
        it 'successfully decodes the jwt and sets lti_course_label in the session' do
1✔
182
          request.headers['Referer'] = host
1✔
183
          post_as instructor, :redirect_login, params: { state: session.id.to_s, id_token: lti_jwt }
1✔
184
          expect(session[:lti_course_label]).to eq('csc108')
1✔
185
        end
186

187
        it 'successfully decodes the jwt and sets lti_user_id in the session' do
1✔
188
          request.headers['Referer'] = host
1✔
189
          post_as instructor, :redirect_login, params: { state: session.id.to_s, id_token: lti_jwt }
1✔
190
          expect(LtiUser.count).to eq(1)
1✔
191
        end
192

193
        it 'successfully creates a new lti object' do
1✔
194
          request.headers['Referer'] = host
1✔
195
          post_as instructor, :redirect_login, params: { state: session.id.to_s, id_token: lti_jwt }
1✔
196
          expect(LtiDeployment.count).to eq(1)
1✔
197
        end
198
      end
199
    end
200

201
    context 'with LTI role authorization' do
1✔
202
      let(:admin_lti_uri) { LtiDeployment::LTI_ROLES[:admin] }
2✔
203
      let(:student_lti_uri) { LtiDeployment::LTI_ROLES[:learner] }
2✔
204
      let(:ta_lti_uri) { LtiDeployment::LTI_ROLES[:ta] }
2✔
205
      let(:instructor_lti_uri) { LtiDeployment::LTI_ROLES[:instructor] }
3✔
206

207
      context 'when LTI role is Instructor' do
1✔
208
        let(:lti_jwt) { generate_lti_jwt([instructor_lti_uri], nonce) }
2✔
209

210
        it 'redirects to course chooser' do
1✔
211
          request.headers['Referer'] = host
1✔
212
          post_as instructor, :redirect_login, params: { state: session.id.to_s, id_token: lti_jwt }
1✔
213
          expect(response).to redirect_to(choose_course_lti_deployment_path(LtiDeployment.first))
1✔
214
        end
215
      end
216

217
      context 'when LTI role is Admin' do
1✔
218
        let(:lti_jwt) { generate_lti_jwt([admin_lti_uri], nonce) }
2✔
219

220
        it 'redirects to course chooser' do
1✔
221
          request.headers['Referer'] = host
1✔
222
          post_as instructor, :redirect_login, params: { state: session.id.to_s, id_token: lti_jwt }
1✔
223
          expect(response).to redirect_to(choose_course_lti_deployment_path(LtiDeployment.first))
1✔
224
        end
225
      end
226

227
      context 'when LTI role is Student' do
1✔
228
        let(:lti_jwt) { generate_lti_jwt([student_lti_uri], nonce) }
2✔
229

230
        it 'redirects to "not set up" page' do
1✔
231
          request.headers['Referer'] = host
1✔
232
          post_as instructor, :redirect_login, params: { state: session.id.to_s, id_token: lti_jwt }
1✔
233
          expect(response).to redirect_to(course_not_set_up_lti_deployment_path(LtiDeployment.first))
1✔
234
        end
235
      end
236

237
      context 'when LTI role is TA (even with Instructor claim)' do
1✔
238
        let(:lti_jwt) { generate_lti_jwt([ta_lti_uri, instructor_lti_uri], nonce) }
2✔
239

240
        it 'redirects to "not set up" page' do
1✔
241
          request.headers['Referer'] = host
1✔
242
          post_as instructor, :redirect_login, params: { state: session.id.to_s, id_token: lti_jwt }
1✔
243
          expect(response).to redirect_to(course_not_set_up_lti_deployment_path(LtiDeployment.first))
1✔
244
        end
245
      end
246
    end
247

248
    context 'get' do
1✔
249
      it 'returns an error if not logged in' do
1✔
250
        request.headers['Referer'] = host
1✔
251
        get :redirect_login
1✔
252
        expect(subject).to render_template('shared/http_status')
1✔
253
      end
254

255
      it 'returns an error if cookie is not present' do
1✔
256
        request.headers['Referer'] = host
1✔
257
        get_as instructor, :redirect_login
1✔
258
        expect(subject).to render_template('shared/http_status')
1✔
259
      end
260
    end
261

262
    context 'with a cookie' do
1✔
263
      let(:lti_data) do
1✔
264
        { host: 'example.com',
7✔
265
          client_id: 'client_id',
266
          deployment_id: '28:f97330a96452fc363a34e0ef6d8d0d3e9e1007d2',
267
          lms_course_name: 'Introduction to Computer Science',
268
          lms_course_label: 'CSC108',
269
          lms_course_id: 1,
270
          lti_user_id: 'user_id',
271
          user_roles: mock_roles }
272
      end
273
      let(:payload) do
1✔
UNCOV
274
        { aud: client_id,
×
275
          iss: 'https://example.com',
276
          LtiDeployment::LTI_CLAIMS[:deployment_id] => 'some_deployment_id',
277
          LtiDeployment::LTI_CLAIMS[:context] => {
278
            label: 'csc108',
279
            title: 'test'
280
          },
281
          LtiDeployment::LTI_CLAIMS[:custom] => {
282
            course_id: 1,
283
            user_id: 1
284
          },
285
          LtiDeployment::LTI_CLAIMS[:roles] => mock_roles }
286
      end
287

288
      before do
1✔
289
        cookies.permanent.encrypted[:lti_data] = { value: JSON.generate(lti_data), expires: 5.minutes.from_now }
7✔
290
      end
291

292
      it 'successfully decodes the jwt and redirects' do
1✔
293
        request.headers['Referer'] = host
1✔
294
        get_as instructor, :redirect_login
1✔
295
        expect(response).to redirect_to(choose_course_lti_deployment_path(LtiDeployment.first))
1✔
296
      end
297

298
      it 'successfully decodes the jwt and sets lti_course_id in the session' do
1✔
299
        request.headers['Referer'] = host
1✔
300
        get_as instructor, :redirect_login
1✔
301
        expect(LtiDeployment.first.lms_course_id).to eq(1)
1✔
302
      end
303

304
      it 'successfully decodes the jwt and sets lti_course_name in the session' do
1✔
305
        request.headers['Referer'] = host
1✔
306
        get_as instructor, :redirect_login
1✔
307
        expect(LtiDeployment.first.lms_course_name).to eq('Introduction to Computer Science')
1✔
308
      end
309

310
      it 'successfully decodes the jwt and sets lti_course_label in the session' do
1✔
311
        request.headers['Referer'] = host
1✔
312
        get_as instructor, :redirect_login
1✔
313
        expect(session[:lti_course_label]).to eq('CSC108')
1✔
314
      end
315

316
      it 'successfully decodes the jwt and sets lti_user_id in the session' do
1✔
317
        request.headers['Referer'] = host
1✔
318
        get_as instructor, :redirect_login
1✔
319
        expect(LtiUser.count).to eq(1)
1✔
320
      end
321

322
      it 'successfully creates a new lti object' do
1✔
323
        request.headers['Referer'] = host
1✔
324
        get_as instructor, :redirect_login
1✔
325
        expect(LtiDeployment.count).to eq(1)
1✔
326
      end
327

328
      it 'deletes the data cookie' do
1✔
329
        request.headers['Referer'] = host
1✔
330
        get_as instructor, :redirect_login
1✔
331
        expect(response.cookies).to include('lti_data' => nil)
1✔
332
      end
333
    end
334
  end
335

336
  describe '#check_host' do
1✔
337
    before do
1✔
338
      request.env['HTTP_REFERER'] = root_url
2✔
339
    end
340

341
    it 'does not redirect to an error with a known host' do
1✔
342
      get_as instructor, :redirect_login
1✔
343
      expect(subject).to respond_with(:success)
1✔
344
    end
345

346
    it 'does redirect to an error with an unknown host' do
1✔
347
      request.headers['Referer'] = 'http://example.com'
1✔
348
      post_as instructor, :launch, params: { lti_message_hint: 'hint',
1✔
349
                                             login_hint: 'hint',
350
                                             client_id: 'LMS defined ID', target_link_uri: 'test.com' }
351
      expect(response).to have_http_status(:unprocessable_content)
1✔
352
    end
353
  end
354
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc