Add initial invitation framework for chat channels (#528)

* Add initial invitation framework for chat channels

* Update chat_channel specs

* Add DIY concept of accepting invite

* Add some js and fix spec

* Add full invite accept functionality

* Remove console.log from chat
This commit is contained in:
Ben Halpern 2018-07-02 16:31:20 -04:00 committed by GitHub
parent a3aa5b5bd2
commit 94e5154668
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 520 additions and 41 deletions

View file

@ -188,6 +188,25 @@
}
}
.chatNonChatView{
position: absolute;
left: 0;
right: 0;
bottom: 0;
top: 0;
padding: 80px 7%;
background: white;
}
.chatNonChatView_exitbutton{
position: absolute;
left: 4%;
top: 60px;
font-size:25px;
background: white;
border:0px;
}
.chat__videocallexitbutton{
border: 0px;
font-size: 20px;
@ -343,6 +362,18 @@
border-top: 1px solid $light-medium-gray;
}
.chat__channelinvitationsindicator button {
background: $green;
text-align: center;
padding: 10px 0px;
display: block;
border-radius: 3px;
margin: 3px 0px;
border: 0px;
width: 93%;
font-size: 0.9em;
}
.chatchannels {
display: flex;
flex-direction: column;

View file

@ -16,7 +16,11 @@ module Api
end
def show
@user = User.find(params[:id])
if params[:id] == "by_username"
@user = User.find_by_username(params[:url])
else
@user = User.find(params[:id])
end
end
def less_than_one_day_old?(user)

View file

@ -0,0 +1,26 @@
class ChatChannelMembershipsController < ApplicationController
def create
@chat_channel = ChatChannel.find(params[:chat_channel_id])
authorize @chat_channel, :update?
ChatChannelMembership.create(
user_id: params[:user_id],
chat_channel_id: @chat_channel.id,
status: "pending"
)
end
def update
@chat_channel_membership = ChatChannelMembership.find(params[:id])
authorize @chat_channel_membership
if params[:user_action] == "accept"
@chat_channel_membership.update(status: "active")
else
@chat_channel_membership.update(status: "rejected")
end
@chat_channels_memberships = current_user.
chat_channel_memberships.includes(:chat_channel).
where(status: "pending").
order("chat_channel_memberships.updated_at DESC")
render "chat_channels/index.json"
end
end

View file

@ -6,6 +6,9 @@ class ChatChannelsController < ApplicationController
if params[:state] == "unopened"
authorize ChatChannel
render_unopened_json_response
elsif params[:state] == "pending"
authorize ChatChannel
render_pending_json_response
else
skip_authorization
render_channels_html
@ -17,6 +20,27 @@ class ChatChannelsController < ApplicationController
authorize @chat_channel
end
def create
authorize ChatChannel
@chat_channel = ChatChannelCreationService.new(current_user, params[:chat_channel]).create
if @chat_channel.valid?
render json: { status: "success", chat_channel: @chat_channel.to_json(only: [:channel_name,:slug]) }, status: 200
else
render json: { errors: @chat_channel.errors.full_messages }
end
end
def update
@chat_channel = ChatChannel.find(params[:id])
authorize @chat_channel
ChatChannelUpdateService.new(@chat_channel, chat_channel_params).update
if @chat_channel.valid?
render json: { status: "success", chat_channel: @chat_channel.to_json(only: [:channel_name,:slug]) }, status: 200
else
render json: { errors: @chat_channel.errors.full_messages }
end
end
def open
@chat_channel = ChatChannel.find(params[:id])
authorize @chat_channel
@ -60,7 +84,7 @@ class ChatChannelsController < ApplicationController
private
def chat_channel_params
params.require(:chat_channel).permit(:command)
params.require(:chat_channel).permit(policy(ChatChannel).permitted_attributes)
end
def render_unopened_json_response
@ -75,6 +99,19 @@ class ChatChannelsController < ApplicationController
render "index.json"
end
def render_pending_json_response
if current_user
@chat_channels_memberships = current_user.
chat_channel_memberships.includes(:chat_channel).
where(status: "pending").
order("chat_channel_memberships.updated_at DESC")
else
@chat_channels_memberships = []
end
render "index.json"
end
def render_channels_html
return unless current_user
if params[:slug]

View file

@ -142,4 +142,33 @@ export function getJSONContents(url, successCb, failureCb) {
.then(response => response.json())
.then(successCb)
.catch(failureCb);
}
export function getChannelInvites(successCb, failureCb) {
fetch('/chat_channels?state=pending', {
Accept: 'application/json',
'Content-Type': 'application/json',
credentials: 'same-origin',
})
.then(response => response.json())
.then(successCb)
.catch(failureCb);
};
export function sendChannelInviteAction(id, action, successCb, failureCb) {
fetch('/chat_channel_memberships/'+id, {
method: 'PUT',
headers: {
Accept: 'application/json',
'X-CSRF-Token': window.csrfToken,
'Content-Type': 'application/json',
},
body: JSON.stringify({
user_action: action,
}),
credentials: 'same-origin',
})
.then(response => response.json())
.then(successCb)
.catch(failureCb);
}

View file

@ -0,0 +1,31 @@
import { h, Component } from 'preact';
export default class ChannelDetails extends Component {
render() {
const channel = this.props.channel
const users = Object.values(channel.channel_users).map((user) => {
return <div>
<a
href={'/'+user.username}
style={{color:user.darker_color,padding: "3px 0px"}}
data-content={`users/by_username?url=${user.username}`}
>
{user.name}
</a>
</div>
});
let subHeader = ''
if (users.length === 80) {
subHeader = <h3>Recently Active Members</h3>
}
return <div>
<h1>{channel.channel_name}</h1>
{subHeader}
{users}
</div>
}
}

View file

@ -1,6 +1,6 @@
import { h, Component } from 'preact';
import PropTypes from 'prop-types';
import { conductModeration, getAllMessages, sendMessage, sendOpen, getChannels, getContent } from './actions';
import { conductModeration, getAllMessages, sendMessage, sendOpen, getChannels, getContent, getChannelInvites, sendChannelInviteAction } from './actions';
import { hideMessages, scrollToBottom, setupObserver, setupNotifications, getNotificationState } from './util';
import Alert from './alert';
import Channels from './channels';
@ -8,6 +8,7 @@ import Compose from './compose';
import Message from './message';
import Content from './content';
import Video from './video';
import View from './view';
import setupPusher from '../src/utils/pusher';
@ -42,7 +43,9 @@ export default class Chat extends Component {
isMobileDevice: typeof window.orientation !== "undefined",
subscribedPusherChannels: [],
activeVideoChannelId: null,
incomingVideoCallChannelIds: []
incomingVideoCallChannelIds: [],
nonChatView: null,
inviteChannels: []
};
}
@ -77,7 +80,9 @@ export default class Chat extends Component {
if (document.getElementById('chatchannels__channelslist')) {
document.getElementById('chatchannels__channelslist').addEventListener('scroll', this.handleChannelScroll);
}
getChannelInvites(this.handleChannelInvites,null);
}
componentDidUpdate() {
if (!this.state.scrolled) {
scrollToBottom();
@ -320,6 +325,10 @@ export default class Chat extends Component {
}
}
handleChannelInvites = response => {
this.setState({inviteChannels: response})
}
handleKeyDown = e => {
const enterPressed = e.keyCode === 13;
const targetValue = e.target.value;
@ -446,21 +455,26 @@ export default class Chat extends Component {
}
triggerActiveContent = e => {
e.preventDefault();
e.stopPropagation();
const target = e.target
let newActiveContent = this.state.activeContent
if (e.target.dataset.content && e.target.dataset.content != "exit") {
e.preventDefault();
if (target.dataset.content === 'channel-details') {
newActiveContent[this.state.activeChannelId] = {
type_of: "channel-details",
channel: this.state.activeChannel
}
} else if (target.dataset.content && target.dataset.content.startsWith('users/')) {
newActiveContent[this.state.activeChannelId] = {type_of: "loading-user"}
getContent('/api/'+target.dataset.content, this.setActiveContent, null)
} else if (target.tagName.toLowerCase() === 'a' && target.href.startsWith('https://dev.to/')) {
e.preventDefault();
newActiveContent[this.state.activeChannelId] = {type_of: "loading-post"}
getContent(`/api/articles/by_path?url=${target.href.split('https://dev.to')[1]}`, this.setActiveContent, null)
} else if (target.dataset.content === "exit") {
e.preventDefault();
newActiveContent[this.state.activeChannelId] = null
}
this.setState({activeContent: newActiveContent})
this.setState({activeContent: newActiveContent})
return false;
}
setActiveContent = response => {
@ -482,6 +496,20 @@ export default class Chat extends Component {
this.setState({ chatChannels: newChannelsObj });
};
handleInvitationAccept = e => {
const id = e.target.dataset.content
sendChannelInviteAction(id, 'accept', this.handleChannelInviteResult, null)
}
handleInvitationDecline = e => {
const id = e.target.dataset.content
sendChannelInviteAction(id, 'decline', this.handleChannelInviteResult, null)
}
handleChannelInviteResult = response => {
this.setState({inviteChannels: response})
}
triggerChannelTypeFilter = e => {
const type = e.target.dataset.channelType;
this.setState({
@ -492,6 +520,14 @@ export default class Chat extends Component {
getChannels(this.state.filterQuery, null, this.props, 0, filters, this.loadChannels);
}
triggerNonChatView = e => {
this.setState({nonChatView: e.target.dataset.content})
}
triggerExitView = () => {
this.setState({nonChatView: null})
}
handleFailure = err => {
console.error(err);
};
@ -538,6 +574,7 @@ export default class Chat extends Component {
const notificationsPermission = this.state.notificationsPermission;
let notificationsButton = "";
let notificationsState = "";
let invitesButton = ''
if (notificationsPermission === "waiting-permission") {
notificationsButton = <div><button className="chat__notificationsbutton " onClick={this.triggerNotificationRequest}>Turn on Notifications</button></div>;
} else if (notificationsPermission === "granted") {
@ -545,12 +582,18 @@ export default class Chat extends Component {
} else if (notificationsPermission === "denied") {
notificationsState = <div className="chat_chatconfig chat_chatconfig--off">Notificatins Off</div>
}
if (this.state.inviteChannels.length > 0) {
invitesButton = <div class='chat__channelinvitationsindicator'><button onClick={this.triggerNonChatView} data-content='invitations'>
New Invitations!
</button></div>
}
if (this.state.expanded) {
return (
<div className="chat__channels chat__channels--expanded">
{notificationsButton}
<button className="chat__channelstogglebutt" onClick={this.toggleExpand}>{"<"}</button>
<input placeholder='Filter' onKeyUp={this.triggerChannelFilter} />
{invitesButton}
<div className='chat__channeltypefilter'>
<button data-channel-type='all' onClick={this.triggerChannelTypeFilter} className={`chat__channeltypefilterbutton chat__channeltypefilterbutton--${this.state.channelTypeFilter === 'all' ? 'active' : 'inactive'}`}>
all
@ -619,8 +662,8 @@ export default class Chat extends Component {
</div>
</div>
<Content
onTriggerContent={this.triggerActiveContent}
resource={this.state.activeContent[this.state.activeChannelId]}
onExit={this.triggerActiveContent}
activeChannelId={this.state.activeChannelId}
pusherKey={this.props.pusherKey}
githubToken={this.props.githubToken}
@ -638,10 +681,10 @@ export default class Chat extends Component {
let channelHeaderInner = ''
if (currentChannel.channel_type === "direct") {
const username = currentChannel.slug.replace(`${window.currentUser.username}/`, '').replace(`/${window.currentUser.username}`, '');
channelHeaderInner = <a href={'/'+username}>@{username}</a>
channelHeaderInner = <a href={'/'+username} onClick={this.triggerActiveContent} data-content={`users/by_username?url=${username}`}>@{username}</a>
}
else {
channelHeaderInner = currentChannel.channel_name;
channelHeaderInner = <a href={'/'+currentChannel.adjusted_slug} onClick={this.triggerActiveContent} data-content='channel-details'>{currentChannel.channel_name}</a>;
}
channelHeader = <div className="activechatchannel__header">
{channelHeaderInner}
@ -654,11 +697,21 @@ export default class Chat extends Component {
} else if (this.state.incomingVideoCallChannelIds.includes(this.state.activeChannelId) ) {
incomingCall = <div className="activechatchannel__incomingcall" onClick={this.answerVideoCall}>👋 Incoming Video Call </div>
}
let nonChatView = ''
if (this.state.nonChatView) {
nonChatView = <View
channels={this.state.inviteChannels}
onViewExit={this.triggerExitView}
onAcceptInvitation={this.handleInvitationAccept}
onDeclineInvitation={this.handleInvitationDecline}
/>
}
return (
<div className={"chat chat--" + (this.state.expanded ? "expanded" : "contracted")}>
<div className={"chat chat--" + (this.state.expanded ? "expanded" : "contracted")} data-no-instant>
{this.renderChatChannels()}
<div className="chat__activechat">
{vid}
{nonChatView}
{this.renderActiveChatChannel(channelHeader, incomingCall)}
</div>
</div>

View file

@ -2,11 +2,12 @@ import { h, Component } from 'preact';
import PropTypes from 'prop-types';
import CodeEditor from './codeEditor';
import GithubRepo from './githubRepo';
import ChannelDetails from './channelDetails';
import UserDetails from './userDetails';
export default class Content extends Component {
static propTypes = {
resource: PropTypes.object,
onExit: PropTypes.func,
activeChannelId: PropTypes.number,
pusherKey: PropTypes.string,
};
@ -15,10 +16,12 @@ export default class Content extends Component {
return ""
} else {
return (
<div className="activechatchannel__activecontent" id="chat_activecontent">
<div
className="activechatchannel__activecontent" id="chat_activecontent"
onClick={this.props.onTriggerContent}
>
<button
class="activechatchannel__activecontentexitbutton"
onClick={this.props.onExit}
data-content="exit"
>×</button>
{display(this.props)}
@ -43,20 +46,7 @@ function display(props) {
display: "block",
backgroundColor: "#f5f6f7"}}></div>
} else if (props.resource.type_of === "user") {
return <div><img
src={props.resource.profile_image}
style={{height: "210px",
width: "210px",
margin:" 15px auto",
display: "block",
borderRadius: "500px"}} />
<h1 style={{textAlign: "center"}}>
<a href={"/"+props.resource.username}>{props.resource.name}</a>
</h1>
<div style={{fontStyle: "italic"}}>
{props.resource.summary}
</div>
</div>
return <UserDetails user={props.resource} />
} else if (props.resource.type_of === "article") {
return (
<div class="container">
@ -80,6 +70,11 @@ function display(props) {
githubToken={props.githubToken}
resource={props.resource}
/>
} else if (props.resource.type_of === "channel-details") {
return <ChannelDetails
channel={props.resource.channel}
activeChannelId={props.activeChannelId}
/>
} else if (props.resource.type_of === "code_editor") {
return <CodeEditor
activeChannelId={props.activeChannelId}

View file

@ -0,0 +1,24 @@
import { h, Component } from 'preact';
export default class UserDetails extends Component {
render() {
const user = this.props.user
return <div><img
src={user.profile_image}
style={{height: "210px",
width: "210px",
margin:" 15px auto",
display: "block",
borderRadius: "500px"}} />
<h1 style={{textAlign: "center"}}>
<a href={"/"+user.username}>{user.name}</a>
</h1>
<div style={{fontStyle: "italic"}}>
{user.summary}
</div>
</div>
}
}

View file

@ -0,0 +1,26 @@
import { h, Component } from 'preact';
export default class View extends Component {
render() {
const channels = this.props.channels.map((channel) => {
return <div>{channel.channel_name}
<button onClick={this.props.onAcceptInvitation} data-content={channel.membership_id}>Accept</button>
<button onClick={this.props.onDeclineInvitation} data-content={channel.membership_id}>Decline</button>
</div>
});
return <div className="chatNonChatView">
<button
class="chatNonChatView_exitbutton"
data-content="exit"
onClick={this.props.onViewExit}
>×</button>
<h1>Channel Invitations</h1>
<h2>Invitations are a work in progress </h2>
{channels}
</div>
}
}

View file

@ -5,6 +5,13 @@ class ChatChannel < ApplicationRecord
has_many :messages
has_many :chat_channel_memberships
has_many :users, through: :chat_channel_memberships
has_many :active_memberships, -> { where status: "active" }, class_name: 'ChatChannelMembership'
has_many :pending_memberships, -> { where status: "pending" }, class_name: 'ChatChannelMembership'
has_many :rejected_memberships, -> { where status: "rejected" }, class_name: 'ChatChannelMembership'
has_many :active_users, :through => :active_memberships, class_name: "User", :source => :user
has_many :pending_users, :through => :pending_memberships, class_name: "User", :source => :user
has_many :rejected_users, :through => :rejected_memberships, class_name: "User", :source => :user
validates :channel_type, presence: true, inclusion: { in: %w(open invite_only direct) }
validates :status, presence: true, inclusion: { in: %w(active inactive) }
@ -28,7 +35,7 @@ class ChatChannel < ApplicationRecord
end
def has_member?(user)
users.include?(user)
active_users.include?(user)
end
def last_opened_at(user = nil)
@ -79,7 +86,6 @@ class ChatChannel < ApplicationRecord
end
end
def adjusted_slug(user = nil, caller_type="reciever")
user ||= current_user
if channel_type == "direct" && caller_type == "reciever"
@ -92,7 +98,7 @@ class ChatChannel < ApplicationRecord
end
def viewable_by
chat_channel_memberships.pluck(:user_id)
active_memberships.pluck(:user_id)
end
def messages_count
@ -100,15 +106,16 @@ class ChatChannel < ApplicationRecord
end
def channel_human_names
chat_channel_memberships.
order("last_opened_at DESC").limit(20).includes(:user).map do |m|
active_memberships.
order("last_opened_at DESC").limit(5).includes(:user).map do |m|
m.user.name
end
end
def channel_users
# Purely for algolia indexing
pics_obj = {}
chat_channel_memberships.
active_memberships.
order("last_opened_at DESC").limit(80).includes(:user).each_with_index do |m, i|
pics_obj[m.user.username] = {
profile_image: i < 11 ? ProfileImage.new(m.user).get(90) : nil,

View file

@ -4,6 +4,7 @@ class ChatChannelMembership < ApplicationRecord
validates :user_id, presence: true, uniqueness: { scope: :chat_channel_id }
validates :chat_channel_id, presence: true, uniqueness: { scope: :user_id }
validates :status, inclusion: { in: %w{active inactive pending rejected left_channel} }
validate :permission
private

View file

@ -0,0 +1,6 @@
class ChatChannelMembershipPolicy < ApplicationPolicy
def update?
record.present? && user.id == record.user_id
end
end

View file

@ -4,6 +4,14 @@ class ChatChannelPolicy < ApplicationPolicy
user
end
def create?
true
end
def update?
user_can_edit_channel
end
def moderate?
!user_is_banned? && user_is_admin?
end
@ -16,8 +24,17 @@ class ChatChannelPolicy < ApplicationPolicy
user_part_of_channel
end
def permitted_attributes
%i[channel_name slug command]
end
private
def user_can_edit_channel
record.present? && user.has_role?(:super_admin)
end
def user_part_of_channel_or_open
record.present? && (record.channel_type == "open" || record.has_member?(user))
end

View file

@ -0,0 +1,13 @@
class ChatChannelCreationService
attr_accessor :user, :params
def initialize(user, params)
@user = user
@params = params
end
def create
user.chat_channels.create(channel_type: "invite_only",
channel_name: params[:channel_name], slug: params[:slug])
end
end

View file

@ -0,0 +1,12 @@
class ChatChannelUpdateService
attr_accessor :chat_channel, :params
def initialize(chat_channel, params)
@chat_channel = chat_channel
@params = params
end
def update
chat_channel.update(params)
end
end

View file

@ -1,4 +1,5 @@
json.type_of "user"
json.id @user.id
json.username @user.username
json.name @user.name
json.summary @user.summary

View file

@ -7,4 +7,5 @@ json.array! (@chat_channels_memberships.sort_by { |m| m.chat_channel.last_messag
json.last_opened_at membership.chat_channel.last_opened_at
json.last_message_at membership.chat_channel.last_message_at
json.adjusted_slug membership.chat_channel.adjusted_slug(current_user)
json.membership_id membership.id
end

View file

@ -72,7 +72,8 @@ Rails.application.routes.draw do
end
resources :messages, only: [:create]
resources :chat_channels, only: [:index, :show]
resources :chat_channels, only: [:index, :show, :create, :update]
resources :chat_channel_memberships, only: [ :create, :update]
resources :articles, only: [:update,:create,:destroy]
resources :comments, only: [:create,:update,:destroy]
resources :users, only: [:update]

View file

@ -0,0 +1,5 @@
class AddStatusToChatChannelMemberships < ActiveRecord::Migration[5.1]
def change
add_column :chat_channel_memberships, :status, :string, default: "active"
end
end

View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20180624230435) do
ActiveRecord::Schema.define(version: 20180629201047) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -167,6 +167,7 @@ ActiveRecord::Schema.define(version: 20180624230435) do
t.datetime "created_at", null: false
t.boolean "has_unopened_messages", default: false
t.datetime "last_opened_at", default: "2017-01-01 05:00:00"
t.string "status", default: "active"
t.datetime "updated_at", null: false
t.bigint "user_id", null: false
t.index ["chat_channel_id"], name: "index_chat_channel_memberships_on_chat_channel_id"

View file

@ -0,0 +1,4 @@
FactoryBot.define do
factory :chat_channel_membership do
end
end

View file

@ -17,4 +17,13 @@ RSpec.describe ChatChannel, type: :model do
expect(chat_channel.users.size).to eq(2)
expect(chat_channel.has_member?(User.first)).to eq(true)
end
it "lists active memberships" do
chat_channel = ChatChannel.create_with_users([create(:user),create(:user)])
expect(chat_channel.active_users.size).to eq(2)
expect(chat_channel.channel_users.size).to eq(2)
ChatChannelMembership.last.update(status: "left_channel")
expect(chat_channel.active_users.size).to eq(1)
expect(chat_channel.channel_users.size).to eq(1)
end
end

View file

@ -0,0 +1,27 @@
require "rails_helper"
RSpec.describe ChatChannelMembershipPolicy do
subject { described_class.new(user, chat_channel_membership) }
let(:chat_channel_membership) { build(:chat_channel_membership) }
context "when user is not signed-in" do
let(:user) { nil }
it { within_block_is_expected.to raise_error(Pundit::NotAuthorizedError) }
end
context "when user belongs to membership" do
let(:user) { create(:user) }
let(:chat_channel) { create(:chat_channel ) }
let(:chat_channel_membership) { create(:chat_channel_membership, user_id: user.id, chat_channel_id: chat_channel.id ) }
it { is_expected.to permit_actions(%i[update]) }
end
context "when user does not belong to membership" do
let(:user) { create(:user) }
let(:other_user) { create(:user) }
let(:chat_channel) { create(:chat_channel ) }
let(:chat_channel_membership) { create(:chat_channel_membership, user_id: other_user.id, chat_channel_id: chat_channel.id ) }
it { is_expected.to forbid_actions(%i[update]) }
end
end

View file

@ -14,20 +14,20 @@ RSpec.describe ChatChannelPolicy do
context "when user is not a part of channel" do
let(:user) { build(:user) }
it { is_expected.to permit_actions(%i[index]) }
it { is_expected.to forbid_actions(%i[show open moderate]) }
it { is_expected.to forbid_actions(%i[show open moderate update]) }
end
context "when user is a part of channel" do
let(:user) { create(:user) }
before { chat_channel.add_users [user] }
it { is_expected.to permit_actions(%i[index show open]) }
it { is_expected.to forbid_actions(%i[moderate]) }
it { is_expected.to forbid_actions(%i[moderate update]) }
end
context "when user is an admin but not part of channel" do
let(:user) { create(:user) }
before { user.add_role(:super_admin) }
it { is_expected.to permit_actions(%i[index moderate]) }
it { is_expected.to permit_actions(%i[index moderate update]) }
it { is_expected.to forbid_actions(%i[show open]) }
end
end

View file

@ -0,0 +1,53 @@
require "rails_helper"
RSpec.describe "ChatChannelMemberships", type: :request do
let(:user) { create(:user) }
let(:second_user) { create(:user) }
let(:chat_channel) { create(:chat_channel) }
before do
sign_in user
chat_channel.add_users([user])
end
describe "POST /chat_channel_memberships" do
it "creates chat channel invitation" do
user.add_role(:super_admin)
mems_num = ChatChannelMembership.all.size
post "/chat_channel_memberships", params: {user_id: second_user.id, chat_channel_id: chat_channel.id}
expect(ChatChannelMembership.all.size).to eq(mems_num + 1)
expect(ChatChannelMembership.last.status).to eq("pending")
end
it "denies chat channel invitation to non-authorized user" do
expect do
post "/chat_channel_memberships", params: {user_id: second_user.id, chat_channel_id: chat_channel.id}
end.to raise_error(Pundit::NotAuthorizedError)
end
end
describe "PUT /chat_channel_memberships/:id" do
before do
user.add_role(:super_admin)
post "/chat_channel_memberships", params: {user_id: second_user.id, chat_channel_id: chat_channel.id}
end
it "accepts chat channel invitation" do
membership = ChatChannelMembership.last
sign_in second_user
put "/chat_channel_memberships/#{membership.id}", params: {user_action: "accept"}
expect(ChatChannelMembership.find(membership.id).status).to eq("active")
end
it "rejects chat channel invitation" do
membership = ChatChannelMembership.last
sign_in second_user
put "/chat_channel_memberships/#{membership.id}", params: {user_action: "reject"}
expect(ChatChannelMembership.find(membership.id).status).to eq("rejected")
end
it "disallows non-logged-user" do
membership = ChatChannelMembership.last
expect do
put "/chat_channel_memberships/#{membership.id}", params: {user_action: "accept"}
expect(ChatChannelMembership.find(membership.id).status).to eq("active")
end.to raise_error(Pundit::NotAuthorizedError)
end
end
end

View file

@ -49,6 +49,29 @@ RSpec.describe "ChatChannels", type: :request do
end
end
describe "get /chat_channels?state=pending" do
it "returns pending channels" do
pending_membership = ChatChannelMembership.create(chat_channel_id: invite_channel.id,
user_id:user.id, status: "pending")
sign_in user
get "/chat_channels?state=pending"
expect(response.body).to include(invite_channel.slug)
end
it "returns no pending channels if no invites" do
sign_in user
get "/chat_channels?state=pending"
expect(response.body).not_to include(invite_channel.slug)
end
it "returns no pending channels if not pending" do
pending_membership = ChatChannelMembership.create(chat_channel_id: invite_channel.id,
user_id:user.id, status: "rejected")
sign_in user
get "/chat_channels?state=pending"
expect(response.body).not_to include(invite_channel.slug)
end
end
describe "GET /chat_channels/:id" do
context "when request is valid" do
before do
@ -71,6 +94,48 @@ RSpec.describe "ChatChannels", type: :request do
end
end
describe "POST /chat_channels" do
it "creates chat_channel for current user" do
post "/chat_channels",
params: { chat_channel: { channel_name: "Hello Channel", slug: "hello-channel" } },
headers: { HTTP_ACCEPT: "application/json" }
expect(ChatChannel.last.slug).to eq("hello-channel")
expect(ChatChannel.last.active_users).to include(user)
end
it "returns errors if channel is invalid" do
#slug should be taken
post "/chat_channels",
params: { chat_channel: { channel_name: "HEy hey hoho", slug: chat_channel.slug } },
headers: { HTTP_ACCEPT: "application/json" }
expect(response.body).to include("Slug has already been taken")
end
end
describe "PUT /chat_channels/:id" do
it "updates channel for valid user" do
user.add_role(:super_admin)
put "/chat_channels/#{chat_channel.id}",
params: { chat_channel: { channel_name: "Hello Channel", slug: "hello-channelly" } },
headers: { HTTP_ACCEPT: "application/json" }
expect(ChatChannel.last.slug).to eq("hello-channelly")
end
it "dissallows invalid users" do
expect do
put "/chat_channels/#{chat_channel.id}",
params: { chat_channel: { channel_name: "Hello Channel", slug: "hello-channelly" } },
headers: { HTTP_ACCEPT: "application/json" }
end.to raise_error(Pundit::NotAuthorizedError)
end
it "returns errors if channel is invalid" do
#slug should be taken
user.add_role(:super_admin)
put "/chat_channels/#{chat_channel.id}",
params: { chat_channel: { channel_name: "HEy hey hoho", slug: invite_channel.slug } },
headers: { HTTP_ACCEPT: "application/json" }
expect(response.body).to include("Slug has already been taken")
end
end
describe "POST /chat_channels/:id/moderate" do
it "raises NotAuthorizedError if user is not logged in" do
expect do