Compare commits
2 Commits
develop
...
feat/widge
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6730093a84 | ||
|
|
d63a71c59d |
@@ -2,7 +2,7 @@
|
||||
# For Specific Channels like whatsapp, email etc . it smartly generated appropriate the source id when none is provided.
|
||||
|
||||
class ContactInboxBuilder
|
||||
pattr_initialize [:contact, :inbox, :source_id, { hmac_verified: false }]
|
||||
pattr_initialize [:contact, :inbox, :source_id, :pubsub_token, { hmac_verified: false }]
|
||||
|
||||
def perform
|
||||
@source_id ||= generate_source_id
|
||||
@@ -65,7 +65,8 @@ class ContactInboxBuilder
|
||||
source_id: @source_id
|
||||
}
|
||||
|
||||
::ContactInbox.where(attrs).first_or_create!(hmac_verified: hmac_verified || false)
|
||||
# A nil pubsub_token is filled in by has_secure_token; the widget supplies the one its visitor already streams on.
|
||||
::ContactInbox.where(attrs).first_or_create!(hmac_verified: hmac_verified || false, pubsub_token: pubsub_token)
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
Rails.logger.info("[ContactInboxBuilder] RecordNotUnique #{@source_id} #{@contact.id} #{@inbox.id}")
|
||||
update_old_contact_inbox
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# for contact inbox logic it uses the contact inbox builder
|
||||
|
||||
class ContactInboxWithContactBuilder
|
||||
pattr_initialize [:inbox!, :contact_attributes!, :source_id, :hmac_verified]
|
||||
pattr_initialize [:inbox!, :contact_attributes!, :source_id, :pubsub_token, :hmac_verified]
|
||||
|
||||
def perform
|
||||
find_or_create_contact_and_contact_inbox
|
||||
@@ -40,6 +40,7 @@ class ContactInboxWithContactBuilder
|
||||
contact: @contact,
|
||||
inbox: @inbox,
|
||||
source_id: @source_id,
|
||||
pubsub_token: pubsub_token,
|
||||
hmac_verified: hmac_verified
|
||||
).perform
|
||||
end
|
||||
|
||||
@@ -10,6 +10,7 @@ class RoomChannel < ApplicationCable::Channel
|
||||
end
|
||||
|
||||
def update_presence
|
||||
adopt_visitor_contact
|
||||
update_subscription
|
||||
broadcast_presence
|
||||
end
|
||||
@@ -30,7 +31,8 @@ class RoomChannel < ApplicationCable::Channel
|
||||
end
|
||||
|
||||
def update_subscription
|
||||
return if @current_account.blank?
|
||||
# A visitor whose contact does not exist yet has nothing to track.
|
||||
return if @current_user.blank?
|
||||
|
||||
::OnlineStatusTracker.update_presence(@current_account.id, @current_user.class.name, @current_user.id)
|
||||
end
|
||||
@@ -41,19 +43,35 @@ class RoomChannel < ApplicationCable::Channel
|
||||
|
||||
def current_user
|
||||
@current_user ||= if params[:user_id].blank?
|
||||
ContactInbox.find_by!(pubsub_token: pubsub_token).contact
|
||||
ContactInbox.find_by(pubsub_token: pubsub_token)&.contact
|
||||
else
|
||||
User.find_by!(pubsub_token: pubsub_token, id: params[:user_id])
|
||||
end
|
||||
end
|
||||
|
||||
def current_account
|
||||
return if current_user.blank?
|
||||
|
||||
@current_account ||= if @current_user.is_a? Contact
|
||||
@current_account ||= if @current_user.is_a?(User)
|
||||
@current_user.accounts.find(params[:account_id])
|
||||
elsif @current_user
|
||||
@current_user.account
|
||||
else
|
||||
@current_user.accounts.find(params[:account_id])
|
||||
visitor_account
|
||||
end
|
||||
end
|
||||
|
||||
# Until the visitor's contact exists, the signed widget token is the only proof the stream is theirs;
|
||||
# it also names the inbox whose agent presence they should see.
|
||||
def visitor_account
|
||||
visitor_token = ::Widget::TokenService.new(token: params[:auth_token]).decode_token
|
||||
raise ActiveRecord::RecordNotFound if visitor_token[:pubsub_token].blank? || visitor_token[:pubsub_token] != pubsub_token
|
||||
|
||||
::Inbox.find(visitor_token[:inbox_id]).account
|
||||
end
|
||||
|
||||
# The contact is created lazily (WebsiteTokenHelper#ensure_contact); pick it up on the next ping.
|
||||
def adopt_visitor_contact
|
||||
return if @current_user.present? || params[:user_id].present?
|
||||
|
||||
@current_user = ContactInbox.find_by(pubsub_token: pubsub_token)&.contact
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,6 +8,8 @@ class Api::V1::Widget::BaseController < ApplicationController
|
||||
private
|
||||
|
||||
def conversations
|
||||
return ::Conversation.none if @contact_inbox.nil?
|
||||
|
||||
if @contact_inbox.hmac_verified?
|
||||
verified_contact_inbox_ids = @contact.contact_inboxes.where(inbox_id: auth_token_params[:inbox_id], hmac_verified: true).map(&:id)
|
||||
@conversations = @contact.conversations.where(contact_inbox_id: verified_contact_inbox_ids)
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
class Api::V1::Widget::ConfigsController < Api::V1::Widget::BaseController
|
||||
include WidgetHelper
|
||||
|
||||
before_action :set_global_config
|
||||
|
||||
def create
|
||||
build_contact
|
||||
set_token
|
||||
@contact_inbox ||= @web_widget.create_contact_inbox(
|
||||
source_id: auth_token_params[:source_id], pubsub_token: auth_token_params[:pubsub_token], additional_attributes: additional_attributes
|
||||
)
|
||||
@contact = @contact_inbox.contact
|
||||
@token, @pubsub_token = visitor_session(@web_widget, request.headers['X-Auth-Token'], auth_token_params, @contact_inbox)
|
||||
end
|
||||
|
||||
private
|
||||
@@ -18,25 +23,12 @@ class Api::V1::Widget::ConfigsController < Api::V1::Widget::BaseController
|
||||
)
|
||||
end
|
||||
|
||||
# Unlike the other widget endpoints, this one also serves visitors without a token.
|
||||
def set_contact
|
||||
@contact_inbox = @web_widget.inbox.contact_inboxes.find_by(
|
||||
source_id: auth_token_params[:source_id]
|
||||
)
|
||||
@contact_inbox = @web_widget.inbox.contact_inboxes.find_by(source_id: auth_token_params[:source_id])
|
||||
@contact = @contact_inbox&.contact
|
||||
end
|
||||
|
||||
def build_contact
|
||||
return if @contact.present?
|
||||
|
||||
@contact_inbox = @web_widget.create_contact_inbox(additional_attributes)
|
||||
@contact = @contact_inbox.contact
|
||||
end
|
||||
|
||||
def set_token
|
||||
payload = { source_id: @contact_inbox.source_id, inbox_id: @web_widget.inbox.id }
|
||||
@token = ::Widget::TokenService.new(payload: payload).generate_token
|
||||
end
|
||||
|
||||
def additional_attributes
|
||||
if @web_widget.inbox.account.feature_enabled?('ip_lookup')
|
||||
{ created_at_ip: request.remote_ip }
|
||||
|
||||
@@ -3,6 +3,7 @@ class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
|
||||
|
||||
before_action :validate_hmac, only: [:set_user]
|
||||
before_action :validate_hmac_for_identified_update, only: [:update]
|
||||
before_action :ensure_contact, only: [:update, :set_user, :destroy_custom_attributes]
|
||||
|
||||
def show; end
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||
include Events::Types
|
||||
before_action :ensure_contact, only: [:create]
|
||||
before_action :render_not_found_if_empty, only: [:toggle_typing, :toggle_status, :set_custom_attributes, :destroy_custom_attributes]
|
||||
|
||||
def index
|
||||
|
||||
@@ -3,10 +3,4 @@ class Api::V1::Widget::DirectUploadsController < ActiveStorage::DirectUploadsCon
|
||||
include WebsiteTokenHelper
|
||||
before_action :set_web_widget
|
||||
before_action :set_contact
|
||||
|
||||
def create
|
||||
return if @contact.nil? || @current_account.nil?
|
||||
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
class Api::V1::Widget::EventsController < Api::V1::Widget::BaseController
|
||||
include Events::Types
|
||||
# Both events reach listeners that need the contact inbox: campaigns start a conversation on it,
|
||||
# and webwidget_triggered webhooks/agent bots receive it.
|
||||
before_action :ensure_contact
|
||||
|
||||
def create
|
||||
Rails.configuration.dispatcher.dispatch(permitted_params[:name], Time.zone.now, contact_inbox: @contact_inbox,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
|
||||
before_action :prevent_reply_to_resolved_conversation, only: [:create]
|
||||
before_action :set_conversation, only: [:create]
|
||||
before_action :ensure_contact, :set_conversation, only: [:create]
|
||||
before_action :set_message, only: [:update]
|
||||
|
||||
def index
|
||||
|
||||
@@ -1,22 +1,41 @@
|
||||
module WebsiteTokenHelper
|
||||
def auth_token_params
|
||||
@auth_token_params ||= ::Widget::TokenService.new(token: request.headers['X-Auth-Token']).decode_token
|
||||
@auth_token_params ||= begin
|
||||
decoded = ::Widget::TokenService.new(token: request.headers['X-Auth-Token']).decode_token
|
||||
# A token only identifies a session in the inbox it was minted for.
|
||||
decoded[:inbox_id] == @web_widget.inbox.id ? decoded : {}
|
||||
end
|
||||
end
|
||||
|
||||
def set_web_widget
|
||||
@web_widget = ::Channel::WebWidget.find_by!(website_token: permitted_params[:website_token])
|
||||
@current_account = @web_widget.inbox.account
|
||||
|
||||
render json: { error: 'Account is suspended' }, status: :unauthorized unless @current_account.active?
|
||||
end
|
||||
|
||||
def set_contact
|
||||
@contact_inbox = @web_widget.inbox.contact_inboxes.find_by(
|
||||
source_id: auth_token_params[:source_id]
|
||||
)
|
||||
@contact = @contact_inbox&.contact
|
||||
raise ActiveRecord::RecordNotFound unless @contact
|
||||
raise ActiveRecord::RecordNotFound if auth_token_params[:source_id].blank?
|
||||
|
||||
@contact_inbox = @web_widget.inbox.contact_inboxes.find_by(source_id: auth_token_params[:source_id])
|
||||
@contact = @contact_inbox&.contact
|
||||
# A row whose contact is gone (dependent: :destroy_async) is not a session; the next page load issues a new one.
|
||||
raise ActiveRecord::RecordNotFound if @contact_inbox && @contact.nil?
|
||||
|
||||
Current.contact = @contact
|
||||
end
|
||||
|
||||
# A visitor only gets a contact once they do something worth keeping (open the widget, send a
|
||||
# message, identify themselves); loading the page is not that. The token carries the ids to use,
|
||||
# and the row is only ever created on the stream the widget is already subscribed to.
|
||||
def ensure_contact
|
||||
return if @contact.present?
|
||||
raise ActiveRecord::RecordNotFound if auth_token_params[:pubsub_token].blank?
|
||||
|
||||
additional_attributes = @current_account.feature_enabled?('ip_lookup') ? { created_at_ip: request.remote_ip } : {}
|
||||
@contact_inbox = @web_widget.create_contact_inbox(
|
||||
source_id: auth_token_params[:source_id], pubsub_token: auth_token_params[:pubsub_token], additional_attributes: additional_attributes
|
||||
)
|
||||
@contact = @contact_inbox.contact
|
||||
Current.contact = @contact
|
||||
end
|
||||
|
||||
|
||||
@@ -8,8 +8,9 @@ class WidgetsController < ActionController::Base
|
||||
before_action :skip_crawler_requests
|
||||
before_action :ensure_location_is_supported
|
||||
before_action :set_token
|
||||
before_action :set_contact
|
||||
before_action :build_contact
|
||||
before_action :set_contact_inbox
|
||||
before_action :ensure_contact_inbox
|
||||
before_action :set_visitor_session
|
||||
after_action :allow_iframe_requests
|
||||
|
||||
private
|
||||
@@ -33,30 +34,34 @@ class WidgetsController < ActionController::Base
|
||||
end
|
||||
|
||||
def set_token
|
||||
@token = permitted_params[:cw_conversation]
|
||||
@auth_token_params = if @token.present?
|
||||
::Widget::TokenService.new(token: @token).decode_token
|
||||
else
|
||||
{}
|
||||
end
|
||||
@auth_token_params = ::Widget::TokenService.new(token: permitted_params[:cw_conversation]).decode_token
|
||||
# A token only identifies a session in the inbox it was minted for.
|
||||
@auth_token_params = {} unless @auth_token_params[:inbox_id] == @web_widget.inbox.id
|
||||
end
|
||||
|
||||
def set_contact
|
||||
def set_contact_inbox
|
||||
return if @auth_token_params[:source_id].nil?
|
||||
|
||||
@contact_inbox = ::ContactInbox.find_by(
|
||||
inbox_id: @web_widget.inbox.id,
|
||||
source_id: @auth_token_params[:source_id]
|
||||
)
|
||||
@contact_inbox = ::ContactInbox.find_by(inbox_id: @web_widget.inbox.id, source_id: @auth_token_params[:source_id])
|
||||
return unless @contact_inbox && @contact_inbox.contact.nil?
|
||||
|
||||
@contact = @contact_inbox&.contact
|
||||
# A row whose contact is gone (dependent: :destroy_async) is not a session to continue.
|
||||
@contact_inbox = nil
|
||||
@auth_token_params = {}
|
||||
end
|
||||
|
||||
def build_contact
|
||||
return if @contact.present?
|
||||
# A token issued by a server that no longer creates the contact on load carries the ids to reuse,
|
||||
# so the row lands on the stream the visitor is already subscribed to.
|
||||
def ensure_contact_inbox
|
||||
return if @contact_inbox.present?
|
||||
|
||||
@contact_inbox, @token = build_contact_inbox_with_token(@web_widget, additional_attributes)
|
||||
@contact = @contact_inbox.contact
|
||||
@contact_inbox = @web_widget.create_contact_inbox(
|
||||
source_id: @auth_token_params[:source_id], pubsub_token: @auth_token_params[:pubsub_token], additional_attributes: additional_attributes
|
||||
)
|
||||
end
|
||||
|
||||
def set_visitor_session
|
||||
@token, @pubsub_token = visitor_session(@web_widget, permitted_params[:cw_conversation], @auth_token_params, @contact_inbox)
|
||||
end
|
||||
|
||||
def ensure_account_is_active
|
||||
|
||||
@@ -1,9 +1,28 @@
|
||||
module WidgetHelper
|
||||
def build_contact_inbox_with_token(web_widget, additional_attributes = {})
|
||||
contact_inbox = web_widget.create_contact_inbox(additional_attributes)
|
||||
payload = { source_id: contact_inbox.source_id, inbox_id: web_widget.inbox.id }
|
||||
token = ::Widget::TokenService.new(payload: payload).generate_token
|
||||
# Token and pubsub_token for the visitor's session. A current token is handed back as is;
|
||||
# otherwise one is minted, continuing the row's ids when there is a row and fresh ones when not.
|
||||
# A token without a pubsub_token predates lazy creation: with a row it is reissued from the row,
|
||||
# without one it is not a session. Nothing is written to the database until
|
||||
# WebsiteTokenHelper#ensure_contact.
|
||||
def visitor_session(web_widget, token, auth_token_params, contact_inbox)
|
||||
session = contact_inbox ? contact_inbox.slice(:source_id, :pubsub_token) : auth_token_params
|
||||
session = {} if session[:pubsub_token].blank?
|
||||
return [token, session[:pubsub_token]] if session.present? && session[:pubsub_token] == auth_token_params[:pubsub_token]
|
||||
|
||||
[contact_inbox, token]
|
||||
source_id = session[:source_id] || SecureRandom.uuid
|
||||
pubsub_token = session[:pubsub_token] || ContactInbox.generate_unique_secure_token
|
||||
[visitor_token(web_widget, source_id, pubsub_token), pubsub_token]
|
||||
end
|
||||
|
||||
def build_contact_inbox_with_token(web_widget)
|
||||
contact_inbox = web_widget.create_contact_inbox
|
||||
[contact_inbox, visitor_token(web_widget, contact_inbox.source_id, contact_inbox.pubsub_token)]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def visitor_token(web_widget, source_id, pubsub_token)
|
||||
payload = { source_id: source_id, inbox_id: web_widget.inbox.id, pubsub_token: pubsub_token }
|
||||
::Widget::TokenService.new(payload: payload).generate_token
|
||||
end
|
||||
end
|
||||
|
||||
@@ -49,6 +49,7 @@ window.onload = () => {
|
||||
window.WOOT_WIDGET = app.mount('#app');
|
||||
window.actionCable = new ActionCableConnector(
|
||||
window.WOOT_WIDGET,
|
||||
window.chatwootPubsubToken
|
||||
window.chatwootPubsubToken,
|
||||
window.authToken
|
||||
);
|
||||
};
|
||||
|
||||
@@ -10,7 +10,8 @@ class BaseActionCableConnector {
|
||||
app,
|
||||
pubsubToken,
|
||||
websocketHost = '',
|
||||
presenceInterval = PRESENCE_INTERVAL
|
||||
presenceInterval = PRESENCE_INTERVAL,
|
||||
subscriptionParams = {}
|
||||
) {
|
||||
const websocketURL = websocketHost ? `${websocketHost}/cable` : undefined;
|
||||
|
||||
@@ -21,6 +22,7 @@ class BaseActionCableConnector {
|
||||
pubsub_token: pubsubToken,
|
||||
account_id: app.$store.getters.getCurrentAccountId,
|
||||
user_id: app.$store.getters.getCurrentUserID,
|
||||
...subscriptionParams,
|
||||
},
|
||||
{
|
||||
updatePresence() {
|
||||
|
||||
@@ -16,8 +16,11 @@ const isMessageInActiveConversation = (getters, message) => {
|
||||
const WIDGET_PRESENCE_INTERVAL = 60000;
|
||||
|
||||
class ActionCableConnector extends BaseActionCableConnector {
|
||||
constructor(app, pubsubToken) {
|
||||
super(app, pubsubToken, '', WIDGET_PRESENCE_INTERVAL);
|
||||
constructor(app, pubsubToken, authToken) {
|
||||
// Until the visitor's contact exists, the signed widget token authorises the stream.
|
||||
super(app, pubsubToken, '', WIDGET_PRESENCE_INTERVAL, {
|
||||
auth_token: authToken,
|
||||
});
|
||||
this.events = {
|
||||
'message.created': this.onMessageCreated,
|
||||
'message.updated': this.onMessageUpdated,
|
||||
|
||||
@@ -100,10 +100,12 @@ class Channel::WebWidget < ApplicationRecord
|
||||
}
|
||||
end
|
||||
|
||||
def create_contact_inbox(additional_attributes = {})
|
||||
::ContactInboxWithContactBuilder.new({
|
||||
inbox: inbox,
|
||||
contact_attributes: { additional_attributes: additional_attributes }
|
||||
}).perform
|
||||
def create_contact_inbox(source_id: nil, pubsub_token: nil, additional_attributes: {})
|
||||
::ContactInboxWithContactBuilder.new(
|
||||
inbox: inbox,
|
||||
contact_attributes: { additional_attributes: additional_attributes },
|
||||
source_id: source_id,
|
||||
pubsub_token: pubsub_token
|
||||
).perform
|
||||
end
|
||||
end
|
||||
|
||||
@@ -25,12 +25,14 @@ json.website_channel_config do
|
||||
end
|
||||
|
||||
json.contact do
|
||||
json.email @contact.email
|
||||
json.id @contact.id
|
||||
json.identifier @contact.identifier
|
||||
json.name @contact.name
|
||||
json.phone_number @contact.phone_number
|
||||
json.pubsub_token @contact_inbox.pubsub_token
|
||||
if @contact
|
||||
json.email @contact.email
|
||||
json.id @contact.id
|
||||
json.identifier @contact.identifier
|
||||
json.name @contact.name
|
||||
json.phone_number @contact.phone_number
|
||||
end
|
||||
json.pubsub_token @pubsub_token
|
||||
end
|
||||
|
||||
json.global_config @global_config
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
json.id @contact.id
|
||||
json.has_email @contact.email.present?
|
||||
json.has_name @contact.name.present?
|
||||
json.has_phone_number @contact.phone_number.present?
|
||||
json.identifier @contact.identifier
|
||||
if @contact
|
||||
json.id @contact.id
|
||||
json.has_email @contact.email.present?
|
||||
json.has_name @contact.name.present?
|
||||
json.has_phone_number @contact.phone_number.present?
|
||||
json.identifier @contact.identifier
|
||||
end
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
allowMessagesAfterResolved: <%= @web_widget.inbox.allow_messages_after_resolved %>,
|
||||
disableBranding: <%= @web_widget.inbox.account.feature_enabled?('disable_branding') %>,
|
||||
}
|
||||
window.chatwootPubsubToken = '<%= @contact_inbox.pubsub_token %>'
|
||||
window.chatwootPubsubToken = '<%= @pubsub_token %>'
|
||||
window.authToken = '<%= @token %>'
|
||||
window.globalConfig = <%= raw @global_config.to_json %>
|
||||
</script>
|
||||
|
||||
@@ -5,6 +5,22 @@ describe ContactInboxBuilder do
|
||||
let(:contact) { create(:contact, email: 'xyc@example.com', phone_number: '+23423424123', account: account) }
|
||||
|
||||
describe '#perform' do
|
||||
describe 'web widget inbox' do
|
||||
let(:web_widget) { create(:channel_widget, account: account) }
|
||||
|
||||
it 'creates the contact inbox on the pubsub_token the visitor already streams on' do
|
||||
contact_inbox = described_class.new(contact: contact, inbox: web_widget.inbox, source_id: 'visitor', pubsub_token: 'stream').perform
|
||||
|
||||
expect(contact_inbox.pubsub_token).to eq('stream')
|
||||
end
|
||||
|
||||
it 'generates a pubsub_token when none is given' do
|
||||
contact_inbox = described_class.new(contact: contact, inbox: web_widget.inbox).perform
|
||||
|
||||
expect(contact_inbox.pubsub_token).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
describe 'twilio sms inbox' do
|
||||
let!(:twilio_sms) { create(:channel_twilio_sms, account: account) }
|
||||
let!(:twilio_inbox) { create(:inbox, channel: twilio_sms, account: account) }
|
||||
|
||||
@@ -4,6 +4,11 @@ RSpec.describe RoomChannel do
|
||||
let!(:contact_inbox) { create(:contact_inbox) }
|
||||
let!(:account) { create(:account) }
|
||||
let!(:user) { create(:user, account: account) }
|
||||
let(:web_widget) { create(:channel_widget, account: account) }
|
||||
let(:visitor_pubsub_token) { ContactInbox.generate_unique_secure_token }
|
||||
let(:visitor_token) do
|
||||
Widget::TokenService.new(payload: { source_id: 'visitor', inbox_id: web_widget.inbox.id, pubsub_token: visitor_pubsub_token }).generate_token
|
||||
end
|
||||
|
||||
before do
|
||||
stub_connection
|
||||
@@ -21,4 +26,47 @@ RSpec.describe RoomChannel do
|
||||
expect(subscription).to have_stream_for(user.pubsub_token)
|
||||
expect(subscription).to have_stream_for("account_#{account.id}")
|
||||
end
|
||||
|
||||
it 'subscribes a visitor without a contact when their widget token vouches for the stream' do
|
||||
subscribe(pubsub_token: visitor_pubsub_token, auth_token: visitor_token)
|
||||
expect(subscription).to be_confirmed
|
||||
expect(subscription).to have_stream_for(visitor_pubsub_token)
|
||||
end
|
||||
|
||||
it 'still tells a visitor without a contact which agents are online' do
|
||||
expect { subscribe(pubsub_token: visitor_pubsub_token, auth_token: visitor_token) }
|
||||
.to have_broadcasted_to(visitor_pubsub_token).with(hash_including(event: 'presence.update'))
|
||||
end
|
||||
|
||||
it 'does not track presence for a visitor without a contact' do
|
||||
subscribe(pubsub_token: visitor_pubsub_token, auth_token: visitor_token)
|
||||
perform :update_presence
|
||||
|
||||
expect(OnlineStatusTracker.get_available_contact_ids(account.id)).to be_empty
|
||||
end
|
||||
|
||||
it 'rejects a stream the widget token does not vouch for' do
|
||||
expect { subscribe(pubsub_token: "account_#{account.id}", auth_token: visitor_token) }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
|
||||
it 'rejects an unknown stream without a widget token' do
|
||||
expect { subscribe(pubsub_token: visitor_pubsub_token) }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
|
||||
it 'ignores presence pings from a subscription that was rejected' do
|
||||
expect { subscribe(pubsub_token: visitor_pubsub_token) }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
|
||||
expect { perform :update_presence }.not_to raise_error
|
||||
expect(subscription).not_to have_stream_for(visitor_pubsub_token)
|
||||
end
|
||||
|
||||
it 'starts tracking presence once the visitor gets a contact' do
|
||||
subscribe(pubsub_token: visitor_pubsub_token, auth_token: visitor_token)
|
||||
contact = create(:contact, account: account)
|
||||
create(:contact_inbox, contact: contact, inbox: web_widget.inbox, pubsub_token: visitor_pubsub_token)
|
||||
|
||||
perform :update_presence
|
||||
|
||||
expect(OnlineStatusTracker.get_presence(account.id, 'Contact', contact.id)).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,7 +5,7 @@ RSpec.describe '/api/v1/widget/config', type: :request do
|
||||
let(:web_widget) { create(:channel_widget, account: account) }
|
||||
let!(:contact) { create(:contact, account: account) }
|
||||
let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: web_widget.inbox) }
|
||||
let(:payload) { { source_id: contact_inbox.source_id, inbox_id: web_widget.inbox.id } }
|
||||
let(:payload) { { source_id: contact_inbox.source_id, inbox_id: web_widget.inbox.id, pubsub_token: contact_inbox.pubsub_token } }
|
||||
let(:token) { Widget::TokenService.new(payload: payload).generate_token }
|
||||
|
||||
describe 'POST /api/v1/widget/config' do
|
||||
@@ -30,6 +30,10 @@ RSpec.describe '/api/v1/widget/config', type: :request do
|
||||
expect(response).to have_http_status(:success)
|
||||
response_data = response.parsed_body
|
||||
expect(response_data.keys).to include(*response_keys)
|
||||
created = Contact.find(response_data['contact']['id'])
|
||||
expect(response_data['contact']['pubsub_token']).to eq(created.contact_inboxes.first.pubsub_token)
|
||||
auth_token = Widget::TokenService.new(token: response_data['website_channel_config']['auth_token']).decode_token
|
||||
expect(auth_token[:pubsub_token]).to eq(response_data['contact']['pubsub_token'])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -45,7 +49,9 @@ RSpec.describe '/api/v1/widget/config', type: :request do
|
||||
expect(response).to have_http_status(:success)
|
||||
response_data = response.parsed_body
|
||||
expect(response_data.keys).to include(*response_keys)
|
||||
expect(response_data['contact']['id']).to eq(contact.id)
|
||||
expect(response_data['contact']['pubsub_token']).to eq(contact_inbox.pubsub_token)
|
||||
expect(response_data['website_channel_config']['auth_token']).to eq(token)
|
||||
end
|
||||
|
||||
it 'returns 401 if account is suspended' do
|
||||
@@ -60,6 +66,26 @@ RSpec.describe '/api/v1/widget/config', type: :request do
|
||||
end
|
||||
end
|
||||
|
||||
context 'with correct website token and a token issued without a contact' do
|
||||
it 'creates the contact with the ids the token carries' do
|
||||
visitor_payload = { source_id: 'visitor', inbox_id: web_widget.inbox.id, pubsub_token: 'stream' }
|
||||
visitor_token = Widget::TokenService.new(payload: visitor_payload).generate_token
|
||||
|
||||
expect do
|
||||
post '/api/v1/widget/config',
|
||||
params: params,
|
||||
headers: { 'X-Auth-Token' => visitor_token },
|
||||
as: :json
|
||||
end.to change(Contact, :count).by(1)
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
response_data = response.parsed_body
|
||||
expect(response_data['contact']['pubsub_token']).to eq('stream')
|
||||
expect(web_widget.inbox.contact_inboxes.find_by!(source_id: 'visitor').pubsub_token).to eq('stream')
|
||||
expect(response_data['website_channel_config']['auth_token']).to eq(visitor_token)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with correct website token and invalid X-Auth-Token' do
|
||||
it 'returns widget config and new contact with error message' do
|
||||
expect do
|
||||
@@ -72,6 +98,7 @@ RSpec.describe '/api/v1/widget/config', type: :request do
|
||||
expect(response).to have_http_status(:success)
|
||||
response_data = response.parsed_body
|
||||
expect(response_data.keys).to include(*response_keys)
|
||||
expect(response_data['contact']['id']).to be_present
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,6 +8,23 @@ RSpec.describe '/api/v1/widget/contacts', type: :request do
|
||||
let(:payload) { { source_id: contact_inbox.source_id, inbox_id: web_widget.inbox.id } }
|
||||
let(:token) { Widget::TokenService.new(payload: payload).generate_token }
|
||||
|
||||
describe 'GET /api/v1/widget/contact' do
|
||||
it 'returns nothing for a visitor without a contact' do
|
||||
visitor_payload = { source_id: 'visitor', inbox_id: web_widget.inbox.id, pubsub_token: 'stream' }
|
||||
visitor_token = Widget::TokenService.new(payload: visitor_payload).generate_token
|
||||
|
||||
expect do
|
||||
get '/api/v1/widget/contact',
|
||||
params: { website_token: web_widget.website_token },
|
||||
headers: { 'X-Auth-Token' => visitor_token },
|
||||
as: :json
|
||||
end.not_to change(Contact, :count)
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.parsed_body).to eq({})
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PATCH /api/v1/widget/contact' do
|
||||
let(:params) { { website_token: web_widget.website_token, identifier: 'test' } }
|
||||
|
||||
@@ -18,6 +35,26 @@ RSpec.describe '/api/v1/widget/contacts', type: :request do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the visitor has no contact yet' do
|
||||
it 'creates the contact with the prechat details' do
|
||||
visitor_payload = { source_id: 'visitor', inbox_id: web_widget.inbox.id, pubsub_token: 'stream' }
|
||||
visitor_token = Widget::TokenService.new(payload: visitor_payload).generate_token
|
||||
|
||||
expect do
|
||||
patch '/api/v1/widget/contact',
|
||||
params: { website_token: web_widget.website_token, name: 'Pre Chat', email: 'prechat@example.com' },
|
||||
headers: { 'X-Auth-Token' => visitor_token },
|
||||
as: :json
|
||||
end.to change(Contact, :count).by(1)
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.parsed_body['has_email']).to be true
|
||||
visitor_contact_inbox = web_widget.inbox.contact_inboxes.find_by!(source_id: 'visitor')
|
||||
expect(visitor_contact_inbox.pubsub_token).to eq('stream')
|
||||
expect(visitor_contact_inbox.contact.email).to eq('prechat@example.com')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with correct website token' do
|
||||
let(:identify_action) { double }
|
||||
|
||||
@@ -194,6 +231,42 @@ RSpec.describe '/api/v1/widget/contacts', type: :request do
|
||||
let(:correct_identifier_hash) { OpenSSL::HMAC.hexdigest('sha256', web_widget.hmac_token, params[:identifier].to_s) }
|
||||
let(:incorrect_identifier_hash) { 'test' }
|
||||
|
||||
context 'when the visitor has no contact yet' do
|
||||
it 'creates the contact for the identifier' do
|
||||
visitor_payload = { source_id: 'visitor', inbox_id: web_widget.inbox.id, pubsub_token: 'stream' }
|
||||
visitor_token = Widget::TokenService.new(payload: visitor_payload).generate_token
|
||||
|
||||
expect do
|
||||
patch '/api/v1/widget/contact/set_user',
|
||||
params: params.merge(identifier_hash: correct_identifier_hash),
|
||||
headers: { 'X-Auth-Token' => visitor_token },
|
||||
as: :json
|
||||
end.to change(Contact, :count).by(1)
|
||||
|
||||
body = response.parsed_body
|
||||
visitor_contact_inbox = web_widget.inbox.contact_inboxes.find_by!(source_id: 'visitor')
|
||||
expect(visitor_contact_inbox.contact.id).to eq(body['id'])
|
||||
expect(visitor_contact_inbox.contact.identifier).to eq('test')
|
||||
expect(visitor_contact_inbox.pubsub_token).to eq('stream')
|
||||
expect(visitor_contact_inbox.hmac_verified?).to be(true)
|
||||
expect(body).not_to have_key('widget_auth_token')
|
||||
end
|
||||
|
||||
it 'does not create a contact when the identifier hash is invalid' do
|
||||
visitor_payload = { source_id: 'visitor', inbox_id: web_widget.inbox.id, pubsub_token: 'stream' }
|
||||
visitor_token = Widget::TokenService.new(payload: visitor_payload).generate_token
|
||||
|
||||
expect do
|
||||
patch '/api/v1/widget/contact/set_user',
|
||||
params: params.merge(identifier_hash: incorrect_identifier_hash),
|
||||
headers: { 'X-Auth-Token' => visitor_token },
|
||||
as: :json
|
||||
end.not_to(change { [Contact.count, ContactInbox.count] })
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the current contact identifier is different from param identifier' do
|
||||
before do
|
||||
contact.update(identifier: 'random')
|
||||
@@ -263,18 +336,22 @@ RSpec.describe '/api/v1/widget/contacts', type: :request do
|
||||
|
||||
context 'with invalid website token' do
|
||||
it 'returns unauthorized' do
|
||||
post '/api/v1/widget/destroy_custom_attributes', params: { website_token: '' }
|
||||
post '/api/v1/widget/contact/destroy_custom_attributes', params: { website_token: '' }
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with correct website token' do
|
||||
it 'calls destroy custom attributes' do
|
||||
post '/api/v1/widget/destroy_custom_attributes',
|
||||
contact.update!(custom_attributes: { 'test' => 'x', 'keep' => 'y' })
|
||||
|
||||
post '/api/v1/widget/contact/destroy_custom_attributes',
|
||||
params: params,
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
as: :json
|
||||
expect(contact.reload.custom_attributes).to eq({})
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(contact.reload.custom_attributes).to eq('keep' => 'y')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -45,12 +45,52 @@ RSpec.describe '/api/v1/widget/conversations/toggle_typing', type: :request do
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a conversation but invalid source id' do
|
||||
it 'returns the correct conversation params' do
|
||||
context 'with a token whose visitor has no contact yet' do
|
||||
it 'returns no conversation without creating anything' do
|
||||
allow(Rails.configuration.dispatcher).to receive(:dispatch)
|
||||
|
||||
payload = { source_id: 'invalid source id', inbox_id: web_widget.inbox.id }
|
||||
payload = { source_id: 'visitor', inbox_id: web_widget.inbox.id, pubsub_token: 'stream' }
|
||||
token = Widget::TokenService.new(payload: payload).generate_token
|
||||
|
||||
expect do
|
||||
get '/api/v1/widget/conversations',
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
params: { website_token: web_widget.website_token },
|
||||
as: :json
|
||||
end.not_to(change { [Contact.count, ContactInbox.count] })
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.parsed_body).to eq({})
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an invalid or expired token' do
|
||||
it 'returns not found for a token that cannot be decoded' do
|
||||
get '/api/v1/widget/conversations',
|
||||
headers: { 'X-Auth-Token' => 'garbage' },
|
||||
params: { website_token: web_widget.website_token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
|
||||
it 'returns not found for an expired token' do
|
||||
expired_token = JWT.encode(payload.merge(exp: 1.day.ago.to_i), Rails.application.secret_key_base, 'HS256')
|
||||
|
||||
get '/api/v1/widget/conversations',
|
||||
headers: { 'X-Auth-Token' => expired_token },
|
||||
params: { website_token: web_widget.website_token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the contact behind the token has been deleted' do
|
||||
it 'returns not found instead of treating the row as a visitor' do
|
||||
token
|
||||
contact.delete
|
||||
|
||||
get '/api/v1/widget/conversations',
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
params: { website_token: web_widget.website_token },
|
||||
@@ -62,6 +102,38 @@ RSpec.describe '/api/v1/widget/conversations/toggle_typing', type: :request do
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/widget/conversations' do
|
||||
it 'creates the contact along with the first conversation of a visitor' do
|
||||
visitor_payload = { source_id: 'visitor', inbox_id: web_widget.inbox.id, pubsub_token: 'stream' }
|
||||
visitor_token = Widget::TokenService.new(payload: visitor_payload).generate_token
|
||||
|
||||
expect do
|
||||
post '/api/v1/widget/conversations',
|
||||
headers: { 'X-Auth-Token' => visitor_token },
|
||||
params: conversation_params,
|
||||
as: :json
|
||||
end.to change(Contact, :count).by(1)
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
visitor_contact_inbox = web_widget.inbox.contact_inboxes.find_by!(source_id: 'visitor')
|
||||
expect(visitor_contact_inbox.pubsub_token).to eq('stream')
|
||||
expect(visitor_contact_inbox.conversations.count).to eq(1)
|
||||
expect(visitor_contact_inbox.contact.email).to eq('contact-email@chatwoot.com')
|
||||
end
|
||||
|
||||
it 'does not create a conversation when the contact behind the token has been deleted' do
|
||||
token
|
||||
contact.delete
|
||||
|
||||
expect do
|
||||
post '/api/v1/widget/conversations',
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
params: conversation_params,
|
||||
as: :json
|
||||
end.not_to(change { [Contact.count, Conversation.count] })
|
||||
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
|
||||
it 'creates a conversation with correct details' do
|
||||
post '/api/v1/widget/conversations',
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
|
||||
@@ -42,6 +42,27 @@ RSpec.describe '/api/v1/widget/direct_uploads', type: :request do
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['content_type']).to eq('image/png')
|
||||
end
|
||||
|
||||
it 'creates a blob for a visitor without a contact' do
|
||||
visitor_payload = { source_id: 'visitor', inbox_id: web_widget.inbox.id, pubsub_token: 'stream' }
|
||||
visitor_token = Widget::TokenService.new(payload: visitor_payload).generate_token
|
||||
|
||||
expect do
|
||||
post api_v1_widget_direct_uploads_url,
|
||||
params: {
|
||||
website_token: web_widget.website_token,
|
||||
blob: {
|
||||
filename: 'avatar.png',
|
||||
byte_size: '1234',
|
||||
checksum: 'dsjbsdhbfif3874823mnsdbf',
|
||||
content_type: 'image/png'
|
||||
}
|
||||
},
|
||||
headers: { 'X-Auth-Token' => visitor_token }
|
||||
end.not_to change(Contact, :count)
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -34,6 +34,53 @@ RSpec.describe '/api/v1/widget/events', type: :request do
|
||||
.with(params[:name], anything, contact_inbox: contact_inbox,
|
||||
event_info: { test_id: 'test', browser_language: nil, widget_language: nil, browser: anything })
|
||||
end
|
||||
|
||||
it 'gives a visitor without a contact one when they open the widget' do
|
||||
visitor_payload = { source_id: 'visitor', inbox_id: web_widget.inbox.id, pubsub_token: 'stream' }
|
||||
visitor_token = Widget::TokenService.new(payload: visitor_payload).generate_token
|
||||
|
||||
expect do
|
||||
post '/api/v1/widget/events',
|
||||
params: params,
|
||||
headers: { 'X-Auth-Token' => visitor_token },
|
||||
as: :json
|
||||
end.to change(Contact, :count).by(1).and change(ContactInbox, :count).by(1)
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
visitor_contact_inbox = web_widget.inbox.contact_inboxes.find_by!(source_id: 'visitor')
|
||||
expect(visitor_contact_inbox.pubsub_token).to eq('stream')
|
||||
expect(Rails.configuration.dispatcher).to have_received(:dispatch)
|
||||
.with(params[:name], anything, hash_including(contact_inbox: visitor_contact_inbox))
|
||||
end
|
||||
|
||||
it 'does not create a contact for a token minted before pubsub_token was part of it' do
|
||||
legacy_token = Widget::TokenService.new(payload: { source_id: 'purged', inbox_id: web_widget.inbox.id }).generate_token
|
||||
|
||||
expect do
|
||||
post '/api/v1/widget/events',
|
||||
params: params,
|
||||
headers: { 'X-Auth-Token' => legacy_token },
|
||||
as: :json
|
||||
end.not_to(change { [Contact.count, ContactInbox.count] })
|
||||
|
||||
expect(response).to have_http_status(:not_found)
|
||||
expect(Rails.configuration.dispatcher).not_to have_received(:dispatch).with(params[:name], anything, anything)
|
||||
end
|
||||
|
||||
it 'does not create a contact for a token minted for another inbox' do
|
||||
other_widget = create(:channel_widget, account: account)
|
||||
token
|
||||
|
||||
expect do
|
||||
post '/api/v1/widget/events',
|
||||
params: params.merge(website_token: other_widget.website_token),
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
as: :json
|
||||
end.not_to(change { [Contact.count, ContactInbox.count] })
|
||||
|
||||
expect(response).to have_http_status(:not_found)
|
||||
expect(Rails.configuration.dispatcher).not_to have_received(:dispatch).with(params[:name], anything, anything)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -38,6 +38,21 @@ RSpec.describe '/api/v1/widget/messages', type: :request do
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['payload'].length).to eq(0)
|
||||
end
|
||||
|
||||
it 'returns empty messages for a visitor without a contact', :skip_before do
|
||||
visitor_payload = { source_id: 'visitor', inbox_id: web_widget.inbox.id, pubsub_token: 'stream' }
|
||||
visitor_token = Widget::TokenService.new(payload: visitor_payload).generate_token
|
||||
|
||||
expect do
|
||||
get api_v1_widget_messages_url,
|
||||
params: { website_token: web_widget.website_token },
|
||||
headers: { 'X-Auth-Token' => visitor_token },
|
||||
as: :json
|
||||
end.not_to change(Contact, :count)
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.parsed_body['payload']).to eq([])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -76,6 +91,22 @@ RSpec.describe '/api/v1/widget/messages', type: :request do
|
||||
end
|
||||
|
||||
context 'when post request is made' do
|
||||
it 'creates the contact and conversation with the first message of a visitor', :skip_before do
|
||||
visitor_payload = { source_id: 'visitor', inbox_id: web_widget.inbox.id, pubsub_token: 'stream' }
|
||||
visitor_token = Widget::TokenService.new(payload: visitor_payload).generate_token
|
||||
|
||||
expect do
|
||||
post api_v1_widget_messages_url,
|
||||
params: { website_token: web_widget.website_token, message: { content: 'hello world', timestamp: Time.current } },
|
||||
headers: { 'X-Auth-Token' => visitor_token },
|
||||
as: :json
|
||||
end.to change(Contact, :count).by(1).and change(Conversation, :count).by(1)
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.parsed_body['content']).to eq('hello world')
|
||||
expect(web_widget.inbox.contact_inboxes.find_by!(source_id: 'visitor').pubsub_token).to eq('stream')
|
||||
end
|
||||
|
||||
it 'creates message in conversation' do
|
||||
conversation.destroy! # Test all params
|
||||
message_params = { content: 'hello world', timestamp: Time.current }
|
||||
|
||||
@@ -5,26 +5,90 @@ describe '/widget', type: :request do
|
||||
let(:web_widget) { create(:channel_widget, account: account) }
|
||||
let(:contact) { create(:contact, account: account) }
|
||||
let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: web_widget.inbox) }
|
||||
let(:payload) { { source_id: contact_inbox.source_id, inbox_id: web_widget.inbox.id } }
|
||||
let(:payload) { { source_id: contact_inbox.source_id, inbox_id: web_widget.inbox.id, pubsub_token: contact_inbox.pubsub_token } }
|
||||
let(:token) { Widget::TokenService.new(payload: payload).generate_token }
|
||||
let(:rendered_token) { Widget::TokenService.new(token: response.body[/window\.authToken = '([^']+)'/, 1]).decode_token }
|
||||
let(:rendered_pubsub_token) { response.body[/window\.chatwootPubsubToken = '([^']+)'/, 1] }
|
||||
|
||||
describe 'GET /widget' do
|
||||
it 'renders the page correctly when called with website_token' do
|
||||
it 'renders the page with a token for the created contact when called with website_token' do
|
||||
url = widget_url(website_token: web_widget.website_token)
|
||||
|
||||
expect { get url }.to change(Contact, :count).by(1).and change(ContactInbox, :count).by(1)
|
||||
|
||||
expect(response).to be_successful
|
||||
expect(response.body).not_to include(token)
|
||||
created = web_widget.inbox.contact_inboxes.find_by!(source_id: rendered_token[:source_id])
|
||||
expect(rendered_token[:inbox_id]).to eq(web_widget.inbox.id)
|
||||
expect(rendered_token[:pubsub_token]).to eq(created.pubsub_token)
|
||||
expect(rendered_pubsub_token).to eq(created.pubsub_token)
|
||||
end
|
||||
|
||||
it 'renders the page correctly when called with website_token and cw_conversation' do
|
||||
it 'keeps the session when called with website_token and cw_conversation' do
|
||||
url = widget_url(website_token: web_widget.website_token, cw_conversation: token)
|
||||
|
||||
expect { get url }.not_to(change { [Contact.count, ContactInbox.count] })
|
||||
|
||||
expect(response).to be_successful
|
||||
expect(response.body).to include(token)
|
||||
expect(rendered_pubsub_token).to eq(contact_inbox.pubsub_token)
|
||||
end
|
||||
|
||||
it 'creates the contact with the ids of a token issued without one' do
|
||||
visitor_payload = { source_id: 'visitor', inbox_id: web_widget.inbox.id, pubsub_token: 'stream' }
|
||||
visitor_token = Widget::TokenService.new(payload: visitor_payload).generate_token
|
||||
url = widget_url(website_token: web_widget.website_token, cw_conversation: visitor_token)
|
||||
|
||||
expect { get url }.to change(Contact, :count).by(1).and change(ContactInbox, :count).by(1)
|
||||
|
||||
expect(web_widget.inbox.contact_inboxes.find_by!(source_id: 'visitor').pubsub_token).to eq('stream')
|
||||
expect(response.body).to include(visitor_token)
|
||||
expect(rendered_pubsub_token).to eq('stream')
|
||||
end
|
||||
|
||||
it 'reissues a token minted before pubsub_token was part of it' do
|
||||
legacy_token = Widget::TokenService.new(payload: payload.except(:pubsub_token)).generate_token
|
||||
|
||||
expect { get widget_url(website_token: web_widget.website_token, cw_conversation: legacy_token) }
|
||||
.not_to(change { [Contact.count, ContactInbox.count] })
|
||||
|
||||
expect(response.body).not_to include(legacy_token)
|
||||
expect(rendered_token).to include(source_id: contact_inbox.source_id, pubsub_token: contact_inbox.pubsub_token)
|
||||
expect(rendered_pubsub_token).to eq(contact_inbox.pubsub_token)
|
||||
end
|
||||
|
||||
it 'recreates the contact for a token minted before pubsub_token was part of it when its contact is gone' do
|
||||
legacy_token = Widget::TokenService.new(payload: { source_id: 'purged', inbox_id: web_widget.inbox.id }).generate_token
|
||||
|
||||
expect { get widget_url(website_token: web_widget.website_token, cw_conversation: legacy_token) }
|
||||
.to change(Contact, :count).by(1)
|
||||
|
||||
expect(response.body).not_to include(legacy_token)
|
||||
created = web_widget.inbox.contact_inboxes.find_by!(source_id: 'purged')
|
||||
expect(rendered_token).to include(source_id: 'purged', pubsub_token: created.pubsub_token)
|
||||
end
|
||||
|
||||
it 'starts a new session when the contact behind the token has been deleted' do
|
||||
token
|
||||
contact.delete
|
||||
|
||||
expect { get widget_url(website_token: web_widget.website_token, cw_conversation: token) }.to change(Contact, :count).by(1)
|
||||
|
||||
expect(response).to be_successful
|
||||
expect(response.body).not_to include(token)
|
||||
expect(rendered_token[:source_id]).not_to eq(contact_inbox.source_id)
|
||||
expect(rendered_pubsub_token).not_to eq(contact_inbox.pubsub_token)
|
||||
end
|
||||
|
||||
it 'starts a new session for a token minted for another inbox' do
|
||||
other_widget = create(:channel_widget, account: account)
|
||||
token
|
||||
|
||||
expect { get widget_url(website_token: other_widget.website_token, cw_conversation: token) }.to change(Contact, :count).by(1)
|
||||
|
||||
expect(response.body).not_to include(token)
|
||||
expect(rendered_token[:inbox_id]).to eq(other_widget.inbox.id)
|
||||
expect(rendered_token[:source_id]).not_to eq(contact_inbox.source_id)
|
||||
expect(rendered_pubsub_token).not_to eq(contact_inbox.pubsub_token)
|
||||
end
|
||||
|
||||
it 'creates a contact for a regular browser' do
|
||||
|
||||
Reference in New Issue
Block a user