From 94e5154668ee1898240755df79b5e482cf672742 Mon Sep 17 00:00:00 2001 From: Ben Halpern Date: Mon, 2 Jul 2018 16:31:20 -0400 Subject: [PATCH] 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 --- app/assets/stylesheets/chat.scss | 31 ++++++++ app/controllers/api/v0/users_controller.rb | 6 +- .../chat_channel_memberships_controller.rb | 26 +++++++ app/controllers/chat_channels_controller.rb | 39 +++++++++- app/javascript/chat/actions.js | 29 +++++++ app/javascript/chat/channelDetails.jsx | 31 ++++++++ app/javascript/chat/chat.jsx | 75 ++++++++++++++++--- app/javascript/chat/content.jsx | 29 +++---- app/javascript/chat/userDetails.jsx | 24 ++++++ app/javascript/chat/view.jsx | 26 +++++++ app/models/chat_channel.rb | 19 +++-- app/models/chat_channel_membership.rb | 1 + .../chat_channel_membership_policy.rb | 6 ++ app/policies/chat_channel_policy.rb | 17 +++++ app/services/chat_channel_creation_service.rb | 13 ++++ app/services/chat_channel_update_service.rb | 12 +++ app/views/api/v0/users/show.json.jbuilder | 1 + app/views/chat_channels/index.json.jbuilder | 1 + config/routes.rb | 3 +- ..._add_status_to_chat_channel_memberships.rb | 5 ++ db/schema.rb | 3 +- spec/factories/chat_channel_memberships.rb | 4 + spec/models/chat_channel_spec.rb | 9 +++ .../chat_channel_membership_policy_spec.rb | 27 +++++++ spec/policies/chat_channel_policy_spec.rb | 6 +- .../requests/chat_channel_memberships_spec.rb | 53 +++++++++++++ spec/requests/chat_channels_spec.rb | 65 ++++++++++++++++ 27 files changed, 520 insertions(+), 41 deletions(-) create mode 100644 app/controllers/chat_channel_memberships_controller.rb create mode 100644 app/javascript/chat/channelDetails.jsx create mode 100644 app/javascript/chat/userDetails.jsx create mode 100644 app/javascript/chat/view.jsx create mode 100644 app/policies/chat_channel_membership_policy.rb create mode 100644 app/services/chat_channel_creation_service.rb create mode 100644 app/services/chat_channel_update_service.rb create mode 100644 db/migrate/20180629201047_add_status_to_chat_channel_memberships.rb create mode 100644 spec/factories/chat_channel_memberships.rb create mode 100644 spec/policies/chat_channel_membership_policy_spec.rb create mode 100644 spec/requests/chat_channel_memberships_spec.rb diff --git a/app/assets/stylesheets/chat.scss b/app/assets/stylesheets/chat.scss index 83123aae8..dc3125b60 100644 --- a/app/assets/stylesheets/chat.scss +++ b/app/assets/stylesheets/chat.scss @@ -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; diff --git a/app/controllers/api/v0/users_controller.rb b/app/controllers/api/v0/users_controller.rb index 7bad8a5f5..72370e659 100644 --- a/app/controllers/api/v0/users_controller.rb +++ b/app/controllers/api/v0/users_controller.rb @@ -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) diff --git a/app/controllers/chat_channel_memberships_controller.rb b/app/controllers/chat_channel_memberships_controller.rb new file mode 100644 index 000000000..8f9665ca6 --- /dev/null +++ b/app/controllers/chat_channel_memberships_controller.rb @@ -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 \ No newline at end of file diff --git a/app/controllers/chat_channels_controller.rb b/app/controllers/chat_channels_controller.rb index 3de57427f..16e929f11 100644 --- a/app/controllers/chat_channels_controller.rb +++ b/app/controllers/chat_channels_controller.rb @@ -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] diff --git a/app/javascript/chat/actions.js b/app/javascript/chat/actions.js index 6d268819b..19d6c9a04 100644 --- a/app/javascript/chat/actions.js +++ b/app/javascript/chat/actions.js @@ -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); } \ No newline at end of file diff --git a/app/javascript/chat/channelDetails.jsx b/app/javascript/chat/channelDetails.jsx new file mode 100644 index 000000000..e5a852532 --- /dev/null +++ b/app/javascript/chat/channelDetails.jsx @@ -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
+ + {user.name} + +
+ }); + let subHeader = '' + if (users.length === 80) { + subHeader =

Recently Active Members

+ } + return
+

