diff --git a/.rubocop.yml b/.rubocop.yml
index a83324d7..2105cfb1 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -53,10 +53,13 @@ RSpec/NestedGroups:
Max: 6
RSpec/MultipleExpectations:
- Max: 7
+ Max: 20
RSpec/ExampleLength:
- Max: 15
+ Max: 40
+
+RSpec/RSpec/MultipleMemoizedHelpers:
+ Max: 6
Rails/I18nLocaleTexts:
Enabled: false
diff --git a/app/controllers/setup_controller.rb b/app/controllers/setup_controller.rb
index 3c89e34d..ca45ccf9 100644
--- a/app/controllers/setup_controller.rb
+++ b/app/controllers/setup_controller.rb
@@ -16,16 +16,17 @@ class SetupController < ApplicationController
def create
@account = Account.new(account_params)
@account.timezone = Accounts.normalize_timezone(@account.timezone)
-
@user = @account.users.new(user_params)
+ @encrypted_config = EncryptedConfig.new(encrypted_config_params)
unless URI.parse(encrypted_config_params[:value].to_s).class.in?([URI::HTTP, URI::HTTPS])
- @encrypted_config = EncryptedConfig.new(encrypted_config_params)
@encrypted_config.errors.add(:value, 'should be a valid URL')
return render :index, status: :unprocessable_entity
end
+ return render :index, status: :unprocessable_entity unless @account.valid?
+
if @user.save
encrypted_configs = [
{ key: EncryptedConfig::APP_URL_KEY, value: encrypted_config_params[:value] },
diff --git a/app/javascript/submission_form/signature_step.vue b/app/javascript/submission_form/signature_step.vue
index af749b30..ff2269c5 100644
--- a/app/javascript/submission_form/signature_step.vue
+++ b/app/javascript/submission_form/signature_step.vue
@@ -10,6 +10,7 @@
data-tip="Type text"
>
accounts.id)
#
class EncryptedConfig < ApplicationRecord
- FILES_STORAGE_KEY = 'active_storage'
- EMAIL_SMTP_KEY = 'action_mailer_smtp'
- ESIGN_CERTS_KEY = 'esign_certs'
- APP_URL_KEY = 'app_url'
- WEBHOOK_URL_KEY = 'webhook_url'
+ CONFIG_KEYS = [
+ FILES_STORAGE_KEY = 'active_storage',
+ EMAIL_SMTP_KEY = 'action_mailer_smtp',
+ ESIGN_CERTS_KEY = 'esign_certs',
+ APP_URL_KEY = 'app_url',
+ WEBHOOK_URL_KEY = 'webhook_url'
+ ].freeze
belongs_to :account
+ validates :key, inclusion: { in: CONFIG_KEYS }
+
encrypts :value
serialize :value, JSON
diff --git a/app/models/user.rb b/app/models/user.rb
index 807b017d..3d9bec43 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -40,7 +40,9 @@
# fk_rails_... (account_id => accounts.id)
#
class User < ApplicationRecord
- ROLES = %w[admin].freeze
+ ROLES = [
+ ADMIN_ROLE = 'admin'
+ ].freeze
EMAIL_REGEXP = /[^@,\s]+@[^@,\s]+/
@@ -51,10 +53,11 @@ class User < ApplicationRecord
devise :database_authenticatable, :recoverable, :rememberable, :validatable, :trackable
devise :registerable, :omniauthable, omniauth_providers: [:google_oauth2] if Docuseal.multitenant?
- attribute :role, :string, default: 'admin'
+ attribute :role, :string, default: ADMIN_ROLE
attribute :uuid, :string, default: -> { SecureRandom.uuid }
scope :active, -> { where(deleted_at: nil) }
+ scope :admins, -> { where(role: ADMIN_ROLE) }
def access_token
super || build_access_token.tap(&:save!)
diff --git a/config/environments/test.rb b/config/environments/test.rb
index 8f3f63ce..b2f86f2e 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -59,4 +59,10 @@ Rails.application.configure do
# Annotate rendered view with file names.
# config.action_view.annotate_rendered_view_with_filenames = true
+
+ config.active_record.encryption = {
+ primary_key: 'test master key',
+ deterministic_key: 'test deterministic key',
+ key_derivation_salt: 'test key derivation salt'
+ }
end
diff --git a/config/storage.yml b/config/storage.yml
index d48d61f0..3fc3dcd8 100644
--- a/config/storage.yml
+++ b/config/storage.yml
@@ -27,3 +27,8 @@ azure:
storage_access_key: <%= ENV['AZURE_STORAGE_ACCESS_KEY'] %>
container: <%= ENV['AZURE_CONTAINER'] %>
public: <%= ENV['ACTIVE_STORAGE_PUBLIC'] == 'true' %>
+
+test:
+ service: Disk
+ root: <%= Rails.root.join("tmp/storage") %>
+ public: true
diff --git a/lib/action_mailer_configs_interceptor.rb b/lib/action_mailer_configs_interceptor.rb
index 16646791..4f948302 100644
--- a/lib/action_mailer_configs_interceptor.rb
+++ b/lib/action_mailer_configs_interceptor.rb
@@ -4,6 +4,8 @@ module ActionMailerConfigsInterceptor
module_function
def delivering_email(message)
+ return message unless Rails.env.production?
+
if Docuseal.demo?
message.delivery_method(:test)
diff --git a/lib/load_active_storage_configs.rb b/lib/load_active_storage_configs.rb
index 263066fc..37e998f7 100644
--- a/lib/load_active_storage_configs.rb
+++ b/lib/load_active_storage_configs.rb
@@ -18,6 +18,7 @@ module LoadActiveStorageConfigs
def reload
return if Docuseal.multitenant?
return if IS_ENV_CONFIGURED
+ return if Rails.env.test?
encrypted_config = EncryptedConfig.find_by(key: EncryptedConfig::FILES_STORAGE_KEY)
diff --git a/lib/submitters/submit_values.rb b/lib/submitters/submit_values.rb
index a34cd126..bff29f00 100644
--- a/lib/submitters/submit_values.rb
+++ b/lib/submitters/submit_values.rb
@@ -19,7 +19,7 @@ module Submitters
SendWebhookRequestJob.perform_later(submitter)
end
- submitter.submission.template.account.users.active.each do |user|
+ submitter.submission.template.account.users.active.admins.each do |user|
SubmitterMailer.completed_email(submitter, user).deliver_later!
end
diff --git a/spec/factories/accounts.rb b/spec/factories/accounts.rb
new file mode 100644
index 00000000..785a37b4
--- /dev/null
+++ b/spec/factories/accounts.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :account do
+ name { Faker::Company.name }
+ locale { 'en-US' }
+ timezone { 'UTC' }
+ end
+end
diff --git a/spec/factories/encrypted_configs.rb b/spec/factories/encrypted_configs.rb
new file mode 100644
index 00000000..1451cbae
--- /dev/null
+++ b/spec/factories/encrypted_configs.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :encrypted_config do
+ account
+ end
+end
diff --git a/spec/factories/submissions.rb b/spec/factories/submissions.rb
new file mode 100644
index 00000000..83712e8e
--- /dev/null
+++ b/spec/factories/submissions.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :submission do
+ template
+ created_by_user factory: %i[user]
+
+ before(:create) do |submission, _|
+ submission.template_fields = submission.template.fields
+ submission.template_schema = submission.template.schema
+ submission.template_submitters = submission.template.submitters
+ end
+ end
+end
diff --git a/spec/factories/submitters.rb b/spec/factories/submitters.rb
new file mode 100644
index 00000000..b753d93b
--- /dev/null
+++ b/spec/factories/submitters.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :submitter do
+ submission
+ email { Faker::Internet.email }
+ end
+end
diff --git a/spec/factories/templates.rb b/spec/factories/templates.rb
new file mode 100644
index 00000000..7faa84d3
--- /dev/null
+++ b/spec/factories/templates.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :template do
+ account
+
+ author factory: %i[user]
+ name { Faker::Book.title }
+
+ after(:create) do |template|
+ blob = ActiveStorage::Blob.create_and_upload!(
+ io: Rails.root.join('spec/fixtures/sample-document.pdf').open,
+ filename: 'sample-document.pdf',
+ content_type: 'application/pdf'
+ )
+ attachment = ActiveStorage::Attachment.create!(
+ blob:,
+ name: :documents,
+ record: template
+ )
+
+ Templates::ProcessDocument.call(attachment)
+
+ template.schema = [{ attachment_uuid: attachment.uuid, name: 'sample-document' }]
+ template.submitters = [
+ {
+ 'name' => 'First Submitter',
+ 'uuid' => '513848eb-1096-4abc-a743-68596b5aaa4c'
+ }
+ ]
+ template.fields = [
+ {
+ 'uuid' => '21637fc9-0655-45df-8952-04ec64949e85',
+ 'submitter_uuid' => '513848eb-1096-4abc-a743-68596b5aaa4c',
+ 'name' => 'First Name',
+ 'type' => 'text',
+ 'required' => true,
+ 'areas' => [
+ {
+ 'x' => 0.09027777777777778,
+ 'y' => 0.1197252208047105,
+ 'w' => 0.3069444444444444,
+ 'h' => 0.03336604514229637,
+ 'attachment_uuid' => attachment.uuid,
+ 'page' => 0
+ }
+ ]
+ },
+ {
+ 'uuid' => '1f97f8e3-dc82-4586-aeea-6ebed6204e46',
+ 'submitter_uuid' => '513848eb-1096-4abc-a743-68596b5aaa4c',
+ 'name' => '',
+ 'type' => 'signature',
+ 'required' => true,
+ 'areas' => []
+ }
+ ]
+
+ template.save!
+ end
+ end
+end
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
new file mode 100644
index 00000000..007f8a50
--- /dev/null
+++ b/spec/factories/users.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :user do
+ account
+ first_name { Faker::Name.first_name }
+ last_name { Faker::Name.last_name }
+ password { 'password' }
+ role { User::ADMIN_ROLE }
+ email { Faker::Internet.email }
+ end
+end
diff --git a/spec/fixtures/sample-document.pdf b/spec/fixtures/sample-document.pdf
new file mode 100644
index 00000000..774c2ea7
Binary files /dev/null and b/spec/fixtures/sample-document.pdf differ
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
index ef70e9fd..dd87f1f1 100644
--- a/spec/rails_helper.rb
+++ b/spec/rails_helper.rb
@@ -7,6 +7,7 @@ require_relative '../config/environment'
abort('The Rails environment is running in production mode!') if Rails.env.production?
require 'rspec/rails'
require 'capybara/cuprite'
+require 'capybara/rspec'
require 'webmock/rspec'
WebMock.disable_net_connect!(allow_localhost: true)
@@ -45,6 +46,7 @@ RSpec.configure do |config|
config.filter_rails_from_backtrace!
config.include FactoryBot::Syntax::Methods
+ config.include Devise::Test::IntegrationHelpers
config.before(:each, type: :system) do
if ENV['HEADLESS'] == 'false'
diff --git a/spec/system/account_settings_spec.rb b/spec/system/account_settings_spec.rb
new file mode 100644
index 00000000..888a1ebf
--- /dev/null
+++ b/spec/system/account_settings_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Account Settings' do
+ let!(:account) { create(:account) }
+ let!(:user) { create(:user, account:) }
+ let!(:encrypted_config) { create(:encrypted_config, account:, key: EncryptedConfig::APP_URL_KEY, value: 'https://www.test.com') }
+
+ before do
+ sign_in(user)
+ visit settings_account_path
+ end
+
+ it 'shows pre-filled account settings page' do
+ expect(page).to have_content('Account')
+ expect(page).to have_field('Company Name', with: account.name)
+ expect(page).to have_field('Timezone', with: account.timezone)
+ expect(page).to have_field('Time format', with: account.locale)
+ expect(page).to have_field('App URL', with: encrypted_config.value)
+ end
+
+ it 'updates the account settings' do
+ fill_in 'Company Name', with: 'New Company Name'
+ fill_in 'App URL', with: 'https://example.com'
+ select '(GMT+01:00) Berlin', from: 'Timezone'
+ select 'Spanish (Spain)', from: 'Time format'
+
+ click_button 'Update'
+
+ account.reload
+ encrypted_config.reload
+
+ expect(account.name).to eq('New Company Name')
+ expect(account.timezone).to eq('Berlin')
+ expect(account.locale).to eq('es-ES')
+ expect(encrypted_config.value).to eq('https://example.com')
+ end
+end
diff --git a/spec/system/api_settings_spec.rb b/spec/system/api_settings_spec.rb
new file mode 100644
index 00000000..4870964c
--- /dev/null
+++ b/spec/system/api_settings_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'API Settings' do
+ let!(:account) { create(:account) }
+ let!(:user) { create(:user, account:) }
+
+ before do
+ sign_in(user)
+ visit settings_api_index_path
+ end
+
+ it 'shows verify signed PDF page' do
+ expect(page).to have_content('API')
+ expect(page).to have_field('X-Auth-Token', with: user.access_token.token)
+ end
+end
diff --git a/spec/system/dashboard_spec.rb b/spec/system/dashboard_spec.rb
new file mode 100644
index 00000000..2632cbaa
--- /dev/null
+++ b/spec/system/dashboard_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Dashboard Page' do
+ let!(:account) { create(:account) }
+ let!(:user) { create(:user, account:) }
+
+ before do
+ sign_in(user)
+ end
+
+ context 'when are no templates' do
+ it 'shows empty state' do
+ visit root_path
+
+ expect(page).to have_content('Welcome to DocuSeal')
+ expect(page).to have_content('Streamline document workflows, from creating customizable')
+ expect(page).to have_link('Create Template', href: new_template_path)
+ end
+ end
+
+ context 'when there are templates' do
+ let!(:authors) { create_list(:user, 5, account:) }
+ let!(:templates) { authors.map { |author| create(:template, account:, author:) } }
+
+ before do
+ visit root_path
+ end
+
+ it 'shows the list of templates' do
+ templates.each do |template|
+ expect(page).to have_content(template.name)
+ expect(page).to have_content(template.author.full_name)
+ end
+
+ expect(page).to have_content('Templates')
+ expect(page).to have_link('Create', href: new_template_path)
+ end
+
+ it 'initializes the template creation process' do
+ click_link 'Create'
+
+ within('#modal') do
+ fill_in 'template[name]', with: 'New Template'
+
+ expect do
+ click_button 'Create'
+ end.to change(Template, :count).by(1)
+
+ expect(page).to have_current_path(edit_template_path(Template.last), ignore_query: true)
+ end
+ end
+ end
+end
diff --git a/spec/system/email_settings_spec.rb b/spec/system/email_settings_spec.rb
new file mode 100644
index 00000000..5e2a00e5
--- /dev/null
+++ b/spec/system/email_settings_spec.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Email Settings' do
+ let!(:account) { create(:account) }
+ let!(:user) { create(:user, account:) }
+
+ before do
+ sign_in(user)
+ end
+
+ context 'when SMTP settings are not set' do
+ it 'setup SMTP settings' do
+ visit settings_email_index_path
+
+ fill_in 'Host', with: 'smtp.example.com'
+ fill_in 'Port', with: '587'
+ fill_in 'Username', with: 'user@example.com'
+ fill_in 'Password', with: 'password'
+ fill_in 'Domain', with: 'example.com'
+ fill_in 'Send from Email', with: 'user@example.com'
+ select 'Plain', from: 'Authentication'
+ choose 'TLS'
+
+ expect do
+ click_button 'Save'
+ end.to change(EncryptedConfig, :count).by(1)
+
+ encrypted_config = EncryptedConfig.find_by(account:, key: EncryptedConfig::EMAIL_SMTP_KEY)
+
+ expect(encrypted_config.value['host']).to eq('smtp.example.com')
+ expect(encrypted_config.value['port']).to eq('587')
+ expect(encrypted_config.value['username']).to eq('user@example.com')
+ expect(encrypted_config.value['password']).to eq('password')
+ expect(encrypted_config.value['domain']).to eq('example.com')
+ expect(encrypted_config.value['authentication']).to eq('plain')
+ expect(encrypted_config.value['security']).to eq('tls')
+ expect(encrypted_config.value['from_email']).to eq('user@example.com')
+ end
+ end
+
+ context 'when SMTP settings are set' do
+ let!(:encrypted_config) do
+ create(:encrypted_config, account:, key: EncryptedConfig::EMAIL_SMTP_KEY, value: {
+ host: 'smtp.example.com',
+ port: '587',
+ username: 'user@example.co',
+ password: 'password',
+ domain: 'example.com',
+ authentication: 'plain',
+ security: 'tls',
+ from_email: 'user@example.co'
+ })
+ end
+
+ before do
+ visit settings_email_index_path
+ end
+
+ it 'shows pre-filled SMTP settings' do
+ expect(page).to have_content('Email SMTP')
+ expect(page).to have_field('Host', with: encrypted_config.value['host'])
+ expect(page).to have_field('Port', with: encrypted_config.value['port'])
+ expect(page).to have_field('Username', with: encrypted_config.value['username'])
+ expect(page).to have_field('Password', with: encrypted_config.value['password'])
+ expect(page).to have_field('Domain', with: encrypted_config.value['domain'])
+ expect(page).to have_select('Authentication', selected: 'Plain')
+ expect(page).to have_field('Send from Email', with: encrypted_config.value['from_email'])
+ end
+
+ it 'updates SMTP settings' do
+ fill_in 'Host', with: 'smtp.gmail.com'
+ fill_in 'Port', with: '465'
+ fill_in 'Username', with: 'user@gmail.com'
+ fill_in 'Password', with: 'new_password'
+ fill_in 'Domain', with: 'gmail.com'
+ fill_in 'Send from Email', with: 'user@gmail.com'
+ select 'Plain', from: 'Authentication'
+ choose 'SSL'
+
+ expect do
+ click_button 'Save'
+ end.not_to change(EncryptedConfig, :count)
+
+ encrypted_config.reload
+
+ expect(encrypted_config.value['host']).to eq('smtp.gmail.com')
+ expect(encrypted_config.value['port']).to eq('465')
+ expect(encrypted_config.value['username']).to eq('user@gmail.com')
+ expect(encrypted_config.value['password']).to eq('new_password')
+ expect(encrypted_config.value['domain']).to eq('gmail.com')
+ expect(encrypted_config.value['authentication']).to eq('plain')
+ expect(encrypted_config.value['security']).to eq('ssl')
+ expect(encrypted_config.value['from_email']).to eq('user@gmail.com')
+ end
+ end
+end
diff --git a/spec/system/esign_spec.rb b/spec/system/esign_spec.rb
new file mode 100644
index 00000000..c5f7a395
--- /dev/null
+++ b/spec/system/esign_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'PDF Signature Settings' do
+ let!(:account) { create(:account) }
+ let!(:user) { create(:user, account:) }
+
+ before do
+ sign_in(user)
+ visit settings_esign_index_path
+ end
+
+ it 'shows verify signed PDF page' do
+ expect(page).to have_content('PDF Signature')
+ expect(page).to have_content('Upload signed PDF file to validate its signature')
+ expect(page).to have_content('Verify Signed PDF')
+ expect(page).to have_content('Click to upload or drag and drop files')
+ end
+end
diff --git a/spec/system/newsletters_spec.rb b/spec/system/newsletters_spec.rb
new file mode 100644
index 00000000..a4fc3c18
--- /dev/null
+++ b/spec/system/newsletters_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Newsletter' do
+ let(:user) { create(:user, account: create(:account)) }
+
+ before do
+ sign_in(user)
+ stub_request(:post, Docuseal::NEWSLETTER_URL).to_return(status: 200)
+ visit newsletter_path
+ end
+
+ it 'shows the newsletter page' do
+ expect(page).to have_content('Developer Newsletters')
+ expect(page).to have_button('Submit')
+ expect(page).to have_content('Skip')
+ expect(page).to have_field('user[email]', with: user.email)
+ end
+
+ it 'submits the newsletter form' do
+ click_button 'Submit'
+
+ expect(a_request(:post, Docuseal::NEWSLETTER_URL)).to have_been_made.once
+ end
+
+ it 'skips the newsletter form' do
+ click_on 'Skip'
+
+ expect(page).to have_current_path(root_path, ignore_query: true)
+ end
+end
diff --git a/spec/system/personalization_spec.rb b/spec/system/personalization_spec.rb
new file mode 100644
index 00000000..6e878e11
--- /dev/null
+++ b/spec/system/personalization_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Personalization' do
+ let!(:account) { create(:account) }
+ let!(:user) { create(:user, account:) }
+
+ before do
+ sign_in(user)
+ visit settings_personalization_path
+ end
+
+ it 'shows the personalization page' do
+ expect(page).to have_content('Email Templates')
+ expect(page).to have_content('Signature Request Email')
+ expect(page).to have_content('Form Completed Email')
+ expect(page).to have_content('Documents Copy Email')
+ expect(page).to have_content('Company Logo')
+ expect(page).to have_content('Unlock with DocuSeal Enterprise')
+ expect(page).to have_content('Display your company name and logo when signing documents')
+ end
+end
diff --git a/spec/system/profile_settings_spec.rb b/spec/system/profile_settings_spec.rb
new file mode 100644
index 00000000..351b636c
--- /dev/null
+++ b/spec/system/profile_settings_spec.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Profile Settings' do
+ let(:user) { create(:user, account: create(:account)) }
+
+ before do
+ sign_in(user)
+ visit settings_profile_index_path
+ end
+
+ it 'shows the profile settings page' do
+ expect(page).to have_content('Profile')
+ expect(page).to have_field('user[email]', with: user.email)
+ expect(page).to have_field('user[first_name]', with: user.first_name)
+ expect(page).to have_field('user[last_name]', with: user.last_name)
+
+ expect(page).to have_content('Change Password')
+ expect(page).to have_field('user[password]')
+ expect(page).to have_field('user[password_confirmation]')
+ end
+
+ context 'when changes contact information' do
+ it 'updates first name, last name and email' do
+ fill_in 'First name', with: 'Devid'
+ fill_in 'Last name', with: 'Beckham'
+ fill_in 'Email', with: 'david.beckham@example.com'
+
+ all(:button, 'Update')[0].click
+
+ user.reload
+
+ expect(user.first_name).to eq('Devid')
+ expect(user.last_name).to eq('Beckham')
+ expect(user.email).to eq('david.beckham@example.com')
+ end
+ end
+
+ context 'when changes password' do
+ it 'updates password' do
+ fill_in 'New password', with: 'newpassword'
+ fill_in 'Confirm new password', with: 'newpassword'
+
+ all(:button, 'Update')[1].click
+
+ expect(page).to have_content('Password has been changed')
+ end
+
+ it 'does not update if password confirmation does not match' do
+ fill_in 'New password', with: 'newpassword'
+ fill_in 'Confirm new password', with: 'newpassword1'
+
+ all(:button, 'Update')[1].click
+
+ expect(page).to have_content("Password confirmation doesn't match Password")
+ end
+ end
+end
diff --git a/spec/system/setup_spec.rb b/spec/system/setup_spec.rb
new file mode 100644
index 00000000..58592888
--- /dev/null
+++ b/spec/system/setup_spec.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'App Setup', js: true do
+ let(:form_data) do
+ {
+ first_name: 'John',
+ last_name: 'Doe',
+ email: 'john.doe@example.com',
+ company_name: 'Example Company',
+ password: 'password',
+ app_url: 'https://example.com'
+ }
+ end
+
+ before do
+ visit setup_index_path
+ end
+
+ it 'shows the setup page' do
+ expect(page).to have_content('Initial Setup')
+
+ ['First name', 'Last name', 'Email', 'Company name', 'Password', 'App URL'].each do |field|
+ expect(page).to have_field(field)
+ end
+ end
+
+ context 'when valid information' do
+ it 'setups the app' do
+ fill_setup_form(form_data)
+
+ expect do
+ click_button 'Submit'
+ sleep 1
+ end.to change(Account, :count).by(1).and change(User, :count).by(1).and change(EncryptedConfig, :count).by(2)
+
+ user = User.last
+ encrypted_config_app_url = EncryptedConfig.find_by(account: user.account,
+ key: EncryptedConfig::APP_URL_KEY)
+ encrypted_config_esign_certs = EncryptedConfig.find_by(account: user.account,
+ key: EncryptedConfig::ESIGN_CERTS_KEY)
+
+ expect(user.first_name).to eq(form_data[:first_name])
+ expect(user.last_name).to eq(form_data[:last_name])
+ expect(user.email).to eq(form_data[:email])
+ expect(user.account.timezone).to eq('UTC')
+ expect(user.account.locale).to eq('en-US')
+ expect(user.account.name).to eq(form_data[:company_name])
+ expect(encrypted_config_app_url.value).to eq(form_data[:app_url])
+ expect(encrypted_config_esign_certs.value).to be_present
+ end
+ end
+
+ context 'when invalid information' do
+ it 'does not setup the app if the password is too short' do
+ fill_setup_form(form_data.merge(password: 'pass'))
+
+ expect do
+ click_button 'Submit'
+ end.not_to(change(User, :count))
+
+ expect(page).to have_content('Password is too short (minimum is 6 characters)')
+ end
+ end
+
+ context 'when the app is already setup' do
+ let!(:user) { create(:user, account: create(:account)) }
+
+ it 'redirects to the dashboard page' do
+ sign_in(user)
+ visit setup_index_path
+
+ expect(page).to have_content('Welcome to DocuSeal')
+ end
+ end
+
+ private
+
+ def fill_setup_form(form_data)
+ fill_in 'First name', with: form_data[:first_name]
+ fill_in 'Last name', with: form_data[:last_name]
+ fill_in 'Email', with: form_data[:email]
+ fill_in 'Company name', with: form_data[:company_name]
+ fill_in 'Password', with: form_data[:password]
+ fill_in 'App URL', with: form_data[:app_url]
+ end
+end
diff --git a/spec/system/storage_settings_spec.rb b/spec/system/storage_settings_spec.rb
new file mode 100644
index 00000000..d57564d7
--- /dev/null
+++ b/spec/system/storage_settings_spec.rb
@@ -0,0 +1,247 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Storage Settings' do
+ let!(:account) { create(:account) }
+ let!(:user) { create(:user, account:) }
+
+ before do
+ sign_in(user)
+ end
+
+ context 'when storage settings are not set' do
+ before do
+ visit settings_storage_index_path
+ end
+
+ context 'when Disk is selected' do
+ it 'shows default storage settings page' do
+ expect(page).to have_content('Storage')
+ expect(page).to have_content('Store all files on disk')
+ expect(page).to have_content('No configs are needed but make sure your disk is persistent')
+ expect(page).to have_checked_field('Disk')
+ end
+ end
+
+ context 'when AWS S3 is selected' do
+ it 'setups AWS S3 storage settings' do
+ choose 'AWS'
+
+ fill_in 'Access key ID', with: 'access_key_id'
+ fill_in 'Secret access key', with: 'secret_access_key'
+ fill_in 'Region', with: 'us-west-1'
+ fill_in 'Bucket', with: 'bucket'
+ fill_in 'Endpoint', with: 'https://s3.us-west-1.amazonaws.com'
+
+ expect do
+ click_button 'Save'
+ end.to change(EncryptedConfig, :count).by(1)
+
+ encrypted_config = EncryptedConfig.find_by(account:, key: EncryptedConfig::FILES_STORAGE_KEY)
+ configs = encrypted_config.value['configs']
+
+ expect(encrypted_config.value['service']).to eq('aws_s3')
+ expect(configs['access_key_id']).to eq('access_key_id')
+ expect(configs['secret_access_key']).to eq('secret_access_key')
+ expect(configs['region']).to eq('us-west-1')
+ expect(configs['bucket']).to eq('bucket')
+ expect(configs['endpoint']).to eq('https://s3.us-west-1.amazonaws.com')
+ end
+ end
+
+ context 'when Google Cloud Storage is selected' do
+ it 'setups Google Cloud Storage settings' do
+ choose 'GCP'
+
+ fill_in 'Project', with: 'project_id'
+ fill_in 'Bucket', with: 'bucket'
+ fill_in 'Credentials (JSON key content)', with: '{ "type": "service_account", "project_id": "project_id" }'
+
+ expect do
+ click_button 'Save'
+ end.to change(EncryptedConfig, :count).by(1)
+
+ encrypted_config = EncryptedConfig.find_by(account:, key: EncryptedConfig::FILES_STORAGE_KEY)
+ configs = encrypted_config.value['configs']
+
+ expect(encrypted_config.value['service']).to eq('google')
+ expect(configs['project']).to eq('project_id')
+ expect(configs['bucket']).to eq('bucket')
+ expect(configs['credentials']).to eq('{ "type": "service_account", "project_id": "project_id" }')
+ end
+ end
+
+ context 'when Azure is selected' do
+ it 'setup Azure storage settings' do
+ choose 'Azure'
+
+ fill_in 'Storage Account Name', with: 'storage_account_name'
+ fill_in 'Container', with: 'container'
+ fill_in 'Storage Access Key', with: 'storage_access_key'
+
+ expect do
+ click_button 'Save'
+ end.to change(EncryptedConfig, :count).by(1)
+
+ encrypted_config = EncryptedConfig.find_by(account:, key: EncryptedConfig::FILES_STORAGE_KEY)
+ configs = encrypted_config.value['configs']
+
+ expect(encrypted_config.value['service']).to eq('azure')
+ expect(configs['storage_account_name']).to eq('storage_account_name')
+ expect(configs['container']).to eq('container')
+ expect(configs['storage_access_key']).to eq('storage_access_key')
+ end
+ end
+ end
+
+ context 'when storage settings are set' do
+ context 'when updates the same storage settings' do
+ context 'when AWS S3' do
+ let!(:encrypted_config) do
+ create(:encrypted_config, account:, key: EncryptedConfig::FILES_STORAGE_KEY, value: {
+ service: 'aws_s3',
+ configs: {
+ access_key_id: 'access_key_id',
+ secret_access_key: 'secret_access_key',
+ region: 'us-west-1',
+ bucket: 'bucket',
+ endpoint: 'https://s3.us-west-1.amazonaws.com'
+ }
+ })
+ end
+
+ it 'updates AWS S3 storage settings' do
+ visit settings_storage_index_path
+
+ fill_in 'Access key ID', with: 'new_access_key_id'
+ fill_in 'Secret access key', with: 'new_secret_access_key'
+ fill_in 'Region', with: 'us-west-2'
+ fill_in 'Bucket', with: 'new_bucket'
+ fill_in 'Endpoint', with: 'https://s3.us-west-2.amazonaws.com'
+
+ expect do
+ click_button 'Save'
+ end.not_to(change(EncryptedConfig, :count))
+
+ encrypted_config.reload
+ configs = encrypted_config.value['configs']
+
+ expect(encrypted_config.value['service']).to eq('aws_s3')
+ expect(configs['access_key_id']).to eq('new_access_key_id')
+ expect(configs['secret_access_key']).to eq('new_secret_access_key')
+ expect(configs['region']).to eq('us-west-2')
+ expect(configs['bucket']).to eq('new_bucket')
+ expect(configs['endpoint']).to eq('https://s3.us-west-2.amazonaws.com')
+ end
+ end
+
+ context 'when Google Cloud Storage' do
+ let!(:encrypted_config) do
+ create(:encrypted_config, account:, key: EncryptedConfig::FILES_STORAGE_KEY, value: {
+ service: 'google',
+ configs: {
+ project: 'project_id',
+ bucket: 'bucket',
+ credentials: '{ "type": "service_account", "project_id": "project_id" }'
+ }
+ })
+ end
+
+ it 'updates Google Cloud Storage settings' do
+ visit settings_storage_index_path
+
+ fill_in 'Project', with: 'new_project_id'
+ fill_in 'Bucket', with: 'new_bucket'
+ fill_in 'Credentials (JSON key content)',
+ with: '{ "type": "new_service_account", "project_id": "new_project_id" }'
+
+ expect do
+ click_button 'Save'
+ end.not_to(change(EncryptedConfig, :count))
+
+ encrypted_config.reload
+ configs = encrypted_config.value['configs']
+
+ expect(encrypted_config.value['service']).to eq('google')
+ expect(configs['project']).to eq('new_project_id')
+ expect(configs['bucket']).to eq('new_bucket')
+ expect(configs['credentials']).to eq('{ "type": "new_service_account", "project_id": "new_project_id" }')
+ end
+ end
+
+ context 'when Azure' do
+ let!(:encrypted_config) do
+ create(:encrypted_config, account:, key: EncryptedConfig::FILES_STORAGE_KEY, value: {
+ service: 'azure',
+ configs: {
+ storage_account_name: 'storage_account_name',
+ container: 'container',
+ storage_access_key: 'storage_access_key'
+ }
+ })
+ end
+
+ it 'updates Azure storage settings' do
+ visit settings_storage_index_path
+
+ fill_in 'Storage Account Name', with: 'new_storage_account_name'
+ fill_in 'Container', with: 'new_container'
+ fill_in 'Storage Access Key', with: 'new_storage_access_key'
+
+ expect do
+ click_button 'Save'
+ end.not_to(change(EncryptedConfig, :count))
+
+ encrypted_config.reload
+ configs = encrypted_config.value['configs']
+
+ expect(encrypted_config.value['service']).to eq('azure')
+ expect(configs['storage_account_name']).to eq('new_storage_account_name')
+ expect(configs['container']).to eq('new_container')
+ expect(configs['storage_access_key']).to eq('new_storage_access_key')
+ end
+ end
+ end
+
+ context 'when switches to another storage settings' do
+ context 'when Google Cloud Storage' do
+ let!(:encrypted_config) do
+ create(:encrypted_config, account:, key: EncryptedConfig::FILES_STORAGE_KEY, value: {
+ service: 'google',
+ configs: {
+ project: 'project_id',
+ bucket: 'bucket',
+ credentials: '{ "type": "service_account", "project_id": "project_id" }'
+ }
+ })
+ end
+
+ it 'switches to AWS S3' do
+ visit settings_storage_index_path
+ choose 'AWS'
+
+ fill_in 'Access key ID', with: 'access_key_id'
+ fill_in 'Secret access key', with: 'secret_access_key'
+ fill_in 'Region', with: 'us-west-1'
+ fill_in 'Bucket', with: 'bucket'
+ fill_in 'Endpoint', with: 'https://s3.us-west-1.amazonaws.com'
+
+ expect do
+ click_button 'Save'
+ end.not_to(change(EncryptedConfig, :count))
+
+ encrypted_config.reload
+ configs = encrypted_config.value['configs']
+
+ expect(encrypted_config.value['service']).to eq('aws_s3')
+ expect(configs['access_key_id']).to eq('access_key_id')
+ expect(configs['secret_access_key']).to eq('secret_access_key')
+ expect(configs['region']).to eq('us-west-1')
+ expect(configs['bucket']).to eq('bucket')
+ expect(configs['endpoint']).to eq('https://s3.us-west-1.amazonaws.com')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/system/submit_form_spec.rb b/spec/system/submit_form_spec.rb
new file mode 100644
index 00000000..b9b44d19
--- /dev/null
+++ b/spec/system/submit_form_spec.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Submit Form' do
+ let!(:account) { create(:account) }
+ let!(:user) { create(:user, account:) }
+ let!(:template) { create(:template, account:, author: user) }
+
+ before do
+ sign_in(user)
+ end
+
+ context 'when initialized by shared link' do
+ before do
+ visit start_form_path(slug: template.slug)
+ end
+
+ it 'shows start form page' do
+ expect(page).to have_content('You have been invited to submit a form')
+ expect(page).to have_content(template.name)
+ expect(page).to have_content("Invited by #{template.account.name}")
+ end
+
+ it 'complete the form' do
+ fill_in 'Email', with: 'john.dou@example.com'
+ click_button 'Start'
+
+ fill_in 'First Name', with: 'Adam'
+ click_on 'Next'
+ click_on 'type_text_button'
+ fill_in 'signature_text_input', with: 'Adam'
+
+ expect do
+ click_on 'Submit'
+ end.not_to(change(Submitter, :count))
+
+ submitter = Submitter.find_by(email: 'john.dou@example.com')
+
+ expect(page).to have_button('Download')
+ expect(submitter.email).to eq('john.dou@example.com')
+ expect(submitter.ip).to eq('127.0.0.1')
+ expect(submitter.ua).to be_present
+ expect(submitter.opened_at).to be_present
+ expect(submitter.completed_at).to be_present
+ expect(submitter.values.values).to include('Adam')
+ end
+ end
+
+ context 'when initialized by shared email address' do
+ let!(:submission) { create(:submission, template:, created_by_user: user) }
+ let!(:submitters) { template.submitters.map { |s| create(:submitter, submission:, uuid: s['uuid']) } }
+ let(:submitter) { submitters.first }
+
+ before do
+ visit submit_form_path(slug: submitter.slug)
+ end
+
+ it 'completes the form' do
+ fill_in 'First Name', with: 'Sally'
+ click_on 'Next'
+ click_on 'type_text_button'
+ fill_in 'signature_text_input', with: 'Sally'
+ click_on 'Submit'
+
+ submitter.reload
+
+ expect(page).to have_button('Download')
+ expect(submitter.ip).to eq('127.0.0.1')
+ expect(submitter.ua).to be_present
+ expect(submitter.opened_at).to be_present
+ expect(submitter.completed_at).to be_present
+ expect(submitter.values.values).to include('Sally')
+ end
+
+ it 'sends completed email' do
+ fill_in 'First Name', with: 'Adam'
+ click_on 'Next'
+ click_on 'type_text_button'
+ fill_in 'signature_text_input', with: 'Adam'
+
+ expect do
+ click_on 'Submit'
+ end.to change { ActionMailer::Base.deliveries.size }.by(1)
+
+ email = ActionMailer::Base.deliveries.last
+
+ expect(email.subject).to eq("#{submitter.email} has completed the \"#{template.name}\" form")
+
+ expect(email.body.encoded).to include "Hi #{user.first_name},"
+ expect(email.body.encoded).to include "#{submitter.email} has completed the \"#{template.name}\" form."
+ expect(email.body.encoded).to have_link('View Submission')
+ end
+ end
+end
diff --git a/spec/system/team_settings_spec.rb b/spec/system/team_settings_spec.rb
new file mode 100644
index 00000000..f8b24ceb
--- /dev/null
+++ b/spec/system/team_settings_spec.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Team Settings' do
+ let(:account) { create(:account) }
+ let(:current_user) { create(:user, account:) }
+
+ before do
+ sign_in(current_user)
+ end
+
+ context 'when multiple users' do
+ let!(:users) { create_list(:user, 2, account:) }
+
+ before do
+ visit settings_users_path
+ end
+
+ it 'shows all users' do
+ within '.table' do
+ users.each do |user|
+ expect(page).to have_content(user.full_name)
+ expect(page).to have_content(user.email)
+ end
+ end
+ end
+
+ it 'creates a new user' do
+ click_link 'New User'
+
+ within '#modal' do
+ fill_in 'First name', with: 'Joseph'
+ fill_in 'Last name', with: 'Smith'
+ fill_in 'Email', with: 'joseph.smith@example.com'
+ fill_in 'Password', with: 'password'
+
+ expect do
+ click_button 'Submit'
+ end.to change(User, :count).by(1)
+
+ user = User.last
+
+ expect(user.first_name).to eq('Joseph')
+ expect(user.last_name).to eq('Smith')
+ expect(user.email).to eq('joseph.smith@example.com')
+ expect(user.account).to eq(account)
+ end
+ end
+
+ it 'updates a user' do
+ first(:link, 'Edit').click
+
+ fill_in 'First name', with: 'Adam'
+ fill_in 'Last name', with: 'Meier'
+ fill_in 'Email', with: 'adam.meier@example.com'
+ fill_in 'Password', with: 'new_password'
+
+ expect do
+ click_button 'Submit'
+ end.not_to change(User, :count)
+
+ user = User.find_by(email: 'adam.meier@example.com')
+
+ expect(user.first_name).to eq('Adam')
+ expect(user.last_name).to eq('Meier')
+ expect(user.email).to eq('adam.meier@example.com')
+ end
+
+ it 'removes a user' do
+ expect do
+ accept_confirm('Are you sure?') do
+ first(:link, 'Delete').click
+ end
+ end.to change { User.active.count }.by(-1)
+
+ expect(page).to have_content('User has been removed')
+ end
+ end
+
+ context 'when single user' do
+ before do
+ visit settings_users_path
+ end
+
+ it 'does not allow to remove the current user' do
+ expect do
+ accept_confirm('Are you sure?') do
+ first(:link, 'Delete').click
+ end
+ end.not_to(change { User.admins.count })
+
+ expect(page).to have_content('Unable to remove user')
+ end
+ end
+end
diff --git a/spec/system/template_spec.rb b/spec/system/template_spec.rb
new file mode 100644
index 00000000..d95a1632
--- /dev/null
+++ b/spec/system/template_spec.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Template' do
+ let!(:account) { create(:account) }
+ let!(:user) { create(:user, account:) }
+ let!(:template) { create(:template, account:, author: user) }
+
+ before do
+ sign_in(user)
+ end
+
+ context 'when no submissions' do
+ it 'shows the template page' do
+ visit template_path(template)
+
+ expect(page).to have_content(template.name)
+ expect(page).to have_content('There are no Submissions')
+ expect(page).to have_content('Send an invitation to fill and complete the form')
+ expect(page).to have_link('Add Recipients', href: new_template_submission_path(template))
+ expect(page).to have_link('Submit it Yourself')
+ end
+ end
+
+ context 'when there are submissions' do
+ let!(:submission) { create(:submission, template:, created_by_user: user) }
+ let!(:submitters) { template.submitters.map { |s| create(:submitter, submission:, uuid: s['uuid']) } }
+
+ it 'shows the template page with submissions' do
+ visit template_path(template)
+
+ submitters.each do |submitter|
+ expect(page).to have_content(submitter.email)
+ end
+
+ expect(page).to have_content(template.name)
+ end
+ end
+
+ context 'when managing a template' do
+ before do
+ visit template_path(template)
+ end
+
+ it 'removes a template' do
+ expect do
+ accept_confirm('Are you sure?') do
+ click_button 'Remove'
+ end
+ end.to change { Template.active.count }.by(-1)
+
+ expect(page).to have_content('Template has been archived')
+ expect(page).to have_current_path(root_path, ignore_query: true)
+ end
+
+ it 'edits a template' do
+ click_link 'Edit'
+
+ expect(page).to have_current_path(edit_template_path(template), ignore_query: true)
+ end
+
+ it 'clones a template' do
+ click_link 'Clone'
+
+ within '#modal' do
+ fill_in 'template[name]', with: 'New Template Name'
+
+ expect do
+ click_button 'Submit'
+ end.to change { Template.active.count }.by(1)
+
+ template = Template.last
+
+ expect(template.name).to eq('New Template Name')
+ expect(page).to have_current_path(edit_template_path(template), ignore_query: true)
+ end
+ end
+ end
+end
diff --git a/spec/system/webhook_settings_spec.rb b/spec/system/webhook_settings_spec.rb
new file mode 100644
index 00000000..1e94eb62
--- /dev/null
+++ b/spec/system/webhook_settings_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Webhook Settings' do
+ let!(:account) { create(:account) }
+ let!(:user) { create(:user, account:) }
+
+ before do
+ sign_in(user)
+ visit settings_webhooks_path
+ end
+
+ it 'shows webhook settings page' do
+ expect(page).to have_content('Webhooks')
+ expect(page).to have_field('Webhook URL')
+ expect(page).to have_button('Save')
+ end
+
+ it 'updates the webhook URL' do
+ fill_in 'Webhook URL', with: 'https://example.com'
+
+ expect do
+ click_button 'Save'
+ end.to change(EncryptedConfig, :count).by(1)
+
+ encrypted_config = EncryptedConfig.find_by(account:, key: EncryptedConfig::WEBHOOK_URL_KEY)
+
+ expect(encrypted_config.value).to eq('https://example.com')
+ end
+end