d2l-valence 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/.codeclimate.yml +22 -0
- data/.gitignore +17 -0
- data/.rubocop.yml +13 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +71 -0
- data/LICENSE.txt +22 -0
- data/README.md +266 -0
- data/Rakefile +6 -0
- data/d2l-valence.gemspec +30 -0
- data/lib/d2l/valence.rb +15 -0
- data/lib/d2l/valence/app_context.rb +49 -0
- data/lib/d2l/valence/auth_tokens.rb +60 -0
- data/lib/d2l/valence/encrypt.rb +34 -0
- data/lib/d2l/valence/host.rb +43 -0
- data/lib/d2l/valence/request.rb +104 -0
- data/lib/d2l/valence/response.rb +66 -0
- data/lib/d2l/valence/timestamp_error.rb +42 -0
- data/lib/d2l/valence/user_context.rb +58 -0
- data/lib/d2l/valence/version.rb +5 -0
- data/spec/d2l/valence/app_context_spec.rb +14 -0
- data/spec/d2l/valence/auth_tokens_spec.rb +30 -0
- data/spec/d2l/valence/host_spec.rb +64 -0
- data/spec/d2l/valence/request/authenticated_uri_spec.rb +75 -0
- data/spec/d2l/valence/request/execute/invalid_time_stamp_spec.rb +40 -0
- data/spec/d2l/valence/request/execute/lti_links/create_spec.rb +66 -0
- data/spec/d2l/valence/request/execute/lti_links/delete_spec.rb +39 -0
- data/spec/d2l/valence/request/execute/lti_links/list_spec.rb +41 -0
- data/spec/d2l/valence/request/execute/lti_links/update_spec.rb +73 -0
- data/spec/d2l/valence/request/execute/lti_links/view_spec.rb +41 -0
- data/spec/d2l/valence/request/execute/version_spec.rb +41 -0
- data/spec/d2l/valence/request/execute/whoami_spec.rb +40 -0
- data/spec/d2l/valence/response/code_spec.rb +34 -0
- data/spec/d2l/valence/response/server_skew_spec.rb +33 -0
- data/spec/d2l/valence/timestamp_error_spec.rb +21 -0
- data/spec/d2l/valence/user_context_spec.rb +6 -0
- data/spec/fixtures/vcr_cassettes/request/execute/create_lti_link.yml +71 -0
- data/spec/fixtures/vcr_cassettes/request/execute/delete_lti_link.yml +48 -0
- data/spec/fixtures/vcr_cassettes/request/execute/get_a_lti_link.yml +66 -0
- data/spec/fixtures/vcr_cassettes/request/execute/get_lti_links.yml +70 -0
- data/spec/fixtures/vcr_cassettes/request/execute/get_version.yml +66 -0
- data/spec/fixtures/vcr_cassettes/request/execute/get_whoami.yml +61 -0
- data/spec/fixtures/vcr_cassettes/request/execute/invalid_timestamp.yml +112 -0
- data/spec/fixtures/vcr_cassettes/request/execute/put_lti_link.yml +139 -0
- data/spec/spec_helper.rb +20 -0
- data/spec/support/common_context.rb +37 -0
- metadata +228 -0
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe D2L::Valence::AppContext, type: :service do
|
4
|
+
include_context :common_context
|
5
|
+
|
6
|
+
context '.auth_url' do
|
7
|
+
subject { described_class.new(brightspace_host: auth_host, app_id: app_id, app_key: app_key) }
|
8
|
+
let(:expected_url) { "https://partners.brightspace.com/d2l/auth/api/token?x_a=#{app_id}&x_b=#{auth_key}&x_target=#{CGI.escape(callback_uri.to_s)}" }
|
9
|
+
|
10
|
+
it('will generate an authentication URL') do
|
11
|
+
expect(subject.auth_url(callback_uri)).to eq expected_url
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe D2L::Valence::AuthTokens, type: :service do
|
4
|
+
include_context :common_context
|
5
|
+
|
6
|
+
context '.generate' do
|
7
|
+
subject { described_class.new(request: request) }
|
8
|
+
let(:request) do
|
9
|
+
D2L::Valence::Request.new(
|
10
|
+
user_context: user_context,
|
11
|
+
http_method: 'GET',
|
12
|
+
route: '/d2l/api/lp/:version/users/whoami'
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
let(:signature) { "#{request.http_method}&#{CGI.unescape(request.path)}&#{subject.adjusted_timestamp}" }
|
17
|
+
let(:signature_by_app_key) { D2L::Valence::Encrypt.generate_from(app_key, signature) }
|
18
|
+
let(:signature_by_user_key) { D2L::Valence::Encrypt.generate_from(user_key, signature) }
|
19
|
+
let(:tokens) { subject.generate }
|
20
|
+
|
21
|
+
it 'will generate the right token values' do
|
22
|
+
expect(tokens[described_class::APP_ID_PARAM]).to eq app_id
|
23
|
+
expect(tokens[described_class::SIGNATURE_BY_APP_KEY_PARAM]).to eq signature_by_app_key
|
24
|
+
expect(tokens[described_class::USER_ID_PARAM]).to eq user_id
|
25
|
+
expect(tokens[described_class::SIGNATURE_BY_USER_KEY_PARAM]).to eq signature_by_user_key
|
26
|
+
expect(tokens[described_class::TIMESTAMP_PARAM]).to eq subject.adjusted_timestamp
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe D2L::Valence::Host do
|
4
|
+
context '.initialize' do
|
5
|
+
let(:host) { 'www.somehost.com' }
|
6
|
+
|
7
|
+
context 'for scheme string' do
|
8
|
+
subject { described_class.new(scheme: 'HTTPS', host: host) }
|
9
|
+
|
10
|
+
its(:scheme) { is_expected.to eq :https }
|
11
|
+
its(:host) { is_expected.to eq host }
|
12
|
+
its(:port) { is_expected.to be_nil}
|
13
|
+
end
|
14
|
+
|
15
|
+
context 'for scheme symbol' do
|
16
|
+
subject { described_class.new(scheme: :http, host: host) }
|
17
|
+
|
18
|
+
its(:scheme) { is_expected.to eq :http }
|
19
|
+
its(:host) { is_expected.to eq host }
|
20
|
+
its(:port) { is_expected.to be_nil}
|
21
|
+
end
|
22
|
+
|
23
|
+
context 'for unsupported scheme' do
|
24
|
+
it('will raise an exception') do
|
25
|
+
expect { described_class.new(scheme: :sftp, host: host) }.to raise_error RuntimeError, 'sftp is an unsupported scheme. Please use either HTTP or HTTPS'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context '.to_uri' do
|
31
|
+
let(:host) { 'www.somehost.com' }
|
32
|
+
|
33
|
+
context 'for HTTP' do
|
34
|
+
subject { described_class.new(scheme: :http, host: host, port: port) }
|
35
|
+
|
36
|
+
context 'with no port' do
|
37
|
+
let(:port) { nil }
|
38
|
+
|
39
|
+
its('to_uri.to_s') { is_expected.to eq 'http://www.somehost.com' }
|
40
|
+
end
|
41
|
+
context 'with specific port' do
|
42
|
+
let(:port) { 8080 }
|
43
|
+
|
44
|
+
its('to_uri.to_s') { is_expected.to eq 'http://www.somehost.com:8080' }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context 'for HTTPS' do
|
49
|
+
subject { described_class.new(scheme: :https, host: host, port: port) }
|
50
|
+
|
51
|
+
context 'with no port' do
|
52
|
+
let(:port) { nil }
|
53
|
+
|
54
|
+
its('to_uri.to_s') { is_expected.to eq 'https://www.somehost.com' }
|
55
|
+
end
|
56
|
+
context 'with specific port' do
|
57
|
+
let(:port) { 4040 }
|
58
|
+
|
59
|
+
its('to_uri.to_s') { is_expected.to eq 'https://www.somehost.com:4040' }
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe D2L::Valence::Request, type: :service do
|
4
|
+
include_context :common_context
|
5
|
+
|
6
|
+
subject do
|
7
|
+
described_class.new(
|
8
|
+
user_context: user_context,
|
9
|
+
http_method: http_method,
|
10
|
+
route: route,
|
11
|
+
route_params: route_params,
|
12
|
+
query_params: query_params
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
context '.authenticated_uri' do
|
17
|
+
context 'with no parameters' do
|
18
|
+
let(:http_method) { 'GET' }
|
19
|
+
let(:route) { '/d2l/api/lp/:version/users/whoami' }
|
20
|
+
let(:route_params) { {} }
|
21
|
+
let(:query_params) { {} }
|
22
|
+
let(:expected_path) { '/d2l/api/lp/1.0/users/whoami' }
|
23
|
+
|
24
|
+
its('authenticated_uri.path') { is_expected.to eq expected_path }
|
25
|
+
it('will include the authentication query parameters') do
|
26
|
+
auth_token_parameters.each { |t| expect(subject.authenticated_uri.query).to include "#{t}=" }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'with a specific API version' do
|
31
|
+
let(:api_version) { '1.1' }
|
32
|
+
let(:http_method) { 'GET' }
|
33
|
+
let(:route) { '/d2l/api/lp/:version/users/whoami' }
|
34
|
+
let(:route_params) { {} }
|
35
|
+
let(:query_params) { {} }
|
36
|
+
let(:expected_path) { "/d2l/api/lp/#{api_version}/users/whoami" }
|
37
|
+
|
38
|
+
its('authenticated_uri.path') { is_expected.to eq expected_path }
|
39
|
+
end
|
40
|
+
|
41
|
+
context 'with route parameters' do
|
42
|
+
let(:http_method) { 'GET' }
|
43
|
+
let(:route) { '/d2l/api/lp/:version/:org_unit_id/groupcategories/:groupCategoryId' }
|
44
|
+
let(:route_params) do
|
45
|
+
{
|
46
|
+
org_unit_id: 4,
|
47
|
+
groupCategoryId: 23,
|
48
|
+
}
|
49
|
+
end
|
50
|
+
let(:query_params) { {} }
|
51
|
+
let(:expected_path) { "/d2l/api/lp/#{api_version}/#{route_params[:org_unit_id]}/groupcategories/#{route_params[:groupCategoryId]}" }
|
52
|
+
|
53
|
+
its('authenticated_uri.path') { is_expected.to eq expected_path }
|
54
|
+
it('will include the authentication query parameters') do
|
55
|
+
auth_token_parameters.each { |t| expect(subject.authenticated_uri.query).to include "#{t}=" }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
context 'with query parameters' do
|
60
|
+
let(:http_method) { 'GET' }
|
61
|
+
let(:route) { '/d2l/api/lp/:version/users/' }
|
62
|
+
let(:query_params) { {userName: 'student123'} }
|
63
|
+
let(:route_params) { {} }
|
64
|
+
let(:expected_path) { "/d2l/api/lp/1.0/users/" }
|
65
|
+
|
66
|
+
its('authenticated_uri.path') { is_expected.to eq expected_path }
|
67
|
+
it('will include the query params') do
|
68
|
+
query_params.each { |k,v| expect(subject.authenticated_uri.query).to include "#{k}=#{v}" }
|
69
|
+
end
|
70
|
+
it('will include the authentication query parameters') do
|
71
|
+
auth_token_parameters.each { |t| expect(subject.authenticated_uri.query).to include "#{t}=" }
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe D2L::Valence::Request, type: :service do
|
4
|
+
include_context :common_context
|
5
|
+
|
6
|
+
subject do
|
7
|
+
described_class.new(
|
8
|
+
user_context: user_context,
|
9
|
+
http_method: http_method,
|
10
|
+
route: route,
|
11
|
+
route_params: route_params,
|
12
|
+
query_params: query_params
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
context '.execute' do
|
17
|
+
let(:user_id) { ENV['D2L_USER_ID'] }
|
18
|
+
let(:user_key) { ENV['D2L_USER_KEY'] }
|
19
|
+
|
20
|
+
context 'with the timestamp is invalid' do
|
21
|
+
let(:http_method) { 'GET' }
|
22
|
+
let(:route) { '/d2l/api/lp/:version/users/whoami' }
|
23
|
+
let(:route_params) { {} }
|
24
|
+
let(:query_params) { {} }
|
25
|
+
let(:api_version) { '1.15' }
|
26
|
+
let(:skewed_start_time) { Time.at(1491940559) }
|
27
|
+
# NB: The commented line below is the needed for the regeneration of VCR cassettes
|
28
|
+
# let(:skewed_start_time) { Time.at(Time.now.to_i - 20000) }
|
29
|
+
|
30
|
+
context 'on second try', vcr: {cassette_name: 'request/execute/invalid_timestamp'} do
|
31
|
+
it 'will succeed' do
|
32
|
+
Timecop.travel skewed_start_time do
|
33
|
+
expect(subject.execute.code).to eq :INVALID_TIMESTAMP
|
34
|
+
expect(subject.execute.code).to eq :HTTP_200
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe D2L::Valence::Request, type: :service do
|
4
|
+
include_context :common_context
|
5
|
+
|
6
|
+
subject do
|
7
|
+
described_class.new(
|
8
|
+
user_context: user_context,
|
9
|
+
http_method: http_method,
|
10
|
+
route: route,
|
11
|
+
route_params: route_params,
|
12
|
+
query_params: query_params
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
context '.execute' do
|
17
|
+
let(:user_id) { ENV['D2L_USER_ID'] }
|
18
|
+
let(:user_key) { ENV['D2L_USER_KEY'] }
|
19
|
+
|
20
|
+
context 'for POST', vcr: {cassette_name: 'request/execute/create_lti_link'} do
|
21
|
+
let(:http_method) { 'POST' }
|
22
|
+
let(:route) { '/d2l/api/le/:version/lti/link/:orgUnitId' }
|
23
|
+
let(:route_params) { {orgUnitId: 8041} }
|
24
|
+
let(:query_params) do
|
25
|
+
{
|
26
|
+
Title: 'LTI Link',
|
27
|
+
Url: 'http://myapplication.com/tool/launch',
|
28
|
+
Description: 'Link for external tool',
|
29
|
+
Key: '2015141297208',
|
30
|
+
PlainSecret: 'a30be7c3550149b7a7daac3065f0e5e5',
|
31
|
+
IsVisible: false,
|
32
|
+
SignMessage: true,
|
33
|
+
SignWithTc: true,
|
34
|
+
SendTcInfo: true,
|
35
|
+
SendContextInfo: true,
|
36
|
+
SendUserId: true,
|
37
|
+
SendUserName: true,
|
38
|
+
SendUserEmail: true,
|
39
|
+
SendLinkTitle: true,
|
40
|
+
SendLinkDescription: true,
|
41
|
+
SendD2LUserName: true,
|
42
|
+
SendD2LOrgDefinedId: true,
|
43
|
+
SendD2LOrgRoleId: true,
|
44
|
+
UseToolProviderSecuritySettings: false,
|
45
|
+
CustomParameters: nil
|
46
|
+
}
|
47
|
+
end
|
48
|
+
let(:api_version) { '1.15' }
|
49
|
+
let(:response) { subject.execute }
|
50
|
+
|
51
|
+
before do
|
52
|
+
Timecop.freeze Time.at(1491960645)
|
53
|
+
end
|
54
|
+
|
55
|
+
after { Timecop.return }
|
56
|
+
|
57
|
+
it 'will return the version information' do
|
58
|
+
expect(response.to_hash['LtiLinkId']).to_not be_nil
|
59
|
+
query_params.each do |k, v|
|
60
|
+
expect("#{k}: #{response.to_hash[k.to_s]}").to eq "#{k}: #{v}" if not [:PlainSecret, :CustomParameters].include? k
|
61
|
+
end
|
62
|
+
expect(response.code).to eq :HTTP_200
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe D2L::Valence::Request, type: :service do
|
4
|
+
include_context :common_context
|
5
|
+
|
6
|
+
subject do
|
7
|
+
described_class.new(
|
8
|
+
user_context: user_context,
|
9
|
+
http_method: http_method,
|
10
|
+
route: route,
|
11
|
+
route_params: route_params,
|
12
|
+
query_params: query_params
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
context '.execute' do
|
17
|
+
let(:user_id) { ENV['D2L_USER_ID'] }
|
18
|
+
let(:user_key) { ENV['D2L_USER_KEY'] }
|
19
|
+
|
20
|
+
context 'for DELETE an lti link', vcr: {cassette_name: 'request/execute/delete_lti_link'} do
|
21
|
+
let(:http_method) { 'DELETE' }
|
22
|
+
let(:route) { '/d2l/api/le/:version/lti/link/:ltiLinkId' }
|
23
|
+
let(:route_params) { {ltiLinkId: 144931} }
|
24
|
+
let(:query_params) { {} }
|
25
|
+
let(:api_version) { '1.15' }
|
26
|
+
let(:response) { subject.execute }
|
27
|
+
|
28
|
+
before do
|
29
|
+
Timecop.freeze Time.at(1491960880)
|
30
|
+
end
|
31
|
+
|
32
|
+
after { Timecop.return }
|
33
|
+
|
34
|
+
it 'will delete the LTI Link' do
|
35
|
+
expect(response.code).to eq :HTTP_200
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe D2L::Valence::Request, type: :service do
|
4
|
+
include_context :common_context
|
5
|
+
|
6
|
+
subject do
|
7
|
+
described_class.new(
|
8
|
+
user_context: user_context,
|
9
|
+
http_method: http_method,
|
10
|
+
route: route,
|
11
|
+
route_params: route_params,
|
12
|
+
query_params: query_params
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
context '.execute' do
|
17
|
+
let(:user_id) { ENV['D2L_USER_ID'] }
|
18
|
+
let(:user_key) { ENV['D2L_USER_KEY'] }
|
19
|
+
|
20
|
+
context 'for GET all lti links', vcr: {cassette_name: 'request/execute/get_lti_links'} do
|
21
|
+
let(:http_method) { 'GET' }
|
22
|
+
let(:route) { '/d2l/api/le/:version/lti/link/:orgUnitId/' }
|
23
|
+
let(:route_params) { {orgUnitId: 8041} }
|
24
|
+
let(:query_params) { {} }
|
25
|
+
let(:api_version) { '1.15' }
|
26
|
+
let(:response) { subject.execute }
|
27
|
+
|
28
|
+
before do
|
29
|
+
Timecop.freeze Time.at(1491960964)
|
30
|
+
end
|
31
|
+
|
32
|
+
after { Timecop.return }
|
33
|
+
|
34
|
+
its(:execute) { is_expected.to be_a D2L::Valence::Response }
|
35
|
+
it 'will return all lti links for the associated unit' do
|
36
|
+
expect(response.code).to eq :HTTP_200
|
37
|
+
response.to_hash.each { |lti_record| expect(lti_record['LtiLinkId']).to_not be_nil }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe D2L::Valence::Request, type: :service do
|
4
|
+
include_context :common_context
|
5
|
+
|
6
|
+
subject do
|
7
|
+
described_class.new(
|
8
|
+
user_context: user_context,
|
9
|
+
http_method: http_method,
|
10
|
+
route: route,
|
11
|
+
route_params: route_params,
|
12
|
+
query_params: query_params
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
context '.execute' do
|
17
|
+
let(:user_id) { ENV['D2L_USER_ID'] }
|
18
|
+
let(:user_key) { ENV['D2L_USER_KEY'] }
|
19
|
+
|
20
|
+
context 'for PUT with an lti link', vcr: {cassette_name: 'request/execute/put_lti_link'} do
|
21
|
+
let(:incorrect_details) { query_params.dup.merge(Title: 'The wrong title') }
|
22
|
+
let!(:existing_lit_link) do
|
23
|
+
Timecop.freeze Time.at(1491961066) do
|
24
|
+
D2L::Valence::Request.new(
|
25
|
+
user_context: user_context,
|
26
|
+
http_method: 'POST',
|
27
|
+
route: '/d2l/api/le/:version/lti/link/:orgUnitId',
|
28
|
+
route_params: {orgUnitId: 8041},
|
29
|
+
query_params: incorrect_details
|
30
|
+
).execute.to_hash
|
31
|
+
end
|
32
|
+
end
|
33
|
+
let(:http_method) { 'PUT' }
|
34
|
+
let(:route) { '/d2l/api/le/:version/lti/link/:ltiLinkId' }
|
35
|
+
let(:route_params) { {ltiLinkId: existing_lit_link['LtiLinkId']} }
|
36
|
+
let(:query_params) do
|
37
|
+
{
|
38
|
+
Title: 'LTI Link',
|
39
|
+
Url: 'http://myapplication.com/tool/launch',
|
40
|
+
Description: 'Link for external tool',
|
41
|
+
Key: '2015141297208',
|
42
|
+
PlainSecret: 'a30be7c3550149b7a7daac3065f0e5e5',
|
43
|
+
IsVisible: false,
|
44
|
+
SignMessage: true,
|
45
|
+
SignWithTc: true,
|
46
|
+
SendTcInfo: true,
|
47
|
+
SendContextInfo: true,
|
48
|
+
SendUserId: true,
|
49
|
+
SendUserName: true,
|
50
|
+
SendUserEmail: true,
|
51
|
+
SendLinkTitle: true,
|
52
|
+
SendLinkDescription: true,
|
53
|
+
SendD2LUserName: true,
|
54
|
+
SendD2LOrgDefinedId: true,
|
55
|
+
SendD2LOrgRoleId: true,
|
56
|
+
UseToolProviderSecuritySettings: true,
|
57
|
+
CustomParameters: nil
|
58
|
+
}
|
59
|
+
end
|
60
|
+
let(:api_version) { '1.15' }
|
61
|
+
let(:response) do
|
62
|
+
Timecop.freeze Time.at(1491961067) do
|
63
|
+
subject.execute
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'will update the LTI Link' do
|
68
|
+
expect(response.code).to eq :HTTP_200
|
69
|
+
expect(response.to_hash['Title']).to eq query_params[:Title]
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|