Add service for sync system notifications

This commit is contained in:
Urban Rotnik 2019-02-04 14:33:22 +01:00
parent 5c85595ab4
commit 9f3187abdf
8 changed files with 241 additions and 83 deletions

View file

@ -59,6 +59,7 @@ gem 'delayed_paperclip',
git: 'https://github.com/jrgifford/delayed_paperclip.git', git: 'https://github.com/jrgifford/delayed_paperclip.git',
ref: 'fcf574c' ref: 'fcf574c'
gem 'faker' # Generate fake data gem 'faker' # Generate fake data
gem 'httparty'
gem 'i18n-js', '~> 3.0' # Localization in javascript files gem 'i18n-js', '~> 3.0' # Localization in javascript files
gem 'jbuilder' # JSON structures via a Builder-style DSL gem 'jbuilder' # JSON structures via a Builder-style DSL
gem 'logging', '~> 2.0.0' gem 'logging', '~> 2.0.0'
@ -119,6 +120,7 @@ group :development, :test do
gem 'rubocop', '>= 0.59.0', require: false gem 'rubocop', '>= 0.59.0', require: false
gem 'scss_lint', require: false gem 'scss_lint', require: false
gem 'starscope', require: false gem 'starscope', require: false
gem 'timecop'
end end
group :test do group :test do
@ -131,6 +133,7 @@ group :test do
gem 'poltergeist' gem 'poltergeist'
gem 'shoulda-matchers' gem 'shoulda-matchers'
gem 'simplecov', require: false gem 'simplecov', require: false
gem 'webmock'
end end
group :production do group :production do

View file

@ -170,6 +170,8 @@ GEM
coffee-script-source (1.12.2) coffee-script-source (1.12.2)
commit_param_routing (0.0.1) commit_param_routing (0.0.1)
concurrent-ruby (1.0.5) concurrent-ruby (1.0.5)
crack (0.4.3)
safe_yaml (~> 1.0.0)
crass (1.0.4) crass (1.0.4)
cucumber (3.1.0) cucumber (3.1.0)
builder (>= 2.1.2) builder (>= 2.1.2)
@ -238,7 +240,10 @@ GEM
globalid (0.4.1) globalid (0.4.1)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
hammerjs-rails (2.0.8) hammerjs-rails (2.0.8)
hashdiff (0.3.8)
hashie (3.5.7) hashie (3.5.7)
httparty (0.16.2)
multi_xml (>= 0.5.2)
i18n (0.9.5) i18n (0.9.5)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
i18n-js (3.0.3) i18n-js (3.0.3)
@ -452,6 +457,7 @@ GEM
ruby-progressbar (1.10.0) ruby-progressbar (1.10.0)
ruby_dep (1.5.0) ruby_dep (1.5.0)
rubyzip (1.2.1) rubyzip (1.2.1)
safe_yaml (1.0.4)
sanitize (4.6.6) sanitize (4.6.6)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.4.4) nokogiri (>= 1.4.4)
@ -510,6 +516,7 @@ GEM
thor (0.20.0) thor (0.20.0)
thread_safe (0.3.6) thread_safe (0.3.6)
tilt (2.0.8) tilt (2.0.8)
timecop (0.9.1)
tinymce-rails (4.7.13) tinymce-rails (4.7.13)
railties (>= 3.1.1) railties (>= 3.1.1)
turbolinks (5.1.1) turbolinks (5.1.1)
@ -524,6 +531,10 @@ GEM
uniform_notifier (1.11.0) uniform_notifier (1.11.0)
warden (1.2.7) warden (1.2.7)
rack (>= 1.0) rack (>= 1.0)
webmock (3.5.1)
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff
webpacker (2.0) webpacker (2.0)
activesupport (>= 4.2) activesupport (>= 4.2)
multi_json (~> 1.2) multi_json (~> 1.2)
@ -581,6 +592,7 @@ DEPENDENCIES
faker faker
figaro figaro
hammerjs-rails hammerjs-rails
httparty
i18n-js (~> 3.0) i18n-js (~> 3.0)
jbuilder jbuilder
jquery-rails jquery-rails
@ -633,11 +645,13 @@ DEPENDENCIES
sneaky-save! sneaky-save!
spinjs-rails spinjs-rails
starscope starscope
timecop
tinymce-rails (~> 4.7.13) tinymce-rails (~> 4.7.13)
turbolinks (~> 5.1.1) turbolinks (~> 5.1.1)
tzinfo-data tzinfo-data
uglifier (>= 1.3.0) uglifier (>= 1.3.0)
underscore-rails underscore-rails
webmock
webpacker (~> 2.0) webpacker (~> 2.0)
whacamole whacamole
wicked_pdf (~> 1.1.0) wicked_pdf (~> 1.1.0)

