rack-lti 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.travis.yml +6 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +146 -0
- data/Rakefile +8 -0
- data/lib/rack/lti/config.rb +55 -0
- data/lib/rack/lti/middleware.rb +79 -0
- data/lib/rack/lti/version.rb +5 -0
- data/lib/rack/lti.rb +11 -0
- data/lib/rack-lti.rb +1 -0
- data/rack-lti.gemspec +33 -0
- data/test/config_test.rb +112 -0
- data/test/lti_test.rb +12 -0
- data/test/middleware_test.rb +142 -0
- metadata +137 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 277e9014df9050362c2e13d446193138204f4938
|
4
|
+
data.tar.gz: 8d0074eb73c29330d8fe0641fb9a16766c2b94c6
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 93e6f3179d41671d631a8238814220fefd789abb2814801dfcc580aeab170794d3bdfed0e1a044de11b3f09d7b24e04689c0d3964f4fcdd6e9f67c49b80f693e
|
7
|
+
data.tar.gz: 37a52e7d3499561ef840020ad1093a3910b0812dfcf233172b82d8b5a03c5f20521472b06c6780181b1e72dcb4ba233f8755e1dd35425b4b6c6a174ec178d307
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Zach Pendleton
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,146 @@
|
|
1
|
+
# Rack::LTI
|
2
|
+
|
3
|
+
[![Build Status](https://travis-ci.org/zachpendleton/rack-lti.png)](https://travis-ci.org/zachpendleton/rack-lti)
|
4
|
+
|
5
|
+
Rack::LTI exposes LTI launch and config URLs in your Rack application, handling
|
6
|
+
authorization, storing launch parameters, and generating config information for
|
7
|
+
consumers.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
gem 'rack-lti'
|
14
|
+
|
15
|
+
## Usage
|
16
|
+
|
17
|
+
Rack::LTI should work with any Rack-based app. This means Rails 3.x and
|
18
|
+
Sinatra, and probably whatever wonky framework you happen to be using.
|
19
|
+
|
20
|
+
Rack::LTI is tested on MRI Ruby 1.9 and 2.0, and the 1.9 branches of JRuby
|
21
|
+
and Rubinius. It will not work on any flavor of 1.8; upgrade already.
|
22
|
+
|
23
|
+
### Rails 3
|
24
|
+
|
25
|
+
Add Rack::LTI to your `config/application.rb`:
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
class Application < Rails::Application
|
29
|
+
config.middleware.use Rack::LTI,
|
30
|
+
consumer_key: ->(key, consumer_id) { key == 'key_value' },
|
31
|
+
consumer_secret: ->(secret) { secret == 'top_secret' }
|
32
|
+
|
33
|
+
app_path: '/',
|
34
|
+
config_path: '/lti/config.xml',
|
35
|
+
launch_path: '/lti/launch',
|
36
|
+
|
37
|
+
title: 'My LTI App',
|
38
|
+
description: 'My LTI App description',
|
39
|
+
|
40
|
+
nonce_validator: ->(nonce) { !FakeNonceStore.include?(nonce) },
|
41
|
+
success: ->(params, session) {
|
42
|
+
params['launch_params'] = params unless session.nil?
|
43
|
+
},
|
44
|
+
time_limit: 60*60,
|
45
|
+
|
46
|
+
extensions: {
|
47
|
+
'canvas.instructure.com' => {
|
48
|
+
course_navigation: {
|
49
|
+
default: 'enabled',
|
50
|
+
text: 'My LTI App'
|
51
|
+
}
|
52
|
+
}
|
53
|
+
},
|
54
|
+
|
55
|
+
custom_params: {
|
56
|
+
preferred_name: 'El Tigre Chino'
|
57
|
+
}
|
58
|
+
end
|
59
|
+
```
|
60
|
+
|
61
|
+
### Sinatra
|
62
|
+
|
63
|
+
Add Rack::LTI to your app:
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
class Application < Sinatra::Base
|
67
|
+
use Rack::LTI,
|
68
|
+
consumer_key: 'my_key',
|
69
|
+
consumer_secret: 'my_secret',
|
70
|
+
|
71
|
+
app_path: '/',
|
72
|
+
config_path: '/lti/config.xml',
|
73
|
+
launch_path: '/lti/launch',
|
74
|
+
|
75
|
+
title: 'My LTI App',
|
76
|
+
description: 'My LTI App description',
|
77
|
+
|
78
|
+
nonce_validator: ->(nonce) { !FakeNonceStore.include?(nonce) },
|
79
|
+
success: ->(params, session) {
|
80
|
+
params['launch_params'] = params unless session.nil?
|
81
|
+
},
|
82
|
+
time_limit: 60*60,
|
83
|
+
|
84
|
+
extensions: {
|
85
|
+
'canvas.instructure.com' => {
|
86
|
+
course_navigation: {
|
87
|
+
default: 'enabled',
|
88
|
+
text: 'My LTI App'
|
89
|
+
}
|
90
|
+
}
|
91
|
+
},
|
92
|
+
|
93
|
+
custom_params: {
|
94
|
+
preferred_name: 'El Tigre Chino'
|
95
|
+
}
|
96
|
+
end
|
97
|
+
```
|
98
|
+
|
99
|
+
## Configuration
|
100
|
+
|
101
|
+
Rack::LTI takes either a configuration hash or block at initialization. Allowed
|
102
|
+
values are:
|
103
|
+
|
104
|
+
* `consumer_key` The consumer_key to check against the key given at launch.
|
105
|
+
This value can be a string or a lambda. If a lambda, it is passed the key
|
106
|
+
used by the consumer as well as their tool_consumer_instance_guid.
|
107
|
+
* `consumer_secret` The consumer_secret to check against the secret given at
|
108
|
+
launch. Like the consumer key, this value can be a string or a lambda. If a
|
109
|
+
lambda, it is passed the key and tool_consumer_instance_guid of the
|
110
|
+
consumer.
|
111
|
+
* `app_path` The path to redirect to on a successful launch. This should be
|
112
|
+
the main page of your application. Defaults to '/'.
|
113
|
+
* `config_path` The path to serve LTI config XML from. Defaults to
|
114
|
+
'/lti/config.xml'.
|
115
|
+
* `launch_path` The path to receive LTI launch requests at. Defaults to
|
116
|
+
'/lti/launch'.
|
117
|
+
* `title` The title of your LTI application.
|
118
|
+
* `description` The description of your LTI application.
|
119
|
+
* `nonce_validator` A lambda used to validate the current request's nonce.
|
120
|
+
It is passed the nonce to verify. If not provided, all nonces are allowed.
|
121
|
+
* `time_limit` The time limit, in seconds, to consider requests valid within.
|
122
|
+
If not passed, the default is 3600 seconds (one hour).
|
123
|
+
* `success` A lambda called on successful launch. It is passed the launch
|
124
|
+
params as a hash and the session if present. Can be used to cache params
|
125
|
+
for the current user, find the current user, etc. If not given, the launch
|
126
|
+
params are stored in the 'launch_params' key of the session.
|
127
|
+
* `extensions` A hash of extension information to include with the config.
|
128
|
+
Format is platform -> option -> properties. See usage examples above for
|
129
|
+
more detail.
|
130
|
+
* `custom_params` A hash of custom parameters to accept from the client. See
|
131
|
+
usage examples above for more detail.
|
132
|
+
|
133
|
+
## About LTI
|
134
|
+
|
135
|
+
Interested in learning more about LTI? Here are some links to get you started:
|
136
|
+
|
137
|
+
* [Introduction to LTI](http://www.imsglobal.org/toolsinteroperability2.cfm)
|
138
|
+
* [1.1.1 Implementation Guide](http://www.imsglobal.org/LTI/v1p1p1/ltiIMGv1p1p1.html)
|
139
|
+
|
140
|
+
## Contributing
|
141
|
+
|
142
|
+
1. Fork it
|
143
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
144
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
145
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
146
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'ims/lti'
|
2
|
+
|
3
|
+
module Rack::LTI
|
4
|
+
class Config < Hash
|
5
|
+
DEFAULT = {
|
6
|
+
app_path: '/',
|
7
|
+
config_path: '/lti/config.xml',
|
8
|
+
description: 'An LTI Application.',
|
9
|
+
launch_path: '/lti/launch',
|
10
|
+
nonce_validator: true,
|
11
|
+
success: ->(params, session) { session['launch_params'] = params if session },
|
12
|
+
time_limit: 60*60,
|
13
|
+
title: 'LTI App'
|
14
|
+
}
|
15
|
+
|
16
|
+
def initialize(options = {})
|
17
|
+
DEFAULT.merge(options).each { |k, v| self[k] = v }
|
18
|
+
instance_eval { yield(self) } if block_given?
|
19
|
+
end
|
20
|
+
|
21
|
+
[:consumer_key, :consumer_secret, :nonce_validator].each do |method|
|
22
|
+
define_method(method) do |*args|
|
23
|
+
if self[method].respond_to?(:call)
|
24
|
+
self[method].call(*args)
|
25
|
+
else
|
26
|
+
self[method]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def public?
|
32
|
+
self[:consumer_key].nil? && self[:consumer_secret].nil?
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_xml(options = {})
|
36
|
+
# Stringify keys for IMS::LTI
|
37
|
+
config = self.merge(options).inject({}) do |h, v|
|
38
|
+
h[v[0].to_s] = v[1]
|
39
|
+
h
|
40
|
+
end
|
41
|
+
|
42
|
+
IMS::LTI::ToolConfig.new(config).to_xml(indent: 2)
|
43
|
+
end
|
44
|
+
|
45
|
+
def method_missing(method, *args, &block)
|
46
|
+
if method.match(/=$/)
|
47
|
+
self[method.to_s[0..-2].to_sym] = args.first
|
48
|
+
elsif self.has_key?(method)
|
49
|
+
self[method]
|
50
|
+
else
|
51
|
+
super
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'ims/lti'
|
2
|
+
require 'oauth/request_proxy/rack_request'
|
3
|
+
require 'rack/lti/config'
|
4
|
+
|
5
|
+
module Rack::LTI
|
6
|
+
class Middleware
|
7
|
+
attr_reader :app, :config
|
8
|
+
|
9
|
+
def initialize(app, options = {}, &block)
|
10
|
+
@app = app
|
11
|
+
@config = Config.new(options, &block)
|
12
|
+
end
|
13
|
+
|
14
|
+
def call(env)
|
15
|
+
request = Rack::Request.new(env)
|
16
|
+
|
17
|
+
if routes.has_key?(request.path)
|
18
|
+
env['rack.lti'] = true
|
19
|
+
send(routes[request.path], request, env)
|
20
|
+
else
|
21
|
+
@app.call(env)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def routes
|
26
|
+
{
|
27
|
+
@config.config_path => :config_action,
|
28
|
+
@config.launch_path => :launch_action
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def config_action(request, env)
|
35
|
+
response = [@config.to_xml(launch_url: request.url.sub(@config.config_path, @config.launch_path))]
|
36
|
+
[200, { 'Content-Type' => 'application/xml', 'Content-Length' => response[0].length.to_s }, response]
|
37
|
+
end
|
38
|
+
|
39
|
+
def launch_action(request, env)
|
40
|
+
provider = IMS::LTI::ToolProvider.new(@config.consumer_key(*request.params.values_at('oauth_consumer_key', 'tool_consumer_instance_guid')),
|
41
|
+
@config.consumer_secret(*request.params.values_at('oauth_consumer_key', 'tool_consumer_instance_guid')),
|
42
|
+
request.params)
|
43
|
+
|
44
|
+
if valid?(provider, request)
|
45
|
+
@config.success.call(provider.to_params, env['rack.session'])
|
46
|
+
[301, { 'Content-Length' => '0', 'Content-Type' => 'text/html', 'Location' => @config.app_path }, []]
|
47
|
+
else
|
48
|
+
response = 'Invalid launch.'
|
49
|
+
[403, { 'Content-Type' => 'text/plain', 'Content-Length' => response.length.to_s }, [response]]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def valid?(provider, request)
|
54
|
+
valid_request?(provider, request) &&
|
55
|
+
valid_nonce?(request.params['oauth_nonce']) &&
|
56
|
+
valid_timestamp?(request.params['oauth_timestamp'].to_i)
|
57
|
+
end
|
58
|
+
|
59
|
+
def valid_request?(provider, request)
|
60
|
+
@config.public? ? true : provider.valid_request?(request)
|
61
|
+
end
|
62
|
+
|
63
|
+
def valid_nonce?(nonce)
|
64
|
+
if @config.nonce_validator.respond_to?(:call)
|
65
|
+
@config.nonce_validator.call(nonce)
|
66
|
+
else
|
67
|
+
@config.nonce_validator
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def valid_timestamp?(timestamp)
|
72
|
+
if @config.time_limit.nil?
|
73
|
+
true
|
74
|
+
else
|
75
|
+
(Time.now.to_i - @config.time_limit) <= timestamp
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
data/lib/rack/lti.rb
ADDED
data/lib/rack-lti.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'rack/lti'
|
data/rack-lti.gemspec
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'rack/lti/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'rack-lti'
|
8
|
+
spec.version = Rack::LTI::VERSION
|
9
|
+
spec.authors = ['Zach Pendleton']
|
10
|
+
spec.email = ['zachpendleton@gmail.com']
|
11
|
+
spec.description = <<-END
|
12
|
+
Rack::LTI provides LTI launch and configuration endpoints to your
|
13
|
+
Rack-based application. It handles configuration, authorization, and
|
14
|
+
routing.
|
15
|
+
|
16
|
+
For more information about LTI, see http://www.imsglobal.org/toolsinteroperability2.cfm.
|
17
|
+
END
|
18
|
+
spec.summary = %q{Middleware for handling LTI launches inside your Rack app.}
|
19
|
+
spec.homepage = 'https://github.com/zachpendleton/rack-lti'
|
20
|
+
spec.license = 'MIT'
|
21
|
+
|
22
|
+
spec.files = `git ls-files`.split($/)
|
23
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
24
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
25
|
+
spec.require_paths = ['lib']
|
26
|
+
|
27
|
+
spec.add_development_dependency 'bundler', '~> 1.3'
|
28
|
+
spec.add_development_dependency 'minitest', '~> 4.7.0'
|
29
|
+
spec.add_development_dependency 'rake'
|
30
|
+
|
31
|
+
spec.add_dependency 'ims-lti', '~> 1.1.2'
|
32
|
+
spec.add_dependency 'rack'
|
33
|
+
end
|
data/test/config_test.rb
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'rexml/document'
|
3
|
+
require 'rack/lti/config'
|
4
|
+
|
5
|
+
class ConfigTest < Minitest::Unit::TestCase
|
6
|
+
def setup
|
7
|
+
@config = Rack::LTI::Config.new
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_config_accepts_hash_style_setters
|
11
|
+
@config[:setting] = 'value'
|
12
|
+
assert_equal 'value', @config[:setting]
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_config_accepts_accessor_style_setters
|
16
|
+
@config.setting = 'value'
|
17
|
+
assert_equal 'value', @config.setting
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_config_accepts_a_block
|
21
|
+
config = Rack::LTI::Config.new do |c|
|
22
|
+
c[:title] = 'custom title'
|
23
|
+
end
|
24
|
+
|
25
|
+
assert_equal 'custom title', config[:title]
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_config_populates_default_values
|
29
|
+
assert_equal '/', @config.app_path
|
30
|
+
assert_equal '/lti/config.xml', @config.config_path
|
31
|
+
assert_equal 'An LTI Application.', @config.description
|
32
|
+
assert_equal '/lti/launch', @config.launch_path
|
33
|
+
assert_equal true, @config.nonce_validator
|
34
|
+
assert_equal 3600, @config.time_limit
|
35
|
+
assert_equal 'LTI App', @config.title
|
36
|
+
assert_instance_of Proc, @config.success
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_consumer_key_returns_primitive_values
|
40
|
+
@config[:consumer_key] = 1
|
41
|
+
assert_equal 1, @config.consumer_key
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_consumer_key_calls_a_proc_if_given
|
45
|
+
@config[:consumer_key] = ->(n) { n + 1 }
|
46
|
+
assert_equal 2, @config.consumer_key(1)
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_consumer_secret_returns_primitive_values
|
50
|
+
@config[:consumer_secret] = 1
|
51
|
+
assert_equal 1, @config.consumer_secret
|
52
|
+
end
|
53
|
+
|
54
|
+
def test_consumer_secret_calls_a_proc_if_given
|
55
|
+
@config[:consumer_secret] = ->(n) { n + 1 }
|
56
|
+
assert_equal 2, @config.consumer_secret(1)
|
57
|
+
end
|
58
|
+
|
59
|
+
def test_nonce_validator_returns_primitive_values
|
60
|
+
@config[:nonce_validator] = 1
|
61
|
+
assert_equal 1, @config.nonce_validator
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_nonce_validator_calls_a_proc_if_given
|
65
|
+
@config[:nonce_validator] = ->(n) { n + 1 }
|
66
|
+
assert_equal 2, @config.nonce_validator(1)
|
67
|
+
end
|
68
|
+
|
69
|
+
def test_public_returns_true_if_no_key_or_secret_is_set
|
70
|
+
@config[:consumer_key] = nil
|
71
|
+
@config[:consumer_secret] = nil
|
72
|
+
assert @config.public?
|
73
|
+
end
|
74
|
+
|
75
|
+
def test_to_xml_returns_an_xml_lti_config
|
76
|
+
body = REXML::Document.new(@config.to_xml(launch_url: 'http://example.com/launch'))
|
77
|
+
|
78
|
+
assert_equal @config.title,
|
79
|
+
REXML::XPath.match(body, '//blti:title').first.text
|
80
|
+
assert_equal @config.description,
|
81
|
+
REXML::XPath.match(body, '//blti:description').first.text
|
82
|
+
assert_equal 'http://example.com/launch',
|
83
|
+
REXML::XPath.match(body, '//blti:launch_url').first.text
|
84
|
+
end
|
85
|
+
|
86
|
+
def test_to_xml_includes_extensions
|
87
|
+
@config[:extensions] = {
|
88
|
+
'canvas.instructure.com' => {
|
89
|
+
'course_navigation' => {
|
90
|
+
'privacy_level' => 'anonymous',
|
91
|
+
'text' => 'Tool title',
|
92
|
+
'url' => 'http://example.com'
|
93
|
+
}
|
94
|
+
}
|
95
|
+
}
|
96
|
+
|
97
|
+
body = REXML::Document.new(@config.to_xml(launch_url: 'http://example.com/launch'))
|
98
|
+
assert_equal 'anonymous',
|
99
|
+
REXML::XPath.match(body, '//lticm:property[@name="privacy_level"]').first.text
|
100
|
+
assert_equal 'Tool title',
|
101
|
+
REXML::XPath.match(body, '//lticm:property[@name="text"]').first.text
|
102
|
+
assert_equal 'http://example.com',
|
103
|
+
REXML::XPath.match(body, '//lticm:property[@name="url"]').first.text
|
104
|
+
end
|
105
|
+
|
106
|
+
def test_to_xml_includes_custom_params
|
107
|
+
@config[:custom_params] = { ck1: 'one', ck2: 'two' }
|
108
|
+
body = REXML::Document.new(@config.to_xml(launch_url: 'http://example.com/launch'))
|
109
|
+
assert_equal 'one', REXML::XPath.match(body, '//blti:custom/lticm:property[@name="ck1"]').first.text
|
110
|
+
assert_equal 'two', REXML::XPath.match(body, '//blti:custom/lticm:property[@name="ck2"]').first.text
|
111
|
+
end
|
112
|
+
end
|
data/test/lti_test.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'rack-lti'
|
3
|
+
|
4
|
+
class LtiTest < Minitest::Unit::TestCase
|
5
|
+
def setup
|
6
|
+
@app = ->(env) { [200, [], ['Hi']] }
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_lti_proxies_new_calls_to_middleware
|
10
|
+
assert_instance_of Rack::LTI::Middleware, Rack::LTI.new(@app)
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'rack'
|
3
|
+
require 'rack/lti/middleware'
|
4
|
+
|
5
|
+
class MiddlewareTest < Minitest::Unit::TestCase
|
6
|
+
def setup
|
7
|
+
@app = ->(env) { [200, {}, ['hi']] }
|
8
|
+
@lti_app = Rack::LTI::Middleware.new(@app)
|
9
|
+
@params = {
|
10
|
+
lti_message_type: 'basic-lti-launch-request',
|
11
|
+
lti_version: 'LTI-1p0',
|
12
|
+
resource_link_id: '88391-e1919-bb3456',
|
13
|
+
resource_link_title: 'Resource Title',
|
14
|
+
user_id: '0ae836b9-7fc9-4060-006f-27b2066ac545',
|
15
|
+
roles: 'instructor',
|
16
|
+
tool_consumer_instance_guid: 'guid',
|
17
|
+
oauth_consumer_key: 'key',
|
18
|
+
oauth_nonce: '12345',
|
19
|
+
oauth_timestamp: Time.now.to_i.to_s
|
20
|
+
}.reduce({}) { |m, h| m[h[0].to_s] = h[1]; m }
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_middleware_accepts_an_app
|
24
|
+
assert_equal @lti_app.app, @app
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_routes_returns_the_recognized_routes
|
28
|
+
known_routes = { @lti_app.config.config_path => :config_action,
|
29
|
+
@lti_app.config.launch_path => :launch_action }
|
30
|
+
assert_equal known_routes, @lti_app.routes
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_call_returns_a_valid_rack_response
|
34
|
+
response = @lti_app.call(Rack::MockRequest.env_for('/'))
|
35
|
+
|
36
|
+
assert_equal response, @app.call(nil)
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_call_intercepts_known_routes
|
40
|
+
env = Rack::MockRequest.env_for('/lti/launch')
|
41
|
+
@lti_app.call(env)
|
42
|
+
|
43
|
+
assert_equal true, env['rack.lti']
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_call_ignores_unknown_routes
|
47
|
+
env = Rack::MockRequest.env_for('/')
|
48
|
+
@lti_app.call(env)
|
49
|
+
|
50
|
+
assert_equal nil, env['rack.lti']
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_call_returns_403_on_invalid_launch
|
54
|
+
@lti_app.stub(:valid?, false) do
|
55
|
+
response = @lti_app.call(Rack::MockRequest.env_for('/lti/launch'))
|
56
|
+
assert_equal 403, response[0]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def test_call_returns_403_on_invalid_nonce
|
61
|
+
@lti_app.config.nonce_validator ->(nonce) { false }
|
62
|
+
|
63
|
+
@lti_app.stub(:valid_request?, true) do
|
64
|
+
response = @lti_app.call(Rack::MockRequest.env_for('/lti/launch'))
|
65
|
+
assert_equal 403, response[0]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def test_call_returns_403_on_expired_timestamp
|
70
|
+
@lti_app.config.nonce_validator = true
|
71
|
+
@lti_app.config.time_limit = 30
|
72
|
+
|
73
|
+
@lti_app.stub(:valid_request?, true) do
|
74
|
+
env = Rack::MockRequest.env_for('/lti/launch',
|
75
|
+
oauth_timestamp: Time.now - 60*60)
|
76
|
+
response = @lti_app.call(env)
|
77
|
+
assert_equal 403, response[0]
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def test_call_stores_launch_params_in_the_session
|
82
|
+
@lti_app.stub(:valid_request?, true) do
|
83
|
+
env = Rack::MockRequest.env_for('/lti/launch', method: 'post',
|
84
|
+
'rack.session' => {},
|
85
|
+
params: @params)
|
86
|
+
@lti_app.call(env)
|
87
|
+
assert_equal @params.keys.sort,
|
88
|
+
env['rack.session']['launch_params'].keys.sort
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def test_call_redirects_to_app_path_on_success
|
93
|
+
@lti_app.stub(:valid_request?, true) do
|
94
|
+
env = Rack::MockRequest.env_for('/lti/launch', method: 'post',
|
95
|
+
params: @params)
|
96
|
+
response = @lti_app.call(env)
|
97
|
+
assert_equal 301, response[0]
|
98
|
+
assert_equal @lti_app.config[:app_path], response[1]['Location']
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def test_call_succeeds_if_sessions_are_not_used
|
103
|
+
@lti_app.stub(:valid_request?, true) do
|
104
|
+
env = Rack::MockRequest.env_for('/lti/launch', method: 'post',
|
105
|
+
params: @params)
|
106
|
+
response = @lti_app.call(env)
|
107
|
+
assert_equal 301, response[0]
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def test_call_returns_xml_when_config_path_is_used
|
112
|
+
response = @lti_app.call(Rack::MockRequest.env_for('/lti/config.xml'))
|
113
|
+
assert_equal 'application/xml', response[1]['Content-Type']
|
114
|
+
end
|
115
|
+
|
116
|
+
def test_consumer_key_is_passed_request_information
|
117
|
+
@lti_app.config[:consumer_key] = ->(key, guid) {
|
118
|
+
assert_equal 'key', key
|
119
|
+
assert_equal 'guid', guid
|
120
|
+
}
|
121
|
+
|
122
|
+
@lti_app.stub(:valid?, true) do
|
123
|
+
env = Rack::MockRequest.env_for('/lti/launch', method: 'post',
|
124
|
+
params: @params)
|
125
|
+
@lti_app.call(env)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def test_consumer_secret_is_passed_request_information
|
130
|
+
@lti_app.config[:consumer_secret] = ->(key, guid) {
|
131
|
+
assert_equal 'key', key
|
132
|
+
assert_equal 'guid', guid
|
133
|
+
}
|
134
|
+
|
135
|
+
@lti_app.stub(:valid?, true) do
|
136
|
+
env = Rack::MockRequest.env_for('/lti/launch', method: 'post',
|
137
|
+
params: @params)
|
138
|
+
@lti_app.call(env)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
metadata
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rack-lti
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Zach Pendleton
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-04-16 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.3'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: minitest
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 4.7.0
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 4.7.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: ims-lti
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.1.2
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ~>
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 1.1.2
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rack
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description: |2
|
84
|
+
Rack::LTI provides LTI launch and configuration endpoints to your
|
85
|
+
Rack-based application. It handles configuration, authorization, and
|
86
|
+
routing.
|
87
|
+
|
88
|
+
For more information about LTI, see http://www.imsglobal.org/toolsinteroperability2.cfm.
|
89
|
+
email:
|
90
|
+
- zachpendleton@gmail.com
|
91
|
+
executables: []
|
92
|
+
extensions: []
|
93
|
+
extra_rdoc_files: []
|
94
|
+
files:
|
95
|
+
- .gitignore
|
96
|
+
- .travis.yml
|
97
|
+
- Gemfile
|
98
|
+
- LICENSE.txt
|
99
|
+
- README.md
|
100
|
+
- Rakefile
|
101
|
+
- lib/rack-lti.rb
|
102
|
+
- lib/rack/lti.rb
|
103
|
+
- lib/rack/lti/config.rb
|
104
|
+
- lib/rack/lti/middleware.rb
|
105
|
+
- lib/rack/lti/version.rb
|
106
|
+
- rack-lti.gemspec
|
107
|
+
- test/config_test.rb
|
108
|
+
- test/lti_test.rb
|
109
|
+
- test/middleware_test.rb
|
110
|
+
homepage: https://github.com/zachpendleton/rack-lti
|
111
|
+
licenses:
|
112
|
+
- MIT
|
113
|
+
metadata: {}
|
114
|
+
post_install_message:
|
115
|
+
rdoc_options: []
|
116
|
+
require_paths:
|
117
|
+
- lib
|
118
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
119
|
+
requirements:
|
120
|
+
- - '>='
|
121
|
+
- !ruby/object:Gem::Version
|
122
|
+
version: '0'
|
123
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
124
|
+
requirements:
|
125
|
+
- - '>='
|
126
|
+
- !ruby/object:Gem::Version
|
127
|
+
version: '0'
|
128
|
+
requirements: []
|
129
|
+
rubyforge_project:
|
130
|
+
rubygems_version: 2.0.0
|
131
|
+
signing_key:
|
132
|
+
specification_version: 4
|
133
|
+
summary: Middleware for handling LTI launches inside your Rack app.
|
134
|
+
test_files:
|
135
|
+
- test/config_test.rb
|
136
|
+
- test/lti_test.rb
|
137
|
+
- test/middleware_test.rb
|