noid-rails 3.0.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.
- checksums.yaml +7 -0
- data/.gitignore +36 -0
- data/.rspec +2 -0
- data/.rubocop.yml +24 -0
- data/.rubocop_todo.yml +107 -0
- data/.travis.yml +11 -0
- data/CONTRIBUTING.md +159 -0
- data/Gemfile +39 -0
- data/LICENSE +15 -0
- data/README.md +214 -0
- data/Rakefile +23 -0
- data/app/models/minter_state.rb +16 -0
- data/db/migrate/20160610010003_create_minter_states.rb +16 -0
- data/db/migrate/20161021203429_rename_minter_state_random_to_rand.rb +7 -0
- data/lib/generators/noid/rails/install_generator.rb +22 -0
- data/lib/generators/noid/rails/seed_generator.rb +37 -0
- data/lib/noid/rails/config.rb +35 -0
- data/lib/noid/rails/engine.rb +10 -0
- data/lib/noid/rails/minter/base.rb +65 -0
- data/lib/noid/rails/minter/db.rb +82 -0
- data/lib/noid/rails/minter/file.rb +65 -0
- data/lib/noid/rails/minter.rb +5 -0
- data/lib/noid/rails/rspec.rb +67 -0
- data/lib/noid/rails/service.rb +26 -0
- data/lib/noid/rails/version.rb +7 -0
- data/lib/noid-rails.rb +29 -0
- data/lib/tasks/noid_tasks.rake +59 -0
- data/noid-rails.gemspec +31 -0
- data/spec/models/minter_state_spec.rb +41 -0
- data/spec/spec_helper.rb +40 -0
- data/spec/support/minterstate_table.rb +13 -0
- data/spec/support/shared_examples/minter.rb +39 -0
- data/spec/test_app_templates/lib/generators/test_app_generator.rb +16 -0
- data/spec/unit/config_spec.rb +45 -0
- data/spec/unit/db_minter_spec.rb +88 -0
- data/spec/unit/file_minter_spec.rb +54 -0
- data/spec/unit/noid_spec.rb +43 -0
- data/spec/unit/rspec_spec.rb +90 -0
- data/spec/unit/service_spec.rb +32 -0
- metadata +225 -0
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Noid
|
4
|
+
module Rails
|
5
|
+
# Generates the migrations into the host application
|
6
|
+
class InstallGenerator < ::Rails::Generators::Base
|
7
|
+
source_root ::File.expand_path('../templates', __FILE__)
|
8
|
+
|
9
|
+
desc <<~DESCRIPTION
|
10
|
+
Copies DB migrations
|
11
|
+
DESCRIPTION
|
12
|
+
|
13
|
+
def banner
|
14
|
+
say_status('info', 'Installing noid-rails', :blue)
|
15
|
+
end
|
16
|
+
|
17
|
+
def migrations
|
18
|
+
rake 'noid_rails_engine:install:migrations'
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Noid
|
4
|
+
module Rails
|
5
|
+
# Initializes the database with a noid namespace
|
6
|
+
class SeedGenerator < ::Rails::Generators::Base
|
7
|
+
source_root ::File.expand_path('../templates', __FILE__)
|
8
|
+
argument :namespace, type: :string, default: Noid::Rails.config.namespace
|
9
|
+
argument :template, type: :string, default: Noid::Rails.config.template
|
10
|
+
|
11
|
+
desc <<~DESCRIPTION
|
12
|
+
Seeds DB from Noid::Rails.config (or command-line overrides)
|
13
|
+
DESCRIPTION
|
14
|
+
|
15
|
+
def banner
|
16
|
+
say_status('info', "Initializing database table for namespace:template of '#{namespace}:#{template}'", :blue)
|
17
|
+
end
|
18
|
+
|
19
|
+
def checks
|
20
|
+
if namespace != Noid::Rails.config.namespace
|
21
|
+
say_status('warn', 'Be sure to use an initializer to do ' \
|
22
|
+
"'Noid::Rails.config.namespace = #{namespace}'", :red)
|
23
|
+
end
|
24
|
+
return if template == Noid::Rails.config.template
|
25
|
+
say_status('warn', 'Be sure to use an initializer to do ' \
|
26
|
+
"Noid::Rails.config.template = #{template}'", :red)
|
27
|
+
end
|
28
|
+
|
29
|
+
def seed_row
|
30
|
+
MinterState.seed!(
|
31
|
+
namespace: namespace,
|
32
|
+
template: template
|
33
|
+
)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Noid
|
4
|
+
module Rails
|
5
|
+
# Configuration parameters for creating identifiers
|
6
|
+
class Config
|
7
|
+
attr_writer :template, :statefile, :namespace, :minter_class, :identifier_in_use
|
8
|
+
|
9
|
+
def template
|
10
|
+
@template ||= '.reeddeeddk'
|
11
|
+
end
|
12
|
+
|
13
|
+
def statefile
|
14
|
+
@statefile ||= '/tmp/minter-state'
|
15
|
+
end
|
16
|
+
|
17
|
+
def namespace
|
18
|
+
@namespace ||= 'default'
|
19
|
+
end
|
20
|
+
|
21
|
+
def minter_class
|
22
|
+
@minter_class ||= Minter::File
|
23
|
+
end
|
24
|
+
|
25
|
+
# A check to guarantee the identifier is not already in use. When true,
|
26
|
+
# the minter will continue to cycle through ids until it finds one that
|
27
|
+
# returns false
|
28
|
+
def identifier_in_use
|
29
|
+
@identifier_in_use = lambda do |_id|
|
30
|
+
false
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'noid'
|
4
|
+
|
5
|
+
module Noid
|
6
|
+
module Rails
|
7
|
+
module Minter
|
8
|
+
# @abstract the base class for minters
|
9
|
+
class Base < ::Noid::Minter
|
10
|
+
##
|
11
|
+
# @param template [#to_s] a NOID template
|
12
|
+
# @see Noid::Template
|
13
|
+
def initialize(template = default_template)
|
14
|
+
super(template: template.to_s)
|
15
|
+
end
|
16
|
+
|
17
|
+
##
|
18
|
+
# Sychronously mint a new identifier.
|
19
|
+
#
|
20
|
+
# @return [String] the minted identifier
|
21
|
+
def mint
|
22
|
+
Mutex.new.synchronize do
|
23
|
+
loop do
|
24
|
+
pid = next_id
|
25
|
+
return pid unless identifier_in_use?(pid)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
##
|
31
|
+
# @return [Hash{Symbol => String, Object}] representation of the current minter state
|
32
|
+
def read
|
33
|
+
raise NotImplementedError, 'Implement #read in child class'
|
34
|
+
end
|
35
|
+
|
36
|
+
##
|
37
|
+
# Updates the minter state to that of the `minter` parameter.
|
38
|
+
#
|
39
|
+
# @param minter [Minter::Base]
|
40
|
+
# @return [void]
|
41
|
+
def write!(_)
|
42
|
+
raise NotImplementedError, 'Implement #write! in child class'
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def identifier_in_use?(id)
|
48
|
+
Noid::Rails.config.identifier_in_use.call(id)
|
49
|
+
end
|
50
|
+
|
51
|
+
##
|
52
|
+
# @return [#to_s] the default template for this
|
53
|
+
def default_template
|
54
|
+
Noid::Rails.config.template
|
55
|
+
end
|
56
|
+
|
57
|
+
##
|
58
|
+
# @return [String] a new identifier
|
59
|
+
def next_id
|
60
|
+
raise NotImplementedError, 'Implement #next_id in child class'
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'noid'
|
4
|
+
|
5
|
+
module Noid
|
6
|
+
module Rails
|
7
|
+
module Minter
|
8
|
+
# A minter backed by a database table. You would select this if you
|
9
|
+
# need to mint identifers on several distributed front-ends that do not
|
10
|
+
# share a common file system.
|
11
|
+
class Db < Base
|
12
|
+
def read
|
13
|
+
deserialize(instance)
|
14
|
+
end
|
15
|
+
|
16
|
+
def write!(minter)
|
17
|
+
serialize(instance, minter)
|
18
|
+
end
|
19
|
+
|
20
|
+
protected
|
21
|
+
|
22
|
+
# @param [MinterState] inst minter state to be converted
|
23
|
+
# @return [Hash{Symbol => String, Object}] minter state as a Hash, like #read
|
24
|
+
# @see #read, Noid::Rails::Minter::Base#read
|
25
|
+
def deserialize(inst)
|
26
|
+
filtered_hash = inst.as_json.slice('template', 'counters', 'seq', 'rand', 'namespace')
|
27
|
+
if filtered_hash['counters']
|
28
|
+
filtered_hash['counters'] = JSON.parse(filtered_hash['counters'],
|
29
|
+
symbolize_names: true)
|
30
|
+
end
|
31
|
+
filtered_hash.symbolize_keys
|
32
|
+
end
|
33
|
+
|
34
|
+
# @param [MinterState] inst a locked row/object to be updated
|
35
|
+
# @param [::Noid::Minter] minter state containing the updates
|
36
|
+
def serialize(inst, minter)
|
37
|
+
# namespace and template are the same, now update the other attributes
|
38
|
+
inst.update_attributes!(
|
39
|
+
seq: minter.seq,
|
40
|
+
counters: JSON.generate(minter.counters),
|
41
|
+
rand: Marshal.dump(minter.instance_variable_get(:@rand))
|
42
|
+
)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Uses pessimistic lock to ensure the record fetched is the same one updated.
|
46
|
+
# Should be fast enough to avoid terrible deadlock.
|
47
|
+
# Must lock because of multi-connection context! (transaction is per connection -- not enough)
|
48
|
+
# The DB table will only ever have at most one row per namespace.
|
49
|
+
# The 'default' namespace row is inserted by `rails generate noid:rails:seed`
|
50
|
+
# or autofilled by instance below.
|
51
|
+
# If you want another namespace, edit your config initialzer to something like:
|
52
|
+
# Noid::Rails.config.namespace = 'druid'
|
53
|
+
# Noid::Rails.config.template = '.reeedek'
|
54
|
+
# and in your app run:
|
55
|
+
# bundle exec rails generate noid:rails:seed
|
56
|
+
def next_id
|
57
|
+
id = nil
|
58
|
+
MinterState.transaction do
|
59
|
+
locked = instance
|
60
|
+
minter = ::Noid::Minter.new(deserialize(locked))
|
61
|
+
id = minter.mint
|
62
|
+
serialize(locked, minter)
|
63
|
+
end
|
64
|
+
id
|
65
|
+
end
|
66
|
+
|
67
|
+
# @return [MinterState]
|
68
|
+
def instance
|
69
|
+
MinterState.lock.find_by!(
|
70
|
+
namespace: Noid::Rails.config.namespace,
|
71
|
+
template: Noid::Rails.config.template
|
72
|
+
)
|
73
|
+
rescue ActiveRecord::RecordNotFound
|
74
|
+
MinterState.seed!(
|
75
|
+
namespace: Noid::Rails.config.namespace,
|
76
|
+
template: Noid::Rails.config.template
|
77
|
+
)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'noid'
|
4
|
+
|
5
|
+
module Noid
|
6
|
+
module Rails
|
7
|
+
module Minter
|
8
|
+
# A file based minter. This is a simple case.
|
9
|
+
class File < Base
|
10
|
+
attr_reader :statefile
|
11
|
+
|
12
|
+
def initialize(template = default_template, statefile = default_statefile)
|
13
|
+
@statefile = statefile
|
14
|
+
super(template)
|
15
|
+
end
|
16
|
+
|
17
|
+
def default_statefile
|
18
|
+
Noid::Rails.config.statefile
|
19
|
+
end
|
20
|
+
|
21
|
+
def read
|
22
|
+
with_file do |f|
|
23
|
+
state_for(f)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def write!(minter)
|
28
|
+
with_file do |f|
|
29
|
+
# Wipe prior contents so the new state can be written from the beginning of the file
|
30
|
+
f.truncate(0)
|
31
|
+
f.write(Marshal.dump(minter.dump))
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
protected
|
36
|
+
|
37
|
+
def with_file
|
38
|
+
::File.open(statefile, 'a+b', 0o644) do |f|
|
39
|
+
f.flock(::File::LOCK_EX)
|
40
|
+
# Files opened in append mode seek to end of file
|
41
|
+
f.rewind
|
42
|
+
yield f
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# rubocop:disable Security/MarshalLoad
|
47
|
+
def state_for(io_object)
|
48
|
+
Marshal.load(io_object.read)
|
49
|
+
rescue TypeError, ArgumentError
|
50
|
+
{ template: template }
|
51
|
+
end
|
52
|
+
# rubocop:enable Security/MarshalLoad
|
53
|
+
|
54
|
+
def next_id
|
55
|
+
state = read
|
56
|
+
state[:template] &&= state[:template].to_s
|
57
|
+
minter = ::Noid::Minter.new(state) # minter w/in the minter, lives only for an instant
|
58
|
+
id = minter.mint
|
59
|
+
write!(minter)
|
60
|
+
id
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Noid
|
4
|
+
module Rails
|
5
|
+
##
|
6
|
+
# Provides a test minter conforming to the `Noid::Rails::Minter`
|
7
|
+
# interface for use in unit tests. The test minter is faster and avoids
|
8
|
+
# unexpected interactions with cleanup code commonly runs in test suites
|
9
|
+
# (e.g. database cleanup).
|
10
|
+
#
|
11
|
+
# Applications should reenable their production minter for integration tests
|
12
|
+
# when appropriate
|
13
|
+
#
|
14
|
+
# @example general use
|
15
|
+
# Noid::Rails::RSpec.disable_production_minter!
|
16
|
+
# # some unit tests with the test minter
|
17
|
+
# Noid::Rails::RSpec.enable_production_minter!
|
18
|
+
# # some integration tests with the original minter
|
19
|
+
#
|
20
|
+
# @example using a custom test minter
|
21
|
+
# Noid::Rails::RSpec.disable_production_minter!(test_minter: Minter)
|
22
|
+
#
|
23
|
+
# @example use when included in RSpec config
|
24
|
+
# require 'noid/rails/rspec'
|
25
|
+
#
|
26
|
+
# RSpec.configure do |config|
|
27
|
+
# config.include(Noid::Rails::RSpec)
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# before(:suite) { disable_production_minter! }
|
31
|
+
# after(:suite) { enable_production_minter! }
|
32
|
+
#
|
33
|
+
module RSpec
|
34
|
+
DEFAULT_TEST_MINTER = Noid::Rails::Minter::File
|
35
|
+
|
36
|
+
##
|
37
|
+
# Replaces the configured production minter with a test minter.
|
38
|
+
#
|
39
|
+
# @param test_minter [Class] a Noid::Rails::Minter implementation
|
40
|
+
# to use as a replacement minter
|
41
|
+
# @return [void]
|
42
|
+
def disable_production_minter!(test_minter: DEFAULT_TEST_MINTER)
|
43
|
+
return nil if @original_minter
|
44
|
+
|
45
|
+
@original_minter = Noid::Rails.config.minter_class
|
46
|
+
|
47
|
+
Noid::Rails.configure do |noid_config|
|
48
|
+
noid_config.minter_class = test_minter
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
##
|
53
|
+
# Re-enables the original configured minter.
|
54
|
+
#
|
55
|
+
# @return [void]
|
56
|
+
def enable_production_minter!
|
57
|
+
return nil unless @original_minter
|
58
|
+
|
59
|
+
Noid::Rails.configure do |noid_config|
|
60
|
+
noid_config.minter_class = @original_minter
|
61
|
+
end
|
62
|
+
|
63
|
+
@original_minter = nil
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'noid'
|
4
|
+
|
5
|
+
module Noid
|
6
|
+
module Rails
|
7
|
+
# A service that validates and mints identifiers
|
8
|
+
class Service
|
9
|
+
attr_reader :minter
|
10
|
+
|
11
|
+
def initialize(minter = default_minter)
|
12
|
+
@minter = minter
|
13
|
+
end
|
14
|
+
|
15
|
+
delegate :valid?, to: :minter
|
16
|
+
|
17
|
+
delegate :mint, to: :minter
|
18
|
+
|
19
|
+
protected
|
20
|
+
|
21
|
+
def default_minter
|
22
|
+
Noid::Rails.config.minter_class.new
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/noid-rails.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'noid/rails/version'
|
4
|
+
require 'noid/rails/config'
|
5
|
+
require 'noid/rails/engine'
|
6
|
+
require 'noid/rails/service'
|
7
|
+
require 'noid/rails/minter'
|
8
|
+
|
9
|
+
module Noid
|
10
|
+
# A package to integrate Noid identifers with Rails projects
|
11
|
+
module Rails
|
12
|
+
class << self
|
13
|
+
def configure
|
14
|
+
yield config
|
15
|
+
end
|
16
|
+
|
17
|
+
def config
|
18
|
+
@config ||= Config.new
|
19
|
+
end
|
20
|
+
|
21
|
+
def treeify(identifier)
|
22
|
+
raise ArgumentError, 'Identifier must be a string of size > 0 in order to be treeified' if identifier.blank?
|
23
|
+
head = identifier.split('/').first
|
24
|
+
head.gsub!(/#.*/, '')
|
25
|
+
(head.scan(/..?/).first(4) + [identifier]).join('/')
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'noid-rails'
|
4
|
+
require 'noid'
|
5
|
+
require 'yaml'
|
6
|
+
|
7
|
+
namespace :noid do
|
8
|
+
namespace :rails do
|
9
|
+
namespace :migrate do
|
10
|
+
desc 'Migrate minter state file from YAML to Marshal'
|
11
|
+
task :yaml_to_marshal do
|
12
|
+
statefile = ENV.fetch('RAILS_NOID_STATEFILE', Noid::Rails.config.statefile)
|
13
|
+
raise "File not found: #{statefile}\nAborting" unless File.exist?(statefile)
|
14
|
+
puts "Migrating #{statefile} from YAML to Marshal serialization..."
|
15
|
+
File.open(statefile, 'a+b', 0o644) do |f|
|
16
|
+
f.flock(File::LOCK_EX)
|
17
|
+
f.rewind
|
18
|
+
begin
|
19
|
+
yaml_state = YAML.safe_load(f)
|
20
|
+
rescue Psych::SyntaxError
|
21
|
+
raise "File not valid YAML: #{statefile}\nAborting."
|
22
|
+
end
|
23
|
+
minter = Noid::Minter.new(yaml_state)
|
24
|
+
f.truncate(0)
|
25
|
+
new_state = Marshal.dump(minter.dump)
|
26
|
+
f.write(new_state)
|
27
|
+
end
|
28
|
+
puts 'Done!'
|
29
|
+
end
|
30
|
+
|
31
|
+
desc 'Migrate minter state from file to database'
|
32
|
+
task file_to_database: :environment do
|
33
|
+
statefile = ENV.fetch('RAILS_NOID_STATEFILE', Noid::Rails.config.statefile)
|
34
|
+
raise "File not found: #{statefile}\nAborting" unless File.exist?(statefile)
|
35
|
+
puts "Migrating #{statefile} to database..."
|
36
|
+
state = Noid::Rails::Minter::File.new.read
|
37
|
+
minter = Noid::Minter.new(state)
|
38
|
+
new_state = Noid::Rails::Minter::Db.new
|
39
|
+
new_state.write!(minter)
|
40
|
+
puts 'Done!'
|
41
|
+
end
|
42
|
+
|
43
|
+
desc 'Migrate minter state from database to file'
|
44
|
+
task database_to_file: :environment do
|
45
|
+
statefile = ENV.fetch('RAILS_NOID_STATEFILE', Noid::Rails.config.statefile)
|
46
|
+
if File.exist?(statefile)
|
47
|
+
raise "File already exists (delete it first if it's not valuable): " \
|
48
|
+
"#{statefile}\nAborting"
|
49
|
+
end
|
50
|
+
puts "Migrating minter state from database to #{statefile}..."
|
51
|
+
state = Noid::Rails::Minter::Db.new.read
|
52
|
+
minter = Noid::Minter.new(state)
|
53
|
+
new_state = Noid::Rails::Minter::File.new
|
54
|
+
new_state.write!(minter)
|
55
|
+
puts 'Done!'
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/noid-rails.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('../lib', __FILE__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'noid/rails/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = 'noid-rails'
|
9
|
+
spec.version = Noid::Rails::VERSION
|
10
|
+
spec.authors = ['Michael J. Giarlo']
|
11
|
+
spec.email = ['leftwing@alumni.rutgers.edu']
|
12
|
+
spec.summary = 'Noid identifier services for Rails-based applications'
|
13
|
+
spec.description = 'Noid identifier services for Rails-based applications.'
|
14
|
+
spec.homepage = 'https://github.com/samvera/noid-rails'
|
15
|
+
spec.license = 'Apache2'
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0")
|
18
|
+
spec.test_files = spec.files.grep(%r{^spec/})
|
19
|
+
spec.require_paths = ['lib']
|
20
|
+
|
21
|
+
spec.add_dependency 'actionpack', '>= 5.0.0', '< 6'
|
22
|
+
spec.add_dependency 'noid', '~> 0.9'
|
23
|
+
|
24
|
+
spec.add_development_dependency 'bundler', '~> 1.7'
|
25
|
+
spec.add_development_dependency 'engine_cart', '~> 1.0'
|
26
|
+
spec.add_development_dependency 'rake', '>= 11'
|
27
|
+
spec.add_development_dependency 'rspec', '~> 3.2'
|
28
|
+
spec.add_development_dependency 'rubocop', '~> 0.52.0'
|
29
|
+
spec.add_development_dependency 'rubocop-rspec', '~> 1.20.1'
|
30
|
+
spec.add_development_dependency 'sqlite3'
|
31
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe MinterState, type: :model do
|
4
|
+
include MinterStateHelper
|
5
|
+
before { reset_minter_state_table }
|
6
|
+
after { reset_minter_state_table }
|
7
|
+
|
8
|
+
let(:state) { described_class.new }
|
9
|
+
let(:first) { described_class.first }
|
10
|
+
|
11
|
+
it 'db is seeded with first row' do
|
12
|
+
expect { first }.not_to raise_error
|
13
|
+
expect(first.namespace).to eq 'default'
|
14
|
+
expect(first.template).to eq '.reeddeeddk'
|
15
|
+
expect(first.seq).to eq 0
|
16
|
+
expect(described_class.group(:namespace).count).to eq('default' => 1)
|
17
|
+
end
|
18
|
+
describe 'validation' do
|
19
|
+
it 'blocks invalid template' do
|
20
|
+
expect { state.save! }.to raise_error(ActiveRecord::RecordInvalid) # empty
|
21
|
+
state.template = 'bad_template'
|
22
|
+
expect { state.save! }.to raise_error(ActiveRecord::RecordInvalid)
|
23
|
+
state.template = 'reeddddk' # close, but missing '.'
|
24
|
+
expect { state.save! }.to raise_error(ActiveRecord::RecordInvalid)
|
25
|
+
end
|
26
|
+
it 'allows valid template (edit)' do
|
27
|
+
first.template = '.reeddddk'
|
28
|
+
expect { first.save! }.not_to raise_error # OK!
|
29
|
+
end
|
30
|
+
it 'blocks new record in same namespace' do
|
31
|
+
state.template = '.reeddddk'
|
32
|
+
expect { state.save! }.to raise_error(ActiveRecord::RecordInvalid)
|
33
|
+
end
|
34
|
+
it 'allows new record in distinct namespace' do
|
35
|
+
state.template = '.reeddddk'
|
36
|
+
state.namespace = 'foobar'
|
37
|
+
expect { state.save! }.not_to raise_error # OK!
|
38
|
+
expect(described_class.group(:namespace).count).to eq('default' => 1, 'foobar' => 1)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
ENV['RAILS_ENV'] ||= 'test'
|
4
|
+
|
5
|
+
def coverage_needed?
|
6
|
+
ENV['COVERAGE'] || ENV['TRAVIS']
|
7
|
+
end
|
8
|
+
|
9
|
+
if coverage_needed?
|
10
|
+
require 'simplecov'
|
11
|
+
require 'coveralls'
|
12
|
+
SimpleCov.root(File.expand_path('../..', __FILE__))
|
13
|
+
SimpleCov.formatter = Coveralls::SimpleCov::Formatter
|
14
|
+
SimpleCov.start('rails') do
|
15
|
+
add_filter '/.internal_test_app'
|
16
|
+
add_filter '/lib/generators'
|
17
|
+
add_filter '/spec'
|
18
|
+
add_filter '/lib/noid/rails/version.rb'
|
19
|
+
end
|
20
|
+
SimpleCov.command_name 'spec'
|
21
|
+
end
|
22
|
+
|
23
|
+
require 'engine_cart'
|
24
|
+
EngineCart.load_application!
|
25
|
+
|
26
|
+
require 'noid-rails'
|
27
|
+
require 'byebug' unless ENV['CI']
|
28
|
+
|
29
|
+
Dir[File.dirname(__FILE__) + '/support/**/*.rb'].each { |f| require f }
|
30
|
+
|
31
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
32
|
+
RSpec.configure do |config|
|
33
|
+
config.expect_with :rspec do |expectations|
|
34
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
35
|
+
end
|
36
|
+
config.mock_with :rspec do |mocks|
|
37
|
+
mocks.verify_partial_doubles = true
|
38
|
+
end
|
39
|
+
config.filter_run_when_matching :focus
|
40
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MinterStateHelper
|
4
|
+
# Simple truncation is not enough, since we also need seed data
|
5
|
+
def reset_minter_state_table
|
6
|
+
MinterState.destroy_all
|
7
|
+
MinterState.create!(
|
8
|
+
namespace: 'default',
|
9
|
+
template: '.reeddeeddk',
|
10
|
+
seq: 0
|
11
|
+
)
|
12
|
+
end
|
13
|
+
end
|