View file

@ -37,4 +37,13 @@ class SystemNotification < ApplicationRecord
'user_system_notifications.seen_at' 'user_system_notifications.seen_at'
) )
end end
def self.last_sync_timestamp
# If no notifications are present, the created_at of the
# first user is used as the "initial sync time-point"
SystemNotification
.order(last_time_changed_at: :desc)
.first&.last_time_changed_at&.to_i ||
User.order(created_at: :desc).first&.created_at&.to_i
end
end end

View file

@ -0,0 +1,84 @@
# frozen_string_literal: true
module Notifications
class SyncSystemNotificationsService
extend Service
include HTTParty
base_uri Rails.application.secrets.system_notifications_uri
attr_reader :errors
def initialize
@errors = {}
end
def call
call_api
save_new_notifications if succeed?
self
end
def succeed?
@errors.none?
end
private
def call_api
last_sync = SystemNotification.last_sync_timestamp
channel = Rails.application.secrets.system_notifications_channel
unless last_sync
@errors[:last_sync_timestamp] = 'Cannot find last_sync_timestamp'
return false
end
query = { query: { last_sync_timestamp: last_sync,
notifications_channel: channel },
headers: { 'accept':
'application/vnd.system-notifications.1+json' } }
# rubocop:disable Lint/ShadowedException:
begin
@api_call = self.class.get('/api/system_notifications', query)
if @api_call.response.code.to_i != 200
@errors[:api_error] =
[@api_call.response.code.to_s, @api_call.response.message].join('-')
end
rescue SocketError, HTTParty::Error, StandardError => e
@errors[e.class.to_s.downcase.to_sym] = e.message
end
# rubocop:enable Lint/ShadowedException:
end
def save_new_notifications
@api_call.parsed_response['system_notifications'].each do |sn|
# Save new notification if not exists or override old 1
attrs =
sn.slice('title',
'description',
'modal_title',
'modal_body',
'show_on_login',
'source_id')
.merge('source_created_at':
Time.parse(sn['source_created_at']),
'last_time_changed_at':
Time.parse(sn['last_time_changed_at']))
.symbolize_keys
n = SystemNotification
.where(source_id: attrs[:source_id]).first_or_initialize(attrs)
if n.new_record?
n.save!
elsif n.last_time_changed_at < attrs[:last_time_changed_at]
n.update_attributes!(attrs)
end
end
end
end
end

View file

@ -59,12 +59,18 @@ common: &common
development: development:
secret_key_base: 22f2adf8f5cb73351da28f2292daa840cc2a414ae00ae605b175a8d5c73932f7e5b8ff8ef8f1554a7f1064f9869b15347f7709f0daa6ccb24c50f3cace304f64 secret_key_base: 22f2adf8f5cb73351da28f2292daa840cc2a414ae00ae605b175a8d5c73932f7e5b8ff8ef8f1554a7f1064f9869b15347f7709f0daa6ccb24c50f3cace304f64
system_notifications_uri: <%= ENV["SYSTEM_NOTIFICATIONS_URI"] %>
system_notifications_channel: <%= ENV["SYSTEM_NOTIFICATIONS_CHANNEL"] %>
<<: *common <<: *common
test: test:
secret_key_base: f3719934e04fa8871cf5d33d5c60f05e1b8995e0315265aef9f8b878da49bd2d386eb25ce35545b469a94ccf22f91e0052b93a15194b4f57b0c8d6ce8b150e1e secret_key_base: f3719934e04fa8871cf5d33d5c60f05e1b8995e0315265aef9f8b878da49bd2d386eb25ce35545b469a94ccf22f91e0052b93a15194b4f57b0c8d6ce8b150e1e
system_notifications_uri: 'system-notifications-service.test'
system_notifications_channel: 'test-channel'
<<: *common <<: *common
production: production:
secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
system_notifications_uri: <%= ENV["SYSTEM_NOTIFICATIONS_URI"] %>
system_notifications_channel: <%= ENV["SYSTEM_NOTIFICATIONS_CHANNEL"] %>
<<: *common <<: *common

