aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--app/controllers/referentials_controller.rb6
-rw-r--r--app/models/referential.rb37
-rw-r--r--spec/features/lines_spec.rb20
-rw-r--r--spec/features/networks_spec.rb14
-rw-r--r--spec/features/users/user_delete_spec.rb2
-rw-r--r--spec/models/referential/referential_lock_during_creation_spec.rb80
-rw-r--r--spec/models/referential_spec.rb8
-rw-r--r--spec/spec_helper.rb10
-rw-r--r--spec/support/referential.rb2
9 files changed, 134 insertions, 45 deletions
diff --git a/app/controllers/referentials_controller.rb b/app/controllers/referentials_controller.rb
index ee1236912..227651a59 100644
--- a/app/controllers/referentials_controller.rb
+++ b/app/controllers/referentials_controller.rb
@@ -8,13 +8,13 @@ class ReferentialsController < ChouetteController
def new
new! do
- build_referenial
+ build_referential
end
end
def create
create! do |format|
- build_referenial
+ build_referential
if !!@referential.created_from_id
format.html { redirect_to workbench_path(@referential.workbench) }
@@ -132,7 +132,7 @@ class ReferentialsController < ChouetteController
super
end
- def build_referenial
+ def build_referential
if params[:from]
source_referential = Referential.find(params[:from])
@referential = Referential.new_from(source_referential, current_functional_scope)
diff --git a/app/models/referential.rb b/app/models/referential.rb
index ca20c639f..f89eafee8 100644
--- a/app/models/referential.rb
+++ b/app/models/referential.rb
@@ -12,17 +12,17 @@ class Referential < ActiveRecord::Base
validates_uniqueness_of :slug
- validates_format_of :slug, :with => %r{\A[a-z][0-9a-z_]+\Z}
- validates_format_of :prefix, :with => %r{\A[0-9a-zA-Z_]+\Z}
- validates_format_of :upper_corner, :with => %r{\A-?[0-9]+\.?[0-9]*\,-?[0-9]+\.?[0-9]*\Z}
- validates_format_of :lower_corner, :with => %r{\A-?[0-9]+\.?[0-9]*\,-?[0-9]+\.?[0-9]*\Z}
+ validates_format_of :slug, with: %r{\A[a-z][0-9a-z_]+\Z}
+ validates_format_of :prefix, with: %r{\A[0-9a-zA-Z_]+\Z}
+ validates_format_of :upper_corner, with: %r{\A-?[0-9]+\.?[0-9]*\,-?[0-9]+\.?[0-9]*\Z}
+ validates_format_of :lower_corner, with: %r{\A-?[0-9]+\.?[0-9]*\,-?[0-9]+\.?[0-9]*\Z}
validate :slug_excluded_values
attr_accessor :upper_corner
attr_accessor :lower_corner
has_one :user
- has_many :api_keys, :class_name => 'Api::V1::ApiKey', :dependent => :destroy
+ has_many :api_keys, class_name: 'Api::V1::ApiKey', dependent: :destroy
belongs_to :organisation
validates_presence_of :organisation
@@ -78,7 +78,7 @@ class Referential < ActiveRecord::Base
errors.add(:slug,I18n.t("referentials.errors.public_excluded"))
end
if slug == self.class.connection_config[:username]
- errors.add(:slug,I18n.t("referentials.errors.user_excluded", :user => slug))
+ errors.add(:slug,I18n.t("referentials.errors.user_excluded", user: slug))
end
end
end
@@ -141,7 +141,7 @@ class Referential < ActiveRecord::Base
end
def self.new_from(from, functional_scope)
- Referential.new(
+ Referential.new(
name: I18n.t("activerecord.copy", name: from.name),
slug: "#{from.slug}_clone",
prefix: from.prefix,
@@ -191,9 +191,15 @@ class Referential < ActiveRecord::Base
projection_type || ""
end
- before_validation :assign_line_and_stop_area_referential, :on => :create, if: :workbench
- before_validation :assign_slug, :on => :create
- before_validation :assign_prefix, :on => :create
+ before_validation :assign_line_and_stop_area_referential, on: :create, if: :workbench
+ before_validation :assign_slug, on: :create
+ before_validation :assign_prefix, on: :create
+
+ # Lock the `referentials` table to prevent duplicate referentials from being
+ # created simultaneously in separate transactions. This must be the last hook
+ # to minimise the duration of the lock.
+ before_validation :lock_table, on: :create
+
before_create :create_schema
after_create :clone_schema, if: :created_from
@@ -277,7 +283,7 @@ class Referential < ActiveRecord::Base
def detect_overlapped_referentials
self.class.where(id: overlapped_referential_ids).each do |referential|
- errors.add :metadatas, I18n.t("referentials.errors.overlapped_referential", :referential => referential.name)
+ errors.add :metadatas, I18n.t("referentials.errors.overlapped_referential", referential: referential.name)
end
end
@@ -371,4 +377,13 @@ class Referential < ActiveRecord::Base
not metadatas_overlap?
end
+ private
+
+ def lock_table
+ # No explicit unlock is needed as it will be released at the end of the
+ # transaction.
+ ActiveRecord::Base.connection.execute(
+ 'LOCK referentials IN ACCESS EXCLUSIVE MODE'
+ )
+ end
end
diff --git a/spec/features/lines_spec.rb b/spec/features/lines_spec.rb
index 2a442bd2f..ecb90668c 100644
--- a/spec/features/lines_spec.rb
+++ b/spec/features/lines_spec.rb
@@ -60,22 +60,22 @@ describe "Lines", type: :feature do
# it "creates line and return to show" do
# visit line_referential_lines_path(line_referential)
# click_link "Ajouter une ligne"
- # fill_in "line_name", :with => "Line 1"
- # fill_in "Numéro d'enregistrement", :with => "1"
- # fill_in "Identifiant Neptune", :with => "chouette:test:Line:999"
+ # fill_in "line_name", with: "Line 1"
+ # fill_in "Numéro d'enregistrement", with: "1"
+ # fill_in "Identifiant Neptune", with: "chouette:test:Line:999"
# click_button("Créer ligne")
# expect(page).to have_content("Line 1")
# end
# end
# Fixme #1780
- # describe "new with group of line", :js => true do
+ # describe "new with group of line", truncation: true do
# it "creates line and return to show" do
# visit new_line_referential_line_path(line_referential)
- # fill_in "line_name", :with => "Line 1"
- # fill_in "Numéro d'enregistrement", :with => "1"
- # fill_in "Identifiant Neptune", :with => "test:Line:999"
- # fill_in_token_input('line_group_of_line_tokens', :with => "#{group_of_line.name}")
+ # fill_in "line_name", with: "Line 1"
+ # fill_in "Numéro d'enregistrement", with: "1"
+ # fill_in "Identifiant Neptune", with: "test:Line:999"
+ # fill_in_token_input('line_group_of_line_tokens', with: "#{group_of_line.name}")
# find_button("Créer ligne").trigger("click")
# expect(page).to have_text("Line 1")
# expect(page).to have_text("#{group_of_line.name}")
@@ -87,8 +87,8 @@ describe "Lines", type: :feature do
# it "edit line" do
# visit line_referential_line_path(line_referential, subject)
# click_link "Editer cette ligne"
- # fill_in "line_name", :with => "Line Modified"
- # fill_in "Numéro d'enregistrement", :with => "test-1"
+ # fill_in "line_name", with: "Line Modified"
+ # fill_in "Numéro d'enregistrement", with: "test-1"
# click_button("Editer ligne")
# expect(page).to have_content("Line Modified")
# end
diff --git a/spec/features/networks_spec.rb b/spec/features/networks_spec.rb
index 75070e7fa..19f17d900 100644
--- a/spec/features/networks_spec.rb
+++ b/spec/features/networks_spec.rb
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
require 'spec_helper'
-describe "Networks", :type => :feature do
+describe "Networks", type: :feature do
login_user
let(:line_referential) { create :line_referential }
@@ -54,9 +54,9 @@ describe "Networks", :type => :feature do
# # allow(subject).to receive(:stop_areas).and_return(Array.new(2) { create(:stop_area) })
# visit line_referential_networks_path(line_referential)
# click_link "Ajouter un réseau"
- # fill_in "network_name", :with => "Network 1"
- # fill_in "Numéro d'enregistrement", :with => "test-1"
- # fill_in "Identifiant Neptune", :with => "chouette:test:GroupOfLine:1"
+ # fill_in "network_name", with: "Network 1"
+ # fill_in "Numéro d'enregistrement", with: "test-1"
+ # fill_in "Identifiant Neptune", with: "chouette:test:GroupOfLine:1"
# click_button("Créer réseau")
# expect(page).to have_content("Network 1")
# end
@@ -67,14 +67,14 @@ describe "Networks", :type => :feature do
# # allow(subject).to receive(:stop_areas).and_return(Array.new(2) { create(:stop_area) })
# visit line_referential_network_path(line_referential, subject)
# click_link "Editer ce réseau"
- # fill_in "network_name", :with => "Network Modified"
- # fill_in "Numéro d'enregistrement", :with => "test-1"
+ # fill_in "network_name", with: "Network Modified"
+ # fill_in "Numéro d'enregistrement", with: "test-1"
# click_button("Editer réseau")
# expect(page).to have_content("Network Modified")
# end
# end
- # describe "delete", :js => true do
+ # describe "delete", truncation: true do
# it "delete network and return to the list" do
# subject.stub(:stop_areas).and_return(Array.new(2) { create(:stop_area) })
# visit line_referential_network_path(line_referential, subject)
diff --git a/spec/features/users/user_delete_spec.rb b/spec/features/users/user_delete_spec.rb
index 48f4e35d1..8b2ffcbe5 100644
--- a/spec/features/users/user_delete_spec.rb
+++ b/spec/features/users/user_delete_spec.rb
@@ -7,7 +7,7 @@ Warden.test_mode!
# As a user
# I want to delete my user profile
# So I can close my account
-feature 'User delete', :devise, :js do
+feature 'User delete', :devise, :truncation do
after(:each) do
Warden.test_reset!
diff --git a/spec/models/referential/referential_lock_during_creation_spec.rb b/spec/models/referential/referential_lock_during_creation_spec.rb
new file mode 100644
index 000000000..1e36bd976
--- /dev/null
+++ b/spec/models/referential/referential_lock_during_creation_spec.rb
@@ -0,0 +1,80 @@
+RSpec.describe Referential, type: :model do
+
+ context "when two identical Referentials are created, only one is saved" do
+ let( :workbench ){ create :workbench }
+
+ it "works synchronously" do
+ referential_1 = build(
+ :referential,
+ workbench: workbench,
+ organisation: workbench.organisation
+ )
+ referential_2 = referential_1.dup
+ referential_2.slug = "#{referential_1.slug}_different"
+
+ metadata_1 = build(:referential_metadata)
+ metadata_2 = metadata_1.dup
+
+ referential_1.metadatas << metadata_1
+ referential_2.metadatas << metadata_2
+
+ referential_1.save
+ referential_2.save
+
+ expect(referential_1).to be_persisted
+ expect(referential_2).not_to be_persisted
+ end
+
+ it "works asynchronously", truncation: true do
+ begin
+ referential_1 = build(
+ :referential,
+ workbench: workbench,
+ organisation: workbench.organisation
+ )
+ referential_2 = referential_1.dup
+ referential_2.slug = "#{referential_1.slug}_different"
+ referential_3 = nil
+
+ metadata_1 = build(:referential_metadata)
+ metadata_2 = metadata_1.dup
+
+ referential_1.metadatas << metadata_1
+ referential_2.metadatas << metadata_2
+
+ thread_1 = Thread.new do
+ ActiveRecord::Base.transaction do
+ # seize LOCK
+ referential_1.save
+ sleep 10
+ # release LOCK
+ end
+ end
+
+ thread_2 = Thread.new do
+ sleep 5
+ ActiveRecord::Base.transaction do
+ # waits for LOCK, (because of sleep 5)
+ referential_2.save
+ # when lock was eventually obtained validation failed
+ referential_3 = create(:referential)
+ end
+ end
+
+ thread_1.join
+ thread_2.join
+
+ expect(referential_1).to be_persisted
+ expect(referential_2).not_to be_persisted
+ expect(referential_3).to be_persisted
+ ensure
+ Apartment::Tenant.drop(referential_1.slug) if referential_1.persisted?
+ Apartment::Tenant.drop(referential_2.slug) if referential_2.persisted?
+
+ if referential_3.try(:persisted?)
+ Apartment::Tenant.drop(referential_3.slug)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/referential_spec.rb b/spec/models/referential_spec.rb
index d0b1d6447..7816e7232 100644
--- a/spec/models/referential_spec.rb
+++ b/spec/models/referential_spec.rb
@@ -1,13 +1,6 @@
-require 'spec_helper'
-
describe Referential, :type => :model do
let(:ref) { create :workbench_referential, metadatas: [create(:referential_metadata)] }
- # it "create a rule_parameter_set" do
- # referential = create(:referential)
- # expect(referential.rule_parameter_sets.size).to eq(1)
- # end
-
it { should have_many(:metadatas) }
it { should belong_to(:workbench) }
it { should belong_to(:referential_suite) }
@@ -131,4 +124,5 @@ describe Referential, :type => :model do
end
end
end
+
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 9679952df..9fdca585e 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -64,11 +64,11 @@ RSpec.configure do |config|
Capybara.javascript_driver = :poltergeist
# :meta tests can be run seperately in case of doubt about the tests themselves
# they serve mainly as an explanataion of complicated tests (as e.g. PG information_schema introspection)
- config.filter_run_excluding :meta => true
- config.filter_run_excluding :js => true
- config.filter_run :wip => true
+ config.filter_run_excluding meta: true
+ config.filter_run_excluding truncation: true
+ config.filter_run wip: true
config.run_all_when_everything_filtered = true
- config.include TokenInputHelper, :type => :feature
+ config.include TokenInputHelper, type: :feature
# ## Mock Framework
#
@@ -93,7 +93,7 @@ RSpec.configure do |config|
# You can disable this behaviour by removing the line below, and instead
# explicitly tag your specs with their type, e.g.:
#
- # RSpec.describe UsersController, :type => :controller do
+ # RSpec.describe UsersController, type: :controller do
# # ...
# end
#
diff --git a/spec/support/referential.rb b/spec/support/referential.rb
index b615491da..c0ae35779 100644
--- a/spec/support/referential.rb
+++ b/spec/support/referential.rb
@@ -78,7 +78,7 @@ RSpec.configure do |config|
first_referential.switch
end
- config.before(:each, :js => true) do
+ config.before(:each, truncation: true) do
DatabaseCleaner.strategy = :truncation, { except: %w[spatial_ref_sys] }
end