Create Chat app (#175)

* Setup Pusher gem

* Create ChatChannel and Message model

* Create ChatChannelController

* Add back fix-db-schema-conflicts gem

* Add pusher-js package

* Add validations on Message & ChatChannel models

* Update Route

* Create MessageController

* Update ChatChannelController

* Create Chat app WIP

* Extract messenger render to Message Component

* Implement live chat WIP

* Implement live chat WIP (2)

* Add style and scrollToBottom feature for chat

* Create getUserDataAndCsrfToken function

* Add feature toggle to the new live chat

* Clean up ChatChannelsController & create spec

* Update ChatChannel spec

* Update Message spec

* Create MessagesController spec & refactor

* Clean up Chat app

* Fix lint

* Add character limit to Message
This commit is contained in:
Mac Siri 2018-04-04 17:19:57 -04:00 committed by Ben Halpern
parent 943c3b392f
commit 8003191a4e
33 changed files with 623 additions and 29 deletions

View file

@ -58,6 +58,7 @@ gem "pry", "~> 0.11"
gem "pry-rails", "~> 0.3"
gem "puma", "~> 3.11"
gem "puma_worker_killer", "~> 0.1"
gem "pusher", "~> 1.3"
gem "pundit", "~> 1.1"
gem "rack-host-redirect", "~> 1.3"
gem "rack-timeout", "~> 0.4"
@ -106,6 +107,7 @@ group :development, :test do
gem "capybara", "~> 2.18"
gem "derailed", "~> 0.1"
gem "faker", "~> 1.8"
gem "fix-db-schema-conflicts", "~> 3.0"
gem "memory_profiler", "~> 0.9"
gem "parallel_tests", "~> 2.21"
gem "rspec-rails", "~> 3.7"

View file

@ -273,6 +273,8 @@ GEM
thor (~> 0.14)
fission (0.5.0)
CFPropertyList (~> 2.2)
fix-db-schema-conflicts (3.0.2)
rubocop (>= 0.38.0)
flipflop (2.3.1)
activesupport (>= 4.0)
fog (1.41.0)
@ -604,6 +606,11 @@ GEM
puma (>= 2.7, < 4)
pundit (1.1.0)
activesupport (>= 3.0.0)
pusher (1.3.1)
httpclient (~> 2.7)
multi_json (~> 1.0)
pusher-signature (~> 0.1.8)
pusher-signature (0.1.8)
rack (2.0.4)
rack-host-redirect (1.3.0)
rack
@ -917,6 +924,7 @@ DEPENDENCIES
feedjira (~> 2.1)
fetch-rails (~> 1.0)
figaro (~> 1.1)
fix-db-schema-conflicts (~> 3.0)
flipflop (~> 2.3)
fog (~> 1.41)
front_matter_parser (~> 0.1)
@ -947,6 +955,7 @@ DEPENDENCIES
puma (~> 3.11)
puma_worker_killer (~> 0.1)
pundit (~> 1.1)
pusher (~> 1.3)
rack-host-redirect (~> 1.3)
rack-timeout (~> 0.4)
rack_session_access (~> 0.1)

View file

@ -0,0 +1,70 @@
// High level class
.chatchannel {
height: inherit;
display: flex;
flex-direction: column;
justify-content: space-between;
border: 1px solid #c9c9c9;
}
.chatchannel__messages {
font-size: 15px;
padding: 10px;
display: flex;
flex-direction: column;
flex-grow: 1;
height: 300px;
overflow-y: scroll;
overflow-x: hidden;
text-align: left;
}
.chatchannel__form {
border-top: 1px solid #c9c9c9;
background: #ededed;
width: 100%;
height: 70px;
}
// Chatmessage
.chatmessage {
display: inline-block;
margin-bottom: 3px;
}
.chatmessage__username {
font-weight: 500;
}
.chatmessage__divider {
}
.chatmessage__message {
}
// Messagecomposer
.messagecomposer {
display: flex;
height: inherit;
align-items: stretch;
}
.messagecomposer__input {
border-radius: 3px;
border: 1px solid #c9c9c9;
font-size: 13px;
margin: 10px;
padding: 6px;
resize: none;
width: 80%;
}
.messagecomposer__submit {
margin: 10px 10px 10px 0;
border: 1px solid black;
border-radius: 3px;
background: #66e2d5;
font-weight: 500px;
}

View file

@ -27,4 +27,5 @@
@import 'ltags/LiquidTags';
@import 'chat';
@import 'livechat/chat';

View file

@ -0,0 +1,11 @@
class ChatChannelsController < ApplicationController
def show
@chat_channel = ChatChannel.includes(:messages).find_by(id: params[:id])
if @chat_channel
@chat_channel
else
render json: ["The chat channel you are looking for is either invalid or does not exist"],
status: 401
end
end
end

View file

@ -0,0 +1,42 @@
class MessagesController < ApplicationController
before_action :authenticate_user!
def create
@message = Message.new(message_params)
success = false
if @message.save
begin
message_json = create_pusher_payload(@message)
Pusher.trigger(@message.chat_channel.id, "message-created", message_json)
success = true
rescue Pusher::Error => e
logger.info "PUSHER ERROR: #{e.message}"
end
if success
render json: ["Message created"], status: 201
else
result = "Message created but could not trigger Pusher"
render json: [result, @message.to_json], status: 201
end
else
render json: e.message, status: 401
end
end
private
def create_pusher_payload(new_message)
{
username: new_message.user.username,
message: new_message.message_markdown,
timestamp: new_message.timestamp,
color: new_message.user.bg_color_hex,
}.to_json
end
def message_params
params.require(:message).permit(:message_html, :user_id, :chat_channel_id)
end
end

View file

@ -0,0 +1,27 @@
export function getAllMessages(successCb, failureCb) {
fetch('/chat_channels/1', {
Accept: 'application/json',
'Content-Type': 'application/json',
}).then(response => response.json())
.then(successCb)
.catch(failureCb);
}
export function sendMessage(message, successCb, failureCb) {
fetch('/messages', {
method: 'POST',
headers: {
Accept: 'application/json',
'X-CSRF-Token': window.csrfToken,
'Content-Type': 'application/json',
},
body: JSON.stringify({
message_html: message,
user_id: window.currentUser.id,
chat_channel_id: '1',
}),
credentials: 'same-origin',
}).then(response => response.json())
.then(successCb)
.catch(failureCb);
}

View file

@ -0,0 +1,97 @@
import { h, Component } from 'preact';
import PropTypes from 'prop-types';
import { getAllMessages, sendMessage } from './actions';
import { scrollToBottom } from './util';
import Compose from './compose';
import Message from './message';
import setupPusher from './pusher';
class Chat extends Component {
constructor(props) {
super(props);
this.receiveAllMessages = this.receiveAllMessages.bind(this);
this.receiveNewMessage = this.receiveNewMessage.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleMessageSubmit = this.handleMessageSubmit.bind(this);
this.handleSubmitOnClick = this.handleSubmitOnClick.bind(this);
this.handleFailure = this.handleFailure.bind(this);
this.state = {
messages: [],
};
}
componentDidMount() {
getAllMessages(this.receiveAllMessages);
setupPusher(this.props.pusherKey, this.receiveNewMessage);
}
componentDidUpdate() {
scrollToBottom();
}
receiveAllMessages(res) {
this.setState({ messages: res.messages });
}
receiveNewMessage(message) {
const newMessages = this.state.messages.slice();
newMessages.push(message);
this.setState({ messages: newMessages });
}
handleKeyDown(e) {
if (e.keyCode === 13) {
e.preventDefault();
this.handleMessageSubmit(e.target.value);
e.target.value = '';
}
}
handleMessageSubmit(message) {
sendMessage(message, null, this.handleFailure);
}
handleSubmitOnClick(e) {
e.preventDefault();
const message = document.getElementById('messageform').value;
sendMessage(message, null, this.handleFailure);
document.getElementById('messageform').value = '';
}
handleFailure(err) {
console.error(err);
}
renderMessage() {
return this.state.messages.map(message => (
<Message
user={message.username}
message={message.message}
timeStamp={message.timestamp}
color={message.color}
/>
));
}
render() {
return (
<div className="chatchannel">
<div className="chatchannel__messages" id="messagelist">
{ this.renderMessage() }
</div>
<div className="chatchannel__form">
<Compose
handleKeyDown={this.handleKeyDown}
handleSubmitOnClick={this.handleSubmitOnClick}
/>
</div>
</div>
);
}
}
Chat.propTypes = {
pusherKey: PropTypes.number.isRequired,
};
export default Chat;

View file

@ -0,0 +1,28 @@
import { h } from 'preact';
import PropTypes from 'prop-types';
const Compose = ({
handleSubmitOnClick, handleKeyDown,
}) => (
<div className="messagecomposer">
<textarea
className="messagecomposer__input"
id="messageform"
placeholder="Message goes here"
onKeyDown={handleKeyDown}
/>
<button
className="messagecomposer__submit"
onClick={handleSubmitOnClick}
>
SEND
</button>
</div>
);
Compose.propTypes = {
handleKeyDown: PropTypes.func.isRequired,
handleSubmitOnClick: PropTypes.func.isRequired,
};
export default Compose;

View file

@ -0,0 +1,27 @@
import { h } from 'preact';
import PropTypes from 'prop-types';
/*
* The prop also contain timeStamp, which is currently not in used
*/
const Message = ({
user, message, color,
}) => {
const spanStyle = { color };
return (
<div className="chatmessage">
<span className="chatmessage__username" style={spanStyle}>{ user }</span>
<span className="chatmessage__divider">: </span>
<span className="chatmessage__message">{ message } </span>
</div>
);
};
Message.propTypes = {
user: PropTypes.string.isRequired,
color: PropTypes.string.isRequired,
message: PropTypes.string.isRequired,
};
export default Message;

View file

@ -0,0 +1,11 @@
import Pusher from 'pusher-js';
export default function setupPusher(key, callback) {
const pusher = new Pusher(key, {
cluster: 'us2',
encrypted: true,
});
const channel = pusher.subscribe('1');
channel.bind('message-created', callback);
}

View file

@ -0,0 +1,32 @@
export function getUserDataAndCsrfToken() {
const promise = new Promise((resolve, reject) => {
let i = 0;
const waitingOnUserData = setInterval(() => {
let userData = null;
const dataUserAttribute = document.body.getAttribute('data-user');
const meta = document.querySelector("meta[name='csrf-token']");
if (dataUserAttribute &&
dataUserAttribute !== 'undefined' &&
dataUserAttribute !== undefined &&
meta &&
meta.content !== 'undefined' &&
meta.content !== undefined) {
userData = JSON.parse(dataUserAttribute);
}
i += 1;
if (userData) {
clearInterval(waitingOnUserData);
resolve(userData);
} else if (i === 3000) {
clearInterval(waitingOnUserData);
reject(new Error("Couldn't find user data on page."));
}
}, 5);
});
return promise;
}
export function scrollToBottom() {
const element = document.getElementById('messagelist');
element.scrollTop = element.scrollHeight;
}

View file

@ -0,0 +1,22 @@
import { h, render } from 'preact';
import { getUserDataAndCsrfToken } from '../chat/util';
import Chat from '../chat/chat';
HTMLDocument.prototype.ready = new Promise((resolve) => {
if (document.readyState !== 'loading') { return resolve(); }
document.addEventListener('DOMContentLoaded', () => resolve());
});
document.ready
.then(getUserDataAndCsrfToken()
.then((currentUser) => {
if (document.getElementById('chat')) {
const { pusherKey } = document.getElementById('chat').dataset;
window.currentUser = currentUser;
window.csrfToken = document.querySelector("meta[name='csrf-token']").content;
render(
<Chat pusherKey={pusherKey} />,
document.getElementById('chat'),
);
}
}));

View file

@ -0,0 +1,5 @@
class ChatChannel < ApplicationRecord
has_many :messages
validates :channel_type, presence: true, inclusion: { in: %w(open) }
end

19
app/models/message.rb Normal file
View file

@ -0,0 +1,19 @@
class Message < ApplicationRecord
belongs_to :user
belongs_to :chat_channel
validates :message_html, presence: true, length: { maximum: 600 }
validates :message_markdown, presence: true
before_validation :evaluate_markdown
def timestamp
created_at.strftime("%H:%M:%S")
end
private
def evaluate_markdown
self.message_markdown = message_html
end
end

View file

@ -11,18 +11,19 @@ class User < ApplicationRecord
acts_as_followable
acts_as_follower
has_many :articles
has_many :reactions
belongs_to :organization, optional: true
has_many :comments
has_many :identities
has_many :articles
has_many :collections
has_many :tweets
has_many :notifications
has_many :mentions
has_many :comments
has_many :email_messages, class_name: "Ahoy::Message"
has_many :notes
has_many :github_repos
has_many :identities
has_many :mentions
has_many :messages
has_many :notes
has_many :notifications
has_many :reactions
has_many :tweets
mount_uploader :profile_image, ProfileImageUploader

View file

@ -0,0 +1,6 @@
json.messages @chat_channel.messages do |message|
json.username message.user.username
json.message message.message_markdown
json.timestamp message.timestamp
json.color message.user.bg_color_hex
end

View file

@ -1,5 +1,9 @@
<% if user_signed_in? %>
<%= javascript_pack_tag 'sendbird', defer: true %>
<% if (Flipflop.sendbird?) %>
<%= javascript_pack_tag 'sendbird', defer: true %>
<% else %>
<%= javascript_pack_tag 'chat', defer: true %>
<% end %>
<% end %>
<% title "DEV LIVE" %>
<link rel="canonical" href="https://dev.to/live"/>
@ -31,7 +35,11 @@
<script src="//player.dacast.com/js/player.js" width="1500" height="800" id="<%= ENV["DACAST_STREAM_CODE"]%>" player="jw7" jwurl="juQPHW/th8VUHaO/2KkeVlAdGN9ksJZog2z6SH+TyMU=" class="dacast-video"></script>
</div>
<div class="live-chat-wrapper">
<div id="sb_chat" class="live-chat" data-sendbird-app-id="<%= ENV["SENDBIRD_APP_ID"] %>" data-sendbird-livechat-url="<%= ENV["SENDBIRD_LIVECHAT_URL"] %>"></div>
<% if (Flipflop.sendbird?) %>
<div id="sb_chat" class="live-chat" data-sendbird-app-id="<%= ENV["SENDBIRD_APP_ID"] %>" data-sendbird-livechat-url="<%= ENV["SENDBIRD_LIVECHAT_URL"] %>"></div>
<% else %>
<div id="chat" class="live-chat" data-pusher-key="<%= ENV["PUSHER_KEY"] %>"</div>
<% end %>
</div>
</div>
<div class="live-under-message">DEV Live is in beta. If you are having issues, you may need to try another browser or contact <a href="mailto:members@dev.to">members@dev.to</a> for support. ❤️</div>

View file

@ -28,4 +28,7 @@ Flipflop.configure do
feature :she_coded,
default: false,
description: "Toggle #shecoded sidebar"
feature :sendbird,
default: true,
description: "Toggle between Sendbird and our custom chat"
end

View file

@ -16,9 +16,9 @@ keys = [
"BUFFER_LINKEDIN_ID",
"BUFFER_PROFILE_ID",
"BUFFER_TWITTER_ID",
"CLOUDINARY_CLOUD_NAME",
"CLOUDINARY_API_KEY",
"CLOUDINARY_API_SECRET",
"CLOUDINARY_CLOUD_NAME",
"CLOUDINARY_SECURE",
"DACAST_STREAM_CODE",
"DEPLOYMENT_SIGNATURE",
@ -39,8 +39,14 @@ keys = [
"MAILCHIMP_NEWSLETTER_ID",
"PERIODIC_EMAIL_DIGEST_MAX",
"PERIODIC_EMAIL_DIGEST_MIN",
"PUSHER_APP_ID",
"PUSHER_CLUSTER",
"PUSHER_KEY",
"PUSHER_SECRET",
"RECAPTCHA_SECRET",
"RECAPTCHA_SITE",
"SENDBIRD_APP_ID",
"SENDBIRD_LIVECHAT_URL",
"SERVICE_TIMEOUT",
"SHARE_MEOW_BASE_URL",
"SHARE_MEOW_SECRET_KEY",
@ -51,8 +57,6 @@ keys = [
"STREAM_URL",
"STRIPE_PUBLISHABLE_KEY",
"STRIPE_SECRET_KEY",
"SENDBIRD_APP_ID",
"SENDBIRD_LIVECHAT_URL",
"TWITTER_ACCESS_TOKEN",
"TWITTER_ACCESS_TOKEN_SECRET",
"TWITTER_KEY",

View file

@ -0,0 +1,8 @@
require "pusher"
Pusher.app_id = ENV["PUSHER_APP_ID"]
Pusher.key = ENV["PUSHER_KEY"]
Pusher.secret = ENV["PUSHER_SECRET"]
Pusher.cluster = ENV["PUSHER_CLUSTER"]
Pusher.logger = Rails.logger
Pusher.encrypted = true

View file

@ -57,6 +57,8 @@ Rails.application.routes.draw do
resources :reads, only: [:create]
end
resources :messages, only: [:create]
resources :chat_channels, only: [:show]
resources :articles, only: [:update,:create,:destroy]
resources :comments, only:[:create,:update,:destroy]
resources :users, only:[:update]

View file

@ -0,0 +1,9 @@
class CreateChatChannels < ActiveRecord::Migration[5.1]
def change
create_table :chat_channels do |t|
t.string :channel_type, null: false
t.timestamps
end
end
end

View file

@ -0,0 +1,12 @@
class CreateMessages < ActiveRecord::Migration[5.1]
def change
create_table :messages do |t|
t.string :message_html, null: false
t.string :message_markdown, null: false
t.references :user, foreign_key: true, null: false
t.references :chat_channel, foreign_key: true, null: false
t.timestamps
end
end
end

View file

@ -10,8 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20180321170500) do
ActiveRecord::Schema.define(version: 20180328194253) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -45,6 +44,7 @@ ActiveRecord::Schema.define(version: 20180321170500) do
t.boolean "automatically_renew", default: false
t.text "body_html"
t.text "body_markdown"
t.boolean "boosted", default: false
t.string "cached_tag_list"
t.string "cached_user_name"
t.string "cached_user_username"
@ -68,6 +68,7 @@ ActiveRecord::Schema.define(version: 20180321170500) do
t.integer "job_opportunity_id"
t.string "language"
t.datetime "last_buffered"
t.datetime "last_comment_at", default: "2017-01-01 05:00:00"
t.datetime "last_invoiced_at"
t.decimal "lat", precision: 10, scale: 6
t.boolean "live_now", default: false
@ -105,8 +106,6 @@ ActiveRecord::Schema.define(version: 20180321170500) do
t.string "video_code"
t.string "video_source_url"
t.string "video_thumbnail_url"
t.datetime "last_comment_at", default: "2017-01-01 05:00:00"
t.boolean "boosted", default: false
t.index ["featured_number"], name: "index_articles_on_featured_number"
t.index ["hotness_score"], name: "index_articles_on_hotness_score"
t.index ["published_at"], name: "index_articles_on_published_at"
@ -143,6 +142,12 @@ ActiveRecord::Schema.define(version: 20180321170500) do
t.string "type_of"
end
create_table "chat_channels", force: :cascade do |t|
t.string "channel_type", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "collections", id: :serial, force: :cascade do |t|
t.datetime "created_at", null: false
t.string "description"
@ -216,19 +221,19 @@ ActiveRecord::Schema.define(version: 20180321170500) do
end
create_table "events", force: :cascade do |t|
t.string "title"
t.string "category"
t.datetime "starts_at"
t.string "cover_image"
t.datetime "created_at", null: false
t.text "description_html"
t.text "description_markdown"
t.datetime "ends_at"
t.string "location_name"
t.string "location_url"
t.string "cover_image"
t.text "description_markdown"
t.text "description_html"
t.boolean "published"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "slug"
t.datetime "starts_at"
t.string "title"
t.datetime "updated_at", null: false
end
create_table "feedback_messages", force: :cascade do |t|
@ -319,6 +324,17 @@ ActiveRecord::Schema.define(version: 20180321170500) do
t.integer "user_id"
end
create_table "messages", force: :cascade do |t|
t.bigint "chat_channel_id", null: false
t.datetime "created_at", null: false
t.string "message_html", null: false
t.string "message_markdown", null: false
t.datetime "updated_at", null: false
t.bigint "user_id", null: false
t.index ["chat_channel_id"], name: "index_messages_on_chat_channel_id"
t.index ["user_id"], name: "index_messages_on_user_id"
end
create_table "notes", id: :serial, force: :cascade do |t|
t.text "content"
t.datetime "created_at", null: false
@ -358,13 +374,13 @@ ActiveRecord::Schema.define(version: 20180321170500) do
t.string "state"
t.string "story"
t.text "summary"
t.string "tag_line"
t.string "tech_stack"
t.string "text_color_hex"
t.string "twitter_username"
t.datetime "updated_at", null: false
t.string "url"
t.string "zip_code"
t.string "tag_line"
t.index ["slug"], name: "index_organizations_on_slug", unique: true
end
@ -410,9 +426,9 @@ ActiveRecord::Schema.define(version: 20180321170500) do
t.text "status_notice", default: ""
t.string "title"
t.string "twitter_username"
t.boolean "unique_website_url?", default: true
t.datetime "updated_at", null: false
t.string "website_url"
t.boolean "unique_website_url?", default: true
end
create_table "reactions", id: :serial, force: :cascade do |t|
@ -534,6 +550,7 @@ ActiveRecord::Schema.define(version: 20180321170500) do
t.string "education"
t.string "email", default: "", null: false
t.boolean "email_comment_notifications", default: true
t.boolean "email_digest_periodic", default: true, null: false
t.boolean "email_follower_notifications", default: true
t.boolean "email_membership_newsletter", default: false
t.boolean "email_mention_notifications", default: true
@ -573,6 +590,7 @@ ActiveRecord::Schema.define(version: 20180321170500) do
t.boolean "onboarding_package_requested_again", default: false
t.boolean "org_admin", default: false
t.integer "organization_id"
t.boolean "permit_adjacent_sponsors", default: true
t.datetime "personal_data_updated_at"
t.string "profile_image"
t.integer "reactions_count", default: 0, null: false
@ -617,8 +635,6 @@ ActiveRecord::Schema.define(version: 20180321170500) do
t.string "username"
t.string "website_url"
t.datetime "workshop_expiration"
t.boolean "permit_adjacent_sponsors", default: true
t.boolean "email_digest_periodic", default: true, null: false
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
t.index ["language_settings"], name: "index_users_on_language_settings", using: :gin
t.index ["organization_id"], name: "index_users_on_organization_id"
@ -632,4 +648,6 @@ ActiveRecord::Schema.define(version: 20180321170500) do
t.index ["user_id", "role_id"], name: "index_users_roles_on_user_id_and_role_id"
end
add_foreign_key "messages", "chat_channels"
add_foreign_key "messages", "users"
end

View file

@ -52,6 +52,7 @@
"babel-plugin-transform-react-jsx": "^6.24.1",
"preact": "^8.2.5",
"prop-types": "^15.6.0",
"pusher-js": "^4.2.2",
"sendbird": "^3.0.52"
}
}
}

