Merge branch 'develop' into feat/billing-brl-pix-new-users
This commit is contained in:
@@ -42,7 +42,7 @@ RSpec.describe 'Integration Apps API', type: :request do
|
||||
expect(app['hooks'].first['settings']).to be_nil
|
||||
end
|
||||
|
||||
it 'returns all active apps with sensitive information if user is an admin' do
|
||||
it 'returns all active apps with admin metadata if user is an admin' do
|
||||
first_app = Integrations::App.all.find { |app| app.active?(account) }
|
||||
get api_v1_account_integrations_apps_url(account),
|
||||
headers: admin.create_new_auth_token,
|
||||
@@ -56,19 +56,21 @@ RSpec.describe 'Integration Apps API', type: :request do
|
||||
end
|
||||
|
||||
it 'returns slack app with appropriate redirect url when configured' do
|
||||
with_modified_env SLACK_CLIENT_ID: 'client_id', SLACK_CLIENT_SECRET: 'client_secret' do
|
||||
get api_v1_account_integrations_apps_url(account),
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
allow(GlobalConfigService).to receive(:load).and_call_original
|
||||
allow(GlobalConfigService).to receive(:load).with('SLACK_CLIENT_ID', nil).and_return('client_id')
|
||||
allow(GlobalConfigService).to receive(:load).with('SLACK_CLIENT_SECRET', nil).and_return('client_secret')
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
apps = response.parsed_body['payload']
|
||||
slack_app = apps.find { |app| app['id'] == 'slack' }
|
||||
expect(slack_app['action']).to include('client_id=client_id')
|
||||
end
|
||||
get api_v1_account_integrations_apps_url(account),
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
apps = response.parsed_body['payload']
|
||||
slack_app = apps.find { |app| app['id'] == 'slack' }
|
||||
expect(slack_app['action']).to include('client_id=client_id')
|
||||
end
|
||||
|
||||
it 'will return sensitive information for openai app for admins' do
|
||||
it 'returns visible hook settings for openai app for admins' do
|
||||
openai = create(:integrations_hook, :openai, account: account)
|
||||
get api_v1_account_integrations_apps_url(account),
|
||||
headers: admin.create_new_auth_token,
|
||||
@@ -79,6 +81,34 @@ RSpec.describe 'Integration Apps API', type: :request do
|
||||
app = response.parsed_body['payload'].find { |int_app| int_app['id'] == openai.app.id }
|
||||
expect(app['hooks'].first['settings']).not_to be_nil
|
||||
end
|
||||
|
||||
it 'redacts secrets and only returns visible settings for openai hooks' do
|
||||
openai = create(
|
||||
:integrations_hook,
|
||||
:openai,
|
||||
account: account,
|
||||
settings: { api_key: 'sk-secret', label_suggestion: true }
|
||||
)
|
||||
get api_v1_account_integrations_apps_url(account),
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
app = response.parsed_body['payload'].find { |int_app| int_app['id'] == openai.app.id }
|
||||
expect(app['hooks'].first['settings']).to eq('label_suggestion' => true)
|
||||
end
|
||||
|
||||
it 'keeps slack channel display settings while redacting unspecified settings' do
|
||||
create(:integrations_hook, account: account, settings: { channel_name: 'support', signing_secret: 'secret' })
|
||||
allow(GlobalConfigService).to receive(:load).and_call_original
|
||||
allow(GlobalConfigService).to receive(:load).with('SLACK_CLIENT_SECRET', nil).and_return('client_secret')
|
||||
|
||||
get api_v1_account_integrations_apps_url(account),
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
app = response.parsed_body['payload'].find { |int_app| int_app['id'] == 'slack' }
|
||||
expect(app['hooks'].first['settings']).to eq('channel_name' => 'support')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -117,7 +147,7 @@ RSpec.describe 'Integration Apps API', type: :request do
|
||||
expect(app['hooks'].first['settings']).to be_nil
|
||||
end
|
||||
|
||||
it 'will return sensitive information for openai app for admins' do
|
||||
it 'returns visible hook settings for openai app for admins' do
|
||||
openai = create(:integrations_hook, :openai, account: account)
|
||||
get api_v1_account_integrations_app_url(account_id: account.id, id: openai.app.id),
|
||||
headers: admin.create_new_auth_token,
|
||||
@@ -128,6 +158,53 @@ RSpec.describe 'Integration Apps API', type: :request do
|
||||
app = response.parsed_body
|
||||
expect(app['hooks'].first['settings']).not_to be_nil
|
||||
end
|
||||
|
||||
it 'hides credentials and keeps visible settings for google credential integrations' do
|
||||
hook = create(:integrations_hook, :google_translate, account: account,
|
||||
settings: { project_id: 'project-1',
|
||||
credentials: { private_key: 'secret' } })
|
||||
get api_v1_account_integrations_app_url(account_id: account.id, id: hook.app.id),
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
app = response.parsed_body
|
||||
expect(app['hooks'].first['settings']).to eq('project_id' => 'project-1')
|
||||
end
|
||||
|
||||
it 'returns empty settings for oauth integrations with no visible properties' do
|
||||
hook = create(
|
||||
:integrations_hook,
|
||||
:linear,
|
||||
account: account,
|
||||
settings: { token_type: 'Bearer', refresh_token: 'refresh-secret', expires_in: 7200 }
|
||||
)
|
||||
get api_v1_account_integrations_app_url(account_id: account.id, id: hook.app.id),
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
app = response.parsed_body
|
||||
expect(app['hooks'].first['settings']).to eq({})
|
||||
end
|
||||
|
||||
it 'does not expose leadsquared credential keys in visible settings' do
|
||||
account.enable_features('crm_integration')
|
||||
hook = create(:integrations_hook, :leadsquared, account: account,
|
||||
settings: {
|
||||
'access_key' => 'access-secret',
|
||||
'secret_key' => 'secret',
|
||||
'endpoint_url' => 'https://api.leadsquared.com/',
|
||||
'enable_conversation_activity' => true
|
||||
})
|
||||
get api_v1_account_integrations_app_url(account_id: account.id, id: hook.app.id),
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
settings = response.parsed_body['hooks'].first['settings']
|
||||
expect(settings).to eq(
|
||||
'endpoint_url' => 'https://api.leadsquared.com/',
|
||||
'enable_conversation_activity' => true
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -163,4 +163,21 @@ RSpec.describe DeviseOverrides::SessionsController, type: :controller do
|
||||
expect(response).to redirect_to('/frontend/app/login?error=access-denied')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'session tracking' do
|
||||
let(:user) { create(:user, password: 'Test@123456') }
|
||||
let(:browser_ua) { 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2.1 Safari/605.1.15' }
|
||||
|
||||
context 'with a successful login' do
|
||||
before { request.env['HTTP_USER_AGENT'] = browser_ua }
|
||||
|
||||
it 'creates a UserSession row for the new client_id' do
|
||||
expect { post :create, params: { email: user.email, password: 'Test@123456' } }.to change(user.user_sessions, :count).by(1)
|
||||
|
||||
session = user.user_sessions.last
|
||||
expect(session.browser_name).to eq('Safari')
|
||||
expect(session.platform_name).to eq('macOS')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -12,7 +12,10 @@ RSpec.describe Onboarding::HelpCenterArticleBuilder do
|
||||
|
||||
expect(Firecrawl::Configuration).not_to receive(:client)
|
||||
expect { builder.perform }
|
||||
.to raise_error(Onboarding::HelpCenterErrors::ArticleBuildFailed, /no source urls/)
|
||||
.to raise_error(StandardError) { |error|
|
||||
expect(error.class.name).to eq('Onboarding::HelpCenterErrors::ArticleBuildFailed')
|
||||
expect(error.message).to include('no source urls')
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe UserSessionIpLookupJob do
|
||||
let(:user) { create(:user) }
|
||||
let(:session) { user.user_sessions.create!(client_id: 'c', ip_address: '8.8.8.8', last_activity_at: Time.current) }
|
||||
let(:geo_result) { OpenStruct.new(city: 'Mountain View', country: 'United States', country_code: 'US') }
|
||||
let(:ip_lookup) { instance_double(IpLookupService) }
|
||||
|
||||
before { allow(IpLookupService).to receive(:new).and_return(ip_lookup) }
|
||||
|
||||
it 'backfills geo data on the session' do
|
||||
allow(ip_lookup).to receive(:perform).with('8.8.8.8').and_return(geo_result)
|
||||
|
||||
described_class.perform_now(session)
|
||||
|
||||
session.reload
|
||||
expect(session.city).to eq('Mountain View')
|
||||
expect(session.country).to eq('United States')
|
||||
expect(session.country_code).to eq('US')
|
||||
end
|
||||
|
||||
it 'is a no-op when ip_address is blank' do
|
||||
session.update_columns(ip_address: nil) # rubocop:disable Rails/SkipsModelValidations
|
||||
|
||||
described_class.perform_now(session)
|
||||
|
||||
expect(IpLookupService).not_to have_received(:new)
|
||||
end
|
||||
|
||||
it 'leaves the session untouched when lookup returns nil' do
|
||||
allow(ip_lookup).to receive(:perform).and_return(nil)
|
||||
|
||||
described_class.perform_now(session)
|
||||
|
||||
session.reload
|
||||
expect(session.city).to be_nil
|
||||
expect(session.country).to be_nil
|
||||
end
|
||||
|
||||
it 'swallows lookup errors so a flaky geocoder does not poison the queue' do
|
||||
allow(ip_lookup).to receive(:perform).and_raise(StandardError.new('boom'))
|
||||
|
||||
expect { described_class.perform_now(session) }.not_to raise_error
|
||||
end
|
||||
end
|
||||
@@ -23,6 +23,24 @@ RSpec.describe Integrations::App do
|
||||
end
|
||||
end
|
||||
|
||||
describe '#visible_properties' do
|
||||
context 'when the app has visible properties' do
|
||||
let(:app_name) { 'dialogflow' }
|
||||
|
||||
it 'returns the configured property names as strings' do
|
||||
expect(app.visible_properties).to contain_exactly('project_id', 'region', 'language_code')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the app has no visible properties configured' do
|
||||
let(:app_name) { 'webhook' }
|
||||
|
||||
it 'defaults to an empty list' do
|
||||
expect(app.visible_properties).to eq([])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#action' do
|
||||
let(:app_name) { 'slack' }
|
||||
|
||||
@@ -32,12 +50,13 @@ RSpec.describe Integrations::App do
|
||||
|
||||
context 'when the app is slack' do
|
||||
it 'returns the action URL with client_id and redirect_uri' do
|
||||
with_modified_env SLACK_CLIENT_ID: 'dummy_client_id' do
|
||||
expect(app.action).to include('client_id=dummy_client_id')
|
||||
expect(app.action).to include(
|
||||
"/app/accounts/#{account.id}/settings/integrations/slack"
|
||||
)
|
||||
end
|
||||
allow(GlobalConfigService).to receive(:load).and_call_original
|
||||
allow(GlobalConfigService).to receive(:load).with('SLACK_CLIENT_ID', nil).and_return('dummy_client_id')
|
||||
|
||||
expect(app.action).to include('client_id=dummy_client_id')
|
||||
expect(app.action).to include(
|
||||
"/app/accounts/#{account.id}/settings/integrations/slack"
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -47,9 +66,9 @@ RSpec.describe Integrations::App do
|
||||
|
||||
context 'when the app is slack' do
|
||||
it 'returns true if SLACK_CLIENT_SECRET is present' do
|
||||
with_modified_env SLACK_CLIENT_SECRET: 'random_secret' do
|
||||
expect(app.active?(account)).to be true
|
||||
end
|
||||
allow(GlobalConfigService).to receive(:load).with('SLACK_CLIENT_SECRET', nil).and_return('random_secret')
|
||||
|
||||
expect(app.active?(account)).to be true
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe UserSession do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
describe 'associations' do
|
||||
it { is_expected.to belong_to(:user) }
|
||||
end
|
||||
|
||||
describe 'validations' do
|
||||
subject { described_class.new(user: user, client_id: 'abc') }
|
||||
|
||||
it { is_expected.to validate_presence_of(:client_id) }
|
||||
|
||||
it 'validates uniqueness of client_id scoped to user_id' do
|
||||
described_class.create!(user: user, client_id: 'abc', last_activity_at: Time.current)
|
||||
|
||||
duplicate = described_class.new(user: user, client_id: 'abc')
|
||||
expect(duplicate).not_to be_valid
|
||||
expect(duplicate.errors[:client_id]).to be_present
|
||||
end
|
||||
|
||||
it 'allows the same client_id for different users' do
|
||||
other = create(:user)
|
||||
described_class.create!(user: user, client_id: 'abc', last_activity_at: Time.current)
|
||||
|
||||
expect(described_class.new(user: other, client_id: 'abc', last_activity_at: Time.current)).to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
describe '#current?' do
|
||||
let(:session) { described_class.create!(user: user, client_id: 'abc', last_activity_at: Time.current) }
|
||||
|
||||
it 'returns true when client_id matches' do
|
||||
expect(session.current?('abc')).to be true
|
||||
end
|
||||
|
||||
it 'returns false when client_id differs' do
|
||||
expect(session.current?('xyz')).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe '#should_update_activity?' do
|
||||
let(:session) { described_class.new(user: user, client_id: 'abc') }
|
||||
|
||||
it 'returns true when last_activity_at is nil' do
|
||||
session.last_activity_at = nil
|
||||
expect(session.should_update_activity?).to be true
|
||||
end
|
||||
|
||||
it 'returns true when last_activity_at is older than the throttle window' do
|
||||
session.last_activity_at = 10.minutes.ago
|
||||
expect(session.should_update_activity?).to be true
|
||||
end
|
||||
|
||||
it 'returns false when last_activity_at is within the throttle window' do
|
||||
session.last_activity_at = 1.minute.ago
|
||||
expect(session.should_update_activity?).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -254,4 +254,37 @@ RSpec.describe User do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'sync_user_sessions callback' do
|
||||
let(:user_with_tokens) do
|
||||
u = create(:user)
|
||||
u.tokens = {
|
||||
'client-a' => { 'token' => 'x', 'expiry' => 1.month.from_now.to_i },
|
||||
'client-b' => { 'token' => 'x', 'expiry' => 1.month.from_now.to_i }
|
||||
}
|
||||
u.save!
|
||||
u.user_sessions.create!(client_id: 'client-a', last_activity_at: Time.current)
|
||||
u.user_sessions.create!(client_id: 'client-b', last_activity_at: Time.current)
|
||||
u
|
||||
end
|
||||
|
||||
it 'destroys user_sessions whose client_id is no longer in tokens' do
|
||||
user_with_tokens.tokens = user_with_tokens.tokens.except('client-a')
|
||||
|
||||
expect { user_with_tokens.save! }.to change(user_with_tokens.user_sessions, :count).by(-1)
|
||||
expect(user_with_tokens.user_sessions.pluck(:client_id)).to eq(['client-b'])
|
||||
end
|
||||
|
||||
it 'leaves user_sessions alone when tokens did not change' do
|
||||
user_with_tokens.update!(name: 'New Name')
|
||||
|
||||
expect(user_with_tokens.user_sessions.count).to eq(2)
|
||||
end
|
||||
|
||||
it 'destroys all user_sessions when tokens is cleared' do
|
||||
user_with_tokens.tokens = {}
|
||||
|
||||
expect { user_with_tokens.save! }.to change(user_with_tokens.user_sessions, :count).by(-2)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Profile Sessions API', type: :request do
|
||||
let(:account) { create(:account) }
|
||||
let(:user) { create(:user, account: account) }
|
||||
let(:auth_headers) { user.create_new_auth_token }
|
||||
let(:current_client_id) { auth_headers['client'] }
|
||||
|
||||
describe 'GET /api/v1/profile/sessions' do
|
||||
it 'returns 401 without auth' do
|
||||
get '/api/v1/profile/sessions', as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'returns the current user sessions ordered by last_activity_at desc' do
|
||||
older = user.user_sessions.create!(client_id: current_client_id, browser_name: 'Chrome', last_activity_at: 2.days.ago)
|
||||
newer = user.user_sessions.create!(client_id: 'other-client', browser_name: 'Firefox', last_activity_at: 1.hour.ago)
|
||||
user.update!(tokens: user.tokens.merge('other-client' => { 'token' => 'x', 'expiry' => 1.month.from_now.to_i }))
|
||||
|
||||
get '/api/v1/profile/sessions', headers: auth_headers, as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
sessions = response.parsed_body
|
||||
expect(sessions.map { |s| s['id'] }).to eq([newer.id, older.id])
|
||||
expect(sessions.find { |s| s['id'] == older.id }['current']).to be true
|
||||
expect(sessions.find { |s| s['id'] == newer.id }['current']).to be false
|
||||
end
|
||||
|
||||
it 'excludes sessions whose token has expired' do
|
||||
live = user.user_sessions.create!(client_id: current_client_id, last_activity_at: 1.hour.ago)
|
||||
expired = user.user_sessions.create!(client_id: 'expired-client', last_activity_at: 1.day.ago)
|
||||
user.update!(tokens: user.tokens.merge('expired-client' => { 'token' => 'x', 'expiry' => 1.day.ago.to_i }))
|
||||
|
||||
get '/api/v1/profile/sessions', headers: auth_headers, as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
ids = response.parsed_body.map { |s| s['id'] }
|
||||
expect(ids).to include(live.id)
|
||||
expect(ids).not_to include(expired.id)
|
||||
end
|
||||
|
||||
it 'returns an empty array when no sessions exist' do
|
||||
get '/api/v1/profile/sessions', headers: auth_headers, as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.parsed_body).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE /api/v1/profile/sessions/:id' do
|
||||
let!(:other_session) { user.user_sessions.create!(client_id: 'other-client', last_activity_at: 1.hour.ago) }
|
||||
|
||||
before do
|
||||
# Seed tokens hash so revoke can clean it up
|
||||
user.tokens = user.tokens.merge('other-client' => { 'token' => 'x', 'expiry' => 1.month.from_now.to_i })
|
||||
user.save!
|
||||
end
|
||||
|
||||
it 'destroys the session and removes its token entry' do
|
||||
expect do
|
||||
delete "/api/v1/profile/sessions/#{other_session.id}", headers: auth_headers, as: :json
|
||||
end.to change(user.user_sessions, :count).by(-1)
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(user.reload.tokens.keys).not_to include('other-client')
|
||||
end
|
||||
|
||||
it 'returns 422 when trying to revoke the current session' do
|
||||
current = user.user_sessions.create!(client_id: current_client_id, last_activity_at: Time.current)
|
||||
|
||||
delete "/api/v1/profile/sessions/#{current.id}", headers: auth_headers, as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.parsed_body['error']).to be_present
|
||||
expect(user.user_sessions.exists?(id: current.id)).to be true
|
||||
end
|
||||
|
||||
it 'returns 404 for a nonexistent session id' do
|
||||
delete '/api/v1/profile/sessions/9999999', headers: auth_headers, as: :json
|
||||
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
|
||||
it 'does not allow revoking another user' do
|
||||
other_user = create(:user, account: account)
|
||||
foreign = other_user.user_sessions.create!(client_id: 'foreign', last_activity_at: 1.hour.ago)
|
||||
|
||||
delete "/api/v1/profile/sessions/#{foreign.id}", headers: auth_headers, as: :json
|
||||
|
||||
expect(response).to have_http_status(:not_found)
|
||||
expect(other_user.user_sessions.exists?(id: foreign.id)).to be true
|
||||
end
|
||||
|
||||
it 'returns 401 without auth' do
|
||||
delete "/api/v1/profile/sessions/#{other_session.id}", as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,82 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe UserSessionTrackingService do
|
||||
let(:user) { create(:user) }
|
||||
let(:client_id) { 'client-abc' }
|
||||
let(:request) do
|
||||
instance_double(
|
||||
ActionDispatch::Request,
|
||||
user_agent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2.1 Safari/605.1.15',
|
||||
remote_ip: '8.8.8.8'
|
||||
)
|
||||
end
|
||||
let(:service) { described_class.new(user: user, request: request, client_id: client_id) }
|
||||
|
||||
describe '#create_or_update!' do
|
||||
it 'creates a new UserSession with the right client_id and timestamps' do
|
||||
expect { service.create_or_update! }.to change(user.user_sessions, :count).by(1)
|
||||
|
||||
session = user.user_sessions.last
|
||||
expect(session.client_id).to eq(client_id)
|
||||
expect(session.last_activity_at).to be_within(1.second).of(Time.current)
|
||||
end
|
||||
|
||||
it 'populates request and browser metadata synchronously', :aggregate_failures do
|
||||
service.create_or_update!
|
||||
|
||||
session = user.user_sessions.last
|
||||
expect(session.ip_address).to eq('8.8.8.8')
|
||||
expect(session.browser_name).to eq('Safari')
|
||||
expect(session.platform_name).to eq('macOS')
|
||||
end
|
||||
|
||||
it 'does not call IpLookupService synchronously' do
|
||||
expect(IpLookupService).not_to receive(:new)
|
||||
|
||||
service.create_or_update!
|
||||
end
|
||||
|
||||
it 'enqueues UserSessionIpLookupJob to backfill geo data' do
|
||||
expect { service.create_or_update! }.to have_enqueued_job(UserSessionIpLookupJob)
|
||||
end
|
||||
|
||||
it 'updates an existing session when client_id matches' do
|
||||
existing = user.user_sessions.create!(client_id: client_id, ip_address: '1.1.1.1', last_activity_at: 1.day.ago)
|
||||
|
||||
expect { service.create_or_update! }.not_to change(user.user_sessions, :count)
|
||||
expect(existing.reload.ip_address).to eq('8.8.8.8')
|
||||
expect(existing.last_activity_at).to be_within(1.second).of(Time.current)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#update_activity!' do
|
||||
it 'does nothing when no session exists for the client_id' do
|
||||
expect { service.update_activity! }.not_to change(user.user_sessions, :count)
|
||||
end
|
||||
|
||||
it 'does nothing when the session was recently active' do
|
||||
session = user.user_sessions.create!(client_id: client_id, last_activity_at: 1.minute.ago)
|
||||
before_ts = session.last_activity_at
|
||||
|
||||
service.update_activity!
|
||||
|
||||
expect(session.reload.last_activity_at).to be_within(1.second).of(before_ts)
|
||||
end
|
||||
|
||||
it 'bumps last_activity_at when the session is stale' do
|
||||
session = user.user_sessions.create!(client_id: client_id, last_activity_at: 10.minutes.ago)
|
||||
|
||||
service.update_activity!
|
||||
|
||||
expect(session.reload.last_activity_at).to be_within(1.second).of(Time.current)
|
||||
end
|
||||
|
||||
it 'bumps last_activity_at when last_activity_at is nil' do
|
||||
session = user.user_sessions.create!(client_id: client_id, last_activity_at: nil)
|
||||
|
||||
service.update_activity!
|
||||
|
||||
expect(session.reload.last_activity_at).to be_within(1.second).of(Time.current)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -177,7 +177,7 @@ describe Whatsapp::FacebookApiClient do
|
||||
.with(
|
||||
headers: { 'Authorization' => "Bearer #{access_token}", 'Content-Type' => 'application/json' },
|
||||
body: { override_callback_uri: callback_url, verify_token: verify_token,
|
||||
subscribed_fields: %w[messages smb_message_echoes calls] }.to_json
|
||||
subscribed_fields: %w[messages smb_message_echoes] }.to_json
|
||||
)
|
||||
.to_return(
|
||||
status: 200,
|
||||
@@ -224,7 +224,7 @@ describe Whatsapp::FacebookApiClient do
|
||||
.with(
|
||||
headers: { 'Authorization' => "Bearer #{access_token}", 'Content-Type' => 'application/json' },
|
||||
body: { override_callback_uri: callback_url, verify_token: verify_token,
|
||||
subscribed_fields: %w[messages smb_message_echoes calls] }.to_json
|
||||
subscribed_fields: %w[messages smb_message_echoes] }.to_json
|
||||
)
|
||||
.to_return(status: 400, body: { error: 'Webhook callback override failed' }.to_json)
|
||||
end
|
||||
|
||||
@@ -59,6 +59,41 @@ describe Whatsapp::IncomingMessageWhatsappCloudService do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when document attachment includes an accented filename' do
|
||||
let(:document_params) do
|
||||
{
|
||||
phone_number: whatsapp_channel.phone_number,
|
||||
object: 'whatsapp_business_account',
|
||||
entry: [{
|
||||
changes: [{
|
||||
value: {
|
||||
contacts: [{ profile: { name: 'Sojan Jose' }, wa_id: '2423423243' }],
|
||||
messages: [{
|
||||
from: '2423423243',
|
||||
document: {
|
||||
id: 'b1c68f38-8734-4ad3-b4a1-ef0c10d683',
|
||||
mime_type: 'application/pdf',
|
||||
filename: 'Currículum café.pdf',
|
||||
caption: 'My résumé'
|
||||
},
|
||||
timestamp: '1664799904', type: 'document'
|
||||
}]
|
||||
}
|
||||
}]
|
||||
}]
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
it 'preserves the original filename from the payload' do
|
||||
stub_media_url_request
|
||||
stub_sample_png_request
|
||||
described_class.new(inbox: whatsapp_channel.inbox, params: document_params).perform
|
||||
|
||||
attachment = whatsapp_channel.inbox.messages.first.attachments.first
|
||||
expect(attachment.file.filename.to_s).to eq('Currículum café.pdf')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when invalid attachment message params' do
|
||||
let(:error_params) do
|
||||
{
|
||||
|
||||
@@ -43,7 +43,7 @@ describe Whatsapp::WebhookSetupService do
|
||||
allow(SecureRandom).to receive(:random_number).with(900_000).and_return(123_456)
|
||||
allow(api_client).to receive(:register_phone_number).with('123456789', 223_456)
|
||||
allow(api_client).to receive(:subscribe_waba_webhook)
|
||||
.with(waba_id, anything, 'test_verify_token').and_return({ 'success' => true })
|
||||
.with(waba_id, anything, 'test_verify_token', subscribed_fields: %w[messages smb_message_echoes]).and_return({ 'success' => true })
|
||||
allow(channel).to receive(:save!)
|
||||
end
|
||||
|
||||
@@ -51,7 +51,8 @@ describe Whatsapp::WebhookSetupService do
|
||||
with_modified_env FRONTEND_URL: 'https://app.chatwoot.com' do
|
||||
expect(api_client).to receive(:register_phone_number).with('123456789', 223_456)
|
||||
expect(api_client).to receive(:subscribe_waba_webhook)
|
||||
.with(waba_id, 'https://app.chatwoot.com/webhooks/whatsapp/+1234567890', 'test_verify_token')
|
||||
.with(waba_id, 'https://app.chatwoot.com/webhooks/whatsapp/+1234567890', 'test_verify_token', subscribed_fields: %w[messages
|
||||
smb_message_echoes])
|
||||
service.perform
|
||||
end
|
||||
end
|
||||
@@ -65,14 +66,15 @@ describe Whatsapp::WebhookSetupService do
|
||||
throughput: { level: 'APPLICABLE' }
|
||||
})
|
||||
allow(api_client).to receive(:subscribe_waba_webhook)
|
||||
.with(waba_id, anything, 'test_verify_token').and_return({ 'success' => true })
|
||||
.with(waba_id, anything, 'test_verify_token', subscribed_fields: %w[messages smb_message_echoes]).and_return({ 'success' => true })
|
||||
end
|
||||
|
||||
it 'does NOT register phone, but sets up webhook' do
|
||||
with_modified_env FRONTEND_URL: 'https://app.chatwoot.com' do
|
||||
expect(api_client).not_to receive(:register_phone_number)
|
||||
expect(api_client).to receive(:subscribe_waba_webhook)
|
||||
.with(waba_id, 'https://app.chatwoot.com/webhooks/whatsapp/+1234567890', 'test_verify_token')
|
||||
.with(waba_id, 'https://app.chatwoot.com/webhooks/whatsapp/+1234567890', 'test_verify_token', subscribed_fields: %w[messages
|
||||
smb_message_echoes])
|
||||
service.perform
|
||||
end
|
||||
end
|
||||
@@ -88,7 +90,7 @@ describe Whatsapp::WebhookSetupService do
|
||||
allow(SecureRandom).to receive(:random_number).with(900_000).and_return(123_456)
|
||||
allow(api_client).to receive(:register_phone_number).with('123456789', 223_456)
|
||||
allow(api_client).to receive(:subscribe_waba_webhook)
|
||||
.with(waba_id, anything, 'test_verify_token').and_return({ 'success' => true })
|
||||
.with(waba_id, anything, 'test_verify_token', subscribed_fields: %w[messages smb_message_echoes]).and_return({ 'success' => true })
|
||||
allow(channel).to receive(:save!)
|
||||
end
|
||||
|
||||
@@ -96,7 +98,8 @@ describe Whatsapp::WebhookSetupService do
|
||||
with_modified_env FRONTEND_URL: 'https://app.chatwoot.com' do
|
||||
expect(api_client).to receive(:register_phone_number).with('123456789', 223_456)
|
||||
expect(api_client).to receive(:subscribe_waba_webhook)
|
||||
.with(waba_id, 'https://app.chatwoot.com/webhooks/whatsapp/+1234567890', 'test_verify_token')
|
||||
.with(waba_id, 'https://app.chatwoot.com/webhooks/whatsapp/+1234567890', 'test_verify_token', subscribed_fields: %w[messages
|
||||
smb_message_echoes])
|
||||
service.perform
|
||||
end
|
||||
end
|
||||
@@ -112,7 +115,7 @@ describe Whatsapp::WebhookSetupService do
|
||||
allow(SecureRandom).to receive(:random_number).with(900_000).and_return(123_456)
|
||||
allow(api_client).to receive(:register_phone_number).with('123456789', 223_456)
|
||||
allow(api_client).to receive(:subscribe_waba_webhook)
|
||||
.with(waba_id, anything, 'test_verify_token').and_return({ 'success' => true })
|
||||
.with(waba_id, anything, 'test_verify_token', subscribed_fields: %w[messages smb_message_echoes]).and_return({ 'success' => true })
|
||||
allow(channel).to receive(:save!)
|
||||
end
|
||||
|
||||
@@ -120,7 +123,8 @@ describe Whatsapp::WebhookSetupService do
|
||||
with_modified_env FRONTEND_URL: 'https://app.chatwoot.com' do
|
||||
expect(api_client).to receive(:register_phone_number).with('123456789', 223_456)
|
||||
expect(api_client).to receive(:subscribe_waba_webhook)
|
||||
.with(waba_id, 'https://app.chatwoot.com/webhooks/whatsapp/+1234567890', 'test_verify_token')
|
||||
.with(waba_id, 'https://app.chatwoot.com/webhooks/whatsapp/+1234567890', 'test_verify_token', subscribed_fields: %w[messages
|
||||
smb_message_echoes])
|
||||
service.perform
|
||||
end
|
||||
end
|
||||
@@ -279,14 +283,15 @@ describe Whatsapp::WebhookSetupService do
|
||||
throughput: { level: 'APPLICABLE' }
|
||||
})
|
||||
allow(api_client).to receive(:subscribe_waba_webhook)
|
||||
.with(waba_id, anything, 'existing_verify_token').and_return({ 'success' => true })
|
||||
.with(waba_id, anything, 'existing_verify_token', subscribed_fields: %w[messages smb_message_echoes]).and_return({ 'success' => true })
|
||||
end
|
||||
|
||||
it 'successfully reauthorizes with new access token' do
|
||||
with_modified_env FRONTEND_URL: 'https://app.chatwoot.com' do
|
||||
expect(api_client).not_to receive(:register_phone_number)
|
||||
expect(api_client).to receive(:subscribe_waba_webhook)
|
||||
.with(waba_id, 'https://app.chatwoot.com/webhooks/whatsapp/+1234567890', 'existing_verify_token')
|
||||
.with(waba_id, 'https://app.chatwoot.com/webhooks/whatsapp/+1234567890', 'existing_verify_token',
|
||||
subscribed_fields: %w[messages smb_message_echoes])
|
||||
service_reauth.perform
|
||||
end
|
||||
end
|
||||
@@ -294,7 +299,7 @@ describe Whatsapp::WebhookSetupService do
|
||||
it 'uses the existing webhook verify token during reauthorization' do
|
||||
with_modified_env FRONTEND_URL: 'https://app.chatwoot.com' do
|
||||
expect(api_client).to receive(:subscribe_waba_webhook)
|
||||
.with(waba_id, anything, 'existing_verify_token')
|
||||
.with(waba_id, anything, 'existing_verify_token', subscribed_fields: %w[messages smb_message_echoes])
|
||||
service_reauth.perform
|
||||
end
|
||||
end
|
||||
@@ -308,7 +313,7 @@ describe Whatsapp::WebhookSetupService do
|
||||
throughput: { level: 'APPLICABLE' }
|
||||
})
|
||||
allow(api_client).to receive(:subscribe_waba_webhook)
|
||||
.with(waba_id, anything, 'test_verify_token').and_return({ 'success' => true })
|
||||
.with(waba_id, anything, 'test_verify_token', subscribed_fields: %w[messages smb_message_echoes]).and_return({ 'success' => true })
|
||||
end
|
||||
|
||||
it 'completes successfully without errors' do
|
||||
|
||||
Reference in New Issue
Block a user