Merge pull request #367 from coopdevs/feature/create-push-notifications-job

Push notifications background job for Post
This commit is contained in:
Enrico Stano
2018-05-29 18:38:58 +02:00
committed by GitHub
19 changed files with 314 additions and 26 deletions
+11
View File
@@ -0,0 +1,11 @@
class CreatePushNotificationsJob < ActiveJob::Base
queue_as :default
def perform(event_id:)
event = ::Event.find_by_id(event_id)
raise 'A valid Event must be provided' unless event
::PushNotifications::Creator.new(event: event).create!
end
end
+6
View File
@@ -0,0 +1,6 @@
class PushNotification < ActiveRecord::Base
belongs_to :event, foreign_key: 'event_id'
belongs_to :device_token, foreign_key: 'device_token_id'
validates :event, :device_token, presence: true
end
@@ -1,11 +0,0 @@
module PushNotifications
class PostNotification
def title
"Notification title"
end
def body
"Notification body"
end
end
end
+10 -2
View File
@@ -10,6 +10,7 @@ module Persister
::ActiveRecord::Base.transaction do
post.save!
create_save_event!
enqueue_push_notification_job!
post
end
rescue ActiveRecord::RecordInvalid => _exception
@@ -20,6 +21,7 @@ module Persister
::ActiveRecord::Base.transaction do
post.update_attributes!(params)
create_update_event!
enqueue_push_notification_job!
post
end
rescue ActiveRecord::RecordInvalid => _exception
@@ -28,12 +30,18 @@ module Persister
private
attr_accessor :event
def create_save_event!
::Event.create! action: :created, post: post
@event = ::Event.create! action: :created, post: post
end
def create_update_event!
::Event.create! action: :updated, post: post
@event = ::Event.create! action: :updated, post: post
end
def enqueue_push_notification_job!
CreatePushNotificationsJob.perform_later(event_id: event.id)
end
end
end
@@ -0,0 +1,22 @@
module PushNotifications
class Creator
# Given an Event it will create as many PushNotification resources
# necessary as the resource associated to the Event will require.
#
# @param [Hash] event: <Event>
def initialize(event:)
@event = event
end
def create!
event_notifier = EventNotifierFactory.new(event: event).build
event_notifier.device_tokens.each do |device_token|
PushNotification.create!(event: event, device_token: device_token)
end
end
private
attr_accessor :event
end
end
@@ -0,0 +1,26 @@
module PushNotifications
module EventNotifier
class Post
def initialize(event:)
@event = event
@post = event.post
end
# Conditions for Post:
#
# We need to notify all the users that:
# - are members of the Post's organization
# - have a DeviceToken associated
#
# @return [<DeviceToken>]
def device_tokens
organization = post.organization
DeviceToken.where(user_id: organization.user_ids)
end
private
attr_accessor :event, :post
end
end
end
@@ -0,0 +1,17 @@
module PushNotifications
class EventNotifierFactory
def initialize(event:)
@event = event
end
def build
return EventNotifier::Post.new(event: event) if event.post_id
raise 'The resource associated to the Event is not supported'
end
private
attr_accessor :event
end
end
+3
View File
@@ -44,4 +44,7 @@ Rails.application.configure do
# Avoid seeing all that stuff in tests
config.log_level = :warn
# ActiveJob configuration
config.active_job.queue_adapter = :test
end
@@ -0,0 +1,14 @@
class CreatePushNotifications < ActiveRecord::Migration
def change
create_table :push_notifications do |t|
t.references :event, null: false
t.references :device_token, null: false
t.datetime :processed_at
t.timestamps null: false
end
add_foreign_key :push_notifications, :events
add_foreign_key :push_notifications, :device_tokens
end
end
@@ -0,0 +1,11 @@
class ChangeIndexOnEvents < ActiveRecord::Migration
def change
remove_index :events, :post_id
remove_index :events, :member_id
remove_index :events, :transfer_id
add_index :events, :post_id, where: 'post_id IS NOT NULL'
add_index :events, :member_id, where: 'member_id IS NOT NULL'
add_index :events, :transfer_id, where: 'transfer_id IS NOT NULL'
end
end
+14 -4
View File
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20180525141138) do
ActiveRecord::Schema.define(version: 20180529144243) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -85,9 +85,9 @@ ActiveRecord::Schema.define(version: 20180525141138) do
t.datetime "updated_at"
end
add_index "events", ["member_id"], name: "index_events_on_member_id", unique: true, where: "(member_id IS NOT NULL)", using: :btree
add_index "events", ["post_id"], name: "index_events_on_post_id", unique: true, where: "(post_id IS NOT NULL)", using: :btree
add_index "events", ["transfer_id"], name: "index_events_on_transfer_id", unique: true, where: "(transfer_id IS NOT NULL)", using: :btree
add_index "events", ["member_id"], name: "index_events_on_member_id", where: "(member_id IS NOT NULL)", using: :btree
add_index "events", ["post_id"], name: "index_events_on_post_id", where: "(post_id IS NOT NULL)", using: :btree
add_index "events", ["transfer_id"], name: "index_events_on_transfer_id", where: "(transfer_id IS NOT NULL)", using: :btree
create_table "members", force: :cascade do |t|
t.integer "user_id"
@@ -158,6 +158,14 @@ ActiveRecord::Schema.define(version: 20180525141138) do
add_index "posts", ["tags"], name: "index_posts_on_tags", using: :gin
add_index "posts", ["user_id"], name: "index_posts_on_user_id", using: :btree
create_table "push_notifications", force: :cascade do |t|
t.integer "event_id", null: false
t.integer "device_token_id", null: false
t.datetime "processed_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "transfers", force: :cascade do |t|
t.integer "post_id"
t.text "reason"
@@ -215,4 +223,6 @@ ActiveRecord::Schema.define(version: 20180525141138) do
add_foreign_key "events", "members", name: "events_member_id_fkey"
add_foreign_key "events", "posts", name: "events_post_id_fkey"
add_foreign_key "events", "transfers", name: "events_transfer_id_fkey"
add_foreign_key "push_notifications", "device_tokens"
add_foreign_key "push_notifications", "events"
end
@@ -0,0 +1,2 @@
Fabricator(:device_token) do
end
+2
View File
@@ -0,0 +1,2 @@
Fabricator(:event) do
end
@@ -0,0 +1,31 @@
require 'spec_helper'
RSpec.describe CreatePushNotificationsJob, type: :job do
describe '#perform' do
context 'with an Event that doesn\'t exist' do
let(:event_id) { nil }
it 'raises an error' do
expect {
described_class.new.perform(event_id: event_id)
}.to raise_error 'A valid Event must be provided'
end
end
context 'with an Event that does exist' do
let(:post) { Fabricate(:post) }
let(:event) { Fabricate(:event, post: post, action: :created) }
let(:event_id) { event.id }
it 'calls the PushNotification creator' do
creator = instance_double(::PushNotifications::Creator)
expect(::PushNotifications::Creator).to receive(:new)
.with(event: event)
.and_return(creator)
expect(creator).to receive(:create!)
described_class.new.perform(event_id: event_id)
end
end
end
end
+16
View File
@@ -0,0 +1,16 @@
require 'spec_helper'
RSpec.describe PushNotification do
describe 'Validations' do
it { is_expected.to validate_presence_of(:event) }
it { is_expected.to validate_presence_of(:device_token) }
end
describe 'Associations' do
it { is_expected.to belong_to(:event).with_foreign_key('event_id') }
it { is_expected.to belong_to(:device_token).with_foreign_key('device_token_id') }
it { is_expected.to have_db_column(:event_id) }
it { is_expected.to have_db_column(:device_token_id) }
end
end
+36 -9
View File
@@ -1,6 +1,6 @@
require 'spec_helper'
describe Persister::PostPersister do
RSpec.describe Persister::PostPersister do
let(:organization) { Fabricate(:organization) }
let(:user) { Fabricate(:user) }
let(:category) { Fabricate(:category) }
@@ -14,30 +14,57 @@ describe Persister::PostPersister do
)
end
let(:persister) { ::Persister::PostPersister.new(post) }
let(:event) { Fabricate.build(:event, id: 27) }
describe '#save' do
before { persister.save }
it 'saves the post' do
persister.save
expect(post).to be_persisted
end
# TODO: write better expectation
it 'creates an event' do
expect(Event.where(post_id: post.id).first.action).to eq('created')
expect(::Event).to receive(:create!).with(action: :created, post: post).and_return(event)
persister.save
end
context 'background job' do
before do
allow(::Event).to receive(:create!).and_return(event)
end
it 'enqueues a CreatePushNotificationsJob background job' do
expect {
persister.save
}.to enqueue_job(CreatePushNotificationsJob).with(event_id: 27)
end
end
end
describe '#update_attributes' do
before { persister.update_attributes(title: 'New title') }
it 'updates the resource attributes' do
persister.update_attributes(title: 'New title')
expect(post.title).to eq('New title')
end
# TODO: write better expectation
it 'creates an event' do
expect(Event.where(post_id: post.id).first.action).to eq('updated')
expect(::Event).to receive(:create!).with(action: :updated, post: post).and_return(event)
persister.update_attributes(title: 'New title')
end
context 'background job' do
before do
allow(::Event).to receive(:create!).and_return(event)
end
it 'enqueues a CreatePushNotificationsJob background job' do
expect {
persister.update_attributes(title: 'New title')
}.to enqueue_job(CreatePushNotificationsJob).with(event_id: 27)
end
end
end
end
@@ -0,0 +1,25 @@
require 'spec_helper'
RSpec.describe PushNotifications::Creator do
let(:user) { Fabricate(:user) }
let!(:device_token) { Fabricate(:device_token, user: user, token: 'aloha') }
let(:organization) { Fabricate(:organization) }
let(:post) { Fabricate(:post, organization: organization, user: user) }
let(:event) { Fabricate.build(:event, post: post, action: :created) }
let(:creator) { described_class.new(event: event) }
before do
organization.members.create(user: user)
end
describe '#create!' do
it 'creates as many PushNotification resources as needed' do
expect(PushNotification).to receive(:create!).with(
event: event,
device_token: device_token
).once
creator.create!
end
end
end
@@ -0,0 +1,37 @@
require 'spec_helper'
RSpec.describe ::PushNotifications::EventNotifier::Post do
let(:user) { Fabricate.build(:user) }
let(:organization) { Fabricate(:organization) }
let(:post) { Fabricate(:post, organization: organization, user: user) }
let(:event) { Fabricate.build(:event, post: post, action: :created) }
describe '#users' do
context 'when a user pertains to the Post\'s organization' do
before { organization.members.create(user: user) }
context 'and the user has a DeviceToken associated' do
let!(:device_token) { Fabricate(:device_token, user: user, token: 'aloha') }
it 'returns the device token associated with the user' do
expect(described_class.new(event: event).device_tokens).to eq([device_token])
end
end
context 'but the user has no DeviceToken associated' do
it 'doesn\'t return the user' do
expect(described_class.new(event: event).device_tokens).to eq([])
end
end
end
context 'when a user doesn\'t pertain to the Post\'s organization' do
let(:other_user) { Fabricate(:user) }
let!(:device_token) { Fabricate(:device_token, user: other_user, token: 'WAT') }
it 'doesn\'t return the user' do
expect(described_class.new(event: event).device_tokens).to_not include(device_token)
end
end
end
end
@@ -0,0 +1,31 @@
require 'spec_helper'
RSpec.describe PushNotifications::EventNotifierFactory do
describe '#build' do
let(:user) { Fabricate.build(:user) }
let(:organization) { Fabricate(:organization) }
let(:factory) { described_class.new(event: event) }
context 'when the given Event is associated to a Post' do
let(:post) { Fabricate(:post, organization: organization, user: user) }
let(:event) { Fabricate.build(:event, post: post, action: :created) }
it 'returns a Post notifier' do
notifier = instance_double(::PushNotifications::EventNotifier::Post)
allow(::PushNotifications::EventNotifier::Post).to receive(:new).and_return(notifier)
expect(factory.build).to be(notifier)
end
end
context 'when the given Event is associated to a resource not supported' do
let(:event) { Fabricate.build(:event, action: :created) }
it 'raises an error' do
expect {
factory.build
}.to raise_error('The resource associated to the Event is not supported')
end
end
end
end