diff --git a/Gemfile b/Gemfile index 0576326..632e6b4 100644 --- a/Gemfile +++ b/Gemfile @@ -13,6 +13,7 @@ gem 'rack-cors', require: 'rack/cors' gem 'devise_token_auth' gem 'active_model_serializers' gem 'haversine' +gem 'redis' gem 'faker' group :development, :test do diff --git a/Gemfile.lock b/Gemfile.lock index 075836d..fff8aa1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -171,6 +171,7 @@ GEM rb-fsevent (0.10.4) rb-inotify (0.10.1) ffi (~> 1.0) + redis (4.2.1) responders (3.0.0) actionpack (>= 5.0) railties (>= 5.0) @@ -243,6 +244,7 @@ DEPENDENCIES puma (~> 4.1) rack-cors rails (~> 6.0.3, >= 6.0.3.1) + redis rspec-rails shoulda-matchers spring diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb index 0ff5442..2e57bba 100644 --- a/app/channels/application_cable/connection.rb +++ b/app/channels/application_cable/connection.rb @@ -1,4 +1,21 @@ +# frozen_string_literal: true + module ApplicationCable class Connection < ActionCable::Connection::Base + identified_by :current_user + + def connect + self.current_user = find_verified_user + end + + private + + def find_verified_user + if verified_user = User.find_by(email: request.params[:uid]) + verified_user + else + reject_unauthorized_connection + end + end end end diff --git a/app/channels/offer_conversation_channel.rb b/app/channels/offer_conversation_channel.rb new file mode 100644 index 0000000..0b964e6 --- /dev/null +++ b/app/channels/offer_conversation_channel.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class OfferConversationChannel < ApplicationCable::Channel + def subscribed + stream_from offer_identifier + end + + def unsubscribed + # Any cleanup needed when channel is unsubscribed + end + + private + + def offer_identifier + if params[:room][:offer_id] + channel = "offer_conversation_#{params[:room][:offer_id]}" + else + connection.transmit identifier: params, message: 'No params specified.' + reject && return + end + channel + end +end diff --git a/app/controllers/api/messages_controller.rb b/app/controllers/api/messages_controller.rb index ff0feb1..3d5f2f7 100644 --- a/app/controllers/api/messages_controller.rb +++ b/app/controllers/api/messages_controller.rb @@ -5,6 +5,7 @@ def create message = Offer.find(params[:offer_id]).conversation .messages.create(content: params[:content], sender: current_user) if message.persisted? + # ActionCable.server.broadcast("offer_conversation_#{params[:offer_id]}", message: message.content ) render status: :created else render_error_message(message.errors) diff --git a/app/models/message.rb b/app/models/message.rb index 6504966..bb8739a 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -1,8 +1,11 @@ +# frozen_string_literal: true + class Message < ApplicationRecord belongs_to :conversation belongs_to :sender, class_name: 'User' validates_presence_of :content before_create :validate_user_is_authorized + after_save :broadcast private @@ -10,4 +13,12 @@ def validate_user_is_authorized is_valid_user = sender == conversation.offer.helper || sender == conversation.offer.request.requester raise StandardError, 'You are not authorized to do this!' unless is_valid_user end + + def broadcast + data = { content: content, sender_id: sender.id } + ActionCable.server.broadcast( + "offer_conversation_#{conversation.offer.id}", + message: data + ) + end end diff --git a/config/cable.yml b/config/cable.yml index 07da6f3..d5eac3c 100644 --- a/config/cable.yml +++ b/config/cable.yml @@ -1,10 +1,7 @@ -development: - adapter: async - -test: - adapter: test - -production: +redis: &redis adapter: redis - url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> - channel_prefix: reQuest_api_production + url: redis://localhost:6379/1 +production: *redis +development: *redis +test: + adapter: test \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 9e4a9cc..9eb08a9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,10 +1,12 @@ # frozen_string_literal: true Rails.application.routes.draw do + mount ActionCable.server => '/cable' mount_devise_token_auth_for 'User', at: 'api/auth' namespace :api do - resources :messages, only: [:create] - resources :offers, only: %i[create show update] + resources :offers, only: %i[create show update] do + resources :messages, only: [:create] + end resources :karma_points, only: [:index], constraints: { format: 'json' } resources :requests, only: %i[index], constraints: { format: 'json' } namespace :my_request do diff --git a/spec/channels/connection_spec.rb b/spec/channels/connection_spec.rb new file mode 100644 index 0000000..4a81c0f --- /dev/null +++ b/spec/channels/connection_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +RSpec.describe ApplicationCable::Connection, type: :channel do + let(:user) { create(:user) } + it 'successfully connects' do + connect "/cable?uid=#{user.email}" + expect(connection.current_user).to eq user + end + + it 'rejects connection' do + expect { connect '/cable' }.to have_rejected_connection + end +end diff --git a/spec/channels/offer_conversation_channel_spec.rb b/spec/channels/offer_conversation_channel_spec.rb new file mode 100644 index 0000000..41c9313 --- /dev/null +++ b/spec/channels/offer_conversation_channel_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe OfferConversationChannel, type: :channel do + let(:user) { create(:user) } + describe 'subscription' do + before { stub_connection current_user: user } + describe 'valid parameters' do + before do + subscribe(room: { offer_id: 234 }) + end + + it { + expect(subscription).to be_confirmed + } + + it { + expect(subscription).to have_stream_from("offer_conversation_234") + } + end + + describe 'without valid params' do + + before do + subscribe(room: {}) + end + + it { expect(subscription).to be_rejected } + + it { + expect(transmissions.last).to eq('No params specified.') + } + end + end +end diff --git a/spec/models/message_spec.rb b/spec/models/message_spec.rb index f2cd23c..88928fd 100644 --- a/spec/models/message_spec.rb +++ b/spec/models/message_spec.rb @@ -21,4 +21,13 @@ expect(create(:message)).to be_valid end end + + describe 'broadcast' do + it 'should broadcast after creation' do + message = create(:message) + expect { + message.save + }.to have_broadcasted_to("offer_conversation_#{Message.last.conversation.offer.id}") + end + end end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 522ddc0..709eea2 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -8,6 +8,7 @@ abort('The Rails environment is running in production mode!') if Rails.env.production? require 'spec_helper' + require 'rspec/rails' ActiveRecord::Migration.maintain_test_schema! diff --git a/spec/requests/api/conversions/users_can_send_messages_spec.rb b/spec/requests/api/conversions/users_can_send_messages_spec.rb index e11f791..3cd9201 100644 --- a/spec/requests/api/conversions/users_can_send_messages_spec.rb +++ b/spec/requests/api/conversions/users_can_send_messages_spec.rb @@ -1,4 +1,6 @@ -RSpec.describe 'POST /message users can post messages' do +# frozen_string_literal: true + +RSpec.describe 'POST /offers/:offer_id/message users can post messages' do let(:requester) { create(:user, email: 'requester@mail.com') } let(:req_creds) { requester.create_new_auth_token } let(:req_headers) { { HTTP_ACCEPT: 'application/json' }.merge!(req_creds) } @@ -10,7 +12,7 @@ let(:third_user) { create(:user) } let(:third_user_credentials) { third_user.create_new_auth_token } let(:third_user_headers) { { HTTP_ACCEPT: 'application/json' }.merge!(third_user_credentials) } - + let(:request) { create(:request, requester: requester) } let(:offer) { create(:offer, request: request, helper: helper) } @@ -18,7 +20,7 @@ describe 'successfully as the requester' do before do - post '/api/messages', headers: req_headers, params: { offer_id: offer.id, content: "message content" } + post "/api/offers/#{offer.id}/messages", headers: req_headers, params: { content: 'message content' } end it 'gives a success status' do @@ -27,13 +29,19 @@ it 'creates a message based on the params' do offer.reload - expect(offer.conversation.messages.last['content']).to eq "message content" + expect(offer.conversation.messages.last['content']).to eq 'message content' + end + + it 'dispatches a websocket message' do + expect( + ActionCable.server.worker_pool.executor.worker_task_completed + ).to eq 1 end end describe 'successfully as the helper' do before do - post '/api/messages', headers: helper_headers, params: { offer_id: offer.id, content: "message content" } + post "/api/offers/#{offer.id}/messages", headers: helper_headers, params: { content: 'message content' } end it 'gives a success status' do @@ -42,14 +50,14 @@ it 'creates a message based on the params' do offer.reload - expect(offer.conversation.messages.last['content']).to eq "message content" + expect(offer.conversation.messages.last['content']).to eq 'message content' end end describe 'unsuccessfully' do describe 'without content' do before do - post '/api/messages', headers: helper_headers, params: { offer_id: offer.id } + post "/api/offers/#{offer.id}/messages", headers: helper_headers end it 'gives an error status' do @@ -61,23 +69,23 @@ end end - describe 'without offer_id' do - before do - post '/api/messages', headers: helper_headers, params: { content: 'message content' } - end + # describe 'without offer_id' do + # before do + # post '/api/messages', headers: helper_headers, params: { content: 'message content' } + # end - it 'gives an error status' do - expect(response).to have_http_status 422 - end + # it 'gives an error status' do + # expect(response).to have_http_status 422 + # end - it 'gives an error message' do - expect(response_json['message']).to eq "Couldn't find Offer without an ID" - end - end + # it 'gives an error message' do + # expect(response_json['message']).to eq "Couldn't find Offer without an ID" + # end + # end describe 'unauthorized' do before do - post '/api/messages', headers: third_user_headers, params: { offer_id: offer.id, content: 'message content' } + post "/api/offers/#{offer.id}/messages", headers: third_user_headers, params: { content: 'message content' } end it 'gives an error status' do @@ -91,10 +99,9 @@ describe 'unauthenticated' do before do - post '/api/messages', params: { offer_id: offer.id, content: 'message content' } + post "/api/offers/#{offer.id}/messages", params: { content: 'message content' } end - it 'gives an error status' do expect(response).to have_http_status 401 end