stash-merritt 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +193 -0
  3. data/.rubocop.yml +32 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +12 -0
  6. data/Gemfile +6 -0
  7. data/Gemfile.lock +326 -0
  8. data/LICENSE.md +22 -0
  9. data/README.md +53 -0
  10. data/Rakefile +49 -0
  11. data/lib/datacite/mapping/datacite_xml_factory.rb +212 -0
  12. data/lib/stash/merritt/ezid_helper.rb +50 -0
  13. data/lib/stash/merritt/module_info.rb +12 -0
  14. data/lib/stash/merritt/repository.rb +17 -0
  15. data/lib/stash/merritt/submission_job.rb +90 -0
  16. data/lib/stash/merritt/submission_package/data_one_manifest_builder.rb +41 -0
  17. data/lib/stash/merritt/submission_package/merritt_datacite_builder.rb +22 -0
  18. data/lib/stash/merritt/submission_package/merritt_delete_builder.rb +25 -0
  19. data/lib/stash/merritt/submission_package/merritt_oaidc_builder.rb +130 -0
  20. data/lib/stash/merritt/submission_package/stash_wrapper_builder.rb +59 -0
  21. data/lib/stash/merritt/submission_package.rb +125 -0
  22. data/lib/stash/merritt/sword_helper.rb +58 -0
  23. data/lib/stash/merritt.rb +5 -0
  24. data/lib/stash.rb +5 -0
  25. data/spec/.rubocop.yml +10 -0
  26. data/spec/config/app_config.yml +3 -0
  27. data/spec/config/database.yml +7 -0
  28. data/spec/config/licenses.yml +18 -0
  29. data/spec/data/archive/mrt-datacite.xml +121 -0
  30. data/spec/data/archive/mrt-dataone-manifest.txt +32 -0
  31. data/spec/data/archive/mrt-oaidc.xml +38 -0
  32. data/spec/data/archive/stash-wrapper.xml +213 -0
  33. data/spec/data/archive.zip +0 -0
  34. data/spec/data/dc4-with-funding-references.xml +123 -0
  35. data/spec/db/datacite/mapping/datacite_xml_factory_spec.rb +56 -0
  36. data/spec/db/stash/merritt/merritt_oaidc_builder_spec.rb +72 -0
  37. data/spec/db/stash/merritt/submission_package_spec.rb +174 -0
  38. data/spec/db/stash/merritt/sword_helper_spec.rb +162 -0
  39. data/spec/db_spec_helper.rb +31 -0
  40. data/spec/rspec_custom_matchers.rb +92 -0
  41. data/spec/spec_helper.rb +86 -0
  42. data/spec/unit/stash/merritt/ezid_helper_spec.rb +88 -0
  43. data/spec/unit/stash/merritt/repository_spec.rb +19 -0
  44. data/spec/unit/stash/merritt/submission_job_spec.rb +127 -0
  45. data/spec/util/resource_builder.rb +333 -0
  46. data/stash-merritt.gemspec +48 -0
  47. data/stash-merritt.iml +147 -0
  48. data/stash-merritt.ipr +127 -0
  49. data/travis-local-deps.sh +43 -0
  50. metadata +337 -0