View file

@ -0,0 +1,5 @@
FactoryBot.define do
factory :chat_channel do
channel_type { "open" }
end
end

View file

View file

@ -0,0 +1,6 @@
require "rails_helper"
RSpec.describe ChatChannel, type: :model do
it { is_expected.to have_many(:messages) }
it { is_expected.to validate_presence_of(:channel_type) }
end

View file

@ -0,0 +1,8 @@
require "rails_helper"
RSpec.describe Message, type: :model do
it { is_expected.to belong_to(:user) }
it { is_expected.to belong_to(:chat_channel) }
it { is_expected.to validate_presence_of(:message_html) }
it { is_expected.to validate_presence_of(:message_markdown) }
end

View file

@ -0,0 +1,33 @@
require "rails_helper"
RSpec.describe "ChatChannels", type: :request do
describe "GET /chat_channels/:id" do
context "when request is valid" do
let(:chat_channel) { create(:chat_channel) }
before do
get "/chat_channels/#{chat_channel.id}", headers: { HTTP_ACCEPT: "application/json" }
end
it "returns 200" do
expect(response.status).to eq(200)
end
it "returns the channel" do
expect(response).to render_template(:show)
end
end
context "when request is invalid" do
before { get "/chat_channels/1" }
it "returns proper error message" do
expect(response.body).to include("invalid")
end
it "returns 401" do
expect(response.status).to eq(401)
end
end
end
end

