orcid 0.0.4 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +10 -0
  3. data/.hound.yml +793 -0
  4. data/.travis.yml +14 -0
  5. data/Gemfile +14 -0
  6. data/README.md +107 -6
  7. data/Rakefile +17 -9
  8. data/app/assets/images/orcid/.keep +0 -0
  9. data/app/controllers/orcid/application_controller.rb +13 -4
  10. data/app/controllers/orcid/profile_connections_controller.rb +34 -6
  11. data/app/controllers/orcid/profile_requests_controller.rb +18 -15
  12. data/app/models/orcid/profile.rb +15 -5
  13. data/app/models/orcid/profile_connection.rb +20 -20
  14. data/app/models/orcid/profile_request.rb +39 -20
  15. data/app/models/orcid/work.rb +45 -55
  16. data/app/models/orcid/work/xml_parser.rb +45 -0
  17. data/app/models/orcid/work/xml_renderer.rb +29 -0
  18. data/app/services/orcid/remote/profile_creation_service.rb +40 -32
  19. data/app/services/orcid/remote/profile_query_service.rb +82 -0
  20. data/app/services/orcid/remote/profile_query_service/query_parameter_builder.rb +43 -0
  21. data/app/services/orcid/remote/profile_query_service/search_response.rb +31 -0
  22. data/app/services/orcid/remote/service.rb +16 -13
  23. data/app/services/orcid/remote/work_service.rb +58 -43
  24. data/app/templates/orcid/work.template.v1.1.xml.erb +32 -1
  25. data/app/views/orcid/profile_connections/_orcid_connector.html.erb +14 -0
  26. data/app/views/orcid/profile_connections/new.html.erb +4 -4
  27. data/bin/rails +8 -0
  28. data/config/{application.yml → application.yml.example} +3 -13
  29. data/config/locales/orcid.en.yml +5 -1
  30. data/config/routes.rb +4 -2
  31. data/lib/generators/orcid/install/install_generator.rb +46 -7
  32. data/lib/orcid.rb +23 -11
  33. data/lib/orcid/configuration.rb +32 -13
  34. data/lib/orcid/configuration/provider.rb +27 -13
  35. data/lib/orcid/engine.rb +20 -4
  36. data/lib/orcid/exceptions.rb +33 -4
  37. data/lib/orcid/named_callbacks.rb +3 -1
  38. data/lib/orcid/spec_support.rb +19 -9
  39. data/lib/orcid/version.rb +1 -1
  40. data/lib/tasks/orcid_tasks.rake +3 -3
  41. data/orcid.gemspec +51 -0
  42. data/rubocop.txt +1164 -0
  43. data/script/fast_specs +22 -0
  44. data/spec/controllers/orcid/profile_connections_controller_spec.rb +101 -0
  45. data/spec/controllers/orcid/profile_requests_controller_spec.rb +116 -0
  46. data/spec/factories/orcid_profile_requests.rb +11 -0
  47. data/spec/factories/users.rb +9 -0
  48. data/spec/fast_helper.rb +12 -0
  49. data/spec/features/batch_profile_spec.rb +31 -0
  50. data/spec/features/non_ui_based_interactions_spec.rb +117 -0
  51. data/spec/features/profile_connection_feature_spec.rb +19 -0
  52. data/spec/features/public_api_query_spec.rb +36 -0
  53. data/spec/fixtures/orcid_works.xml +55 -0
  54. data/spec/lib/orcid/configuration/provider_spec.rb +40 -0
  55. data/spec/lib/orcid/configuration_spec.rb +38 -0
  56. data/spec/lib/orcid/named_callbacks_spec.rb +28 -0
  57. data/spec/lib/orcid_spec.rb +97 -0
  58. data/spec/models/orcid/profile_connection_spec.rb +81 -0
  59. data/spec/models/orcid/profile_request_spec.rb +131 -0
  60. data/spec/models/orcid/profile_spec.rb +76 -0
  61. data/spec/models/orcid/work/xml_parser_spec.rb +40 -0
  62. data/spec/models/orcid/work/xml_renderer_spec.rb +18 -0
  63. data/spec/models/orcid/work_spec.rb +53 -0
  64. data/spec/services/orcid/remote/profile_creation_service_spec.rb +40 -0
  65. data/spec/services/orcid/remote/profile_query_service/query_parameter_builder_spec.rb +44 -0
  66. data/spec/services/orcid/remote/profile_query_service/search_response_spec.rb +14 -0
  67. data/spec/services/orcid/remote/profile_query_service_spec.rb +118 -0
  68. data/spec/services/orcid/remote/service_spec.rb +26 -0
  69. data/spec/services/orcid/remote/work_service_spec.rb +44 -0
  70. data/spec/spec_helper.rb +99 -0
  71. data/spec/support/non_orcid_models.rb +11 -0
  72. data/spec/support/stub_callback.rb +25 -0
  73. data/spec/test_app_templates/Gemfile.extra +3 -0
  74. data/spec/test_app_templates/lib/generators/test_app_generator.rb +36 -0
  75. data/spec/views/orcid/profile_connections/new.html.erb_spec.rb +25 -0
  76. data/spec/views/orcid/profile_requests/new.html.erb_spec.rb +23 -0
  77. metadata +119 -29
  78. data/app/runners/orcid/profile_lookup_runner.rb +0 -33
  79. data/app/runners/orcid/runner.rb +0 -15
  80. data/app/services/orcid/remote/profile_lookup_service.rb +0 -56
  81. data/app/services/orcid/remote/profile_lookup_service/search_response.rb +0 -23
  82. data/lib/orcid/query_parameter_builder.rb +0 -38