View file

@ -42,4 +42,31 @@ describe SystemNotification do
describe 'Associations' do describe 'Associations' do
it { is_expected.to have_many(:users) } it { is_expected.to have_many(:users) }
end end
describe 'self.last_sync_timestamp' do
context 'when there is no users or system notifications in db' do
it 'returns nil' do
expect(described_class.last_sync_timestamp).to be_nil
end
end
context 'when there is no system notifications in db' do
it 'returns last user created_at' do
create :user
expect(described_class.last_sync_timestamp)
.to be == User.last.created_at.to_i
end
end
context 'when have some system notifications' do
it 'returns last system notifications last_time_changed_at timestamp' do
create :user
create :system_notification
expect(described_class.last_sync_timestamp)
.to be SystemNotification.last.last_time_changed_at.to_i
end
end
end
end end

View file

@ -0,0 +1,91 @@
# frozen_string_literal: true
require 'rails_helper'
describe Notifications::SyncSystemNotificationsService do
url = 'http://system-notifications-service.test/api/system_notifications'
let!(:user) { create :user }
let(:service_call) do
Notifications::SyncSystemNotificationsService.call
end
let(:first_call_result) do
notifications = (1..10).map do |id|
FactoryBot.attributes_for(:system_notification)
.merge('source_id': id)
end
{ system_notifications: notifications }
end
context 'when request is successful' do
before do |test|
if test.metadata[:add_notifications_before]
create :system_notification,
source_id: 10,
last_time_changed_at: 10.days.ago.to_datetime
end
stub_request(:get, url)
.with(query: { 'last_sync_timestamp':
SystemNotification.last_sync_timestamp,
'notifications_channel': 'test-channel' },
headers: { 'accept':
'application/vnd.system-notifications.1+json' })
.to_return(body: first_call_result.to_json,
status: 200,
headers: { 'Content-Type': 'application/json' })
end
it 'adds 10 notifictions into db' do
expect { service_call }.to(change { SystemNotification.all.count }.by(10))
end
it 'does not add 10 notifications because ther are already in DB' do
first_call_result[:system_notifications].each do |sn|
SystemNotification.create(sn)
end
expect { service_call }.not_to(change { SystemNotification.all.count })
end
it 'updates existing notification', add_notifications_before: true do
expect { service_call }
.to(change { SystemNotification.last.last_time_changed_at })
end
it 'add only 3 notifications' do
first_call_result[:system_notifications][2..8].each do |sn|
SystemNotification.create(sn)
end
expect { service_call }.to(change { SystemNotification.all.count }.by(3))
end
it 'return error when last_sync_timestamp is nil' do
allow(SystemNotification).to receive(:last_sync_timestamp).and_return(nil)
expect(service_call.errors).to have_key(:last_sync_timestamp)
end
end
context 'when request is unsuccessful' do
before do
stub_request(:get, url)
.with(query: { 'last_sync_timestamp':
SystemNotification.last_sync_timestamp,
'notifications_channel': 'test-channel' })
.to_return(status: [500, 'Internal Server Error'])
end
it 'returns api_error with message' do
expect(service_call.errors).to have_key(:api_error)
end
it 'returns error with description about itself' do
allow(Notifications::SyncSystemNotificationsService)
.to receive(:get).and_raise(SocketError)
expect(service_call.errors).to have_key(:socketerror)
end
end
end

View file

