mirror of https://github.com/docusealco/docuseal
commit
2cfa5533a2
@ -0,0 +1,31 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
class TokenRefreshController < ApiBaseController
|
||||
skip_before_action :authenticate_via_token!
|
||||
skip_authorization_check
|
||||
|
||||
def create
|
||||
service = TokenRefreshService.new(token_refresh_params)
|
||||
new_token = service.refresh_token
|
||||
|
||||
if new_token
|
||||
render json: { access_token: new_token }, status: :ok
|
||||
else
|
||||
render json: { error: 'Unable to refresh token. User may not exist.' }, status: :unprocessable_entity
|
||||
end
|
||||
rescue ArgumentError => e
|
||||
render json: { error: e.message }, status: :bad_request
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "Token refresh error: #{e.message}"
|
||||
render json: { error: 'Internal server error during token refresh' }, status: :internal_server_error
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def token_refresh_params
|
||||
params.permit(:account, :partnership, :external_account_id, user: %i[external_id email first_name last_name])
|
||||
.to_h.deep_symbolize_keys
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,21 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module PartnershipContext
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
private
|
||||
|
||||
def current_ability
|
||||
@current_ability ||= Ability.new(current_user, partnership_request_context)
|
||||
end
|
||||
|
||||
def partnership_request_context
|
||||
return nil if params[:accessible_partnership_ids].blank?
|
||||
|
||||
{
|
||||
accessible_partnership_ids: Array.wrap(params[:accessible_partnership_ids]).map(&:to_i),
|
||||
external_account_id: params[:external_account_id],
|
||||
external_partnership_id: params[:external_partnership_id]
|
||||
}
|
||||
end
|
||||
end
|
||||
@ -1,36 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: account_groups
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# name :string not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# external_account_group_id :string not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_account_groups_on_external_account_group_id (external_account_group_id) UNIQUE
|
||||
#
|
||||
class AccountGroup < ApplicationRecord
|
||||
has_many :accounts, dependent: :nullify
|
||||
has_many :users, dependent: :nullify
|
||||
has_many :templates, dependent: :destroy
|
||||
has_many :template_folders, dependent: :destroy
|
||||
|
||||
validates :external_account_group_id, presence: true, uniqueness: true
|
||||
validates :name, presence: true
|
||||
|
||||
def self.find_or_create_by_external_id(external_id, attributes = {})
|
||||
find_by(external_account_group_id: external_id) ||
|
||||
create!(attributes.merge(external_account_group_id: external_id))
|
||||
end
|
||||
|
||||
def default_template_folder
|
||||
template_folders.find_by(name: TemplateFolder::DEFAULT_NAME) ||
|
||||
template_folders.create!(name: TemplateFolder::DEFAULT_NAME,
|
||||
author_id: users.minimum(:id))
|
||||
end
|
||||
end
|
||||
@ -1,19 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module AccountGroupValidation
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
validate :must_belong_to_account_or_account_group
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def must_belong_to_account_or_account_group
|
||||
if account.blank? && account_group.blank?
|
||||
errors.add(:base, 'Must belong to either an account or account group')
|
||||
elsif account.present? && account_group.present?
|
||||
errors.add(:base, 'Cannot belong to both account and account group')
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,19 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module PartnershipValidation
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
validate :must_belong_to_account_or_partnership
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def must_belong_to_account_or_partnership
|
||||
if account.blank? && partnership.blank?
|
||||
errors.add(:base, 'Must belong to either an account or partnership')
|
||||
elsif account.present? && partnership.present?
|
||||
errors.add(:base, 'Cannot belong to both account and partnership')
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,36 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: partnerships
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# name :string not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# external_partnership_id :integer not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_partnerships_on_external_partnership_id (external_partnership_id) UNIQUE
|
||||
#
|
||||
class Partnership < ApplicationRecord
|
||||
has_many :templates, dependent: :destroy
|
||||
has_many :template_folders, dependent: :destroy
|
||||
|
||||
validates :external_partnership_id, presence: true, uniqueness: true
|
||||
validates :name, presence: true
|
||||
|
||||
def self.find_or_create_by_external_id(external_id, name, attributes = {})
|
||||
find_by(external_partnership_id: external_id) ||
|
||||
create!(attributes.merge(external_partnership_id: external_id, name: name))
|
||||
end
|
||||
|
||||
def default_template_folder(author)
|
||||
raise ArgumentError, 'Author is required for partnership template folders' unless author
|
||||
|
||||
template_folders.find_by(name: TemplateFolder::DEFAULT_NAME) ||
|
||||
template_folders.create!(name: TemplateFolder::DEFAULT_NAME,
|
||||
author: author)
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,32 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class TokenRefreshService
|
||||
def initialize(params)
|
||||
@params = params
|
||||
end
|
||||
|
||||
def refresh_token
|
||||
user = find_user
|
||||
return nil unless user
|
||||
|
||||
user.access_token&.destroy
|
||||
user.association(:access_token).reset
|
||||
user.reload
|
||||
|
||||
user.create_access_token!
|
||||
user.access_token.token
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_user
|
||||
external_user_id = @params.dig(:user, :external_id)&.to_i
|
||||
return nil unless external_user_id
|
||||
|
||||
user = User.find_by(external_user_id: external_user_id)
|
||||
|
||||
Rails.logger.warn "Token refresh requested for non-existent user: external_id #{external_user_id}" unless user
|
||||
|
||||
user
|
||||
end
|
||||
end
|
||||
@ -1,3 +0,0 @@
|
||||
<a href="<%= root_path %>" class="mx-auto text-2xl md:text-3xl font-bold items-center flex space-x-3">
|
||||
<span><%= Docuseal.product_name %></span>
|
||||
</a>
|
||||
@ -0,0 +1,20 @@
|
||||
class RenameAccountGroupsToPartnerships < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
# Rename the table
|
||||
rename_table :account_groups, :partnerships
|
||||
|
||||
# Rename the foreign key columns in other tables
|
||||
rename_column :templates, :account_group_id, :partnership_id
|
||||
rename_column :template_folders, :account_group_id, :partnership_id
|
||||
|
||||
# Add global_partnership_id to export_locations
|
||||
add_column :export_locations, :global_partnership_id, :integer
|
||||
|
||||
# Remove partnership relationships since both users and accounts use API context now
|
||||
remove_column :users, :account_group_id, :bigint
|
||||
remove_column :accounts, :account_group_id, :bigint
|
||||
|
||||
# Rename the external ID column to match new naming
|
||||
rename_column :partnerships, :external_account_group_id, :external_partnership_id
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,88 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Abilities
|
||||
# Provides authorization conditions for submission access control.
|
||||
# Only account users can access submissions (partnership users create templates).
|
||||
# Supports partnership inheritance and global template access patterns.
|
||||
module SubmissionConditions
|
||||
module_function
|
||||
|
||||
def collection(user, request_context: nil)
|
||||
return [] if user.account_id.blank?
|
||||
|
||||
submissions_for_user(user, request_context)
|
||||
end
|
||||
|
||||
def entity(submission, user:, request_context: nil)
|
||||
# Only account users can access submissions
|
||||
return false if user.account_id.blank?
|
||||
|
||||
# User can access their own account's submissions
|
||||
return true if submission.account_id == user.account_id
|
||||
|
||||
if submission.template_id.present?
|
||||
template = submission.template || Template.find_by(id: submission.template_id)
|
||||
return false unless template
|
||||
|
||||
return true if user_can_access_template?(user, template, request_context)
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
def submissions_for_user(user, request_context = nil)
|
||||
accessible_template_ids = accessible_template_ids(request_context)
|
||||
|
||||
Submission.where(
|
||||
'submissions.account_id = ? OR submissions.template_id IN (?)',
|
||||
user.account_id,
|
||||
accessible_template_ids
|
||||
)
|
||||
end
|
||||
|
||||
def accessible_template_ids(request_context = nil)
|
||||
template_ids = []
|
||||
|
||||
# Add templates from partnership context (if provided via API)
|
||||
if request_context&.dig(:accessible_partnership_ids).present?
|
||||
accessible_partnership_ids = request_context[:accessible_partnership_ids]
|
||||
partnership_ids = Partnership.where(external_partnership_id: accessible_partnership_ids).pluck(:id)
|
||||
template_ids += Template.where(partnership_id: partnership_ids).pluck(:id)
|
||||
end
|
||||
|
||||
# Add templates from global partnership (accessible to everyone)
|
||||
if ExportLocation.global_partnership_id.present?
|
||||
template_ids += Template.where(partnership_id: ExportLocation.global_partnership_id).pluck(:id)
|
||||
end
|
||||
|
||||
template_ids.uniq
|
||||
end
|
||||
|
||||
def user_can_access_template?(user, template, request_context = nil)
|
||||
# User can access templates from their account
|
||||
return true if template.account_id == user.account_id
|
||||
|
||||
# Check partnership context access
|
||||
return true if partnership_context_accessible?(template, request_context)
|
||||
|
||||
# Check global partnership access
|
||||
return true if global_template_accessible?(template)
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def partnership_context_accessible?(template, request_context)
|
||||
return false if request_context&.dig(:accessible_partnership_ids).blank?
|
||||
return false if template.partnership_id.blank?
|
||||
|
||||
accessible_partnership_ids = request_context[:accessible_partnership_ids]
|
||||
accessible_partnerships = Partnership.where(external_partnership_id: accessible_partnership_ids)
|
||||
|
||||
accessible_partnerships.exists?(id: template.partnership_id)
|
||||
end
|
||||
|
||||
def global_template_accessible?(template)
|
||||
ExportLocation.global_partnership_id.present? &&
|
||||
template.partnership_id == ExportLocation.global_partnership_id
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,63 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Templates
|
||||
module CloneToAccount
|
||||
module_function
|
||||
|
||||
# Clone a partnership template to a specific account
|
||||
# Supports both direct target_account and external_account_id with authorization
|
||||
def call(original_template, author:, target_account: nil, external_account_id: nil, current_user: nil,
|
||||
external_id: nil, name: nil, folder_name: nil)
|
||||
validation_result = validate_inputs(original_template, target_account, external_account_id, current_user)
|
||||
raise validation_result[:error_class], validation_result[:message] if validation_result[:error]
|
||||
|
||||
resolved_target_account = validation_result[:target_account]
|
||||
|
||||
template = Templates::Clone.call(
|
||||
original_template,
|
||||
author: author,
|
||||
external_id: external_id,
|
||||
name: name,
|
||||
folder_name: folder_name,
|
||||
target_account: resolved_target_account
|
||||
)
|
||||
|
||||
# Clear template_accesses since partnership templates shouldn't copy user accesses
|
||||
template.template_accesses.clear
|
||||
|
||||
template
|
||||
end
|
||||
|
||||
def validate_inputs(original_template, target_account, external_account_id, current_user)
|
||||
# Check template type
|
||||
if original_template.partnership_id.blank?
|
||||
return { error: true, error_class: ArgumentError, message: 'Template must be a partnership template' }
|
||||
end
|
||||
|
||||
# Resolve target account
|
||||
if target_account.present?
|
||||
{ error: false, target_account: target_account }
|
||||
elsif external_account_id.present?
|
||||
unless current_user
|
||||
return { error: true, error_class: ArgumentError,
|
||||
message: 'current_user required when using external_account_id' }
|
||||
end
|
||||
|
||||
account = Account.find_by(external_account_id: external_account_id)
|
||||
return { error: true, error_class: ActiveRecord::RecordNotFound, message: 'Account not found' } unless account
|
||||
|
||||
unless current_user.account_id == account.id
|
||||
return { error: true, error_class: ArgumentError, message: 'Unauthorized access to target account' }
|
||||
end
|
||||
|
||||
{ error: false, target_account: account }
|
||||
else
|
||||
{
|
||||
error: true,
|
||||
error_class: ArgumentError,
|
||||
message: 'Either target_account or external_account_id must be provided'
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,69 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Templates
|
||||
module CloneToPartnership
|
||||
module_function
|
||||
|
||||
# Clone a global partnership template to a specific partnership
|
||||
# Supports both direct target_partnership and external_partnership_id with authorization
|
||||
def call(original_template, author:, target_partnership: nil, external_partnership_id: nil, current_user: nil,
|
||||
external_id: nil, name: nil, folder_name: nil)
|
||||
validation_result = validate_inputs(original_template, target_partnership, external_partnership_id, current_user)
|
||||
raise validation_result[:error_class], validation_result[:message] if validation_result[:error]
|
||||
|
||||
resolved_target_partnership = validation_result[:target_partnership]
|
||||
|
||||
template = Templates::Clone.call(
|
||||
original_template,
|
||||
author: author,
|
||||
external_id: external_id,
|
||||
name: name,
|
||||
folder_name: folder_name,
|
||||
target_partnership: resolved_target_partnership
|
||||
)
|
||||
|
||||
# Clear template_accesses since global partnership templates shouldn't copy user accesses
|
||||
template.template_accesses.clear
|
||||
|
||||
template
|
||||
end
|
||||
|
||||
def validate_inputs(original_template, target_partnership, external_partnership_id, current_user)
|
||||
# Check template type - must be global partnership template
|
||||
unless original_template.partnership_id.present? &&
|
||||
ExportLocation.global_partnership_id.present? &&
|
||||
original_template.partnership_id == ExportLocation.global_partnership_id
|
||||
return { error: true, error_class: ArgumentError, message: 'Template must be a global partnership template' }
|
||||
end
|
||||
|
||||
# Resolve target partnership
|
||||
if target_partnership.present?
|
||||
{ error: false, target_partnership: target_partnership }
|
||||
elsif external_partnership_id.present?
|
||||
unless current_user
|
||||
return { error: true, error_class: ArgumentError,
|
||||
message: 'current_user required when using external_partnership_id' }
|
||||
end
|
||||
|
||||
partnership = Partnership.find_by(external_partnership_id: external_partnership_id)
|
||||
unless partnership
|
||||
return {
|
||||
error: true,
|
||||
error_class: ActiveRecord::RecordNotFound,
|
||||
message: 'Partnership not found'
|
||||
}
|
||||
end
|
||||
|
||||
# For partnership cloning, we need to verify via API context since users don't have stored relationships
|
||||
# This is a simplified check - in practice, you'd verify via request context
|
||||
{ error: false, target_partnership: partnership }
|
||||
else
|
||||
{
|
||||
error: true,
|
||||
error_class: ArgumentError,
|
||||
message: 'Either target_partnership or external_partnership_id must be provided'
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,130 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe PartnershipContext do
|
||||
# Create a test class that includes the concern
|
||||
let(:test_class) do
|
||||
Class.new do
|
||||
include PartnershipContext
|
||||
|
||||
attr_accessor :params, :current_user
|
||||
|
||||
def initialize(params = {}, user = nil)
|
||||
@params = params
|
||||
@current_user = user
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
let(:test_instance) { test_class.new(params) }
|
||||
|
||||
describe '#partnership_request_context' do
|
||||
context 'when no partnership parameters are provided' do
|
||||
let(:params) { {} }
|
||||
|
||||
it 'returns nil' do
|
||||
expect(test_instance.send(:partnership_request_context)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when accessible_partnership_ids is blank' do
|
||||
let(:params) { { accessible_partnership_ids: [] } }
|
||||
|
||||
it 'returns nil' do
|
||||
expect(test_instance.send(:partnership_request_context)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when accessible_partnership_ids is nil' do
|
||||
let(:params) { { accessible_partnership_ids: nil } }
|
||||
|
||||
it 'returns nil' do
|
||||
expect(test_instance.send(:partnership_request_context)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when partnership parameters are provided' do
|
||||
let(:params) do
|
||||
{
|
||||
accessible_partnership_ids: %w[123 456],
|
||||
external_account_id: 'ext-account-123',
|
||||
external_partnership_id: 'ext-partnership-456'
|
||||
}
|
||||
end
|
||||
|
||||
it 'returns formatted partnership context' do
|
||||
expected_context = {
|
||||
accessible_partnership_ids: [123, 456],
|
||||
external_account_id: 'ext-account-123',
|
||||
external_partnership_id: 'ext-partnership-456'
|
||||
}
|
||||
|
||||
expect(test_instance.send(:partnership_request_context)).to eq(expected_context)
|
||||
end
|
||||
|
||||
it 'converts accessible_partnership_ids to integers' do
|
||||
result = test_instance.send(:partnership_request_context)
|
||||
expect(result[:accessible_partnership_ids]).to eq([123, 456])
|
||||
expect(result[:accessible_partnership_ids]).to all(be_an(Integer))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when only some parameters are provided' do
|
||||
let(:params) do
|
||||
{
|
||||
accessible_partnership_ids: ['123'],
|
||||
external_account_id: 'ext-account-123'
|
||||
}
|
||||
end
|
||||
|
||||
it 'includes only provided parameters' do
|
||||
expected_context = {
|
||||
accessible_partnership_ids: [123],
|
||||
external_account_id: 'ext-account-123',
|
||||
external_partnership_id: nil
|
||||
}
|
||||
|
||||
expect(test_instance.send(:partnership_request_context)).to eq(expected_context)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with string numbers' do
|
||||
let(:params) { { accessible_partnership_ids: %w[123 456] } }
|
||||
|
||||
it 'converts string numbers to integers' do
|
||||
result = test_instance.send(:partnership_request_context)
|
||||
expect(result[:accessible_partnership_ids]).to eq([123, 456])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#current_ability' do
|
||||
let(:user) { create(:user) }
|
||||
let(:partnership_context) do
|
||||
{
|
||||
accessible_partnership_ids: [123],
|
||||
external_account_id: 'ext-account-123',
|
||||
external_partnership_id: 'ext-partnership-456'
|
||||
}
|
||||
end
|
||||
let(:test_instance) { test_class.new({}, user) }
|
||||
|
||||
before do
|
||||
allow(test_instance).to receive(:partnership_request_context).and_return(partnership_context)
|
||||
end
|
||||
|
||||
it 'creates ability with partnership context' do
|
||||
allow(Ability).to receive(:new).and_call_original
|
||||
test_instance.send(:current_ability)
|
||||
expect(Ability).to have_received(:new).with(user, partnership_context)
|
||||
end
|
||||
|
||||
it 'memoizes the ability instance' do
|
||||
allow(Ability).to receive(:new).and_call_original
|
||||
|
||||
test_instance.send(:current_ability)
|
||||
test_instance.send(:current_ability) # Should use cached version
|
||||
|
||||
expect(Ability).to have_received(:new).once
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -1,8 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :account_group do
|
||||
external_account_group_id { Faker::Number.unique.number(digits: 8) }
|
||||
name { Faker::Company.name }
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :partnership do
|
||||
external_partnership_id { Faker::Number.unique.number(digits: 8) }
|
||||
name { Faker::Company.name }
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,173 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe Abilities::SubmissionConditions do
|
||||
describe '.collection' do
|
||||
context 'when user has no account_id' do
|
||||
let(:user) { build(:user, account_id: nil) }
|
||||
|
||||
it 'returns empty array' do
|
||||
result = described_class.collection(user)
|
||||
expect(result).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user has account_id' do
|
||||
let(:account) { create(:account) }
|
||||
let(:user) { create(:user, account: account) }
|
||||
|
||||
it 'returns submissions for the user account' do
|
||||
# Create submissions for this account
|
||||
template = create(:template, account: account, author: user)
|
||||
submission1 = create(:submission, template: template)
|
||||
submission2 = create(:submission, template: template)
|
||||
|
||||
# Create submission for different account (should not be included)
|
||||
other_account = create(:account)
|
||||
other_user = create(:user, account: other_account)
|
||||
other_template = create(:template, account: other_account, author: other_user)
|
||||
create(:submission, template: other_template)
|
||||
|
||||
result = described_class.collection(user)
|
||||
expect(result).to include(submission1, submission2)
|
||||
expect(result.count).to eq(2)
|
||||
end
|
||||
|
||||
context 'with global partnership templates' do
|
||||
let(:partnership) { create(:partnership) }
|
||||
|
||||
before do
|
||||
allow(ExportLocation).to receive(:global_partnership_id).and_return(partnership.id)
|
||||
end
|
||||
|
||||
it 'includes submissions from global partnership templates' do
|
||||
# Create account submission
|
||||
account_template = create(:template, account: account, author: user)
|
||||
account_submission = create(:submission, template: account_template)
|
||||
|
||||
# Create global partnership submission
|
||||
partnership_template = create(:template, :partnership_template, partnership: partnership)
|
||||
partnership_submission = create(:submission, template: partnership_template, account: account)
|
||||
|
||||
result = described_class.collection(user)
|
||||
expect(result).to include(account_submission, partnership_submission)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with partnership context' do
|
||||
let(:partnership) { create(:partnership, external_partnership_id: 123) }
|
||||
|
||||
it 'includes submissions from accessible partnership templates' do
|
||||
# Create account submission
|
||||
account_template = create(:template, account: account, author: user)
|
||||
account_submission = create(:submission, template: account_template)
|
||||
|
||||
# Create partnership submission
|
||||
partnership_template = create(:template, :partnership_template, partnership: partnership)
|
||||
partnership_submission = create(:submission, template: partnership_template, account: account)
|
||||
|
||||
request_context = { accessible_partnership_ids: [123] }
|
||||
result = described_class.collection(user, request_context: request_context)
|
||||
|
||||
expect(result).to include(account_submission, partnership_submission)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.entity' do
|
||||
let(:account) { create(:account) }
|
||||
let(:user) { create(:user, account: account) }
|
||||
|
||||
context 'with account submission' do
|
||||
let(:template) { create(:template, account: account, author: user) }
|
||||
let(:submission) { create(:submission, template: template) }
|
||||
|
||||
it 'allows access for account owner' do
|
||||
result = described_class.entity(submission, user: user)
|
||||
expect(result).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'with different account submission' do
|
||||
let(:other_account) { create(:account) }
|
||||
let(:other_user) { create(:user, account: other_account) }
|
||||
let(:template) { create(:template, account: other_account, author: other_user) }
|
||||
let(:submission) { create(:submission, template: template) }
|
||||
|
||||
it 'denies access for different account user' do
|
||||
result = described_class.entity(submission, user: user)
|
||||
expect(result).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'with global partnership submission' do
|
||||
let(:partnership) { create(:partnership) }
|
||||
let(:template) { create(:template, :partnership_template, partnership: partnership) }
|
||||
let(:other_account) { create(:account) }
|
||||
let(:submission) { create(:submission, template: template, account: other_account) }
|
||||
|
||||
context 'when global partnership' do
|
||||
before do
|
||||
allow(ExportLocation).to receive(:global_partnership_id).and_return(partnership.id)
|
||||
end
|
||||
|
||||
it 'allows access to global partnership submissions' do
|
||||
result = described_class.entity(submission, user: user)
|
||||
expect(result).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when not global partnership' do
|
||||
before do
|
||||
allow(ExportLocation).to receive(:global_partnership_id).and_return(nil)
|
||||
end
|
||||
|
||||
it 'denies access to non-global partnership submissions' do
|
||||
result = described_class.entity(submission, user: user)
|
||||
expect(result).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with partnership context submission' do
|
||||
let(:partnership) { create(:partnership, external_partnership_id: 456) }
|
||||
let(:template) { create(:template, :partnership_template, partnership: partnership) }
|
||||
let(:other_account) { create(:account) }
|
||||
let(:submission) { create(:submission, template: template, account: other_account) }
|
||||
|
||||
it 'allows access via partnership context' do
|
||||
request_context = { accessible_partnership_ids: [456] }
|
||||
result = described_class.entity(submission, user: user, request_context: request_context)
|
||||
expect(result).to be true
|
||||
end
|
||||
|
||||
it 'denies access without partnership context' do
|
||||
result = described_class.entity(submission, user: user)
|
||||
expect(result).to be false
|
||||
end
|
||||
|
||||
it 'handles integer comparison in partnership context' do
|
||||
partnership = create(:partnership, external_partnership_id: 789)
|
||||
template = create(:template, :partnership_template, partnership: partnership)
|
||||
submission = create(:submission, template: template, account: other_account)
|
||||
|
||||
# accessible_partnership_ids are converted to integers by PartnershipContext concern
|
||||
request_context = { accessible_partnership_ids: [789] }
|
||||
result = described_class.entity(submission, user: user, request_context: request_context)
|
||||
expect(result).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'with user without account' do
|
||||
let(:template_author) { create(:user, account: account) }
|
||||
let(:user) { build(:user, account_id: nil) }
|
||||
let(:template) { create(:template, account: account, author: template_author) }
|
||||
let(:submission) { create(:submission, template: template) }
|
||||
|
||||
it 'denies access' do
|
||||
result = described_class.entity(submission, user: user)
|
||||
expect(result).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,89 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe Abilities::TemplateConditions do
|
||||
describe '.entity' do
|
||||
context 'when using partnership templates' do
|
||||
let(:partnership) { build(:partnership, id: 1, external_partnership_id: 'test-123') }
|
||||
let(:template) { build(:template, partnership_id: 1, account_id: nil) }
|
||||
|
||||
it 'denies access for users without access tokens' do
|
||||
user = build(:user, account_id: nil)
|
||||
allow(user).to receive(:access_token).and_return(nil)
|
||||
allow(ExportLocation).to receive(:global_partnership_id).and_return(nil)
|
||||
|
||||
result = described_class.entity(template, user: user)
|
||||
expect(result).to be false
|
||||
end
|
||||
|
||||
it 'allows access via partnership context' do
|
||||
partnership = create(:partnership)
|
||||
template = build(:template, partnership: partnership, account_id: nil)
|
||||
user = build(:user, account_id: nil)
|
||||
allow(ExportLocation).to receive(:global_partnership_id).and_return(nil)
|
||||
|
||||
request_context = { accessible_partnership_ids: [partnership.external_partnership_id] }
|
||||
result = described_class.entity(template, user: user, request_context: request_context)
|
||||
|
||||
expect(result).to be true
|
||||
end
|
||||
|
||||
it 'handles integer comparison in partnership context' do
|
||||
partnership = create(:partnership, external_partnership_id: 123)
|
||||
template = build(:template, partnership: partnership, account_id: nil)
|
||||
user = build(:user, account_id: nil)
|
||||
allow(ExportLocation).to receive(:global_partnership_id).and_return(nil)
|
||||
|
||||
# accessible_partnership_ids are converted to integers by PartnershipContext concern
|
||||
request_context = { accessible_partnership_ids: [123] }
|
||||
result = described_class.entity(template, user: user, request_context: request_context)
|
||||
expect(result).to be true
|
||||
end
|
||||
|
||||
it 'allows global partnership templates' do
|
||||
user = build(:user, account_id: 1)
|
||||
allow(ExportLocation).to receive(:global_partnership_id).and_return(1)
|
||||
|
||||
result = described_class.entity(template, user: user)
|
||||
expect(result).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when using account templates' do
|
||||
let(:template) { build(:template, account_id: 1, partnership_id: nil) }
|
||||
|
||||
it 'allows access for account owners' do
|
||||
user = build(:user, account_id: 1)
|
||||
result = described_class.entity(template, user: user)
|
||||
expect(result).to be true
|
||||
end
|
||||
|
||||
it 'denies access for different account users' do
|
||||
user = build(:user, account_id: 2)
|
||||
account = instance_double(Account, linked_account_account: nil)
|
||||
allow(user).to receive(:account).and_return(account)
|
||||
|
||||
result = described_class.entity(template, user: user)
|
||||
expect(result).to be false
|
||||
end
|
||||
|
||||
it 'allows access via partnership context with external_account_id' do
|
||||
user = build(:user, account_id: nil)
|
||||
request_context = {
|
||||
accessible_partnership_ids: ['test-123'],
|
||||
external_account_id: 'ext-123'
|
||||
}
|
||||
|
||||
result = described_class.entity(template, user: user, request_context: request_context)
|
||||
expect(result).to be true
|
||||
end
|
||||
end
|
||||
|
||||
it 'allows unowned templates' do
|
||||
template = build(:template, account_id: nil, partnership_id: nil)
|
||||
user = build(:user)
|
||||
|
||||
result = described_class.entity(template, user: user)
|
||||
expect(result).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,47 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe SearchEntries do
|
||||
describe '.index_template' do
|
||||
context 'with partnership template' do
|
||||
let(:partnership) { create(:partnership) }
|
||||
let(:template) do
|
||||
create(:template, :partnership_template, partnership: partnership, name: 'Partnership Template')
|
||||
end
|
||||
|
||||
it 'skips search indexing for partnership templates' do
|
||||
result = described_class.index_template(template)
|
||||
|
||||
expect(result).to be_nil
|
||||
expect(template.reload.search_entry).to be_nil
|
||||
end
|
||||
|
||||
it 'does not raise error when account_id is blank' do
|
||||
expect { described_class.index_template(template) }.not_to raise_error
|
||||
end
|
||||
|
||||
it 'logs the reason for skipping partnership templates' do
|
||||
# Verify the early return works as expected
|
||||
expect(template.account_id).to be_nil
|
||||
expect(template.partnership_id).to be_present
|
||||
|
||||
result = described_class.index_template(template)
|
||||
expect(result).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'with account template' do
|
||||
let(:account) { create(:account) }
|
||||
let(:user) { create(:user, account: account) }
|
||||
let(:template) { create(:template, account: account, author: user, name: 'Test Template') }
|
||||
|
||||
it 'processes account templates normally' do
|
||||
expect(template.account_id).to be_present
|
||||
expect(template.partnership_id).to be_nil
|
||||
|
||||
expect { described_class.index_template(template) }.not_to raise_error(ArgumentError, /account_id.blank?/)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,68 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe Templates::Clone do
|
||||
describe '.call' do
|
||||
let(:author) { build(:user, id: 1) }
|
||||
let(:original_template) do
|
||||
build(
|
||||
:template,
|
||||
id: 1,
|
||||
name: 'Original',
|
||||
submitters: [],
|
||||
fields: [],
|
||||
schema: [],
|
||||
preferences: {}
|
||||
)
|
||||
end
|
||||
|
||||
it 'requires either target_account or target_partnership' do
|
||||
expect do
|
||||
described_class.call(original_template, author: author)
|
||||
end.to raise_error(ArgumentError, 'Either target_account or target_partnership must be provided')
|
||||
end
|
||||
|
||||
it 'creates template with target_account' do
|
||||
target_account = build(:account, id: 2)
|
||||
|
||||
result = described_class.call(original_template, author: author, target_account: target_account)
|
||||
|
||||
expect(result).to be_a(Template)
|
||||
expect(result.account).to eq(target_account)
|
||||
expect(result.partnership).to be_nil
|
||||
expect(result.author).to eq(author)
|
||||
end
|
||||
|
||||
it 'creates template with target_partnership' do
|
||||
target_partnership = create(:partnership)
|
||||
|
||||
result = described_class.call(original_template, author: author, target_partnership: target_partnership)
|
||||
|
||||
expect(result).to be_a(Template)
|
||||
expect(result.partnership).to eq(target_partnership)
|
||||
expect(result.account).to be_nil
|
||||
expect(result.author).to eq(author)
|
||||
end
|
||||
|
||||
it 'sets custom name when provided' do
|
||||
target_account = build(:account, id: 2)
|
||||
|
||||
result = described_class.call(
|
||||
original_template,
|
||||
author: author,
|
||||
target_account: target_account,
|
||||
name: 'Custom Name'
|
||||
)
|
||||
|
||||
expect(result.name).to eq('Custom Name')
|
||||
end
|
||||
|
||||
it 'generates default clone name when no name provided' do
|
||||
target_account = build(:account, id: 2)
|
||||
allow(I18n).to receive(:t).with('clone').and_return('Clone')
|
||||
|
||||
result = described_class.call(original_template, author: author, target_account: target_account)
|
||||
|
||||
expect(result.name).to eq('Original (Clone)')
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,66 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe Templates::CloneToAccount do
|
||||
let(:author) { build(:user, id: 1) }
|
||||
|
||||
describe '.call' do
|
||||
context 'with partnership template' do
|
||||
let(:partnership_template) { build(:template, id: 1, partnership_id: 2, account_id: nil) }
|
||||
let(:target_account) { build(:account, id: 3) }
|
||||
|
||||
it 'clones partnership template to account' do
|
||||
allow(Templates::Clone).to receive(:call).and_return(build(:template))
|
||||
|
||||
result = described_class.call(partnership_template, author: author, target_account: target_account)
|
||||
|
||||
expect(Templates::Clone).to have_received(:call).with(
|
||||
partnership_template,
|
||||
author: author,
|
||||
external_id: nil,
|
||||
name: nil,
|
||||
folder_name: nil,
|
||||
target_account: target_account
|
||||
)
|
||||
expect(result.template_accesses).to be_empty
|
||||
end
|
||||
|
||||
it 'validates partnership template requirement' do
|
||||
account_template = build(:template, partnership_id: nil, account_id: 1)
|
||||
|
||||
expect do
|
||||
described_class.call(account_template, author: author, target_account: target_account)
|
||||
end.to raise_error(ArgumentError, 'Template must be a partnership template')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with external_account_id' do
|
||||
let(:partnership_template) { build(:template, partnership_id: 2, account_id: nil) }
|
||||
let(:current_user) { build(:user, account_id: 3) }
|
||||
let(:target_account) { build(:account, id: 3, external_account_id: 'ext-123') }
|
||||
|
||||
it 'finds account by external_account_id' do
|
||||
allow(Account).to receive(:find_by).with(external_account_id: 'ext-123').and_return(target_account)
|
||||
allow(Templates::Clone).to receive(:call).and_return(build(:template))
|
||||
|
||||
described_class.call(partnership_template,
|
||||
author: author,
|
||||
external_account_id: 'ext-123',
|
||||
current_user: current_user)
|
||||
|
||||
expect(Account).to have_received(:find_by).with(external_account_id: 'ext-123')
|
||||
end
|
||||
|
||||
it 'validates user authorization' do
|
||||
other_user = build(:user, account_id: 999)
|
||||
allow(Account).to receive(:find_by).and_return(target_account)
|
||||
|
||||
expect do
|
||||
described_class.call(partnership_template,
|
||||
author: author,
|
||||
external_account_id: 'ext-123',
|
||||
current_user: other_user)
|
||||
end.to raise_error(ArgumentError, 'Unauthorized access to target account')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,171 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe Templates::CloneToPartnership do
|
||||
let(:account) { create(:account) }
|
||||
let(:partnership) { create(:partnership) }
|
||||
let(:global_partnership) { create(:partnership) }
|
||||
let(:user) { create(:user, account: account) }
|
||||
|
||||
before do
|
||||
allow(ExportLocation).to receive(:global_partnership_id).and_return(global_partnership.id)
|
||||
end
|
||||
|
||||
describe '.call' do
|
||||
context 'with global partnership template' do
|
||||
let(:template) do
|
||||
create(:template, :partnership_template, partnership: global_partnership, name: 'Original Template')
|
||||
end
|
||||
|
||||
it 'clones template to partnership' do
|
||||
result = described_class.call(template, author: user, target_partnership: partnership)
|
||||
|
||||
expect(result).to be_a(Template)
|
||||
expect(result.partnership_id).to eq(partnership.id)
|
||||
expect(result.account_id).to be_nil
|
||||
expect(result.name).to eq('Original Template (Clone)')
|
||||
expect(result.id).not_to eq(template.id)
|
||||
end
|
||||
|
||||
it 'copies template attributes' do
|
||||
template.update!(
|
||||
preferences: { 'test' => 'value' },
|
||||
external_data_fields: { 'field' => 'data' }
|
||||
)
|
||||
|
||||
result = described_class.call(template, author: user, target_partnership: partnership)
|
||||
|
||||
expect(result.preferences).to eq(template.preferences)
|
||||
expect(result.external_data_fields).to eq({})
|
||||
end
|
||||
|
||||
it 'copies submitters' do
|
||||
# Add a submitter to the template's submitters array
|
||||
submitter_uuid = SecureRandom.uuid
|
||||
template.submitters = [{
|
||||
'uuid' => submitter_uuid,
|
||||
'name' => 'Test Submitter'
|
||||
}]
|
||||
template.save!
|
||||
|
||||
result = described_class.call(template, author: user, target_partnership: partnership)
|
||||
|
||||
expect(result.submitters.count).to eq(1)
|
||||
expect(result.submitters.first['name']).to eq('Test Submitter')
|
||||
expect(result.submitters.first['uuid']).not_to eq(submitter_uuid)
|
||||
end
|
||||
|
||||
it 'copies fields' do
|
||||
# Add a field to the template's fields array
|
||||
field_uuid = SecureRandom.uuid
|
||||
template.fields = [{
|
||||
'uuid' => field_uuid,
|
||||
'name' => 'Test Field',
|
||||
'type' => 'text',
|
||||
'required' => true
|
||||
}]
|
||||
template.save!
|
||||
|
||||
result = described_class.call(template, author: user, target_partnership: partnership)
|
||||
|
||||
expect(result.fields.count).to eq(1)
|
||||
expect(result.fields.first['name']).to eq('Test Field')
|
||||
expect(result.fields.first['uuid']).not_to eq(field_uuid)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with partnership template' do
|
||||
let(:template) do
|
||||
create(:template, :partnership_template, partnership: global_partnership, name: 'Partnership Template')
|
||||
end
|
||||
|
||||
it 'clones template to different partnership' do
|
||||
result = described_class.call(template, author: user, target_partnership: partnership)
|
||||
|
||||
expect(result.partnership_id).to eq(partnership.id)
|
||||
expect(result.partnership_id).not_to eq(global_partnership.id)
|
||||
expect(result.account_id).to be_nil
|
||||
expect(result.name).to eq('Partnership Template (Clone)')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with external_id' do
|
||||
let(:template) do
|
||||
create(:template, :partnership_template, partnership: global_partnership, name: 'Global Template')
|
||||
end
|
||||
|
||||
it 'sets external_id when provided' do
|
||||
result = described_class.call(template, author: user, target_partnership: partnership,
|
||||
external_id: 'custom-123')
|
||||
|
||||
expect(result.external_id).to eq('custom-123')
|
||||
end
|
||||
|
||||
it 'does not set external_id when not provided' do
|
||||
result = described_class.call(template, author: user, target_partnership: partnership)
|
||||
|
||||
expect(result.external_id).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'with author' do
|
||||
let(:template) do
|
||||
create(:template, :partnership_template, partnership: global_partnership, name: 'Global Template')
|
||||
end
|
||||
|
||||
it 'sets author when provided' do
|
||||
result = described_class.call(template, author: user, target_partnership: partnership)
|
||||
|
||||
expect(result.author).to eq(user)
|
||||
end
|
||||
|
||||
it 'uses provided author' do
|
||||
original_author = create(:user, :with_partnership)
|
||||
template.update!(author: original_author)
|
||||
|
||||
result = described_class.call(template, author: user, target_partnership: partnership)
|
||||
|
||||
expect(result.author).to eq(user)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when handling errors' do
|
||||
let(:template) do
|
||||
create(:template, :partnership_template, partnership: global_partnership, name: 'Global Template')
|
||||
end
|
||||
|
||||
it 'raises error if partnership is nil' do
|
||||
expect do
|
||||
described_class.call(template, author: user, target_partnership: nil)
|
||||
end.to raise_error(ArgumentError)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with complex template structure' do
|
||||
let(:template) do
|
||||
create(:template, :partnership_template, partnership: global_partnership, name: 'Global Template')
|
||||
end
|
||||
|
||||
before do
|
||||
# Create a complex template with multiple submitters and fields
|
||||
template.submitters = [
|
||||
{ 'uuid' => SecureRandom.uuid, 'name' => 'Submitter 1' },
|
||||
{ 'uuid' => SecureRandom.uuid, 'name' => 'Submitter 2' }
|
||||
]
|
||||
template.fields = [
|
||||
{ 'uuid' => SecureRandom.uuid, 'name' => 'Field 1', 'type' => 'text' },
|
||||
{ 'uuid' => SecureRandom.uuid, 'name' => 'Field 2', 'type' => 'signature' }
|
||||
]
|
||||
template.save!
|
||||
end
|
||||
|
||||
it 'clones all components correctly' do
|
||||
result = described_class.call(template, author: user, target_partnership: partnership)
|
||||
|
||||
expect(result.submitters.count).to eq(2)
|
||||
expect(result.fields.count).to eq(2)
|
||||
expect(result.submitters.pluck('name')).to contain_exactly('Submitter 1', 'Submitter 2')
|
||||
expect(result.fields.pluck('name')).to contain_exactly('Field 1', 'Field 2')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -1,56 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: account_groups
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# name :string not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# external_account_group_id :string not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_account_groups_on_external_account_group_id (external_account_group_id) UNIQUE
|
||||
#
|
||||
describe AccountGroup do
|
||||
let(:account_group) { create(:account_group) }
|
||||
|
||||
describe 'associations' do
|
||||
it 'has many accounts' do
|
||||
expect(account_group).to respond_to(:accounts)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'validations' do
|
||||
it 'validates presence of external_account_group_id' do
|
||||
account_group = build(:account_group, external_account_group_id: nil)
|
||||
expect(account_group).not_to be_valid
|
||||
expect(account_group.errors[:external_account_group_id]).to include("can't be blank")
|
||||
end
|
||||
|
||||
it 'validates uniqueness of external_account_group_id' do
|
||||
create(:account_group, external_account_group_id: 123)
|
||||
duplicate = build(:account_group, external_account_group_id: 123)
|
||||
expect(duplicate).not_to be_valid
|
||||
expect(duplicate.errors[:external_account_group_id]).to include('has already been taken')
|
||||
end
|
||||
|
||||
it 'validates presence of name' do
|
||||
account_group = build(:account_group, name: nil)
|
||||
expect(account_group).not_to be_valid
|
||||
expect(account_group.errors[:name]).to include("can't be blank")
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when account group is destroyed' do
|
||||
it 'nullifies accounts account_group_id' do
|
||||
account = create(:account, account_group: account_group)
|
||||
|
||||
account_group.destroy
|
||||
|
||||
expect(account.reload.account_group).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -1,38 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe AccountGroupValidation do
|
||||
# Test with User model since it includes the concern
|
||||
describe 'validation' do
|
||||
context 'with account only' do
|
||||
it 'is valid' do
|
||||
user = build(:user, account: create(:account), account_group: nil)
|
||||
expect(user).to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
context 'with account_group only' do
|
||||
it 'is valid' do
|
||||
user = build(:user, account: nil, account_group: create(:account_group))
|
||||
expect(user).to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
context 'with neither account nor account_group' do
|
||||
it 'is invalid' do
|
||||
user = build(:user, account: nil, account_group: nil)
|
||||
expect(user).not_to be_valid
|
||||
expect(user.errors[:base]).to include('Must belong to either an account or account group')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with both account and account_group' do
|
||||
it 'is invalid' do
|
||||
user = build(:user, account: create(:account), account_group: create(:account_group))
|
||||
expect(user).not_to be_valid
|
||||
expect(user.errors[:base]).to include('Cannot belong to both account and account group')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe PartnershipValidation do
|
||||
# Test with User model since it includes the concern
|
||||
describe 'validation' do
|
||||
context 'with account only' do
|
||||
it 'is valid' do
|
||||
user = build(:user, account: create(:account))
|
||||
expect(user).to be_valid
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,40 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: partnerships
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# name :string not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# external_partnership_id :integer not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_partnerships_on_external_partnership_id (external_partnership_id) UNIQUE
|
||||
#
|
||||
describe Partnership do
|
||||
let(:partnership) { create(:partnership) }
|
||||
|
||||
describe 'validations' do
|
||||
it 'validates presence of external_partnership_id' do
|
||||
partnership = build(:partnership, external_partnership_id: nil)
|
||||
expect(partnership).not_to be_valid
|
||||
expect(partnership.errors[:external_partnership_id]).to include("can't be blank")
|
||||
end
|
||||
|
||||
it 'validates uniqueness of external_partnership_id' do
|
||||
create(:partnership, external_partnership_id: 123)
|
||||
duplicate = build(:partnership, external_partnership_id: 123)
|
||||
expect(duplicate).not_to be_valid
|
||||
expect(duplicate.errors[:external_partnership_id]).to include('has already been taken')
|
||||
end
|
||||
|
||||
it 'validates presence of name' do
|
||||
partnership = build(:partnership, name: nil)
|
||||
expect(partnership).not_to be_valid
|
||||
expect(partnership.errors[:name]).to include("can't be blank")
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,68 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe TokenRefreshService do
|
||||
describe '#refresh_token' do
|
||||
let(:user_params) do
|
||||
{
|
||||
user: {
|
||||
external_id: 123,
|
||||
email: 'test@example.com',
|
||||
first_name: 'John',
|
||||
last_name: 'Doe'
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
context 'when user exists' do
|
||||
let!(:user) { create(:user, external_user_id: 123) }
|
||||
|
||||
it 'destroys existing token and creates new one' do
|
||||
original_token = user.access_token.token
|
||||
original_token_id = user.access_token.id
|
||||
|
||||
new_token = described_class.new(user_params).refresh_token
|
||||
|
||||
expect(new_token).to be_present
|
||||
expect(new_token).not_to eq(original_token)
|
||||
|
||||
# Verify the original access token was actually destroyed
|
||||
expect(AccessToken.find_by(id: original_token_id)).to be_nil
|
||||
|
||||
# Verify user has a new access token
|
||||
user.reload
|
||||
expect(user.access_token).to be_present
|
||||
expect(user.access_token.token).to eq(new_token)
|
||||
expect(user.access_token.id).not_to eq(original_token_id)
|
||||
end
|
||||
|
||||
it 'handles user without existing access token' do
|
||||
user.access_token.destroy
|
||||
|
||||
new_token = described_class.new(user_params).refresh_token
|
||||
|
||||
expect(new_token).to be_present
|
||||
expect(user.reload.access_token).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user does not exist' do
|
||||
it 'returns nil' do
|
||||
result = described_class.new(user_params).refresh_token
|
||||
|
||||
expect(result).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid params' do
|
||||
it 'returns nil when external_id is missing' do
|
||||
invalid_params = { user: { email: 'test@example.com' } }
|
||||
|
||||
result = described_class.new(invalid_params).refresh_token
|
||||
|
||||
expect(result).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Reference in new issue