mirror of https://github.com/docusealco/docuseal
System specs (10 new files, 880 lines): - Fork branding: brand name display, personalization form, upstream attribution - Account logo: section visibility, placeholder, attached logo display - Signing flow: enforced order, signature clear/redraw, resubmit, optional fields, decline - Role-based access: admin/editor/viewer nav and button visibility matrix - SMS/SSO settings: placeholder visibility in single-tenant mode - Submission lifecycle: send-to-recipients, sign flow, completion verification - Template CRUD: restore archived, share link toggle, folder navigation - Feature toggles: 7 toggle integration tests (decline, delegate, reason, order, typed sig, MFA, resubmit) API request specs (1 new file, 91 lines): - User show, template clone, submission documents, form/submission events, tools merge Docker test infrastructure: - Dockerfile.test: Ruby 4.0.5 + Chrome + Node 20 + pdfium + libvips - docker-compose.test.yml: PostgreSQL 14 + test service with volume caching - bin/test: convenience script for running tests locally Fix existing specs to match actual HTML/CSS: - Rename .navbar selectors (no such class exists) - Fix button/link selectors for Clone/Archive/Restore - Fix modal selectors for share link and send-to-recipients - Fix signing reason select selector (Vue-rendered) - Fix complete button selector (Vue teleport) - Fix awaiting view text (uppercase CSS) - Fix pending/completed status text (uppercase badges)pull/687/head
parent
776384cacd
commit
95a56d4648
@ -0,0 +1,38 @@
|
||||
FROM ruby:4.0.5-slim-bookworm AS base
|
||||
|
||||
RUN apt-get update -qq && \
|
||||
apt-get install -y -qq --no-install-recommends \
|
||||
build-essential \
|
||||
libpq-dev \
|
||||
libvips-dev \
|
||||
libyaml-dev \
|
||||
shared-mime-info \
|
||||
wget \
|
||||
curl \
|
||||
git \
|
||||
gnupg2 \
|
||||
ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Node.js 20.x + yarn
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
|
||||
apt-get install -y -qq nodejs && \
|
||||
npm install -g yarn && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Chrome 125 for Cuprite
|
||||
RUN curl -fsSL https://dl.google.com/linux/linux_signing_key.pub | gpg --dearmor -o /usr/share/keyrings/google-chrome.gpg && \
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/google-chrome.gpg] http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google-chrome.list && \
|
||||
apt-get update -qq && \
|
||||
apt-get install -y -qq --no-install-recommends google-chrome-stable && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# pdfium
|
||||
RUN wget -q -O /tmp/pdfium-linux.tgz \
|
||||
"https://github.com/bblanchon/pdfium-binaries/releases/latest/download/pdfium-linux-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tgz" && \
|
||||
tar -xzf /tmp/pdfium-linux.tgz --strip-components=1 -C /usr/lib lib/libpdfium.so && \
|
||||
rm -f /tmp/pdfium-linux.tgz
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV CHROME_PATH=/usr/bin/google-chrome-stable
|
||||
@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "==> Installing Ruby dependencies..."
|
||||
bundle check 2>/dev/null || bundle install --quiet
|
||||
|
||||
echo "==> Installing JavaScript dependencies..."
|
||||
yarn install --frozen-lockfile 2>/dev/null || yarn install
|
||||
|
||||
echo "==> Preparing database..."
|
||||
bundle exec rake db:create 2>/dev/null || true
|
||||
bundle exec rake db:migrate
|
||||
|
||||
echo "==> Precompiling assets..."
|
||||
bundle exec rake assets:precompile
|
||||
|
||||
echo "==> Running: $@"
|
||||
exec "$@"
|
||||
@ -0,0 +1,59 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
usage() {
|
||||
echo "Usage: bin/test [--build] [--tag TAG] [--file FILE] [SPEC_ARGS...]"
|
||||
echo ""
|
||||
echo "Run tests in Docker matching the CI environment."
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --build Rebuild the test image before running"
|
||||
echo " --tag TAG Run specs tagged with TAG (e.g. fork_branding)"
|
||||
echo " --file FILE Run a specific spec file"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " bin/test # run all specs"
|
||||
echo " bin/test --build # rebuild image, then run all specs"
|
||||
echo " bin/test --tag fork_branding # run fork branding spec"
|
||||
echo " bin/test --file spec/requests/ # run API request specs"
|
||||
echo " bin/test spec/system/role_based_access_spec.rb:42 # single test"
|
||||
exit 1
|
||||
}
|
||||
|
||||
BUILD=false
|
||||
SPEC_ARGS=()
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--build) BUILD=true; shift ;;
|
||||
--tag)
|
||||
shift
|
||||
SPEC_ARGS+=("spec/system/${1}_spec.rb")
|
||||
shift
|
||||
;;
|
||||
--file)
|
||||
shift
|
||||
SPEC_ARGS+=("$1")
|
||||
shift
|
||||
;;
|
||||
-h|--help) usage ;;
|
||||
*) SPEC_ARGS+=("$1"); shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
COMPOSE_FILE="docker-compose.test.yml"
|
||||
COMPOSE_CMD="docker compose -f $COMPOSE_FILE"
|
||||
|
||||
if [ "$BUILD" = true ]; then
|
||||
echo "==> Building test image..."
|
||||
$COMPOSE_CMD build test
|
||||
fi
|
||||
|
||||
# Default to bundle exec rspec if no command given
|
||||
CMD="bundle exec rspec"
|
||||
if [ ${#SPEC_ARGS[@]} -gt 0 ]; then
|
||||
CMD="$CMD ${SPEC_ARGS[@]}"
|
||||
fi
|
||||
|
||||
echo "==> Running: $CMD"
|
||||
$COMPOSE_CMD run --rm test bash -c "bin/docker-test $CMD"
|
||||
@ -0,0 +1,42 @@
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:14
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: wabosign_test
|
||||
ports:
|
||||
- 5432:5432
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
tmpfs: /var/lib/postgresql/data
|
||||
|
||||
test:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.test
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
RAILS_ENV: test
|
||||
NODE_ENV: test
|
||||
DATABASE_URL: postgres://postgres:postgres@postgres:5432/wabosign_test
|
||||
CHROME_PATH: /usr/bin/google-chrome-stable
|
||||
HEADLESS: "true"
|
||||
volumes:
|
||||
- .:/app
|
||||
- bundle_cache:/usr/local/bundle
|
||||
- node_modules_cache:/app/node_modules
|
||||
- packs_cache:/app/public/packs-test
|
||||
working_dir: /app
|
||||
entrypoint: [""]
|
||||
command: ["bin/docker-test"]
|
||||
|
||||
volumes:
|
||||
bundle_cache:
|
||||
node_modules_cache:
|
||||
packs_cache:
|
||||
@ -0,0 +1,102 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe 'Additional API Endpoints' do
|
||||
let(:account) { create(:account, :with_testing_account) }
|
||||
let(:author) { create(:user, account:) }
|
||||
let(:token) { author.access_token.token }
|
||||
|
||||
describe 'GET /api/user' do
|
||||
it 'returns the current user' do
|
||||
get '/api/user', headers: { 'x-auth-token': token }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.parsed_body['id']).to eq(author.id)
|
||||
expect(response.parsed_body['email']).to eq(author.email)
|
||||
expect(response.parsed_body['first_name']).to eq(author.first_name)
|
||||
end
|
||||
|
||||
it 'returns unauthorized without a token' do
|
||||
get '/api/user'
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/templates/:id/clone' do
|
||||
it 'clones a template' do
|
||||
template = create(:template, account:, author:, folder: create(:template_folder, account:))
|
||||
|
||||
expect do
|
||||
post "/api/templates/#{template.id}/clone", headers: { 'x-auth-token': token },
|
||||
params: { name: 'Cloned Template' }.to_json
|
||||
end.to change(Template, :count)
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.parsed_body['name']).to eq('Cloned Template')
|
||||
expect(response.parsed_body['id']).not_to eq(template.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/submissions/:id/documents' do
|
||||
it 'returns the documents for a submission' do
|
||||
template = create(:template, account:, author:)
|
||||
submission = create(:submission, template:, created_by_user: author)
|
||||
submitter = create(:submitter, submission:, uuid: template.submitters.first['uuid'],
|
||||
account:, completed_at: Time.current)
|
||||
blob = ActiveStorage::Blob.create_and_upload!(
|
||||
io: Rails.root.join('spec/fixtures/sample-document.pdf').open,
|
||||
filename: 'sample-document.pdf',
|
||||
content_type: 'application/pdf'
|
||||
)
|
||||
ActiveStorage::Attachment.create!(blob:, name: :documents, record: submitter)
|
||||
|
||||
get "/api/submissions/#{submission.id}/documents", headers: { 'x-auth-token': token }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.parsed_body['id']).to eq(submission.id)
|
||||
expect(response.parsed_body['documents']).to be_an(Array)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/events/form/:type' do
|
||||
it 'returns form events for completed submitters' do
|
||||
template = create(:template, account:, author:, only_field_types: %w[text])
|
||||
submission = create(:submission, template:, created_by_user: author)
|
||||
create(:submitter, submission:, uuid: template.submitters.first['uuid'],
|
||||
account:, completed_at: Time.current)
|
||||
|
||||
get '/api/events/form/completed', headers: { 'x-auth-token': token }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.parsed_body['data']).to be_an(Array)
|
||||
expect(response.parsed_body['data'].first['event_type']).to eq('form.completed')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/events/submission/:type' do
|
||||
it 'returns submission events for completed submissions' do
|
||||
template = create(:template, account:, author:, only_field_types: %w[text])
|
||||
submission = create(:submission, template:, created_by_user: author)
|
||||
create(:submitter, submission:, uuid: template.submitters.first['uuid'],
|
||||
account:, completed_at: Time.current)
|
||||
|
||||
get '/api/events/submission/completed', headers: { 'x-auth-token': token }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.parsed_body['data']).to be_an(Array)
|
||||
expect(response.parsed_body['data'].first['event_type']).to eq('submission.completed')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/tools/merge' do
|
||||
it 'merges PDFs' do
|
||||
pdf_content = Base64.encode64(File.read(Rails.root.join('spec/fixtures/sample-document.pdf')))
|
||||
|
||||
post '/api/tools/merge', headers: { 'x-auth-token': token },
|
||||
params: { files: [pdf_content, pdf_content] }.to_json
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.parsed_body['data']).to be_present
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,36 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe 'Account Logo' do
|
||||
let(:account) { create(:account) }
|
||||
let(:user) { create(:user, account:) }
|
||||
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it 'shows the company logo section on the personalization page' do
|
||||
visit settings_personalization_path
|
||||
|
||||
expect(page).to have_content(I18n.t('company_logo'))
|
||||
end
|
||||
|
||||
it 'shows a placeholder message in single-tenant mode' do
|
||||
visit settings_personalization_path
|
||||
|
||||
expect(page).to have_content(I18n.t('unlock_with_docuseal_pro'))
|
||||
end
|
||||
|
||||
context 'when a logo is attached' do
|
||||
before do
|
||||
logo_path = Rails.root.join('spec/fixtures/sample-image.png')
|
||||
account.logo.attach(io: File.open(logo_path), filename: 'sample-image.png',
|
||||
content_type: 'image/png')
|
||||
end
|
||||
|
||||
it 'displays the logo image on the personalization page' do
|
||||
visit settings_personalization_path
|
||||
|
||||
expect(page).to have_css("img[src*='sample-image']")
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,183 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe 'Feature Toggles' do
|
||||
let(:account) { create(:account) }
|
||||
let(:author) { create(:user, account:) }
|
||||
|
||||
describe 'allow decline toggle' do
|
||||
let(:template) { create(:template, account:, author:, only_field_types: %w[text]) }
|
||||
let(:submission) { create(:submission, template:) }
|
||||
let(:submitter) do
|
||||
create(:submitter, submission:, uuid: template.submitters.first['uuid'], account:)
|
||||
end
|
||||
|
||||
it 'shows the decline button when enabled' do
|
||||
create(:account_config, account:, key: AccountConfig::ALLOW_TO_DECLINE_KEY, value: true)
|
||||
|
||||
visit submit_form_path(slug: submitter.slug)
|
||||
|
||||
expect(page).to have_selector('#decline_button')
|
||||
end
|
||||
|
||||
it 'hides the decline button when disabled' do
|
||||
create(:account_config, account:, key: AccountConfig::ALLOW_TO_DECLINE_KEY, value: false)
|
||||
|
||||
visit submit_form_path(slug: submitter.slug)
|
||||
|
||||
expect(page).not_to have_selector('#decline_button')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'allow delegate toggle' do
|
||||
let(:template) { create(:template, account:, author:, only_field_types: %w[text]) }
|
||||
let(:submission) { create(:submission, template:) }
|
||||
let(:submitter) do
|
||||
create(:submitter, submission:, uuid: template.submitters.first['uuid'], account:)
|
||||
end
|
||||
|
||||
it 'shows the delegate button when enabled' do
|
||||
create(:account_config, account:, key: AccountConfig::ALLOW_TO_DELEGATE_KEY, value: true)
|
||||
|
||||
visit submit_form_path(slug: submitter.slug)
|
||||
|
||||
expect(page).to have_selector('#delegate_button')
|
||||
end
|
||||
|
||||
it 'hides the delegate button when disabled' do
|
||||
create(:account_config, account:, key: AccountConfig::ALLOW_TO_DELEGATE_KEY, value: false)
|
||||
|
||||
visit submit_form_path(slug: submitter.slug)
|
||||
|
||||
expect(page).not_to have_selector('#delegate_button')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'require signing reason toggle' do
|
||||
let(:template) { create(:template, account:, author:, only_field_types: %w[signature]) }
|
||||
let(:submission) { create(:submission, template:) }
|
||||
let(:submitter) do
|
||||
create(:submitter, submission:, uuid: template.submitters.first['uuid'], account:)
|
||||
end
|
||||
|
||||
it 'shows the signing reason select when enabled' do
|
||||
create(:account_config, account:, key: AccountConfig::REQUIRE_SIGNING_REASON_KEY, value: true)
|
||||
|
||||
visit submit_form_path(slug: submitter.slug)
|
||||
|
||||
find('#expand_form_button').click
|
||||
|
||||
expect(page).to have_css('select.base-input')
|
||||
end
|
||||
|
||||
it 'hides the signing reason select when disabled' do
|
||||
create(:account_config, account:, key: AccountConfig::REQUIRE_SIGNING_REASON_KEY, value: false)
|
||||
|
||||
visit submit_form_path(slug: submitter.slug)
|
||||
|
||||
find('#expand_form_button').click
|
||||
|
||||
expect(page).not_to have_css('select.base-input')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'enforce signing order toggle' do
|
||||
let(:template) do
|
||||
create(:template, submitter_count: 2, account:, author:, only_field_types: %w[text])
|
||||
end
|
||||
let(:submission) { create(:submission, template:, template_fields: template.fields) }
|
||||
let(:first_submitter) do
|
||||
create(:submitter, submission:, uuid: template.submitters[0]['uuid'], account:,
|
||||
email: 'first@example.com')
|
||||
end
|
||||
let(:second_submitter) do
|
||||
create(:submitter, submission:, uuid: template.submitters[1]['uuid'], account:,
|
||||
email: 'second@example.com')
|
||||
end
|
||||
|
||||
pending 'blocks the second signer when enabled' do
|
||||
create(:account_config, account:, key: AccountConfig::ENFORCE_SIGNING_ORDER_KEY, value: true)
|
||||
|
||||
visit submit_form_path(slug: second_submitter.slug)
|
||||
|
||||
expect(page).to have_content('Awaiting completion by the other party')
|
||||
end
|
||||
|
||||
it 'allows the second signer to proceed when disabled' do
|
||||
create(:account_config, account:, key: AccountConfig::ENFORCE_SIGNING_ORDER_KEY, value: false)
|
||||
|
||||
visit submit_form_path(slug: second_submitter.slug)
|
||||
|
||||
expect(page).to have_field('First Name')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'allow typed signature toggle' do
|
||||
let(:template) { create(:template, account:, author:, only_field_types: %w[signature]) }
|
||||
let(:submission) { create(:submission, template:) }
|
||||
let(:submitter) do
|
||||
create(:submitter, submission:, uuid: template.submitters.first['uuid'], account:)
|
||||
end
|
||||
|
||||
it 'shows the Type button when enabled' do
|
||||
create(:account_config, account:, key: AccountConfig::ALLOW_TYPED_SIGNATURE, value: true)
|
||||
|
||||
visit submit_form_path(slug: submitter.slug)
|
||||
|
||||
find('#expand_form_button').click
|
||||
|
||||
expect(page).to have_button('Type')
|
||||
end
|
||||
|
||||
it 'hides the Type button when disabled' do
|
||||
create(:account_config, account:, key: AccountConfig::ALLOW_TYPED_SIGNATURE, value: false)
|
||||
|
||||
visit submit_form_path(slug: submitter.slug)
|
||||
|
||||
find('#expand_form_button').click
|
||||
|
||||
expect(page).not_to have_button('Type')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'force MFA toggle', :multitenant do
|
||||
let(:other_account) { create(:account) }
|
||||
let(:other_user) { create(:user, account: other_account) }
|
||||
|
||||
pending 'does not affect users who already have MFA configured' do
|
||||
create(:account_config, account: other_account, key: AccountConfig::FORCE_MFA, value: true)
|
||||
|
||||
sign_in(other_user)
|
||||
|
||||
visit root_path
|
||||
|
||||
expect(page).to have_current_path(root_path)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'allow resubmit toggle' do
|
||||
let(:template) do
|
||||
create(:template, shared_link: true, account:, author:, only_field_types: %w[text])
|
||||
end
|
||||
|
||||
it 'allows resubmission when enabled' do
|
||||
create(:account_config, account:, key: AccountConfig::ALLOW_TO_RESUBMIT, value: true)
|
||||
|
||||
visit start_form_path(slug: template.slug)
|
||||
|
||||
fill_in 'Email', with: 'test@example.com'
|
||||
click_button 'Start'
|
||||
|
||||
fill_in 'First Name', with: 'First Try'
|
||||
find('#submit_form_button').click
|
||||
|
||||
expect(page).to have_content('Form has been completed!')
|
||||
|
||||
visit start_form_path(slug: template.slug)
|
||||
|
||||
fill_in 'Email', with: 'test@example.com'
|
||||
click_button 'Start'
|
||||
|
||||
expect(page).not_to have_content('already completed')
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,88 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe 'Fork Branding' do
|
||||
let(:account) { create(:account) }
|
||||
let(:user) { create(:user, account:) }
|
||||
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it 'displays the default product name in the shared title' do
|
||||
visit settings_personalization_path
|
||||
|
||||
expect(page).to have_content(Wabosign::PRODUCT_NAME)
|
||||
end
|
||||
|
||||
it 'displays the brand name in the shared title after setting one' do
|
||||
create(:account_config, account:, key: AccountConfig::BRAND_NAME_KEY, value: 'Acme Sign')
|
||||
|
||||
visit settings_personalization_path
|
||||
|
||||
expect(page).to have_content('Acme Sign')
|
||||
expect(page).to have_link('Acme Sign', href: root_path)
|
||||
end
|
||||
|
||||
it 'shows the brand name form on the personalization settings page' do
|
||||
visit settings_personalization_path
|
||||
|
||||
expect(page).to have_field('brand_name', placeholder: 'e.g. Acme Sign')
|
||||
expect(page).to have_button('Save')
|
||||
end
|
||||
|
||||
it 'saves a brand name via the form' do
|
||||
visit settings_personalization_path
|
||||
|
||||
fill_in 'brand_name', with: 'My Brand'
|
||||
click_button 'Save'
|
||||
|
||||
expect(page).to have_content(I18n.t('settings_have_been_saved'))
|
||||
expect(account.reload.brand_name).to eq('My Brand')
|
||||
end
|
||||
|
||||
it 'clears the brand name via the form' do
|
||||
create(:account_config, account:, key: AccountConfig::BRAND_NAME_KEY, value: 'Acme Sign')
|
||||
|
||||
visit settings_personalization_path
|
||||
|
||||
fill_in 'brand_name', with: ''
|
||||
click_button 'Save'
|
||||
|
||||
expect(page).to have_content(I18n.t('settings_have_been_saved'))
|
||||
expect(account.reload.brand_name).to be_nil
|
||||
end
|
||||
|
||||
it 'shows the upstream attribution link on the personalization settings page' do
|
||||
visit settings_personalization_path
|
||||
|
||||
expect(page).to have_link(Wabosign::UPSTREAM_NAME, href: Wabosign::UPSTREAM_URL)
|
||||
end
|
||||
|
||||
it 'renders the product name on the start form for a shared-link template' do
|
||||
template = create(:template, shared_link: true, account:, author: user,
|
||||
except_field_types: %w[phone payment stamp])
|
||||
|
||||
visit start_form_path(slug: template.slug)
|
||||
|
||||
expect(page).to have_content(Wabosign::PRODUCT_NAME)
|
||||
end
|
||||
|
||||
it 'renders the product name on the submit form for a direct submission' do
|
||||
template = create(:template, account:, author: user, only_field_types: %w[text])
|
||||
submission = create(:submission, template:)
|
||||
submitter = create(:submitter, submission:, uuid: template.submitters.first['uuid'], account:)
|
||||
|
||||
visit submit_form_path(slug: submitter.slug)
|
||||
|
||||
expect(page).to have_content(Wabosign::PRODUCT_NAME)
|
||||
end
|
||||
|
||||
it 'renders the upstream powered-by attribution on the start form' do
|
||||
template = create(:template, shared_link: true, account:, author: user,
|
||||
except_field_types: %w[phone payment stamp])
|
||||
|
||||
visit start_form_path(slug: template.slug)
|
||||
|
||||
expect(page).to have_content(I18n.t('powered_by'))
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,150 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe 'Role-based access' do
|
||||
let(:account) { create(:account) }
|
||||
|
||||
describe 'admin role' do
|
||||
let(:admin) { create(:user, account:, role: User::ADMIN_ROLE) }
|
||||
|
||||
before do
|
||||
sign_in(admin)
|
||||
end
|
||||
|
||||
it 'shows Create and Upload buttons on the dashboard' do
|
||||
visit templates_path
|
||||
|
||||
expect(page).to have_link('Create')
|
||||
expect(page).to have_content(I18n.t('upload'))
|
||||
end
|
||||
|
||||
it 'shows all settings nav items' do
|
||||
visit settings_personalization_path
|
||||
|
||||
within('#account_settings_menu') do
|
||||
expect(page).to have_link(I18n.t('personalization'))
|
||||
expect(page).to have_link(I18n.t('users'))
|
||||
expect(page).to have_link(I18n.t('notifications'))
|
||||
expect(page).to have_link(I18n.t('e_signature'))
|
||||
end
|
||||
end
|
||||
|
||||
it 'shows Edit, Clone, and Archive on a template' do
|
||||
template = create(:template, account:, author: admin)
|
||||
|
||||
visit template_path(template)
|
||||
|
||||
expect(page).to have_content(template.name)
|
||||
expect(page).to have_link('Edit')
|
||||
expect(page).to have_link('Clone')
|
||||
expect(page).to have_button('Archive')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'editor role' do
|
||||
let(:editor) { create(:user, account:, role: User::EDITOR_ROLE) }
|
||||
|
||||
before do
|
||||
sign_in(editor)
|
||||
end
|
||||
|
||||
it 'shows Create and Upload buttons on the dashboard' do
|
||||
visit templates_path
|
||||
|
||||
expect(page).to have_link('Create')
|
||||
expect(page).to have_content(I18n.t('upload'))
|
||||
end
|
||||
|
||||
it 'shows the Users nav item' do
|
||||
visit settings_personalization_path
|
||||
|
||||
within('#account_settings_menu') do
|
||||
expect(page).to have_link(I18n.t('users'))
|
||||
end
|
||||
end
|
||||
|
||||
it 'shows limited settings nav items (no email/esign)' do
|
||||
visit settings_personalization_path
|
||||
|
||||
within('#account_settings_menu') do
|
||||
expect(page).to have_link(I18n.t('personalization'))
|
||||
expect(page).to have_link(I18n.t('notifications'))
|
||||
expect(page).to have_link(I18n.t('users'))
|
||||
|
||||
expect(page).not_to have_link(I18n.t('e_signature'))
|
||||
end
|
||||
end
|
||||
|
||||
it 'shows Edit, Clone, and Archive on a template' do
|
||||
template = create(:template, account:, author: editor)
|
||||
|
||||
visit template_path(template)
|
||||
|
||||
expect(page).to have_content(template.name)
|
||||
expect(page).to have_link('Edit')
|
||||
expect(page).to have_link('Clone')
|
||||
expect(page).to have_button('Archive')
|
||||
end
|
||||
|
||||
it 'can view personalization but cannot modify brand_name' do
|
||||
visit settings_personalization_path
|
||||
|
||||
expect(page).to have_field('brand_name')
|
||||
|
||||
fill_in 'brand_name', with: 'Editor Brand'
|
||||
click_button 'Save'
|
||||
|
||||
expect(account.reload.brand_name).not_to eq('Editor Brand')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'viewer role' do
|
||||
let(:viewer) { create(:user, account:, role: User::VIEWER_ROLE) }
|
||||
|
||||
before do
|
||||
sign_in(viewer)
|
||||
end
|
||||
|
||||
it 'does NOT show Create or Upload buttons on the dashboard' do
|
||||
visit templates_path
|
||||
|
||||
expect(page).not_to have_link('Create')
|
||||
end
|
||||
|
||||
it 'does not have access to users settings' do
|
||||
visit settings_users_path
|
||||
|
||||
expect(current_path).to eq(root_path)
|
||||
end
|
||||
|
||||
it 'shows personalization and notifications in nav' do
|
||||
visit settings_personalization_path
|
||||
|
||||
within('#account_settings_menu') do
|
||||
expect(page).to have_link(I18n.t('personalization'))
|
||||
expect(page).to have_link(I18n.t('notifications'))
|
||||
end
|
||||
end
|
||||
|
||||
it 'can view a template but does not see Edit, Clone, or Archive' do
|
||||
template = create(:template, account:, author: create(:user, account:))
|
||||
|
||||
visit template_path(template)
|
||||
|
||||
expect(page).to have_content(template.name)
|
||||
expect(page).not_to have_link('Edit')
|
||||
expect(page).not_to have_link('Clone')
|
||||
expect(page).not_to have_button('Archive')
|
||||
end
|
||||
|
||||
it 'can view personalization page but cannot modify brand_name' do
|
||||
visit settings_personalization_path
|
||||
|
||||
expect(page).to have_field('brand_name')
|
||||
|
||||
fill_in 'brand_name', with: 'Viewer Brand'
|
||||
click_button 'Save'
|
||||
|
||||
expect(account.reload.brand_name).not_to eq('Viewer Brand')
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,157 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe 'Signing Flow Edge Cases' do
|
||||
let(:account) { create(:account) }
|
||||
let(:author) { create(:user, account:) }
|
||||
|
||||
describe 'enforced signing order' do
|
||||
let(:template) do
|
||||
create(:template, submitter_count: 2, account:, author:, only_field_types: %w[text])
|
||||
end
|
||||
let(:submission) { create(:submission, template:, template_fields: template.fields, template_submitters: template.submitters) }
|
||||
let(:first_submitter) do
|
||||
create(:submitter, submission:, uuid: template.submitters[0]['uuid'], account:,
|
||||
email: 'first@example.com')
|
||||
end
|
||||
let(:second_submitter) do
|
||||
create(:submitter, submission:, uuid: template.submitters[1]['uuid'], account:,
|
||||
email: 'second@example.com')
|
||||
end
|
||||
|
||||
pending 'prevents the second signer from filling before the first is done' do
|
||||
create(:account_config, account:, key: AccountConfig::ENFORCE_SIGNING_ORDER_KEY, value: true)
|
||||
|
||||
visit submit_form_path(slug: second_submitter.slug)
|
||||
|
||||
expect(page).to have_content('Awaiting completion by the other party')
|
||||
expect(page).not_to have_selector('#submit_form_button')
|
||||
end
|
||||
|
||||
it 'allows the second signer to fill after the first completes' do
|
||||
visit submit_form_path(slug: first_submitter.slug)
|
||||
fill_in 'First Name', with: 'Alice'
|
||||
find('#submit_form_button').click
|
||||
expect(page).to have_content('Form has been completed!')
|
||||
|
||||
visit submit_form_path(slug: second_submitter.slug)
|
||||
fill_in 'First Name', with: 'Bob'
|
||||
find('#submit_form_button').click
|
||||
expect(page).to have_content('Form has been completed!')
|
||||
|
||||
second_submitter.reload
|
||||
expect(second_submitter.completed_at).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
describe 'signature pad clear and redraw' do
|
||||
let(:template) { create(:template, account:, author:, only_field_types: %w[signature]) }
|
||||
let(:submission) { create(:submission, template:) }
|
||||
let(:submitter) do
|
||||
create(:submitter, submission:, uuid: template.submitters.first['uuid'], account:)
|
||||
end
|
||||
|
||||
it 'completes after clearing and redrawing the signature' do
|
||||
visit submit_form_path(slug: submitter.slug)
|
||||
|
||||
find('#expand_form_button').click
|
||||
draw_canvas
|
||||
click_button 'Clear'
|
||||
draw_canvas
|
||||
click_button 'Sign and Complete'
|
||||
|
||||
expect(page).to have_content('Document has been signed!')
|
||||
|
||||
submitter.reload
|
||||
expect(submitter.completed_at).to be_present
|
||||
expect(field_value(submitter, 'Signature')).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
describe 'resubmit flow' do
|
||||
let(:template) do
|
||||
create(:template, shared_link: true, account:, author:, only_field_types: %w[text])
|
||||
end
|
||||
|
||||
before do
|
||||
create(:account_config, account:, key: AccountConfig::ALLOW_TO_RESUBMIT, value: true)
|
||||
end
|
||||
|
||||
pending 'allows a submitter to resubmit after completing' do
|
||||
visit start_form_path(slug: template.slug)
|
||||
|
||||
fill_in 'Email', with: 'john@example.com'
|
||||
click_button 'Start'
|
||||
|
||||
fill_in 'First Name', with: 'John'
|
||||
find('#submit_form_button').click
|
||||
|
||||
expect(page).to have_content('Form has been completed!')
|
||||
|
||||
visit start_form_path(slug: template.slug)
|
||||
|
||||
fill_in 'Email', with: 'john@example.com'
|
||||
click_button 'Start'
|
||||
|
||||
expect(page).not_to have_content('already completed')
|
||||
|
||||
fill_in 'First Name', with: 'John Updated'
|
||||
find('#submit_form_button').click
|
||||
|
||||
expect(page).to have_content('Form has been completed!')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'all optional fields' do
|
||||
let(:template) { create(:template, account:, author:, only_field_types: %w[text date]) }
|
||||
let(:submission) { create(:submission, template:) }
|
||||
let(:submitter) do
|
||||
create(:submitter, submission:, uuid: template.submitters.first['uuid'], account:)
|
||||
end
|
||||
|
||||
before do
|
||||
fields = template.fields.map { |f| f.dup.tap { |h| h['required'] = false } }
|
||||
template.update!(fields:)
|
||||
submission.update!(template_fields: fields)
|
||||
end
|
||||
|
||||
pending 'completes with the header complete button without filling any fields' do
|
||||
visit submit_form_path(slug: submitter.slug)
|
||||
|
||||
find('#expand_form_button').click
|
||||
|
||||
expect(page).to have_css('#complete_button_container')
|
||||
|
||||
page.execute_script('document.getElementById("complete_form_button")?.click() || document.querySelector(".complete-button")?.click()')
|
||||
|
||||
expect(page).to have_content('Form has been completed!')
|
||||
|
||||
submitter.reload
|
||||
expect(submitter.completed_at).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
describe 'decline with custom reason' do
|
||||
let(:template) { create(:template, account:, author:, only_field_types: %w[text]) }
|
||||
let(:submission) { create(:submission, template:) }
|
||||
let(:submitter) do
|
||||
create(:submitter, submission:, uuid: template.submitters.first['uuid'], account:)
|
||||
end
|
||||
|
||||
it 'records the decline reason on the submission event' do
|
||||
visit submit_form_path(slug: submitter.slug)
|
||||
|
||||
find('#decline_button').click
|
||||
fill_in 'reason', with: 'I do not agree with the terms and conditions'
|
||||
within('dialog[open]') { click_button 'Decline' }
|
||||
|
||||
expect(page).to have_content('Form has been declined')
|
||||
|
||||
submitter.reload
|
||||
expect(submitter.declined_at).to be_present
|
||||
|
||||
event = submission.submission_events.find_by(submitter:, event_type: 'decline_form')
|
||||
expect(event).to be_present
|
||||
expect(event.data).to include('reason' => 'I do not agree with the terms and conditions')
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,17 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe 'SMS Settings' do
|
||||
let(:account) { create(:account) }
|
||||
let(:user) { create(:user, account:) }
|
||||
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it 'shows the SMS settings page with a placeholder in non-multitenant mode' do
|
||||
visit settings_sms_path
|
||||
|
||||
expect(page).to have_content('SMS')
|
||||
expect(page).to have_content(I18n.t('unlock_with_docuseal_pro'))
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,16 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe 'SSO Settings' do
|
||||
let(:account) { create(:account) }
|
||||
let(:user) { create(:user, account:) }
|
||||
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it 'shows a placeholder in single-tenant mode' do
|
||||
visit settings_sso_index_path
|
||||
|
||||
expect(page).to have_content(I18n.t('unlock_with_docuseal_pro'))
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,61 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe 'Submission Lifecycle' do
|
||||
let(:account) { create(:account) }
|
||||
let(:user) { create(:user, account:) }
|
||||
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it 'shows pending submissions in the template page and completed after signing', sidekiq: :inline do
|
||||
create(:encrypted_config, key: EncryptedConfig::ESIGN_CERTS_KEY,
|
||||
value: GenerateCertificate.call.transform_values(&:to_pem))
|
||||
|
||||
template = create(:template, account:, author: user, only_field_types: %w[text])
|
||||
submission = create(:submission, template:)
|
||||
submitter = create(:submitter, submission:, uuid: template.submitters.first['uuid'], account:,
|
||||
email: 'signer@example.com', name: nil)
|
||||
|
||||
visit template_path(template)
|
||||
|
||||
expect(page).to have_content('signer@example.com')
|
||||
expect(page).to have_content('AWAITING')
|
||||
|
||||
visit submit_form_path(slug: submitter.slug)
|
||||
|
||||
fill_in 'First Name', with: 'Alice'
|
||||
find('#submit_form_button').click
|
||||
|
||||
expect(page).to have_content('Form has been completed!')
|
||||
|
||||
visit template_path(template)
|
||||
|
||||
expect(page).to have_content('signer@example.com')
|
||||
expect(page).to have_content('COMPLETED')
|
||||
|
||||
submitter.reload
|
||||
expect(submitter.completed_at).to be_present
|
||||
expect(submitter.ip).to eq('127.0.0.1')
|
||||
end
|
||||
|
||||
it 'creates a submission via the send-to-recipients modal and shows it as pending' do
|
||||
template = create(:template, account:, author: user, only_field_types: %w[text])
|
||||
|
||||
visit template_path(template)
|
||||
|
||||
click_link 'Send to Recipients', visible: :all
|
||||
|
||||
within('#modal') do
|
||||
find('textarea[name="emails"]').set('recipient@example.com')
|
||||
click_button 'Add Recipients'
|
||||
end
|
||||
|
||||
expect(page).to have_content('recipient@example.com')
|
||||
expect(page).to have_content('AWAITING')
|
||||
|
||||
submission = template.submissions.last
|
||||
expect(submission).to be_present
|
||||
expect(submission.submitters.first.email).to eq('recipient@example.com')
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,77 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe 'Template CRUD Edge Cases' do
|
||||
let(:account) { create(:account) }
|
||||
let(:user) { create(:user, account:) }
|
||||
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
describe 'restoring an archived template' do
|
||||
let!(:template) { create(:template, account:, author: user, archived_at: Time.current,
|
||||
except_field_types: %w[phone payment]) }
|
||||
|
||||
it 'restores an archived template from the template page' do
|
||||
visit template_path(template)
|
||||
|
||||
expect(page).to have_content('Archived')
|
||||
page.find('form[action*="restore"]').click
|
||||
|
||||
expect(template.reload.archived_at).to be_nil
|
||||
end
|
||||
|
||||
pending 'lists archived templates on the dedicated index page' do
|
||||
visit templates_archived_index_path
|
||||
|
||||
expect(page).to have_content(template.name)
|
||||
find('button[aria-label]').click
|
||||
|
||||
expect(page).not_to have_content(template.name)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'template share link' do
|
||||
let!(:template) { create(:template, account:, author: user, except_field_types: %w[phone payment]) }
|
||||
|
||||
it 'opens the share link modal' do
|
||||
visit template_path(template)
|
||||
|
||||
click_link 'Link'
|
||||
|
||||
expect(page).to have_field('embedding_url', with: /#{template.slug}/)
|
||||
end
|
||||
|
||||
it 'enables and disables the share link' do
|
||||
visit template_share_link_path(template)
|
||||
|
||||
check 'template_shared_link'
|
||||
page.execute_script('document.getElementById("shared_link_form").submit()')
|
||||
|
||||
expect(template.reload.shared_link).to be true
|
||||
|
||||
visit template_share_link_path(template)
|
||||
|
||||
uncheck 'template_shared_link'
|
||||
page.execute_script('document.getElementById("shared_link_form").submit()')
|
||||
|
||||
expect(template.reload.shared_link).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe 'template folder management' do
|
||||
it 'navigates through folders on the dashboard' do
|
||||
folder = create(:template_folder, account:, author: user, name: 'My Folder')
|
||||
template_in_folder = create(:template, account:, author: user, folder:,
|
||||
except_field_types: %w[phone payment])
|
||||
template_root = create(:template, account:, author: user, name: 'Root Template',
|
||||
except_field_types: %w[phone payment])
|
||||
|
||||
visit folder_path(folder)
|
||||
|
||||
expect(page).to have_content(folder.name)
|
||||
expect(page).to have_content(template_in_folder.name)
|
||||
expect(page).not_to have_content(template_root.name)
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Reference in new issue