├── .gitignore ├── .pryrc ├── .rspec ├── .rubocop.yml ├── .rubocop_todo.yml ├── .travis.yml ├── Appraisals ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── app ├── helpers │ └── logux_helper.rb └── logux │ ├── actions.rb │ └── policies.rb ├── bin ├── console └── setup ├── config └── routes.rb ├── docker-compose.yml ├── lib ├── generators │ └── logux │ │ └── model │ │ ├── USAGE │ │ ├── model_generator.rb │ │ └── templates │ │ └── migration.rb.erb ├── logux │ ├── action.rb │ ├── engine.rb │ ├── model.rb │ ├── model │ │ ├── dsl.rb │ │ ├── proxy.rb │ │ ├── updater.rb │ │ └── updates_deprecator.rb │ └── version.rb ├── logux_rails.rb └── tasks │ └── logux_tasks.rake ├── logux_rails.gemspec └── spec ├── dummy ├── app │ ├── assets │ │ └── config │ │ │ └── manifest.js │ ├── controllers │ │ └── application_controller.rb │ ├── helpers │ │ └── application_helper.rb │ ├── logux │ │ ├── actions │ │ │ ├── blog │ │ │ │ └── notes.rb │ │ │ ├── comment.rb │ │ │ └── post.rb │ │ ├── channels │ │ │ └── post.rb │ │ └── policies │ │ │ ├── actions │ │ │ ├── comment.rb │ │ │ ├── policy_without_action.rb │ │ │ └── post.rb │ │ │ └── channels │ │ │ ├── comment.rb │ │ │ ├── policy_without_channel.rb │ │ │ └── post.rb │ ├── models │ │ ├── post.rb │ │ └── user.rb │ └── views │ │ ├── home │ │ └── index.html.erb │ │ ├── layouts │ │ └── application.html.erb │ │ └── posts │ │ └── show.html.erb ├── config │ ├── application.rb │ ├── boot.rb │ ├── database.yml │ ├── environment.rb │ ├── environments │ │ └── test.rb │ ├── routes.rb │ └── secrets.yml └── db │ ├── migrate │ ├── 20180613095808_create_users.rb │ ├── 20181101131807_create_posts.rb │ └── 20181101194822_add_logux_fields_updated_at_to_posts.rb │ └── schema.rb ├── factories ├── logux_actions_factory.rb ├── logux_meta_factory.rb └── post_factory.rb ├── logux ├── action_caller_spec.rb ├── model │ └── updates_deprecator_spec.rb ├── model_spec.rb └── tasks │ ├── actions_spec.rb │ └── channels_spec.rb ├── rails_helper.rb ├── requests ├── request_logux_server_spec.rb ├── request_without_action_spec.rb └── request_without_subscribe_spec.rb ├── spec_helper.rb └── support └── timecop.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | 10 | # rspec failure tracking 11 | .rspec_status 12 | 13 | .DS_Store 14 | 15 | **/*.log 16 | .*.swp 17 | coverage 18 | Gemfile.lock 19 | spec/dummy/db/*.sqlite3 20 | /gemfiles/* 21 | -------------------------------------------------------------------------------- /.pryrc: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | if defined?(PryByebug) 4 | Pry.commands.alias_command 'c', 'continue' 5 | Pry.commands.alias_command 's', 'step' 6 | Pry.commands.alias_command 'n', 'next' 7 | Pry.commands.alias_command 'f', 'finish' 8 | end 9 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format progress 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .rubocop_todo.yml 2 | 3 | require: rubocop-rspec 4 | 5 | AllCops: 6 | Exclude: 7 | - 'spec/dummy/db/**/*.rb' 8 | - 'vendor/**/*' 9 | - 'gemfiles/*' 10 | TargetRubyVersion: 2.4 11 | 12 | Metrics/BlockLength: 13 | Exclude: 14 | - 'spec/**/*.rb' 15 | - 'logux_rails.gemspec' 16 | - 'lib/logux/test/helpers.rb' 17 | 18 | RSpec/DescribeClass: 19 | Exclude: 20 | - 'spec/logux/tasks/*.rb' 21 | - 'spec/requests/*.rb' 22 | 23 | FactoryBot/StaticAttributeDefinedDynamically: 24 | Enabled: false 25 | 26 | RSpec/ExpectChange: 27 | EnforcedStyle: block 28 | 29 | Style/RescueStandardError: 30 | EnforcedStyle: implicit 31 | 32 | Style/DateTime: 33 | Enabled: false 34 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2018-07-26 13:29:22 +0300 using RuboCop version 0.57.0. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 17 10 | Style/Documentation: 11 | Enabled: false 12 | 13 | # Offense count: 2 14 | Style/MixinUsage: 15 | Exclude: 16 | - 'spec/dummy/bin/setup' 17 | - 'spec/dummy/bin/update' 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: minimal 2 | sudo: require 3 | services: 4 | - docker 5 | 6 | before_script: 7 | - unset BUNDLE_GEMFILE 8 | - docker-compose run app bundle install 9 | - docker-compose run app bundle exec appraisal install 10 | script: 11 | - docker-compose run app bundle exec appraisal rspec 12 | - docker-compose run app bundle exec rubocop 13 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | appraise '5.0' do 4 | gem 'activerecord', '~> 5.0' 5 | end 6 | 7 | appraise '5.1' do 8 | gem 'activerecord', '~> 5.1' 9 | end 10 | 11 | appraise '5.2' do 12 | gem 'activerecord', '~> 5.2' 13 | end 14 | 15 | appraise '6.0' do 16 | gem 'activerecord', '~> 6.0' 17 | end 18 | 19 | appraise '6.1' do 20 | gem 'activerecord', '~> 6.1' 21 | end 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | This project adheres to [Semantic Versioning](http://semver.org/). 3 | 4 | ## 1.0.0 5 | 6 | * Support Rails 6.1. 7 | * Support Logux protocol v3 (breaking change, Logux.config.password is renamed to Logux.config.secret). 8 | 9 | ## 0.2 10 | * Core Logux facilities are moved to `logux-rack` gem. 11 | * `Logux::Actions` is soft-deprecated. Please use `Logux::Action` from now on. 12 | * `Logux::Model::UpdatesDeprecator` is now coupled with `Logux::ActionCaller` via Logux configuration. 13 | 14 | ## 0.1.1 15 | * Rails 6.0 support. 16 | 17 | ## 0.1 18 | * Initial release. 19 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } 6 | 7 | # Specify your gem's dependencies in logux_rails.gemspec 8 | gemspec 9 | 10 | # NOTE: Remove this line after logux-rack is released on rubygems 11 | gem 'logux-rack', github: 'logux/logux-rack' 12 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 WildDima 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Logux Rails 2 | 3 | [![Build Status](https://travis-ci.org/logux/logux_rails.svg?branch=master)](https://travis-ci.org/logux/logux_rails) [![Coverage Status](https://coveralls.io/repos/github/logux/logux_rails/badge.svg?branch=master)](https://coveralls.io/github/logux/logux_rails?branch=master) 4 | 5 | Add WebSockets, live-updates and offline-first to Ruby on Rails with [Logux](https://logux.io/). This gem will add [Logux Back-end Protocol](https://logux.io/protocols/backend/spec/) to Ruby on Rails and then you can use Logux Server as a proxy between WebSocket and your Rails application. 6 | 7 | Read [Creating Logux Proxy](https://logux.io/guide/starting/proxy-server/) guide. 8 | 9 | ## Installation 10 | 11 | Add this line to your application's Gemfile: 12 | 13 | ```ruby 14 | gem 'logux_rails' 15 | ``` 16 | 17 | And then execute: 18 | 19 | ```bash 20 | bundle 21 | ``` 22 | 23 | ## Usage 24 | 25 | First of all, you have to configure Logux, by defining server address in, for example, `config/initializers/logux.rb`: 26 | 27 | ```ruby 28 | Logux.configure do |config| 29 | config.logux_host = 'http://localhost:31338' 30 | end 31 | ``` 32 | 33 | Mount `Logux::Rack` in your application routing configuration: 34 | 35 | ```ruby 36 | # config/routes.rb 37 | Rails.application.routes.draw do 38 | mount Logux::Engine => '/' 39 | end 40 | ``` 41 | 42 | After this, POST requests to `/logux` will be processed by `LoguxController`. You can redefine it or inherit from, if it necessary, for example, for implementing custom authorization flow. 43 | 44 | Logux Rails will try to find Action for the specific message from Logux Server. For example, for `project/rename` action, you should define `Action::Project` class, inherited from `Logux::Action` base class, and implement `rename` method. 45 | 46 | ### Rake commands 47 | 48 | Use `rails logux:actions` command to get the list of available action types, or `rails logux:channels` for channels. The default search path is set to `app/logux/actions` and `app/logux/channels` for actions and channels correspondingly, assuming `app` directory is the root of your Rails application. Both command support custom search paths: `rails logux:actions[lib/logux/actions]`. 49 | 50 | ## Development with Docker 51 | 52 | After checking out the repo, run: 53 | 54 | ```bash 55 | docker-compose run app bundle install 56 | docker-compose run app bundle exec appraisal install 57 | ``` 58 | 59 | Run tests with: 60 | 61 | ```bash 62 | docker-compose run app bundle exec appraisal rspec 63 | ``` 64 | 65 | Run RuboCop with: 66 | 67 | ```bash 68 | docker-compose run app bundle exec rubocop 69 | ``` 70 | 71 | ## License 72 | 73 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 74 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | require 'rspec/core/rake_task' 5 | require 'rubocop/rake_task' 6 | 7 | RSpec::Core::RakeTask.new(:spec) 8 | RuboCop::RakeTask.new 9 | 10 | task default: %i[rubocop spec] 11 | -------------------------------------------------------------------------------- /app/helpers/logux_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module LoguxHelper; end 4 | -------------------------------------------------------------------------------- /app/logux/actions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Actions; end 4 | -------------------------------------------------------------------------------- /app/logux/policies.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Policies; end 4 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'bundler/setup' 5 | require 'logux_rails' 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | # (If you use this, don't forget to add pry to your Gemfile!) 11 | # require "pry" 12 | # Pry.start 13 | 14 | require 'irb' 15 | IRB.start(__FILE__) 16 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Logux::Engine.routes.draw do 4 | mount Logux::Rack::App => '/' 5 | end 6 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | app: 5 | image: ruby:2.5.1 6 | environment: 7 | - BUNDLE_PATH=/bundle 8 | - BUNDLE_CONFIG=/app/.bundle/config 9 | 10 | - DB_HOST=db 11 | - DB_NAME=logux_rails 12 | - DB_USERNAME=postgres 13 | - DB_PASSWORD=password 14 | command: bash 15 | working_dir: /app 16 | volumes: 17 | - .:/app:cached 18 | - bundler_data:/bundle 19 | tmpfs: 20 | - /tmp 21 | depends_on: 22 | - db 23 | 24 | db: 25 | image: postgres:10 26 | environment: 27 | - POSTGRES_DB=logux_rails 28 | - POSTGRES_PASSWORD=password 29 | 30 | volumes: 31 | bundler_data: 32 | -------------------------------------------------------------------------------- /lib/generators/logux/model/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | Generates the necessary migration to enable field tracking for logux updates 3 | 4 | Examples: 5 | rails generate logux:model User 6 | 7 | This will generate the migration to add a column with update time data. 8 | 9 | rails generate logux:model User --nullable 10 | 11 | This will generate the migration to add a column with update time data and add `null: false` constraint. Be careful, adding the constraint to the table with a big number of rows can cause lots of locks. 12 | -------------------------------------------------------------------------------- /lib/generators/logux/model/model_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails/generators' 4 | require 'rails/generators/active_record/migration/migration_generator' 5 | 6 | module Logux 7 | module Generators 8 | class ModelGenerator < ::ActiveRecord::Generators::Base # :nodoc: 9 | source_root File.expand_path('templates', __dir__) 10 | 11 | class_option :nullable, 12 | type: :boolean, 13 | optional: true, 14 | desc: 'Define whether field should have not-null constraint' 15 | 16 | def generate_migration 17 | migration_template( 18 | 'migration.rb.erb', 19 | "db/migrate/add_logux_fields_updated_at_to_#{plural_table_name}.rb" 20 | ) 21 | end 22 | 23 | def nullable? 24 | options.fetch(:nullable, false) 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/generators/logux/model/templates/migration.rb.erb: -------------------------------------------------------------------------------- 1 | class <%= @migration_class_name %> < ActiveRecord::Migration<%= ActiveRecord::VERSION::MAJOR < 5 ? '' : '[5.0]' %> 2 | def up 3 | <% if nullable? %> 4 | add_column :<%= plural_table_name %>, :logux_fields_updated_at, :jsonb, null: false, default: {} 5 | <% else %> 6 | add_column :<%= plural_table_name %>, :logux_fields_updated_at, :jsonb, null: true 7 | change_column_default :<%= plural_table_name %>, :logux_fields_updated_at, {} 8 | <% end %> 9 | end 10 | 11 | def down 12 | remove_column :<%= plural_table_name %>, :logux_fields_updated_at 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/logux/action.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Logux 4 | class Action < ::ActionController::Parameters 5 | def action_name 6 | type&.split('/')&.dig(0) 7 | end 8 | 9 | def action_type 10 | type&.split('/')&.last 11 | end 12 | 13 | def channel_name 14 | channel&.split('/')&.dig(0) 15 | end 16 | 17 | def channel_id 18 | channel&.split('/')&.last 19 | end 20 | 21 | def type 22 | require(:type) 23 | end 24 | 25 | def channel 26 | require(:channel) 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/logux/engine.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Logux 4 | class Engine < ::Rails::Engine 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/logux/model.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'model/updater' 4 | require_relative 'model/proxy' 5 | require_relative 'model/dsl' 6 | require_relative 'model/updates_deprecator' 7 | 8 | module Logux 9 | module Model 10 | class InsecureUpdateError < StandardError; end 11 | 12 | def self.included(base) 13 | base.extend(DSL) 14 | 15 | base.before_update :touch_logux_order_for_changes, 16 | unless: -> { changes.key?('logux_fields_updated_at') } 17 | end 18 | 19 | def logux 20 | Proxy.new(self) 21 | end 22 | 23 | private 24 | 25 | def touch_logux_order_for_changes 26 | attributes = changed.each_with_object({}) do |attr, res| 27 | res[attr] = send(attr) 28 | end 29 | 30 | updater = Updater.new(model: self, attributes: attributes) 31 | self.logux_fields_updated_at = updater.updated_attributes 32 | 33 | ActiveSupport::Notifications.instrument( 34 | Logux::Model::UpdatesDeprecator::EVENT, 35 | model: self 36 | ) 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/logux/model/dsl.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Logux 4 | module Model 5 | module DSL 6 | def logux_crdt_map_attributes(*attributes) 7 | @logux_crdt_mapped_attributes = attributes 8 | end 9 | 10 | def logux_crdt_mapped_attributes 11 | @logux_crdt_mapped_attributes ||= [] 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/logux/model/proxy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Logux 4 | module Model 5 | class Proxy 6 | def initialize(model) 7 | @model = model 8 | end 9 | 10 | def update(meta, attributes) 11 | updater = Updater.new( 12 | model: @model, 13 | logux_order: meta.logux_order, 14 | attributes: attributes 15 | ) 16 | @model.update(updater.updated_attributes) 17 | end 18 | 19 | def updated_at(field) 20 | @model.logux_fields_updated_at[field.to_s] 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/logux/model/updater.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Logux 4 | module Model 5 | class Updater 6 | def initialize(model:, attributes:, logux_order: Logux.generate_action_id) 7 | @model = model 8 | @logux_order = logux_order 9 | @attributes = attributes 10 | end 11 | 12 | def updated_attributes 13 | newer_updates.merge(logux_fields_updated_at: fields_updated_at) 14 | end 15 | 16 | private 17 | 18 | def fields_updated_at 19 | @fields_updated_at ||= 20 | newer_updates.slice(*tracked_fields) 21 | .keys 22 | .reduce(@model.logux_fields_updated_at) do |acc, attr| 23 | acc.merge(attr => @logux_order) 24 | end 25 | end 26 | 27 | def newer_updates 28 | @newer_updates ||= @attributes.reject do |attr, _| 29 | field_updated_at = @model.logux.updated_at(attr) 30 | field_updated_at && field_updated_at > @logux_order 31 | end 32 | end 33 | 34 | def tracked_fields 35 | @model.class.logux_crdt_mapped_attributes 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/logux/model/updates_deprecator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Logux 4 | module Model 5 | class UpdatesDeprecator 6 | EVENT = 'logux.insecure_update' 7 | 8 | def self.call(options = {}, &block) 9 | new(options).call(&block) 10 | end 11 | 12 | def initialize(options) 13 | @options = options 14 | end 15 | 16 | def call(&block) 17 | callback = lambda(&method(:handle_insecure_update)) 18 | ActiveSupport::Notifications.subscribed(callback, EVENT, &block) 19 | end 20 | 21 | private 22 | 23 | # rubocop:disable Naming/UncommunicativeMethodParamName 24 | def handle_insecure_update(_, _, _, _, args) 25 | model = args[:model] 26 | 27 | attributes = model.changed.map(&:to_sym) - [:logux_fields_updated_at] 28 | insecure_attributes = 29 | attributes & model.class.logux_crdt_mapped_attributes 30 | return if insecure_attributes.empty? 31 | 32 | notify_about_insecure_update(insecure_attributes) 33 | end 34 | # rubocop:enable Naming/UncommunicativeMethodParamName 35 | 36 | def notify_about_insecure_update(insecure_attributes) 37 | pluralized_attributes = 'attribute'.pluralize(insecure_attributes.count) 38 | 39 | message = <<~TEXT 40 | Logux tracked #{pluralized_attributes} (#{insecure_attributes.join(', ')}) should be updated using model.logux.update(...) 41 | TEXT 42 | 43 | case level 44 | when :warn 45 | ActiveSupport::Deprecation.warn(message) 46 | when :error 47 | raise InsecureUpdateError, message 48 | end 49 | end 50 | 51 | DEFAULT_LEVEL = :warn 52 | 53 | def level 54 | @options[:level] || DEFAULT_LEVEL 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/logux/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Logux 4 | VERSION = '1.0.0' 5 | end 6 | -------------------------------------------------------------------------------- /lib/logux_rails.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'action_controller' 4 | require 'active_support' 5 | require 'logux/rack' 6 | require 'rails/engine' 7 | require 'logux/engine' 8 | 9 | module Logux 10 | autoload :Model, 'logux/model' 11 | 12 | configurable %i[ 13 | action_watcher 14 | action_watcher_options 15 | ] 16 | 17 | configuration_defaults do |config| 18 | config.action_watcher = Logux::Model::UpdatesDeprecator 19 | config.action_watcher_options = { level: :error } 20 | config.logger = Rails.logger if defined?(Rails.logger) 21 | config.logger ||= ActiveSupport::Logger.new(STDOUT) 22 | config.on_error = proc {} 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/tasks/logux_tasks.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'logux/rake_tasks' 4 | 5 | module Logux 6 | class RakeTasks 7 | protected 8 | 9 | def default_actions_path 10 | ::Rails.root.join('app', 'logux', 'actions') 11 | end 12 | 13 | def default_channels_path 14 | ::Rails.root.join('app', 'logux', 'channels') 15 | end 16 | end 17 | end 18 | 19 | Logux::RakeTasks.new 20 | -------------------------------------------------------------------------------- /logux_rails.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path('lib', __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'logux/version' 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = 'logux_rails' 9 | spec.version = Logux::VERSION 10 | spec.authors = ['WildDima'] 11 | spec.email = ['dtopornin@gmail.com'] 12 | 13 | spec.summary = 'Logux client for rails' 14 | spec.description = 'Logux client for rails' 15 | spec.homepage = 'https://logux.io/' 16 | spec.license = 'MIT' 17 | 18 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 19 | f.match(%r{^(test|spec|features)/}) 20 | end 21 | spec.bindir = 'exe' 22 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 23 | spec.require_paths = ['lib'] 24 | 25 | spec.add_dependency 'logux-rack', '>= 0.1.0' 26 | spec.add_dependency 'rails', '>= 5.0' 27 | spec.add_development_dependency 'appraisal', '~> 2.2' 28 | spec.add_development_dependency 'bundler', '~> 1.16' 29 | spec.add_development_dependency 'combustion', '~> 1.1.0' 30 | spec.add_development_dependency 'coveralls' 31 | spec.add_development_dependency 'factory_bot' 32 | spec.add_development_dependency 'pg' 33 | spec.add_development_dependency 'pry' 34 | spec.add_development_dependency 'pry-byebug' 35 | spec.add_development_dependency 'rake', '~> 10.0' 36 | spec.add_development_dependency 'rspec-live_controllers' 37 | spec.add_development_dependency 'rspec-rails' 38 | spec.add_development_dependency 'rubocop', '~> 0.60.0' 39 | spec.add_development_dependency 'rubocop-rspec', '~> 1.27.0' 40 | spec.add_development_dependency 'simplecov' 41 | spec.add_development_dependency 'timecop' 42 | spec.add_development_dependency 'webmock' 43 | end 44 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationController < ActionController::Base 4 | attr_reader :current_user 5 | end 6 | -------------------------------------------------------------------------------- /spec/dummy/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ApplicationHelper 4 | end 5 | -------------------------------------------------------------------------------- /spec/dummy/app/logux/actions/blog/notes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Actions 4 | module Blog 5 | class Notes < Logux::ActionController 6 | def add 7 | respond :processed 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/dummy/app/logux/actions/comment.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Actions 4 | class Comment < Logux::ActionController 5 | def add 6 | respond :processed 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/dummy/app/logux/actions/post.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Actions 4 | class Post < Logux::ActionController 5 | def rename 6 | post = FactoryBot.create(:post) 7 | post.update(title: 'new title') 8 | respond :processed 9 | end 10 | 11 | def touch 12 | post = FactoryBot.create(:post) 13 | post.update(updated_at: Time.now) 14 | respond :processed 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/dummy/app/logux/channels/post.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Channels 4 | class Post < Logux::ChannelController 5 | def subscribe 6 | respond :forbidden if user_id == 1 7 | super 8 | end 9 | 10 | def initial_data 11 | [{ action: 'approve' }] 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/dummy/app/logux/policies/actions/comment.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Policies 4 | module Actions 5 | class Comment < Logux::Policy 6 | def add? 7 | true 8 | end 9 | 10 | def update? 11 | false 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/dummy/app/logux/policies/actions/policy_without_action.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Policies 4 | module Actions 5 | class PolicyWithoutAction < Logux::Policy 6 | def create? 7 | true 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/dummy/app/logux/policies/actions/post.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Policies 4 | module Actions 5 | class Post < Logux::Policy 6 | def add? 7 | true 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/dummy/app/logux/policies/channels/comment.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Policies 4 | module Channels 5 | class Comment < Logux::Policy 6 | def subscribe? 7 | true 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/dummy/app/logux/policies/channels/policy_without_channel.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Policies 4 | module Channels 5 | class PolicyWithoutChannel < Logux::Policy 6 | def subscribe? 7 | true 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/dummy/app/logux/policies/channels/post.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Policies 4 | module Channels 5 | class Post < Logux::Policy 6 | def subscribe? 7 | true 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/dummy/app/models/post.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Post < ActiveRecord::Base 4 | include Logux::Model 5 | 6 | logux_crdt_map_attributes :title, :content 7 | end 8 | -------------------------------------------------------------------------------- /spec/dummy/app/models/user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class User < ActiveRecord::Base 4 | end 5 | -------------------------------------------------------------------------------- /spec/dummy/app/views/home/index.html.erb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logux/logux_rails/064d1bc507a7d75ede61b10deee715fb70e46142/spec/dummy/app/views/home/index.html.erb -------------------------------------------------------------------------------- /spec/dummy/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | 3 | -------------------------------------------------------------------------------- /spec/dummy/app/views/posts/show.html.erb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logux/logux_rails/064d1bc507a7d75ede61b10deee715fb70e46142/spec/dummy/app/views/posts/show.html.erb -------------------------------------------------------------------------------- /spec/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'boot' 4 | 5 | require 'rails' 6 | require 'active_record/railtie' 7 | require 'action_controller/railtie' 8 | require 'action_view/railtie' 9 | Bundler.require(*Rails.groups) 10 | 11 | module Dummy 12 | class Application < Rails::Application 13 | config.active_record.sqlite3.represent_boolean_as_integer = true 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 4 | 5 | require 'bundler/setup' 6 | -------------------------------------------------------------------------------- /spec/dummy/config/database.yml: -------------------------------------------------------------------------------- 1 | test: 2 | adapter: postgresql 3 | host: <%= ENV.fetch("DB_HOST") %> 4 | database: <%= ENV.fetch("DB_NAME") %> 5 | username: <%= ENV.fetch("DB_USERNAME") %> 6 | password: <%= ENV.fetch("DB_PASSWORD") %> 7 | -------------------------------------------------------------------------------- /spec/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'application' 4 | 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in 5 | # config/application.rb. 6 | 7 | # The test environment is used exclusively to run your application's 8 | # test suite. You never need to work with it otherwise. Remember that 9 | # your test database is "scratch space" for the test suite and is wiped 10 | # and recreated between test runs. Don't rely on the data there! 11 | config.cache_classes = true 12 | 13 | # Do not eager load code on boot. This avoids loading your whole application 14 | # just for the purpose of running a single test. If you are using a tool that 15 | # preloads Rails for running tests, you may have to set it to true. 16 | config.eager_load = false 17 | 18 | # Configure public file server for tests with Cache-Control for performance. 19 | config.public_file_server.enabled = true 20 | config.public_file_server.headers = { 21 | 'Cache-Control' => 'public, max-age=3600' 22 | } 23 | 24 | # Show full error reports and disable caching. 25 | config.consider_all_requests_local = true 26 | config.action_controller.perform_caching = false 27 | 28 | # Raise exceptions instead of rendering exception templates. 29 | config.action_dispatch.show_exceptions = false 30 | 31 | # Disable request forgery protection in test environment. 32 | config.action_controller.allow_forgery_protection = false 33 | # config.action_mailer.perform_caching = false 34 | 35 | # Tell Action Mailer not to deliver emails to the real world. 36 | # The :test delivery method accumulates sent emails in the 37 | # ActionMailer::Base.deliveries array. 38 | # config.action_mailer.delivery_method = :test 39 | 40 | # Print deprecation notices to the stderr. 41 | config.active_support.deprecation = :stderr 42 | 43 | # Raises error for missing translations 44 | # config.action_view.raise_on_missing_translations = true 45 | end 46 | -------------------------------------------------------------------------------- /spec/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Rails.application.routes.draw do 4 | mount Logux::Engine => '/' 5 | end 6 | -------------------------------------------------------------------------------- /spec/dummy/config/secrets.yml: -------------------------------------------------------------------------------- 1 | test: 2 | secret_key_base: 'qwerty123' 3 | development: 4 | secret_key_base: 'qwerty123' -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20180613095808_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :users do |t| 4 | t.text :name 5 | 6 | t.timestamps 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20181101131807_create_posts.rb: -------------------------------------------------------------------------------- 1 | class CreatePosts < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :posts do |t| 4 | t.string :title 5 | t.text :content 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20181101194822_add_logux_fields_updated_at_to_posts.rb: -------------------------------------------------------------------------------- 1 | class AddLoguxFieldsUpdatedAtToPosts < ActiveRecord::Migration[5.2] 2 | def up 3 | add_column :posts, :logux_fields_updated_at, :jsonb, null: true 4 | change_column_default :posts, :logux_fields_updated_at, {} 5 | end 6 | 7 | def down 8 | remove_column :posts, :logux_fields_updated_at 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/dummy/db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # Note that this schema.rb definition is the authoritative source for your 6 | # database schema. If you need to create the application database on another 7 | # system, you should be using db:schema:load, not running all the migrations 8 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 9 | # you'll amass, the slower it'll run and the greater likelihood for issues). 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema.define(version: 2018_06_13_095808) do 14 | 15 | create_table "users", force: :cascade do |t| 16 | t.text "name" 17 | t.datetime "created_at", null: false 18 | t.datetime "updated_at", null: false 19 | end 20 | 21 | end 22 | -------------------------------------------------------------------------------- /spec/factories/logux_actions_factory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :logux_actions, class: Logux::Actions do 5 | factory :logux_actions_subscribe do 6 | skip_create 7 | initialize_with do 8 | new({ type: 'logux/subscribe', channel: 'user/1' }.merge(attributes)) 9 | end 10 | end 11 | 12 | factory :logux_actions_subscribe_since do 13 | skip_create 14 | initialize_with do 15 | new({ 16 | type: 'logux/subscribe', 17 | channel: 'user/1', 18 | since: { time: 100 } 19 | }.merge(attributes)) 20 | end 21 | end 22 | 23 | factory :logux_actions_add do 24 | skip_create 25 | initialize_with do 26 | new({ type: 'user/add', key: 'name', value: 'test' }.merge(attributes)) 27 | end 28 | end 29 | 30 | factory :logux_actions_update do 31 | skip_create 32 | initialize_with do 33 | new({ type: 'user/add', key: 'name', value: 'test' }.merge(attributes)) 34 | end 35 | end 36 | 37 | factory :logux_actions_unknown do 38 | skip_create 39 | initialize_with do 40 | new({ type: 'unknown/action' }.merge(attributes)) 41 | end 42 | end 43 | 44 | factory :logux_actions_unknown_subscribe do 45 | skip_create 46 | initialize_with do 47 | new( 48 | { type: 'logux/subscribe', channel: 'unknown/channel' } 49 | .merge(attributes) 50 | ) 51 | end 52 | end 53 | 54 | factory :logux_actions_post do 55 | skip_create 56 | initialize_with do 57 | new( 58 | { type: 'post/rename', key: 'name', value: 'test' }.merge(attributes) 59 | ) 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /spec/factories/logux_meta_factory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :logux_meta, class: Logux::Meta do 5 | skip_create 6 | initialize_with do 7 | new(attributes) 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/factories/post_factory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :post, class: Post do 5 | title { 'initial' } 6 | content { 'initial' } 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/logux/action_caller_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | describe Logux::ActionCaller do 6 | let(:action_caller) { described_class.new(action: action, meta: meta) } 7 | let(:action) { create(:logux_actions_add) } 8 | let(:meta) { create(:logux_meta) } 9 | 10 | context 'when attributes updated' do 11 | context 'when insecure #update is called' do 12 | let(:action) { create(:logux_actions_post, type: 'post/rename') } 13 | 14 | it 'raises exception when #update is called inside action' do 15 | expect do 16 | action_caller.call! 17 | end.to raise_error(Logux::Model::InsecureUpdateError) 18 | end 19 | end 20 | 21 | context 'when secure #update is called' do 22 | let(:action) { create(:logux_actions_post, type: 'post/touch') } 23 | 24 | it 'raises exception when #update is called inside action' do 25 | expect { action_caller.call! }.not_to raise_error 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/logux/model/updates_deprecator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | describe Logux::Model::UpdatesDeprecator do 6 | let(:post) { create(:post) } 7 | 8 | context 'with error level' do 9 | it 'raises error when insecure update is detected' do 10 | expect do 11 | described_class.call(level: :error) do 12 | post.update(title: 'new title') 13 | end 14 | end.to raise_error(Logux::Model::InsecureUpdateError) 15 | end 16 | 17 | it 'does not raise error when update is secure' do 18 | expect do 19 | described_class.call(level: :error) do 20 | post.update(updated_at: Time.now) 21 | end 22 | end.not_to raise_error 23 | end 24 | end 25 | 26 | context 'with warn level' do 27 | it 'outputs deprecation warning when update is detected' do 28 | expect do 29 | described_class.call(level: :warn) do 30 | post.update(title: 'new title') 31 | end 32 | end.to output(/DEPRECATION WARNING/).to_stderr 33 | end 34 | 35 | it 'uses warn level by default' do 36 | expect do 37 | described_class.call do 38 | post.update(title: 'new title') 39 | end 40 | end.to output(/DEPRECATION WARNING/).to_stderr 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/logux/model_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe Logux::Model do 6 | subject(:model) do 7 | create( 8 | :post, 9 | logux_fields_updated_at: { 10 | title: initial_meta.logux_order, 11 | content: initial_meta.logux_order 12 | } 13 | ) 14 | end 15 | 16 | let(:older_update_meta) do 17 | create( 18 | :logux_meta, 19 | id: Time.parse('01-11-2018 12:05').to_datetime.strftime('%Q 10:uuid 0') 20 | ) 21 | end 22 | 23 | let(:initial_meta) do 24 | create( 25 | :logux_meta, 26 | id: Time.parse('01-11-2018 12:10').to_datetime.strftime('%Q 10:uuid 0') 27 | ) 28 | end 29 | 30 | let(:newer_update_meta) do 31 | create( 32 | :logux_meta, 33 | id: Time.parse('01-11-2018 12:15').to_datetime.strftime('%Q 10:uuid 0') 34 | ) 35 | end 36 | 37 | let(:latest_update_meta) do 38 | create( 39 | :logux_meta, 40 | id: Time.parse('01-11-2018 12:20').to_datetime.strftime('%Q 10:uuid 0') 41 | ) 42 | end 43 | 44 | describe '#update' do 45 | it 'updates newer attribute' do 46 | model.logux.update(newer_update_meta, content: 'newer') 47 | expect(model.content).to eq('newer') 48 | end 49 | 50 | it 'keeps attribute updated later' do 51 | model.logux.update(older_update_meta, content: 'older') 52 | expect(model.content).to eq('initial') 53 | end 54 | 55 | # rubocop:disable RSpec/MultipleExpectations 56 | it 'updates multiple fields' do 57 | model.logux.update(latest_update_meta, content: 'latest') 58 | expect(model).to have_attributes(title: 'initial', content: 'latest') 59 | 60 | model.logux.update(newer_update_meta, title: 'newer', content: 'newer') 61 | expect(model).to have_attributes(title: 'newer', content: 'latest') 62 | end 63 | # rubocop:enable RSpec/MultipleExpectations 64 | end 65 | 66 | describe '#update' do 67 | it 'updates logux.updated_at' do 68 | model.update(title: 'something') 69 | 70 | title_updated_at = model.logux.updated_at(:title) 71 | expect(title_updated_at).not_to eq(initial_meta.logux_order) 72 | end 73 | end 74 | 75 | describe '#update_attribute' do 76 | it 'updates logux.updated_at' do 77 | model.update_attribute(:content, 'something') 78 | 79 | content_updated_at = model.logux.updated_at(:content) 80 | expect(content_updated_at).not_to eq(initial_meta.logux_order) 81 | end 82 | end 83 | 84 | describe 'direct attribute assignment' do 85 | it 'updates logux.updated_at' do 86 | model.content = 'something' 87 | model.save 88 | 89 | content_updated_at = model.logux.updated_at(:content) 90 | expect(content_updated_at).not_to eq(initial_meta.logux_order) 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /spec/logux/tasks/actions_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | describe 'rake logux:actions', type: :task do 6 | subject(:task) { Rake::Task['logux:actions'] } 7 | 8 | it 'outputs all action types and corresponding class and method names' do 9 | expect { task.execute }.to output( 10 | %r{blog/notes/add Actions::Blog::Notes#add} 11 | ).to_stdout 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/logux/tasks/channels_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | describe 'rake logux:channels', type: :task do 6 | subject(:task) { Rake::Task['logux:channels'] } 7 | 8 | it 'outputs all channels and corresponding class names' do 9 | expect { task.execute }.to output( 10 | /post Channels::Post/ 11 | ).to_stdout 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/rails_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails/all' 4 | 5 | ENV['RAILS_ENV'] ||= 'test' 6 | 7 | require 'spec_helper' 8 | require 'rspec/rails' 9 | 10 | require 'combustion' 11 | 12 | Combustion.path = 'spec/dummy' 13 | Combustion.initialize! :all, database_reset: true, database_migrate: true 14 | 15 | RSpec.configure do |config| 16 | config.infer_spec_type_from_file_location! 17 | 18 | config.before(:suite) do 19 | Rails.application.load_tasks 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/requests/request_logux_server_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | describe 'Request logux server' do 6 | subject(:request_logux) do 7 | post('/logux', 8 | params: logux_params, 9 | as: :json) 10 | end 11 | 12 | let(:secret) { Logux.configuration.secret } 13 | 14 | let(:logux_params) do 15 | { version: Logux::PROTOCOL_VERSION, 16 | secret: secret, 17 | commands: [ 18 | ['action', 19 | { type: 'logux/subscribe', channel: 'post/123' }, 20 | { time: Time.now.to_i, id: '219_856_768 clientid 0', userId: 1 }], 21 | ['action', 22 | { type: 'comment/add', key: 'text', value: 'hi' }, 23 | { time: Time.now.to_i, id: '219_856_768 clientid 0', userId: 1 }] 24 | ] } 25 | end 26 | 27 | context 'when authorized' do 28 | before { request_logux } 29 | 30 | it 'returns approved chunk' do 31 | expect(response).to logux_approved('219_856_768 clientid 0') 32 | end 33 | 34 | it 'returns processed chunk' do 35 | expect(response).to logux_processed('219_856_768 clientid 0') 36 | end 37 | end 38 | 39 | context 'when no authorized' do 40 | before { request_logux } 41 | 42 | let(:logux_params) do 43 | { version: Logux::PROTOCOL_VERSION, 44 | secret: secret, 45 | commands: [ 46 | ['action', 47 | { type: 'comment/update', key: 'text', value: 'hi' }, 48 | { time: Time.now.to_i, id: '219_856_768 clientid 0', userId: 1 }] 49 | ] } 50 | end 51 | 52 | it 'returns correct body' do 53 | expect(response).to logux_forbidden 54 | end 55 | end 56 | 57 | context 'when secret wrong' do 58 | before { request_logux } 59 | 60 | let(:secret) { 'INTENTIONALLY_WRONG' } 61 | 62 | it 'returns error' do 63 | expect(response).to be_forbidden 64 | end 65 | end 66 | 67 | context 'with proxy' do 68 | let(:logux_params) do 69 | { version: Logux::PROTOCOL_VERSION, 70 | secret: secret, 71 | commands: [ 72 | ['action', 73 | { type: 'logux/subscribe', channel: 'post/123' }, 74 | { time: Time.now.to_i, proxy: 'proxy_id', 75 | id: '219_856_768 clientid 0', userId: 1 }], 76 | ['action', 77 | { type: 'comment/add', key: 'text', value: 'hi' }, 78 | { time: Time.now.to_i, id: '219_856_768 clientid 0', userId: 1 }] 79 | ] } 80 | end 81 | 82 | it 'returns correct chunk' do 83 | expect { request_logux }.to change { logux_store.size }.by(1) 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /spec/requests/request_without_action_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | describe 'Request logux server without action' do 6 | subject(:request_logux) do 7 | post('/logux', 8 | params: logux_params, 9 | as: :json) 10 | end 11 | 12 | let(:secret) { Logux.configuration.secret } 13 | 14 | let(:logux_params) do 15 | { version: Logux::PROTOCOL_VERSION, 16 | secret: secret, 17 | commands: [ 18 | ['action', 19 | { type: action, key: 'text', value: 'hi' }, 20 | { time: Time.now.to_i, id: '219_856_768 clientid 0', userId: 1 }] 21 | ] } 22 | end 23 | 24 | context 'when verify_authorized=true' do 25 | before { Logux.configuration.verify_authorized = true } 26 | 27 | context 'when policy not exists' do 28 | let(:action) { 'notexists/create' } 29 | 30 | it 'returns unknownAction' do 31 | request_logux 32 | expect(response.stream).to have_chunk( 33 | ['unknownAction', '219_856_768 clientid 0'] 34 | ) 35 | end 36 | end 37 | 38 | context 'when policy is exists' do 39 | let(:action) { 'policy_without_action/create' } 40 | 41 | it 'returns processed' do 42 | request_logux 43 | expect(response).to logux_processed('219_856_768 clientid 0') 44 | end 45 | end 46 | end 47 | 48 | context 'when verify_authorized=false' do 49 | let(:action) { 'notexists/create' } 50 | 51 | before { Logux.configuration.verify_authorized = false } 52 | 53 | it 'returns processed' do 54 | request_logux 55 | expect(response).to logux_processed('219_856_768 clientid 0') 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/requests/request_without_subscribe_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | describe 'Request logux server without subscribe' do 6 | subject(:request_logux) do 7 | post('/logux', 8 | params: logux_params, 9 | as: :json) 10 | end 11 | 12 | let(:secret) { Logux.configuration.secret } 13 | 14 | let(:logux_params) do 15 | { version: Logux::PROTOCOL_VERSION, 16 | secret: secret, 17 | commands: [ 18 | ['action', 19 | { type: 'logux/subscribe', channel: channel }, 20 | { time: Time.now.to_i, id: '219_856_768 clientid 0', userId: 1 }] 21 | ] } 22 | end 23 | 24 | context 'when verify_authorized=true' do 25 | before { Logux.configuration.verify_authorized = true } 26 | 27 | context 'when policy not exists' do 28 | let(:channel) { 'notexists/123' } 29 | 30 | it 'returns unknownChannel' do 31 | request_logux 32 | expect(response.stream).to have_chunk( 33 | ['unknownChannel', '219_856_768 clientid 0'] 34 | ) 35 | end 36 | end 37 | 38 | context 'when policy is exists' do 39 | let(:channel) { 'policy_without_channel/123' } 40 | 41 | it 'returns processed' do 42 | request_logux 43 | expect(response).to logux_processed('219_856_768 clientid 0') 44 | end 45 | end 46 | end 47 | 48 | context 'when verify_authorized=false' do 49 | let(:channel) { 'notexists/123' } 50 | 51 | before { Logux.configuration.verify_authorized = false } 52 | 53 | it 'returns processed' do 54 | request_logux 55 | expect(response).to logux_processed('219_856_768 clientid 0') 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'simplecov' 4 | require 'coveralls' 5 | 6 | SimpleCov::Formatter::MultiFormatter.new([ 7 | SimpleCov::Formatter::HTMLFormatter, 8 | Coveralls::SimpleCov::Formatter 9 | ]) 10 | Coveralls.wear! 11 | SimpleCov.start do 12 | add_filter '/spec/' 13 | add_filter '/lib/logux/test/' 14 | end 15 | 16 | require 'bundler/setup' 17 | require 'factory_bot' 18 | require 'logux_rails' 19 | require 'webmock/rspec' 20 | require 'timecop' 21 | require 'pry-byebug' 22 | require 'rspec/live_controllers' 23 | 24 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } 25 | 26 | RSpec.configure do |config| 27 | # Enable flags like --only-failures and --next-failure 28 | config.include RSpec::LiveControllers::Matchers 29 | config.include Logux::Test::Helpers 30 | config.example_status_persistence_file_path = '.rspec_status' 31 | 32 | # Disable RSpec exposing methods globally on `Module` and `main` 33 | config.disable_monkey_patching! 34 | config.expose_dsl_globally = true 35 | 36 | config.expect_with :rspec do |c| 37 | c.syntax = :expect 38 | end 39 | config.include FactoryBot::Syntax::Methods 40 | 41 | config.before(:suite) do 42 | FactoryBot.find_definitions 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/support/timecop.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | shared_context 'timecop', timecop: true do 6 | around do |example| 7 | Timecop.freeze(Time.parse('13-06-2018 12:00')) 8 | example.call 9 | Timecop.return 10 | end 11 | end 12 | --------------------------------------------------------------------------------