orcid 0.0.4 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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