@@ -0,0 +1,76 @@
1
+ require 'spec_helper'
2
+
3
+ module Orcid
4
+ describe Profile do
5
+ let(:orcid_profile_id) { '0001-0002-0003-0004' }
6
+ let(:remote_service) { double('Service') }
7
+ let(:mapper) { double("Mapper") }
8
+ let(:non_orcid_work) { double("A non-ORCID Work") }
9
+ let(:orcid_work) { double("Orcid::Work") }
10
+ let(:xml_renderer) { double("Renderer") }
11
+ let(:xml_parser) { double("Parser") }
12
+ let(:xml) { double("XML Payload")}
13
+
14
+ subject {
15
+ described_class.new(
16
+ orcid_profile_id,
17
+ mapper: mapper,
18
+ remote_service: remote_service,
19
+ xml_renderer: xml_renderer,
20
+ xml_parser: xml_parser
21
+ )
22
+ }
23
+
24
+ def should_map(source, target)
25
+ mapper.should_receive(:map).with(source, target: 'orcid/work').and_return(target)
26
+ end
27
+
28
+ context '#remote_works' do
29
+ let(:parsed_object) { double("Parsed Object")}
30
+ let(:response_body) { double("XML Response") }
31
+ it 'should parse the response body' do
32
+ xml_parser.should_receive(:call).with(response_body).and_return(parsed_object)
33
+ remote_service.should_receive(:call).with(orcid_profile_id, request_method: :get).and_return(response_body)
34
+
35
+ expect(subject.remote_works).to eq(parsed_object)
36
+ end
37
+ end
38
+
39
+
40
+ context '#append_new_work' do
41
+ it 'should transform the input work to xml and deliver to the remote_service' do
42
+ xml_renderer.should_receive(:call).with([orcid_work]).and_return(xml)
43
+ remote_service.should_receive(:call).with(orcid_profile_id, body: xml, request_method: :post)
44
+
45
+ should_map(non_orcid_work, orcid_work)
46
+
47
+ subject.append_new_work(non_orcid_work)
48
+ end
49
+ end
50
+
51
+ context '#replace_works_with' do
52
+ it 'should transform the input work to xml and deliver to the remote_service' do
53
+ xml_renderer.should_receive(:call).with([orcid_work]).and_return(xml)
54
+ remote_service.should_receive(:call).with(orcid_profile_id, body: xml, request_method: :put)
55
+
56
+ should_map(non_orcid_work, orcid_work)
57
+
58
+ subject.replace_works_with(non_orcid_work)
59
+ end
60
+ end
61
+
62
+ context '#verified_authentication' do
63
+ it 'should not be authorized' do
64
+ subject.verified_authentication?.should eq false
65
+ end
66
+ end
67
+
68
+ context '#verified_authentication' do
69
+ it 'should be authorized' do
70
+ Orcid.stub(:access_token_for).and_return(Object.new())
71
+
72
+ subject.verified_authentication?.should eq true
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,40 @@
1
+ require 'spec_helper'
2
+
3
+ module Orcid
4
+ describe Work::XmlParser do
5
+ let(:xml) { fixture_file('orcid_works.xml').read }
6
+
7
+ context '.call' do
8
+ subject { described_class.call(xml) }
9
+ its(:size) { should eq 2 }
10
+ its(:first) { should be_an_instance_of Orcid::Work }
11
+ its(:last) { should be_an_instance_of Orcid::Work }
12
+
13
+ context 'first element' do
14
+ subject { described_class.call(xml).first }
15
+
16
+ its(:title) { should eq "Another Test Drive" }
17
+ its(:put_code) { should eq "303475" }
18
+ its(:work_type) { should eq "test" }
19
+ its(:journal_title) { should_not be_present }
20
+ its(:short_description) { should_not be_present }
21
+ its(:citation_type) { should_not be_present }
22
+ its(:citation) { should_not be_present }
23
+ its(:publication_month) { should_not be_present }
24
+ its(:publication_year) { should_not be_present }
25
+ its(:url) { should_not be_present }
26
+ its(:language_code) { should_not be_present }
27
+ its(:country) { should_not be_present }
28
+ end
29
+
30
+ context 'last element' do
31
+ subject { described_class.call(xml).last }
32
+
33
+ its(:title) { should eq "Test Driven Orcid Integration" }
34
+ its(:put_code) { should eq "303474" }
35
+ its(:work_type) { should eq "test" }
36
+ end
37
+ end
38
+ end
39
+
40
+ end
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+
3
+ module Orcid
4
+ describe Work::XmlRenderer do
5
+ let(:work) { Work.new(title: 'Hello', work_type: 'journal-article') }
6
+ subject { described_class.new(work) }
7
+
8
+ context '#call' do
9
+ it 'should return an XML document' do
10
+ rendered = subject.call
11
+ expect(rendered).to have_tag('orcid-profile orcid-activities orcid-works orcid-work') do
12
+ with_tag('work-title title', text: work.title)
13
+ with_tag('work-type', text: work.work_type)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,53 @@
1
+ require 'spec_helper'
2
+
3
+ module Orcid
4
+ describe Work do
5
+ let(:attributes) {
6
+ {
7
+ title: 'Hello',
8
+ work_type: 'journal-article',
9
+ put_code: '1234',
10
+ external_identifiers: [ {type: 'doi', identifier: 'abc-123' }]
11
+ }
12
+ }
13
+ subject { described_class.new(attributes) }
14
+
15
+ its(:title) { should eq attributes[:title] }
16
+ its(:subtitle) { should eq nil }
17
+ its(:work_type) { should eq attributes[:work_type] }
18
+ its(:put_code) { should eq attributes[:put_code] }
19
+ its(:external_identifiers) { should be_an_instance_of(Array) }
20
+ its(:valid?) { should eq true }
21
+
22
+ context '#id' do
23
+ context 'with put_code' do
24
+ subject { described_class.new(put_code: '123') }
25
+ its(:id) { should eq subject.put_code}
26
+ end
27
+ context 'with title and work type' do
28
+ subject { described_class.new(title: 'Title', work_type: 'journal-article') }
29
+ its(:id) { should eq [subject.title, subject.work_type]}
30
+ end
31
+
32
+ context 'without title, work type, and put_code' do
33
+ subject { described_class.new }
34
+ its(:id) { should eq nil }
35
+ end
36
+ end
37
+
38
+ context '#to_xml' do
39
+ it 'should return an XML document' do
40
+ rendered = subject.to_xml
41
+ expect(rendered).to have_tag('orcid-profile orcid-activities orcid-works orcid-work') do
42
+ with_tag('work-title title', text: subject.title)
43
+ with_tag('work-type', text: subject.work_type)
44
+ with_tag('work-external-identifiers work-external-identifier', count: 1) do
45
+ with_tag('work-external-identifier-type', text: 'doi')
46
+ with_tag('work-external-identifier-id', text: 'abc-123')
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ end
@@ -0,0 +1,40 @@
1
+ require 'fast_helper'
2
+ require 'orcid/remote/profile_creation_service'
3
+
4
+ module Orcid::Remote
5
+ describe ProfileCreationService do
6
+ Given(:payload) { %(<?xml version="1.0" encoding="UTF-8"?>) }
7
+ Given(:config) { {token: token, headers: request_headers, path: 'path/to/somewhere' } }
8
+ Given(:token) { double("Token", post: response) }
9
+ Given(:minted_orcid) { '0000-0001-8025-637X' }
10
+ Given(:request_headers) {
11
+ { 'Content-Type' => 'application/vdn.orcid+xml', 'Accept' => 'application/xml' }
12
+ }
13
+ Given(:callback) { StubCallback.new }
14
+ Given(:callback_config) { callback.configure }
15
+
16
+ Given(:response) {
17
+ double("Response", headers: { location: File.join("/", minted_orcid, "orcid-profile") })
18
+ }
19
+
20
+ When(:returned_value) { described_class.call(payload, config, &callback_config) }
21
+
22
+ context 'with orcid created' do
23
+ Given(:response) {
24
+ double("Response", headers: { location: File.join("/", minted_orcid, "orcid-profile") })
25
+ }
26
+ Then { returned_value.should eq(minted_orcid)}
27
+ And { expect(callback.invoked).to eq [:success, minted_orcid] }
28
+ And { token.should have_received(:post).with(config.fetch(:path), body: payload, headers: request_headers)}
29
+ end
30
+
31
+ context 'with orcid created' do
32
+ Given(:response) {
33
+ double("Response", headers: { location: "" })
34
+ }
35
+ Then { returned_value.should eq(false)}
36
+ And { expect(callback.invoked).to eq [:failure] }
37
+ And { token.should have_received(:post).with(config.fetch(:path), body: payload, headers: request_headers)}
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,44 @@
1
+ require 'fast_helper'
2
+ require 'orcid/remote/profile_query_service/query_parameter_builder'
3
+
4
+ module Orcid::Remote
5
+
6
+ describe ProfileQueryService::QueryParameterBuilder do
7
+ When(:response) { described_class.call(input) }
8
+ context 'single word input' do
9
+ Given(:input) {
10
+ { text: "Hello", email: 'jeremy.n.friesen@gmail.com' }
11
+ }
12
+ Then { expect(response).to eq(q: "email:#{input[:email]} AND text:#{input[:text]}") }
13
+ end
14
+
15
+ context 'empty string and nil' do
16
+ Given(:input) {
17
+ { text: "" , email: nil}
18
+ }
19
+ Then { expect(response).to eq(q: "") }
20
+ end
21
+
22
+ context 'multi-word named input' do
23
+ Given(:input) {
24
+ { other_names: %("Tim O'Connor" -"Oak"), email: 'jeremy.n.friesen@gmail.com' }
25
+ }
26
+ Then { expect(response).to eq(q: "other-names:#{input[:other_names]} AND email:#{input[:email]}") }
27
+ end
28
+
29
+ context 'q is provided along with other params' do
30
+ Given(:input) {
31
+ { q: %("Tim O'Connor" -"Oak"), email: 'jeremy.n.friesen@gmail.com' }
32
+ }
33
+ Then { expect(response).to eq(q: "email:#{input[:email]} AND text:#{input[:q]}") }
34
+ end
35
+
36
+ context 'q is provided with text params' do
37
+ Given(:input) {
38
+ { q: %("Tim O'Connor" -"Oak"), text: 'jeremy.n.friesen@gmail.com' }
39
+ }
40
+ Then { expect(response).to eq(q: "text:((#{input[:q]}) AND (#{input[:text]}))") }
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,14 @@
1
+ require 'spec_helper'
2
+ require 'orcid/remote/profile_query_service/search_response'
3
+
4
+ module Orcid::Remote
5
+ describe ProfileQueryService::SearchResponse do
6
+ Given(:attributes) { {id: 'Hello', label: 'World', junk: 'JUNK!', biography: "Extended Biography"} }
7
+ Given(:search_response) { described_class.new(attributes) }
8
+ Then { expect(search_response.id).to eq(attributes[:id]) }
9
+ And { expect(search_response.biography).to eq(attributes[:biography]) }
10
+ And { expect(search_response.label).to eq(attributes[:label]) }
11
+ And { expect(search_response.orcid_profile_id).to eq(attributes[:id]) }
12
+ And { expect{search_response.junk }.to raise_error }
13
+ end
14
+ end
@@ -0,0 +1,118 @@
1
+ require 'fast_helper'
2
+ require 'orcid/remote/profile_query_service'
3
+ require 'ostruct'
4
+
5
+ module Orcid::Remote
6
+ describe ProfileQueryService do
7
+ Given(:email) { 'corwin@amber.gov' }
8
+ Given(:biography) { 'King of Amber' }
9
+ Given(:orcid_profile_id) { '0001-0002' }
10
+ Given(:config) {
11
+ {
12
+ token: token,
13
+ path: 'somehwere',
14
+ headers: 'headers',
15
+ response_builder: OpenStruct,
16
+ query_parameter_builder: query_parameter_builder
17
+ }
18
+ }
19
+ Given(:query_parameter_builder) { double('Query Builder')}
20
+ Given(:response) { double("Response", body: response_body)} # See below
21
+ Given(:token) { double("Token") }
22
+ Given(:json_response) {
23
+ [
24
+ OpenStruct.new({ 'id' => orcid_profile_id, 'label' => "Corwin Amber (#{email}) [ORCID: #{orcid_profile_id}]", 'biography' => biography })
25
+ ]
26
+ }
27
+ Given(:parameters) { double("Parameters") }
28
+ Given(:normalized_parameters) { double("Normalized Parameters") }
29
+ Given(:callback) { StubCallback.new }
30
+ Given(:callback_config) { callback.configure(:found, :not_found) }
31
+
32
+ context '.call' do
33
+ before(:each) do
34
+ query_parameter_builder.should_receive(:call).with(parameters).and_return(normalized_parameters)
35
+ token.should_receive(:get).with(config[:path], headers: config[:headers], params: normalized_parameters).and_return(response)
36
+ end
37
+ When(:result) { described_class.call(parameters, config, &callback_config) }
38
+ Then { expect(result).to eq(json_response) }
39
+ And { expect(callback.invoked).to eq [:found, json_response] }
40
+ end
41
+
42
+ Given(:response_body) {
43
+ %(
44
+ {
45
+ "message-version": "1.1",
46
+ "orcid-search-results": {
47
+ "orcid-search-result": [
48
+ {
49
+ "relevancy-score": {
50
+ "value": 14.298138
51
+ },
52
+ "orcid-profile": {
53
+ "orcid": null,
54
+ "orcid-identifier": {
55
+ "value": null,
56
+ "uri": "http://orcid.org/#{orcid_profile_id}",
57
+ "path": "#{orcid_profile_id}",
58
+ "host": "orcid.org"
59
+ },
60
+ "orcid-bio": {
61
+ "personal-details": {
62
+ "given-names": {
63
+ "value": "Corwin"
64
+ },
65
+ "family-name": {
66
+ "value": "Amber"
67
+ }
68
+ },
69
+ "biography": {
70
+ "value": "#{biography}",
71
+ "visibility": null
72
+ },
73
+ "contact-details": {
74
+ "email": [
75
+ {
76
+ "value": "#{email}",
77
+ "primary": true,
78
+ "current": true,
79
+ "verified": true,
80
+ "visibility": null,
81
+ "source": null
82
+ }
83
+ ],
84
+ "address": {
85
+ "country": {
86
+ "value": "US",
87
+ "visibility": null
88
+ }
89
+ }
90
+ },
91
+ "keywords": {
92
+ "keyword": [
93
+ {
94
+ "value": "Lord of Amber"
95
+ }
96
+ ],
97
+ "visibility": null
98
+ },
99
+ "delegation": null,
100
+ "applications": null,
101
+ "scope": null
102
+ },
103
+ "orcid-activities": {
104
+ "affiliations": null
105
+ },
106
+ "type": null,
107
+ "group-type": null,
108
+ "client-type": null
109
+ }
110
+ }
111
+ ],
112
+ "num-found": 1
113
+ }
114
+ }
115
+ )
116
+ }
117
+ end
118
+ end
@@ -0,0 +1,26 @@
1
+ require 'fast_helper'
2
+ require 'orcid/remote/service'
3
+
4
+ module Orcid::Remote
5
+ describe Service do
6
+ Given(:context) { double(invoked: true) }
7
+ Given(:runner) {
8
+ described_class.new { |on|
9
+ on.found {|a,b| context.invoked("FOUND", a,b) }
10
+ }
11
+ }
12
+
13
+ describe "calling defined callback" do
14
+ When(:result) { runner.callback(:found, :first, :second) }
15
+ Then { context.should have_received(:invoked).with("FOUND", :first, :second) }
16
+ Then { result == [:first, :second] }
17
+ end
18
+
19
+ describe "calling undefined callback" do
20
+ When(:result) { runner.callback(:missing, :first, :second) }
21
+ Then { context.should_not have_received(:invoked) }
22
+ Then { result == [:first, :second] }
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,44 @@
1
+ require 'fast_helper'
2
+ require 'oauth2/error'
3
+ require 'orcid/remote/work_service'
4
+
5
+ module Orcid::Remote
6
+ describe WorkService do
7
+ let(:payload) { %(<?xml version="1.0" encoding="UTF-8"?>) }
8
+ let(:token) { double("Token") }
9
+ let(:orcid_profile_id) { '0000-0003-1495-7122' }
10
+ let(:request_headers) { { 'Content-Type' => 'application/orcid+xml', 'Accept' => 'application/xml' } }
11
+ let(:response) { double("Response", body: 'Body') }
12
+
13
+ context '.call' do
14
+ let(:token) { double('Token', client: client, token: 'access_token', refresh_token: 'refresh_token')}
15
+ let(:client) { double('Client', id: '123', site: 'URL', options: {})}
16
+ it 'raises a more helpful message' do
17
+ response = double("Response", status: '100', body: 'body')
18
+ response.stub(:error=)
19
+ response.stub(:parsed)
20
+ token.should_receive(:request).and_raise(OAuth2::Error.new(response))
21
+
22
+ expect {
23
+ described_class.call(orcid_profile_id, token: token)
24
+ }.to raise_error(Orcid::RemoteServiceError)
25
+ end
26
+ it 'instantiates and calls underlying instance' do
27
+ token.should_receive(:request).
28
+ with(:post, "v1.1/#{orcid_profile_id}/orcid-works/", body: payload, headers: request_headers).
29
+ and_return(response)
30
+
31
+ expect(
32
+ described_class.call(
33
+ orcid_profile_id,
34
+ body: payload,
35
+ request_method: :post,
36
+ token: token,
37
+ headers: request_headers
38
+ )
39
+ ).to eq(response.body)
40
+ end
41
+ end
42
+
43
+ end
44
+ end