View file

@ -0,0 +1,50 @@
require "rails_helper"
RSpec.describe "Messages", type: :request do
let(:user) { create(:user) }
let(:chat_channel) { create(:chat_channel) }
describe "POST /messages" do
let(:new_message) do
{
message_html: "hi",
user_id: user.id,
chat_channel_id: chat_channel.id,
}
end
it "requires user to be signed in" do
post "/messages", params: { message: new_message }
expect(response.status).to eq(302)
end
# Pusher::Error
context "when user is signed in" do
before do
allow(Pusher).to receive(:trigger).and_return(true)
sign_in user
post "/messages", params: { message: new_message }
end
it "returns 201 upon success" do
expect(response.status).to eq(201)
end
it "returns in json" do
expect(response.content_type).to eq("application/json")
end
end
context "when Pusher isn't cooperating" do
before do
allow(Pusher).to receive(:trigger).and_raise(Pusher::Error)
sign_in user
post "/messages", params: { message: new_message }
end
it "returns proper message" do
expect(response.body).to include("could not trigger Pusher")
end
end
end
end

View file

@ -3462,6 +3462,12 @@ fastparse@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.1.tgz#d1e2643b38a94d7583b479060e6c4affc94071f8"
faye-websocket@0.9.4:
version "0.9.4"
resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.9.4.tgz#885934c79effb0409549e0c0a3801ed17a40cdad"
dependencies:
websocket-driver ">=0.5.1"
faye-websocket@^0.10.0:
version "0.10.0"
resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4"
@ -6893,6 +6899,13 @@ punycode@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.0.tgz#5f863edc89b96db09074bad7947bf09056ca4e7d"
pusher-js@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/pusher-js/-/pusher-js-4.2.2.tgz#de391bfb14d221ef96f462304f8a73b95ce9acc5"
dependencies:
faye-websocket "0.9.4"
xmlhttprequest "^1.8.0"
q@^1.1.2:
version "1.5.0"
resolved "https://registry.yarnpkg.com/q/-/q-1.5.0.tgz#dd01bac9d06d30e6f219aecb8253ee9ebdc308f1"
@ -8819,6 +8832,10 @@ xml-name-validator@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-2.0.1.tgz#4d8b8f1eccd3419aa362061becef515e1e559635"
xmlhttprequest@^1.8.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz#67fe075c5c24fef39f9d65f5f7b7fe75171968fc"
xtend@^4.0.0, xtend@~4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"