stash-merritt 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 +193 -0
- data/.rubocop.yml +32 -0
- data/.ruby-version +1 -0
- data/.travis.yml +12 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +326 -0
- data/LICENSE.md +22 -0
- data/README.md +53 -0
- data/Rakefile +49 -0
- data/lib/datacite/mapping/datacite_xml_factory.rb +212 -0
- data/lib/stash/merritt/ezid_helper.rb +50 -0
- data/lib/stash/merritt/module_info.rb +12 -0
- data/lib/stash/merritt/repository.rb +17 -0
- data/lib/stash/merritt/submission_job.rb +90 -0
- data/lib/stash/merritt/submission_package/data_one_manifest_builder.rb +41 -0
- data/lib/stash/merritt/submission_package/merritt_datacite_builder.rb +22 -0
- data/lib/stash/merritt/submission_package/merritt_delete_builder.rb +25 -0
- data/lib/stash/merritt/submission_package/merritt_oaidc_builder.rb +130 -0
- data/lib/stash/merritt/submission_package/stash_wrapper_builder.rb +59 -0
- data/lib/stash/merritt/submission_package.rb +125 -0
- data/lib/stash/merritt/sword_helper.rb +58 -0
- data/lib/stash/merritt.rb +5 -0
- data/lib/stash.rb +5 -0
- data/spec/.rubocop.yml +10 -0
- data/spec/config/app_config.yml +3 -0
- data/spec/config/database.yml +7 -0
- data/spec/config/licenses.yml +18 -0
- data/spec/data/archive/mrt-datacite.xml +121 -0
- data/spec/data/archive/mrt-dataone-manifest.txt +32 -0
- data/spec/data/archive/mrt-oaidc.xml +38 -0
- data/spec/data/archive/stash-wrapper.xml +213 -0
- data/spec/data/archive.zip +0 -0
- data/spec/data/dc4-with-funding-references.xml +123 -0
- data/spec/db/datacite/mapping/datacite_xml_factory_spec.rb +56 -0
- data/spec/db/stash/merritt/merritt_oaidc_builder_spec.rb +72 -0
- data/spec/db/stash/merritt/submission_package_spec.rb +174 -0
- data/spec/db/stash/merritt/sword_helper_spec.rb +162 -0
- data/spec/db_spec_helper.rb +31 -0
- data/spec/rspec_custom_matchers.rb +92 -0
- data/spec/spec_helper.rb +86 -0
- data/spec/unit/stash/merritt/ezid_helper_spec.rb +88 -0
- data/spec/unit/stash/merritt/repository_spec.rb +19 -0
- data/spec/unit/stash/merritt/submission_job_spec.rb +127 -0
- data/spec/util/resource_builder.rb +333 -0
- data/stash-merritt.gemspec +48 -0
- data/stash-merritt.iml +147 -0
- data/stash-merritt.ipr +127 -0
- data/travis-local-deps.sh +43 -0
- 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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|