@ -1,40 +1,20 @@
# This file was generated by the `rails generate rspec:install` command. Conventionally, all # frozen_string_literal: true
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
# The generated `.rspec` file contains `--require spec_helper` which will cause
# this file to always be loaded, without a need to explicitly require it in any
# files.
#
# Given that it is always loaded, you are encouraged to keep this file as
# light-weight as possible. Requiring heavyweight dependencies from this file
# will add to the boot time of your test suite on EVERY test run, even for an
# individual file that may not need all of that loaded. Instead, consider making
# a separate helper file that requires the additional dependencies and performs
# the additional setup, and require it from the spec files that actually need
# it.
#
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
require 'capybara/rspec' require 'capybara/rspec'
require 'simplecov' require 'simplecov'
require 'faker' require 'faker'
require 'active_record' require 'active_record'
require 'bullet' require 'bullet'
require "json_matchers/rspec" require 'json_matchers/rspec'
require 'webmock/rspec'
# Require all custom matchers # Require all custom matchers
Dir[File.expand_path(File.join(File.dirname(__FILE__),'support','**','*.rb'))].each { |f| require f } Dir[
File.expand_path(File.join(File.dirname(__FILE__), 'support', '**', '*.rb'))
].each { |f| require f }
RSpec.configure do |config| RSpec.configure do |config|
# rspec-expectations config goes here. You can use an alternate
# assertion/expectation library such as wrong or the stdlib/minitest
# assertions if you prefer.
config.expect_with :rspec do |expectations| config.expect_with :rspec do |expectations|
# This option will default to `true` in RSpec 4. It makes the `description`
# and `failure_message` of custom matchers include text for helper methods
# defined using `chain`, e.g.:
# be_bigger_than(2).and_smaller_than(4).description
# # => "be bigger than 2 and smaller than 4"
# ...rather than:
# # => "be bigger than 2"
expectations.include_chain_clauses_in_custom_matcher_descriptions = true expectations.include_chain_clauses_in_custom_matcher_descriptions = true
end end
@ -46,64 +26,8 @@ RSpec.configure do |config|
# `true` in RSpec 4. # `true` in RSpec 4.
mocks.verify_partial_doubles = true mocks.verify_partial_doubles = true
end end
# This option will default to `:apply_to_host_groups` in RSpec 4 (and will
# have no way to turn it off -- the option exists only for backwards
# compatibility in RSpec 3). It causes shared context metadata to be
# inherited by the metadata hash of host groups and examples, rather than
# triggering implicit auto-inclusion in groups with matching metadata.
config.shared_context_metadata_behavior = :apply_to_host_groups config.shared_context_metadata_behavior = :apply_to_host_groups
# The settings below are suggested to provide a good initial experience
# with RSpec, but feel free to customize to your heart's content.
=begin
# This allows you to limit a spec run to individual examples or groups
# you care about by tagging them with `:focus` metadata. When nothing
# is tagged with `:focus`, all examples get run. RSpec also provides
# aliases for `it`, `describe`, and `context` that include `:focus`
# metadata: `fit`, `fdescribe` and `fcontext`, respectively.
config.filter_run_when_matching :focus
# Allows RSpec to persist some state between runs in order to support
# the `--only-failures` and `--next-failure` CLI options. We recommend
# you configure your source control system to ignore this file.
config.example_status_persistence_file_path = "spec/examples.txt"
# Limits the available syntax to the non-monkey patched syntax that is
# recommended. For more details, see:
# - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
# - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
# - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
config.disable_monkey_patching!
# Many RSpec users commonly either run the entire suite or an individual
# file, and it's useful to allow more verbose output when running an
# individual spec file.
if config.files_to_run.one?
# Use the documentation formatter for detailed output,
# unless a formatter has already been configured
# (e.g. via a command-line flag).
config.default_formatter = "doc"
end
# Print the 10 slowest examples and example groups at the
# end of the spec run, to help surface which specs are running
# particularly slow.
config.profile_examples = 10
# Run specs in random order to surface order dependencies. If you find an
# order dependency and want to debug it, you can fix the order by providing
# the seed, which is printed after each run.
# --seed 1234
config.order = :random
# Seed global randomization in this process using the `--seed` CLI option.
# Setting this allows you to use `--seed` to deterministically reproduce
# test failures related to randomization by passing the same `--seed` value
# as the one that triggered the failure.
Kernel.srand config.seed
=end
# Enable bullet gem in tests
if Bullet.enable? if Bullet.enable?
config.before(:each) do config.before(:each) do
Bullet.start_request Bullet.start_request