{channel.channel_name}

+ {subHeader} + {users} +
+ } + +} \ No newline at end of file diff --git a/app/javascript/chat/chat.jsx b/app/javascript/chat/chat.jsx index 94d47fb6d..66e4aae40 100644 --- a/app/javascript/chat/chat.jsx +++ b/app/javascript/chat/chat.jsx @@ -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 =
; } else if (notificationsPermission === "granted") { @@ -545,12 +582,18 @@ export default class Chat extends Component { } else if (notificationsPermission === "denied") { notificationsState =
Notificatins Off
} + if (this.state.inviteChannels.length > 0) { + invitesButton =
+ } if (this.state.expanded) { return (
{notificationsButton} + {invitesButton}
@{username} + channelHeaderInner = @{username} } else { - channelHeaderInner = currentChannel.channel_name; + channelHeaderInner = {currentChannel.channel_name}; } channelHeader =
{channelHeaderInner} @@ -654,11 +697,21 @@ export default class Chat extends Component { } else if (this.state.incomingVideoCallChannelIds.includes(this.state.activeChannelId) ) { incomingCall =
👋 Incoming Video Call
} + let nonChatView = '' + if (this.state.nonChatView) { + nonChatView = + } return ( -
+
{this.renderChatChannels()}
{vid} + {nonChatView} {this.renderActiveChatChannel(channelHeader, incomingCall)}
diff --git a/app/javascript/chat/content.jsx b/app/javascript/chat/content.jsx index 9cef6f48a..75b6bc102 100644 --- a/app/javascript/chat/content.jsx +++ b/app/javascript/chat/content.jsx @@ -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 ( -
+
{display(this.props)} @@ -43,20 +46,7 @@ function display(props) { display: "block", backgroundColor: "#f5f6f7"}}>
} else if (props.resource.type_of === "user") { - return
-

- {props.resource.name} -

-
- {props.resource.summary} -
-
+ return } else if (props.resource.type_of === "article") { return (
@@ -80,6 +70,11 @@ function display(props) { githubToken={props.githubToken} resource={props.resource} /> + } else if (props.resource.type_of === "channel-details") { + return } else if (props.resource.type_of === "code_editor") { return +

+ {user.name} +

+
+ {user.summary} +
+
+ } + +} \ No newline at end of file diff --git a/app/javascript/chat/view.jsx b/app/javascript/chat/view.jsx new file mode 100644 index 000000000..5781a51b6 --- /dev/null +++ b/app/javascript/chat/view.jsx @@ -0,0 +1,26 @@ + + +import { h, Component } from 'preact'; + +export default class View extends Component { + + render() { + const channels = this.props.channels.map((channel) => { + return
{channel.channel_name} + + +
+ }); + return
+ +

Channel Invitations

+

Invitations are a work in progress ❤️

+ {channels} +
+ } + +} \ No newline at end of file diff --git a/app/models/chat_channel.rb b/app/models/chat_channel.rb index 76b3716e6..784acffe6 100644 --- a/app/models/chat_channel.rb +++ b/app/models/chat_channel.rb @@ -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, diff --git a/app/models/chat_channel_membership.rb b/app/models/chat_channel_membership.rb index 95b4df4af..48959af92 100644 --- a/app/models/chat_channel_membership.rb +++ b/app/models/chat_channel_membership.rb @@ -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 diff --git a/app/policies/chat_channel_membership_policy.rb b/app/policies/chat_channel_membership_policy.rb new file mode 100644 index 000000000..9f93edaf8 --- /dev/null +++ b/app/policies/chat_channel_membership_policy.rb @@ -0,0 +1,6 @@ +class ChatChannelMembershipPolicy < ApplicationPolicy + + def update? + record.present? && user.id == record.user_id + end +end \ No newline at end of file diff --git a/app/policies/chat_channel_policy.rb b/app/policies/chat_channel_policy.rb index c3267c8c3..34eb5a1c4 100644 --- a/app/policies/chat_channel_policy.rb +++ b/app/policies/chat_channel_policy.rb @@ -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 diff --git a/app/services/chat_channel_creation_service.rb b/app/services/chat_channel_creation_service.rb new file mode 100644 index 000000000..653f6dede --- /dev/null +++ b/app/services/chat_channel_creation_service.rb @@ -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 \ No newline at end of file diff --git a/app/services/chat_channel_update_service.rb b/app/services/chat_channel_update_service.rb new file mode 100644 index 000000000..2501af339 --- /dev/null +++ b/app/services/chat_channel_update_service.rb @@ -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 \ No newline at end of file diff --git a/app/views/api/v0/users/show.json.jbuilder b/app/views/api/v0/users/show.json.jbuilder index 87437810b..689836e79 100644 --- a/app/views/api/v0/users/show.json.jbuilder +++ b/app/views/api/v0/users/show.json.jbuilder @@ -1,4 +1,5 @@ json.type_of "user" +json.id @user.id json.username @user.username json.name @user.name json.summary @user.summary diff --git a/app/views/chat_channels/index.json.jbuilder b/app/views/chat_channels/index.json.jbuilder index 14b3141c9..90af37b19 100644 --- a/app/views/chat_channels/index.json.jbuilder +++ b/app/views/chat_channels/index.json.jbuilder @@ -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 diff --git a/config/routes.rb b/config/routes.rb index 76358a6f3..4c7d3e33b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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] diff --git a/db/migrate/20180629201047_add_status_to_chat_channel_memberships.rb b/db/migrate/20180629201047_add_status_to_chat_channel_memberships.rb new file mode 100644 index 000000000..8f19c4906 --- /dev/null +++ b/db/migrate/20180629201047_add_status_to_chat_channel_memberships.rb @@ -0,0 +1,5 @@ +class AddStatusToChatChannelMemberships < ActiveRecord::Migration[5.1] + def change + add_column :chat_channel_memberships, :status, :string, default: "active" + end +end diff --git a/db/schema.rb b/db/schema.rb index fa088fe27..f3aa2b194 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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" diff --git a/spec/factories/chat_channel_memberships.rb b/spec/factories/chat_channel_memberships.rb new file mode 100644 index 000000000..5537885f1 --- /dev/null +++ b/spec/factories/chat_channel_memberships.rb @@ -0,0 +1,4 @@ +FactoryBot.define do + factory :chat_channel_membership do + end +end diff --git a/spec/models/chat_channel_spec.rb b/spec/models/chat_channel_spec.rb index b158eaded..03af3b849 100644 --- a/spec/models/chat_channel_spec.rb +++ b/spec/models/chat_channel_spec.rb @@ -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 diff --git a/spec/policies/chat_channel_membership_policy_spec.rb b/spec/policies/chat_channel_membership_policy_spec.rb new file mode 100644 index 000000000..f8d8980ea --- /dev/null +++ b/spec/policies/chat_channel_membership_policy_spec.rb @@ -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 \ No newline at end of file diff --git a/spec/policies/chat_channel_policy_spec.rb b/spec/policies/chat_channel_policy_spec.rb index d9be73279..42e32dd04 100644 --- a/spec/policies/chat_channel_policy_spec.rb +++ b/spec/policies/chat_channel_policy_spec.rb @@ -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 diff --git a/spec/requests/chat_channel_memberships_spec.rb b/spec/requests/chat_channel_memberships_spec.rb new file mode 100644 index 000000000..751b2a1c5 --- /dev/null +++ b/spec/requests/chat_channel_memberships_spec.rb @@ -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 \ No newline at end of file diff --git a/spec/requests/chat_channels_spec.rb b/spec/requests/chat_channels_spec.rb index 4e2027773..a7642d465 100644 --- a/spec/requests/chat_channels_spec.rb +++ b/spec/requests/chat_channels_spec.rb @@ -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