@@ -0,0 +1,92 @@
1
+ require 'rspec/expectations'
2
+ require 'equivalent-xml'
3
+ require 'diffy'
4
+
5
+ module Stash
6
+ module XMLMatchUtils
7
+ def self.to_nokogiri(xml)
8
+ return nil unless xml
9
+ case xml
10
+ when Nokogiri::XML::Element
11
+ xml
12
+ when Nokogiri::XML::Document
13
+ xml.root
14
+ when String
15
+ to_nokogiri(Nokogiri::XML(xml, &:noblanks))
16
+ when REXML::Element
17
+ to_nokogiri(xml.to_s)
18
+ else
19
+ raise "be_xml() expected XML, got #{xml.class}"
20
+ end
21
+ end
22
+
23
+ def self.to_pretty(nokogiri)
24
+ return nil unless nokogiri
25
+ out = StringIO.new
26
+ save_options = Nokogiri::XML::Node::SaveOptions::FORMAT | Nokogiri::XML::Node::SaveOptions::NO_DECLARATION
27
+ nokogiri.write_xml_to(out, encoding: 'UTF-8', indent: 2, save_with: save_options)
28
+ out.string
29
+ end
30
+
31
+ def self.equivalent?(expected, actual, filename = nil)
32
+ expected_xml = to_nokogiri(expected) || raise("expected value #{expected || 'nil'} does not appear to be XML#{" in #{filename}" if filename}")
33
+ actual_xml = to_nokogiri(actual)
34
+
35
+ EquivalentXml.equivalent?(expected_xml, actual_xml, element_order: false, normalize_whitespace: true)
36
+ end
37
+
38
+ def self.failure_message(expected, actual, filename = nil)
39
+ expected_string = to_pretty(to_nokogiri(expected))
40
+ actual_string = to_pretty(to_nokogiri(actual)) || actual
41
+
42
+ now = Time.now.to_i
43
+ FileUtils.mkdir('tmp') unless File.directory?('tmp')
44
+ File.open("tmp/#{now}-expected.xml", 'w') { |f| f.write(expected_string) }
45
+ File.open("tmp/#{now}-actual.xml", 'w') { |f| f.write(actual_string) }
46
+
47
+ diff = Diffy::Diff.new(expected_string, actual_string).to_s(:text)
48
+
49
+ "expected XML differs from actual#{" in #{filename}" if filename}:\n#{diff}"
50
+ end
51
+
52
+ def self.to_xml_string(actual)
53
+ to_pretty(to_nokogiri(actual))
54
+ end
55
+
56
+ def self.failure_message_when_negated(actual, filename = nil)
57
+ "expected not to get XML#{" in #{filename}" if filename}:\n\t#{to_xml_string(actual) || 'nil'}"
58
+ end
59
+ end
60
+ end
61
+
62
+ RSpec::Matchers.define :be_xml do |expected, filename = nil|
63
+ match do |actual|
64
+ Stash::XMLMatchUtils.equivalent?(expected, actual, filename)
65
+ end
66
+
67
+ failure_message do |actual|
68
+ Stash::XMLMatchUtils.failure_message(expected, actual, filename)
69
+ end
70
+
71
+ failure_message_when_negated do |actual|
72
+ Stash::XMLMatchUtils.failure_message_when_negated(actual, filename)
73
+ end
74
+ end
75
+
76
+ RSpec::Matchers.define :be_time do |expected|
77
+ def to_string(time)
78
+ time.is_a?(Time) ? time.utc.round(2).iso8601(2) : time.to_s
79
+ end
80
+
81
+ match do |actual|
82
+ return actual.nil? unless expected
83
+ raise "Expected value #{expected} is not a Time" unless expected.is_a?(Time)
84
+ actual.is_a?(Time) && (to_string(expected) == to_string(actual))
85
+ end
86
+
87
+ failure_message do |actual|
88
+ expected_str = to_string(expected)
89
+ actual_str = to_string(actual)
90
+ "expected time:\n#{expected_str}\n\nbut was:\n#{actual_str}"
91
+ end
92
+ end
@@ -0,0 +1,86 @@
1
+ # ------------------------------------------------------------
2
+ # SimpleCov setup
3
+
4
+ if ENV['COVERAGE']
5
+ require 'simplecov'
6
+ require 'simplecov-console'
7
+
8
+ SimpleCov.minimum_coverage 100
9
+ SimpleCov.start do
10
+ add_filter '/spec/'
11
+ SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
12
+ SimpleCov::Formatter::HTMLFormatter,
13
+ SimpleCov::Formatter::Console,
14
+ ]
15
+ end
16
+ end
17
+
18
+ # ------------------------------------------------------------
19
+ # Rspec configuration
20
+
21
+ RSpec.configure do |config|
22
+ config.raise_errors_for_deprecations!
23
+ config.mock_with :rspec
24
+ end
25
+
26
+ require 'rspec_custom_matchers'
27
+
28
+ # ------------------------------------------------------------
29
+ # ActiveRecord
30
+
31
+ require 'active_record'
32
+ # Note: Even if we're not doing any database work, ActiveRecord callbacks will still raise warnings
33
+ ActiveRecord::Base.raise_in_transactional_callbacks = true
34
+
35
+ # ------------------------------------------------------------
36
+ # StashEngine
37
+
38
+ ENV['STASH_ENV'] = 'test'
39
+ ENV['RAILS_ENV'] = 'test'
40
+
41
+ ::LICENSES = YAML.load_file('spec/config/licenses.yml').with_indifferent_access
42
+ ::APP_CONFIG = OpenStruct.new(YAML.load_file('spec/config/app_config.yml')['test'])
43
+
44
+ stash_engine_path = Gem::Specification.find_by_name('stash_engine').gem_dir
45
+ require "#{stash_engine_path}/config/initializers/hash_to_ostruct.rb"
46
+
47
+ require 'stash_engine'
48
+
49
+ %w(
50
+ app/models/stash_engine
51
+ app/mailers
52
+ app/mailers/stash_engine
53
+ app/jobs/stash_engine
54
+ lib/stash_engine
55
+ ).each do |dir|
56
+ Dir.glob("#{stash_engine_path}/#{dir}/**/*.rb").sort.each(&method(:require))
57
+ end
58
+
59
+ # ------------------------------------------------------------
60
+ # StashDatacite
61
+
62
+ module StashDatacite
63
+ @@resource_class = 'StashEngine::Resource' # rubocop:disable Style/ClassVars
64
+ end
65
+
66
+ require 'stash_datacite'
67
+
68
+ # TODO: do we need all of these?
69
+ stash_datacite_path = Gem::Specification.find_by_name('stash_datacite').gem_dir
70
+ %w(
71
+ app/models/stash_datacite
72
+ app/models/stash_datacite/resource
73
+ lib/stash_datacite
74
+ lib
75
+ ).each do |dir|
76
+ Dir.glob("#{stash_datacite_path}/#{dir}/**/*.rb").sort.each(&method(:require))
77
+ end
78
+
79
+ StashDatacite::ResourcePatch.associate_with_resource(StashEngine::Resource)
80
+
81
+ require 'util/resource_builder'
82
+
83
+ # ------------------------------------------------------------
84
+ # Stash::Merritt
85
+
86
+ require 'stash/merritt'
@@ -0,0 +1,88 @@
1
+ require 'spec_helper'
2
+ require 'ostruct'
3
+
4
+ module Stash
5
+ module Merritt
6
+ describe EzidHelper do
7
+ attr_reader :resource_id
8
+ attr_reader :resource
9
+ attr_reader :identifier_str
10
+ attr_reader :landing_page_url
11
+ attr_reader :helper
12
+ attr_reader :url_helpers
13
+ attr_reader :tenant
14
+
15
+ before(:each) do
16
+ @resource_id = 17
17
+ @resource = double(StashEngine::Resource)
18
+ allow(StashEngine::Resource).to receive(:find).with(resource_id).and_return(resource)
19
+
20
+ @identifier_str = 'doi:10.15146/R38675309'
21
+ @url_helpers = double(Module)
22
+ @landing_page_url = "http://stash.example.edu/stash/#{identifier_str}"
23
+ allow(url_helpers).to receive(:show_path).with(identifier_str).and_return(landing_page_url)
24
+
25
+ @tenant = double(StashEngine::Tenant)
26
+ id_params = {
27
+ shoulder: 'doi:10.15146/R3',
28
+ owner: 'stash_admin',
29
+ account: 'stash',
30
+ password: '3cc9d3fbd9788148c6a32a1415fa673a',
31
+ id_scheme: 'doi'
32
+ }
33
+ allow(tenant).to receive(:identifier_service).and_return(OpenStruct.new(id_params))
34
+ allow(tenant).to receive(:tenant_id).and_return('dataone')
35
+ allow(resource).to receive(:tenant).and_return(tenant)
36
+
37
+ @helper = EzidHelper.new(resource: resource, url_helpers: url_helpers)
38
+ end
39
+
40
+ describe :ensure_identifier do
41
+ it 'returns an existing identifier without bothering EZID' do
42
+ expect(resource).to receive(:identifier_str).and_return(identifier_str)
43
+ expect(::Ezid::Client).not_to receive(:new)
44
+ expect(helper.ensure_identifier).to eq(identifier_str)
45
+ end
46
+
47
+ it 'mints and assigns a new identifier if none is present' do
48
+ identifier = instance_double(::Ezid::MintIdentifierResponse)
49
+ allow(identifier).to receive(:id).and_return(identifier_str)
50
+
51
+ ezid_client = instance_double(::Ezid::Client)
52
+ allow(::Ezid::Client).to receive(:new)
53
+ .with(user: 'stash', password: '3cc9d3fbd9788148c6a32a1415fa673a')
54
+ .and_return(ezid_client)
55
+
56
+ expect(resource).to receive(:identifier_str).and_return(nil)
57
+ expect(ezid_client).to receive(:mint_identifier)
58
+ .with('doi:10.15146/R3', status: 'reserved', profile: 'datacite')
59
+ .and_return(identifier)
60
+ expect(resource).to receive(:ensure_identifier).with(identifier_str)
61
+ expect(helper.ensure_identifier).to eq(identifier_str)
62
+ end
63
+ end
64
+
65
+ describe :update_metadata do
66
+ it 'updates the metadata and landing page' do
67
+ dc3_xml = '<resource/>'
68
+
69
+ ezid_client = instance_double(::Ezid::Client)
70
+ allow(::Ezid::Client).to receive(:new)
71
+ .with(user: 'stash', password: '3cc9d3fbd9788148c6a32a1415fa673a')
72
+ .and_return(ezid_client)
73
+
74
+ expect(ezid_client).to receive(:modify_identifier).with(
75
+ identifier_str,
76
+ datacite: dc3_xml,
77
+ target: landing_page_url,
78
+ status: 'public',
79
+ owner: 'stash_admin'
80
+ )
81
+
82
+ expect(resource).to receive(:identifier_str).and_return(identifier_str)
83
+ helper.update_metadata(dc3_xml: dc3_xml)
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,19 @@
1
+ require 'spec_helper'
2
+
3
+ module Stash
4
+ module Merritt
5
+ describe Repository do
6
+ describe :create_submission_job do
7
+ it 'creates a submission job' do
8
+ url_helpers = double(Module) # yes, apparently URL helpers are an anonymous module
9
+ repo = Repository.new(url_helpers: url_helpers)
10
+ resource_id = 17
11
+ job = repo.create_submission_job(resource_id: resource_id)
12
+ expect(job).to be_a(SubmissionJob)
13
+ expect(job.resource_id).to eq(resource_id)
14
+ expect(job.url_helpers).to be(url_helpers)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,127 @@
1
+ require 'spec_helper'
2
+
3
+ module Stash
4
+ module Merritt
5
+ describe SubmissionJob do
6
+ attr_reader :logger
7
+ attr_reader :tenant
8
+ attr_reader :resource_id
9
+ attr_reader :resource
10
+ attr_reader :url_helpers
11
+ attr_reader :ezid_helper
12
+ attr_reader :package
13
+ attr_reader :sword_helper
14
+ attr_reader :job
15
+
16
+ before(:each) do
17
+ @logger = instance_double(Logger)
18
+ allow(logger).to receive(:debug) { |msg| puts "debug: #{msg}" }
19
+ allow(logger).to receive(:info) { |msg| puts "info: #{msg}" }
20
+ allow(logger).to receive(:warn) { |msg| puts "warn: #{msg}" }
21
+ allow(logger).to receive(:error) { |msg| puts "error: #{msg}" }
22
+
23
+ @rails_logger = Rails.logger
24
+ Rails.logger = logger
25
+
26
+ @tenant = double(StashEngine::Tenant)
27
+ sword_params = {
28
+ collection_uri: 'http://example.edu/sword/example',
29
+ username: 'elvis',
30
+ password: 'presley'
31
+ }.freeze
32
+ allow(tenant).to receive(:sword_params).and_return(sword_params)
33
+ allow(tenant).to receive(:id).and_return('example_u')
34
+
35
+ @resource_id = 37
36
+ @resource = double(StashEngine::Resource)
37
+ allow(StashEngine::Resource).to receive(:find).with(resource_id).and_return(resource)
38
+ allow(resource).to receive(:identifier_str).and_return('doi:10.123/456')
39
+ allow(resource).to receive(:update_uri).and_return(nil)
40
+ allow(resource).to receive(:tenant).and_return(tenant)
41
+ allow(resource).to receive(:tenant_id).and_return('example_u')
42
+
43
+ @url_helpers = double(Module) # yes, apparently URL helpers are an anonymous module
44
+ allow(url_helpers).to(receive(:show_path)) { |identifier| identifier }
45
+
46
+ @ezid_helper = instance_double(EzidHelper)
47
+ allow(EzidHelper).to receive(:new).with(resource: resource, url_helpers: url_helpers).and_return(ezid_helper)
48
+ allow(ezid_helper).to receive(:ensure_identifier)
49
+ allow(ezid_helper).to receive(:update_metadata)
50
+
51
+ @package = instance_double(SubmissionPackage)
52
+ allow(SubmissionPackage).to receive(:new).with(resource: resource).and_return(package)
53
+ allow(package).to receive(:dc3_xml)
54
+ allow(package).to receive(:cleanup!)
55
+
56
+ @sword_helper = instance_double(SwordHelper)
57
+ allow(SwordHelper).to receive(:new).with(package: package, logger: logger).and_return(sword_helper)
58
+ allow(sword_helper).to receive(:submit!)
59
+
60
+ @job = SubmissionJob.new(resource_id: resource_id, url_helpers: url_helpers)
61
+ end
62
+
63
+ after(:each) do
64
+ Rails.logger = @rails_logger
65
+ end
66
+
67
+ describe :submit! do
68
+ it 'ensures an identifier' do
69
+ expect(ezid_helper).to receive(:ensure_identifier)
70
+ job.submit!
71
+ end
72
+
73
+ it 'submits the package' do
74
+ expect(sword_helper).to receive(:submit!)
75
+ job.submit!
76
+ end
77
+
78
+ it 'updates the metadata' do
79
+ dc3_xml = '<resource/>'
80
+ expect(package).to receive(:dc3_xml).and_return(dc3_xml)
81
+ expect(ezid_helper).to receive(:update_metadata).with(dc3_xml: dc3_xml)
82
+ job.submit!
83
+ end
84
+
85
+ it 'cleans up the package' do
86
+ expect(package).to receive(:cleanup!)
87
+ job.submit!
88
+ end
89
+
90
+ it 'returns a result' do
91
+ result = job.submit!
92
+ expect(result).to be_a(Stash::Repo::SubmissionResult)
93
+ expect(result.success?).to be_truthy
94
+ end
95
+
96
+ describe 'error handling' do
97
+ it 'fails on a bad resource ID' do
98
+ bad_id = resource_id * 17
99
+ job = SubmissionJob.new(resource_id: bad_id, url_helpers: url_helpers)
100
+ allow(StashEngine::Resource).to receive(:find).with(bad_id).and_raise(ActiveRecord::RecordNotFound)
101
+ expect(job.submit!.error).to be_a(ActiveRecord::RecordNotFound)
102
+ end
103
+
104
+ it 'fails on an ID minting error' do
105
+ expect(ezid_helper).to receive(:ensure_identifier).and_raise(Ezid::NotAllowedError)
106
+ expect(job.submit!.error).to be_a(Ezid::NotAllowedError)
107
+ end
108
+
109
+ it 'fails on a SWORD submission error' do
110
+ expect(sword_helper).to receive(:submit!).and_raise(RestClient::RequestFailed)
111
+ expect(job.submit!.error).to be_a(RestClient::RequestFailed)
112
+ end
113
+
114
+ it 'fails on a metadata update error' do
115
+ expect(ezid_helper).to receive(:update_metadata).and_raise(Ezid::IdentifierNotFoundError)
116
+ expect(job.submit!.error).to be_a(Ezid::IdentifierNotFoundError)
117
+ end
118
+
119
+ it 'fails on a package cleanup error' do
120
+ expect(package).to receive(:cleanup!).and_raise(Errno::ENOENT)
121
+ expect(job.submit!.error).to be_a(Errno::ENOENT)
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end