From a566e10edde80e14630b841f3da33cd064cff3f5 Mon Sep 17 00:00:00 2001 From: Rolf Timmermans Date: Fri, 4 Mar 2016 11:45:06 +0100 Subject: [PATCH] Refactor and restyle Flipflop. --- .gitignore | 1 + .travis.yml | 28 ++- Gemfile | 9 + LICENSE | 22 +++ README.md | 126 +++++------- Rakefile | 15 +- app/assets/stylesheets/flipflop.css | 70 ------- app/assets/stylesheets/flipflop.scss | 5 + .../flipflop/features_controller.rb | 41 ++-- .../flipflop/strategies_controller.rb | 18 +- app/helpers/flipflop_helper.rb | 9 - app/views/flipflop/features/index.html.erb | 110 ++++++----- app/views/layouts/flipflop.html.erb | 5 + config/routes.rb | 7 +- flipflop.gemspec | 9 +- lib/flipflop.rb | 40 ++-- lib/flipflop/abstract_strategy.rb | 26 --- lib/flipflop/cacheable.rb | 25 --- lib/flipflop/controller_filters.rb | 22 +-- lib/flipflop/cookie_strategy.rb | 67 ------- lib/flipflop/database_strategy.rb | 46 ----- lib/flipflop/declarable.rb | 25 +-- lib/flipflop/declaration_strategy.rb | 20 -- lib/flipflop/definition.rb | 21 -- lib/flipflop/engine.rb | 16 +- lib/flipflop/facade.rb | 26 ++- lib/flipflop/feature_definition.rb | 15 ++ lib/flipflop/feature_set.rb | 66 ++++--- lib/flipflop/forbidden.rb | 7 - lib/flipflop/strategies/abstract_strategy.rb | 50 +++++ .../strategies/active_record_strategy.rb | 44 +++++ lib/flipflop/strategies/cookie_strategy.rb | 62 ++++++ lib/flipflop/strategies/default_strategy.rb | 19 ++ lib/flipflop/strategies/test_strategy.rb | 34 ++++ lib/flipflop/version.rb | 2 +- .../flipflop/install/install_generator.rb | 12 +- .../flipflop/migration/migration_generator.rb | 7 +- lib/generators/flipflop/model/USAGE | 2 +- .../flipflop/model/model_generator.rb | 5 +- .../flipflop/model/templates/feature.rb | 10 +- lib/generators/flipflop/routes/USAGE | 2 +- .../flipflop/routes/routes_generator.rb | 4 +- spec/abstract_strategy_spec.rb | 11 -- spec/cacheable_spec.rb | 49 ----- spec/controller_filters_spec.rb | 27 --- spec/cookie_strategy_spec.rb | 122 ------------ spec/database_strategy_spec.rb | 110 ----------- spec/declarable_spec.rb | 32 --- spec/declaration_strategy_spec.rb | 39 ---- spec/definition_spec.rb | 19 -- spec/feature_set_spec.rb | 67 ------- spec/flipflop_spec.rb | 33 ---- spec/spec_helper.rb | 2 - test/integration/features_test.rb | 184 ++++++++++++++++++ test/test_helper.rb | 3 + test/unit/controller_filters_test.rb | 61 ++++++ test/unit/declarable_test.rb | 75 +++++++ test/unit/feature_definition_test.rb | 42 ++++ test/unit/feature_set_test.rb | 52 +++++ test/unit/flipflop_test.rb | 53 +++++ .../unit/strategies/abstract_strategy_test.rb | 39 ++++ .../strategies/active_record_strategy_test.rb | 130 +++++++++++++ test/unit/strategies/cookie_strategy_test.rb | 144 ++++++++++++++ test/unit/strategies/default_strategy_test.rb | 54 +++++ test/unit/strategies/test_strategy_test.rb | 85 ++++++++ 65 files changed, 1474 insertions(+), 1109 deletions(-) create mode 100644 LICENSE delete mode 100644 app/assets/stylesheets/flipflop.css create mode 100644 app/assets/stylesheets/flipflop.scss delete mode 100644 app/helpers/flipflop_helper.rb create mode 100644 app/views/layouts/flipflop.html.erb delete mode 100644 lib/flipflop/abstract_strategy.rb delete mode 100644 lib/flipflop/cacheable.rb delete mode 100644 lib/flipflop/cookie_strategy.rb delete mode 100644 lib/flipflop/database_strategy.rb delete mode 100644 lib/flipflop/declaration_strategy.rb delete mode 100644 lib/flipflop/definition.rb create mode 100644 lib/flipflop/feature_definition.rb delete mode 100644 lib/flipflop/forbidden.rb create mode 100644 lib/flipflop/strategies/abstract_strategy.rb create mode 100644 lib/flipflop/strategies/active_record_strategy.rb create mode 100644 lib/flipflop/strategies/cookie_strategy.rb create mode 100644 lib/flipflop/strategies/default_strategy.rb create mode 100644 lib/flipflop/strategies/test_strategy.rb delete mode 100644 spec/abstract_strategy_spec.rb delete mode 100644 spec/cacheable_spec.rb delete mode 100644 spec/controller_filters_spec.rb delete mode 100644 spec/cookie_strategy_spec.rb delete mode 100644 spec/database_strategy_spec.rb delete mode 100644 spec/declarable_spec.rb delete mode 100644 spec/declaration_strategy_spec.rb delete mode 100644 spec/definition_spec.rb delete mode 100644 spec/feature_set_spec.rb delete mode 100644 spec/flipflop_spec.rb delete mode 100644 spec/spec_helper.rb create mode 100644 test/integration/features_test.rb create mode 100644 test/test_helper.rb create mode 100644 test/unit/controller_filters_test.rb create mode 100644 test/unit/declarable_test.rb create mode 100644 test/unit/feature_definition_test.rb create mode 100644 test/unit/feature_set_test.rb create mode 100644 test/unit/flipflop_test.rb create mode 100644 test/unit/strategies/abstract_strategy_test.rb create mode 100644 test/unit/strategies/active_record_strategy_test.rb create mode 100644 test/unit/strategies/cookie_strategy_test.rb create mode 100644 test/unit/strategies/default_strategy_test.rb create mode 100644 test/unit/strategies/test_strategy_test.rb diff --git a/.gitignore b/.gitignore index 4040c6c..b355420 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ .bundle Gemfile.lock pkg/* +tmp/* diff --git a/.travis.yml b/.travis.yml index edfe815..afc91f4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,27 @@ language: ruby rvm: - - 2.3.0 - - 2.2.4 - - jruby-19mode +- 1.9.3 +- 2.0 +- 2.1 +- 2.2 +- 2.3.0 +- ruby-head +- jruby-19mode +- jruby-20mode +- rbx-2 +matrix: + allow_failures: + - rvm: ruby-head +notifications: + email: false + slack: + secure: crOO1QGnFn9T1DpVgxkukTSiN8lQq09X8WF8oi1Eoa7Liex4gzWq7f8wlIXPrRFAsNjU47fSrz+L1C6Eg068Vd2df9pkqjW0pLNeqUViJ35TzpYISTtzJrX+x3nvCQLlvS4leP6lkijlsvlu1IN0xnXadW5cmcMoEcPo4Yma1RUklwlreSRIEmJiutYKVFRw3gIZA9vsnXNEcD408mvSY/8Kuw+hmRQupODUalXDpZo1q3HH+ZPQq+/rGuJ7XRf9sBtxjpUF0G4FJZQhVP4CrLNYVBE/83rHJ6HSf6u3SlYVIMiautq0nWpVLPHUrkOPJVeVh6EPtoFeI/cehH1NyoAVvL5a39wFRBlJ4jVPWUrrnihJT/6+P6GM9PSnYogxtIoTsdrYES2FgtWGgwG5uLyw8U6bW7G7rCzQwBP7enVHWVCbDgdSSjE1Mg1I9qhRuL6pHs5des4VKk6pfD3p+BRqLmOZR2jx4v8MFwakSFqQWOMxaD0U1lfxecqSx9OkwWEhCFSnHeXeHInEhY6qKCdZZzT+beYn0xppUMPJGMTqe5+po8gL+5MxQwI8Xs/5hSve5frfmuS7UQf5BnFMOzwoThQrXCFRz58wXvcZTD9eTdVBV44Hsi5OLdYn9K58sNUhgcxGfRgdE7Gy9P7DD4fHPGakD/Tz++HuCmCUXNs= +deploy: + provider: rubygems + api_key: + secure: ObuFc5QWnSgraKzYXLT5EhnlGm/+BQ2IN46q3ykXdk8m80ajpWD0/rqtCmu5SiBd6Z56CVZe2zSsEpGhN7/pC5kdG0hYzdpvbgX9IxJNMjYb7rNh2onXdIHVd3yk3qdNgI8hmrgtocJPdtPXqbZSY6KOSH6eb5rPDmA/bWwyREdicW5KN8ZWsXiLzKXoFZ6xvkNFbAjY8AicIfG+dHBFbx+q1xBOvbS44M+VIt/Qsknt9m5CYaWJ0AK/RZoyRhs0F0yYQSLu0mVt7JbHOL05SqQ3uCNlwiEqX+fIiN2mPb24Xxy6SVXr1kD0+H4EZHpgDeUlylO2myYJBFBnjzKXXr0qh3YTxaEKlWO7HnUdnJDRQ+bjyT0USBv7gXO2fEIBN864AGZf+3cL0aKRW3n3cyqrhiUxim7gro7eQSh6t6x5o+LGYw6dDdhR53XIFZoh0s/azYBbgl0bfM3juBMrD3e4w/ieaE9iI+NVj4Z4DYDRtbGIqxsuLwRmSkg3epvBc4Pa7vTDuCyVtd9lpFK55V1TysxhcPu0lqHf+SNnW3+DDwp+CQusQhFIsOcIQdr6PBNSTP5ZtEEqyEMfmt2QK2BEOYUuL6TxIUa9xuu8zMUahSaLOvekM5R+EwW04C3T5hWbPs7W1Ks2jcVzkL2iNd3I18pW9VvkXNAqveuoT28= + gem: tinify + on: + tags: true + repo: voormedia/flipflop + ruby: 2.3.0 diff --git a/Gemfile b/Gemfile index 3be9c3c..8cbd971 100644 --- a/Gemfile +++ b/Gemfile @@ -1,2 +1,11 @@ source "https://rubygems.org" gemspec + +group :test do + gem "actionpack" + gem "railties" + gem "rails" + gem "sqlite3" + gem "minitest" + gem "capybara" +end diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fa9799f --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License + +Copyright (c) 2011-2013 Learnable Pty Ltd +Copyright (c) 2016 Voormedia + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md index 4850628..5adedf5 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -FlipFlop — flipflop your features -======================== +[Build Status](https://travis-ci.org/voormedia/flipflop) -[![Build Status](https://travis-ci.org/pda/flipflop.png)](https://travis-ci.org/pda/flipflop) +# Flipflop your features -**FlipFlop** provides a declarative, layered way of enabling and disabling application functionality at run-time. +**Flipflop** provides a declarative, layered way of enabling and disabling +application functionality at run-time. It is forked from [Flip](https://github.com/pda/flip). This gem optimizes for: @@ -15,92 +15,82 @@ There are three layers of strategies per feature: * default * database, to flipflop features site-wide for all users -* cookie, to flipflop features just for you (or someone else) +* cookie, to flipflop features for single users There is also a configurable system-wide default - !Rails.env.production?` works nicely. -FlipFlop has a dashboard UI that's easy to understand and use. +Flipflop has a dashboard UI that's easy to understand and use. -![Feature FlipFlopper Dashboard](https://cloud.githubusercontent.com/assets/828243/4934741/a5773568-65a4-11e4-98d8-5e9a32720b2e.png) +## Installation -Install -------- +Add the gem to your `Gemfile`: -**Rails 3.0, 3.1 and 3.2+** +```ruby +gem "flipflop" +``` - # Gemfile - gem "flipflop" +Generate routes, feature model and databse migration: - # Generate the model and migration - > rails g flipflop:install +``` +rails g flipflop:install +``` - # Run the migration - > rake db:migrate +Run the migration: - # Include the Feature model, e.g. config/initializers/feature.rb: - require 'feature' +``` +rake db:migrate +``` -Declaring Features ------------------- +Explicitly include the Feature model, e.g. in `config/initializers/feature.rb`: +``` +require "feature" +``` + +## Declaring Features ```ruby # This is the model class generated by rails g flipflop:install class Feature < ActiveRecord::Base - include FlipFlop::Declarable + include Flipflop::Declarable - # The recommended FlipFlop strategy stack. - strategy FlipFlop::CookieStrategy - strategy FlipFlop::DatabaseStrategy - strategy FlipFlop::DefaultStrategy - default false + # The recommended Flipflop strategy stack. + strategy Flipflop::CookieStrategy + strategy Flipflop::DatabaseStrategy # A basic feature declaration. feature :shiny_things # Override the system-wide default. feature :world_domination, default: true - - # Enabled half the time..? Sure, we can do that. - feature :flakey, - default: proc { rand(2).zero? } - - # Provide a description, normally derived from the feature name. - feature :something, - default: true, - description: "Ability to purchase enrollments in courses" - end ``` +## Checking Features -Checking Features ------------------ - -`FlipFlop.on?` or the dynamic predicate methods are used to check feature state: +`Flipflop.on?` or the dynamic predicate methods are used to check feature +state: ```ruby -FlipFlop.on? :world_domination # true -FlipFlop.world_domination? # true +Flipflop.on?(:world_domination) # true +Flipflop.world_domination? # true -FlipFlop.on? :shiny_things # false -FlipFlop.shiny_things? # false +Flipflop.on?(:shiny_things) # false +Flipflop.shiny_things? # false ``` -Views and controllers use the `feature?(key)` method: +This works everywhere, also in your views and controllers: ```erb
- <% if feature? :world_domination %> + <% if Flipflop.world_domination? %> <%= link_to "Dominate World", world_dominations_path %> <% end %>
``` +## Feature Flipflopping Controllers -Feature FlipFlopping Controllers --------------------------------- - -The `FlipFlop::ControllerFilters` module is mixed into the base `ApplicationController` class. The following controller will respond with 404 Page Not Found to all but the `index` action unless the `:something` feature is enabled: +The `Flipflop::ControllerFilters` module is mixed into the base `ApplicationController` class. The following controller will respond with 404 Page Not Found to all but the `index` action unless the `:something` feature is enabled: ```ruby class SampleController < ApplicationController @@ -116,8 +106,7 @@ class SampleController < ApplicationController end ``` -Dashboard ---------- +## Dashboard The dashboard provides visibility and control over the features. @@ -128,12 +117,13 @@ The gem includes some basic styles: = stylesheet_link_tag "flipflop" ``` -You probably don't want the dashboard to be public. Here's one way of implementing access control. +You probably don't want the dashboard to be public. Here's one way of +implementing access control. app/controllers/admin/features_controller.rb: ```ruby -class Admin::FeaturesController < FlipFlop::FeaturesController +class Admin::FeaturesController < Flipflop::FeaturesController before_filter :assert_authenticated_as_admin end ``` @@ -141,7 +131,7 @@ end app/controllers/admin/strategies_controller.rb: ```ruby -class Admin::StrategiesController < FlipFlop::StrategiesController +class Admin::StrategiesController < Flipflop::StrategiesController before_filter :assert_authenticated_as_admin end ``` @@ -155,29 +145,9 @@ namespace :admin do end end -mount FlipFlop::Engine => "/admin/features" -``` - -Cacheable ---------- - -You can optimize your feature to ensure that it doesn't make a ton of feature -calls by adding Cacheable to your model. -```ruby -extend FlipFlop::Cacheable +mount Flipflop::Engine => "/admin/features" ``` -This will ensure that your features are eager loaded with one call to the -database instead of every call to FlipFlop#on? generating a call to the -database. This is helpful if you have a larger Rails application and more than -a few features defined. - -To start or reset the cache, just call #start_feature_cache. - - ----- -Originally created by Paul Annesley -Copyright © 2011-2013 Learnable Pty Ltd -Copyright © 2016 Voormedia +## License -Licensed with [MIT Licence](http://www.opensource.org/licenses/mit-license.php) +This software is licensed under the MIT License. [View the license](LICENSE). diff --git a/Rakefile b/Rakefile index 570d04f..2d6e5dd 100644 --- a/Rakefile +++ b/Rakefile @@ -1,11 +1,8 @@ -require 'bundler' -Bundler::GemHelper.install_tasks +require "bundler/gem_tasks" +require "rake/testtask" -desc "Run all tests" -task :default => :spec - -desc "Run specs" -task :spec do - command = "bundle exec rspec --color --format documentation spec/*_spec.rb" - system(command) || raise("specs returned non-zero code") +Rake::TestTask.new do |test| + test.pattern = "test/**/*_test.rb" end + +task default: :test diff --git a/app/assets/stylesheets/flipflop.css b/app/assets/stylesheets/flipflop.css deleted file mode 100644 index 557603b..0000000 --- a/app/assets/stylesheets/flipflop.css +++ /dev/null @@ -1,70 +0,0 @@ -/* FlipFlop */ - -.flipflop { - margin: 0; -} - -.flipflop h1 { - color:#666666; - font-size: 229%; - line-height: 44.928px; - margin: 13.5px 0; -} - -.flipflop th.name, .flipflop th.description, .flipflop th.status { - visibility: hidden; -} - -.flipflop td.name { - font-family: Monaco, sans-serif; - font-weight: bold; -} - -.flipflop td.name, .flipflop td.description { - vertical-align: top; -} - -.flipflop th { - font-weight: normal; - text-align: left; - vertical-align: top; -} - -.flipflop th .description { - font-weight: normal; - display: block; - font-size: 80%; -} - -.flipflop th, .flipflop td { - padding: 5px 10px; - width: 160px; - height: 40px; -} - -.flipflop td.off, .flipflop td.on, .flipflop td.pass { - text-align: center; - text-transform: capitalize; -} - -.flipflop td.off { - background-color: #fbb; -} - -.flipflop td.on { - background-color: #cfc; -} - -.flipflop td.pass { - background-color: #eef; -} - -.flipflop form { - display: inline; -} - -.flipflop form input[type=submit] { - font-size: 80%; - padding: 2px 5px; - margin: 0; -} diff --git a/app/assets/stylesheets/flipflop.scss b/app/assets/stylesheets/flipflop.scss new file mode 100644 index 0000000..a3ddb28 --- /dev/null +++ b/app/assets/stylesheets/flipflop.scss @@ -0,0 +1,5 @@ +@import "bootstrap"; + +section { + margin: 5rem 0 0; +} diff --git a/app/controllers/flipflop/features_controller.rb b/app/controllers/flipflop/features_controller.rb index db5b46d..680419d 100644 --- a/app/controllers/flipflop/features_controller.rb +++ b/app/controllers/flipflop/features_controller.rb @@ -1,47 +1,36 @@ -module FlipFlop +require "bootstrap" + +module Flipflop class FeaturesController < ApplicationController + layout "flipflop" def index - @p = FeaturesPresenter.new(FeatureSet.instance) + @feature_set = FeaturesPresenter.new(FeatureSet.instance) end class FeaturesPresenter + include Flipflop::Engine.routes.url_helpers - include FlipFlop::Engine.routes.url_helpers + extend Forwardable + delegate [:features, :strategies] => :@feature_set def initialize(feature_set) @feature_set = feature_set end - def strategies - @feature_set.strategies - end - - def definitions - @feature_set.definitions - end - - def status(definition) - @feature_set.on?(definition.key) ? "on" : "off" + def status(feature) + @feature_set.enabled?(feature.key) ? "on" : "off" end - def default_status(definition) - @feature_set.default_for(definition) ? "on" : "off" - end - - def strategy_status(strategy, definition) - if strategy.knows? definition - strategy.on?(definition) ? "on" : "off" + def strategy_status(strategy, feature) + if strategy.knows?(feature.key) + strategy.enabled?(feature.key) ? "on" : "off" end end - def switch_url(strategy, definition) - feature_strategy_path \ - definition.key, - strategy.name.underscore + def switch_url(strategy, feature) + feature_strategy_path(feature.key, strategy.object_id) end - end - end end diff --git a/app/controllers/flipflop/strategies_controller.rb b/app/controllers/flipflop/strategies_controller.rb index 1438761..dddbdec 100644 --- a/app/controllers/flipflop/strategies_controller.rb +++ b/app/controllers/flipflop/strategies_controller.rb @@ -1,22 +1,21 @@ -module FlipFlop +module Flipflop class StrategiesController < ApplicationController - - include FlipFlop::Engine.routes.url_helpers + include Flipflop::Engine.routes.url_helpers def update - strategy.switch! feature_key, turn_on? - redirect_to flipflop.features_url + strategy.switch!(feature_key, enable?) + redirect_to(flipflop.features_url) end def destroy - strategy.delete! feature_key - redirect_to flipflop.features_url + strategy.clear!(feature_key) + redirect_to(flipflop.features_url) end private - def turn_on? - params[:commit] == "Switch On" + def enable? + params[:commit].to_s.downcase.include?("on") end def feature_key @@ -26,6 +25,5 @@ def feature_key def strategy FeatureSet.instance.strategy(params[:id]) end - end end diff --git a/app/helpers/flipflop_helper.rb b/app/helpers/flipflop_helper.rb deleted file mode 100644 index b99c8d0..0000000 --- a/app/helpers/flipflop_helper.rb +++ /dev/null @@ -1,9 +0,0 @@ -# Access to feature-flipflopping configuration. -module FlipFlopHelper - - # Whether the given feature is switched on - def feature?(key) - FlipFlop.on? key - end - -end diff --git a/app/views/flipflop/features/index.html.erb b/app/views/flipflop/features/index.html.erb index 799c2da..89f1b53 100644 --- a/app/views/flipflop/features/index.html.erb +++ b/app/views/flipflop/features/index.html.erb @@ -1,62 +1,68 @@ -
-

Feature FlipFloppers

+
+

Feature Flipflop

- - - - - - <% @p.strategies.each do |strategy| %> - - <% end %> - +
Feature NameDescriptionStatus - <%= strategy.name %> - <%= strategy.description %> - - Default - The system default when no strategies match. -
+ + + + + + <% @feature_set.strategies.each do |strategy| -%> + + <% end -%> + - <% @p.definitions.each do |definition| %> - - - - - - <%= content_tag :td, class: @p.status(definition) do %> - <%= @p.status definition %> - <% end %> - - <% @p.strategies.each do |strategy| %> - <%= content_tag :td, class: @p.strategy_status(strategy, definition) || "pass" do %> - <%= @p.strategy_status strategy, definition %> - - <% if strategy.switchable? %> - <%= form_tag(@p.switch_url(strategy, definition), method: :put) do %> - <% unless @p.strategy_status(strategy, definition) == "on" %> - <%= submit_tag "Switch On" %> - <% end %> - <% unless @p.strategy_status(strategy, definition) == "off" %> - <%= submit_tag "Switch Off" %> - <% end %> - <% end %> - <% unless @p.strategy_status(strategy, definition).blank? %> - <%= form_tag(@p.switch_url(strategy, definition), method: :delete) do %> - <%= submit_tag "Delete" %> - <% end %> - <% end %> - <% end %> + <% @feature_set.features.each do |feature| -%> + + + + - <% end %> - <% end %> + <% @feature_set.strategies.each do |strategy| -%> + + <% end -%> - <% end %> + <% end -%>
FeatureDescription + <%= strategy.name.humanize -%> +
<%= definition.name %><%= definition.description %>
+ "><%= @feature_set.status(feature) -%> + <%= feature.name.humanize -%><%= feature.description -%> + +
diff --git a/app/views/layouts/flipflop.html.erb b/app/views/layouts/flipflop.html.erb new file mode 100644 index 0000000..aed6fd2 --- /dev/null +++ b/app/views/layouts/flipflop.html.erb @@ -0,0 +1,5 @@ + +<%= stylesheet_link_tag :flipflop -%><%= yield %> diff --git a/config/routes.rb b/config/routes.rb index ec7ed63..0a99d42 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,5 @@ -FlipFlop::Engine.routes.draw do - resources :features, path: "", only: [ :index ] do - resources :strategies, - only: [ :update, :destroy ] +Flipflop::Engine.routes.draw do + resources :features, path: "", only: [:index] do + resources :strategies, only: [:update, :destroy] end end diff --git a/flipflop.gemspec b/flipflop.gemspec index 55023ce..358352f 100644 --- a/flipflop.gemspec +++ b/flipflop.gemspec @@ -4,7 +4,7 @@ require "flipflop/version" Gem::Specification.new do |s| s.name = "flipflop" - s.version = FlipFlop::VERSION + s.version = Flipflop::VERSION s.platform = Gem::Platform::RUBY s.authors = ["Paul Annesley", "Rolf Timmermans", "Jippe Holwerda"] s.email = ["paul@annesley.cc", "rolftimmermans@voormedia.com", "jippeholwerda@voormedia.com"] @@ -20,10 +20,5 @@ Gem::Specification.new do |s| s.add_dependency("activesupport", ">= 3.0", "< 5") s.add_dependency("i18n") - - s.add_development_dependency("rspec", "~> 2.5") - s.add_development_dependency("rspec-its") - s.add_development_dependency("rake") - s.add_development_dependency("rack") - s.add_development_dependency("actionpack", ">= 3.0", "< 5") + s.add_dependency("bootstrap", "~> 4.0.0.alpha3") end diff --git a/lib/flipflop.rb b/lib/flipflop.rb index 1c2974b..f15eb5c 100644 --- a/lib/flipflop.rb +++ b/lib/flipflop.rb @@ -1,28 +1,24 @@ -# ActiveSupport dependencies. -%w{ - concern - inflector - core_ext/hash/reverse_merge - core_ext/object/blank -}.each { |name| require "active_support/#{name}" } +require "active_support/concern" +require "active_support/inflector" +require "active_support/core_ext/hash/reverse_merge" +require "active_support/core_ext/object/blank" +require "active_support/core_ext/object/try" -# FlipFlop files. -%w{ - abstract_strategy - cacheable - controller_filters - cookie_strategy - database_strategy - declarable - declaration_strategy - definition - facade - feature_set - forbidden -}.each { |name| require "flipflop/#{name}" } +require "flipflop/controller_filters" +require "flipflop/declarable" +require "flipflop/feature_set" +require "flipflop/feature_definition" +require "flipflop/facade" + +require "flipflop/strategies/abstract_strategy" +require "flipflop/strategies/active_record_strategy" +require "flipflop/strategies/cookie_strategy" +require "flipflop/strategies/default_strategy" +require "flipflop/strategies/test_strategy" require "flipflop/engine" if defined?(Rails) -module FlipFlop +module Flipflop extend Facade + include Strategies end diff --git a/lib/flipflop/abstract_strategy.rb b/lib/flipflop/abstract_strategy.rb deleted file mode 100644 index bc46b32..0000000 --- a/lib/flipflop/abstract_strategy.rb +++ /dev/null @@ -1,26 +0,0 @@ -module FlipFlop - class AbstractStrategy - - def name - self.class.name.split("::").last.gsub(/Strategy$/, "").underscore - end - - def description; ""; end - - # Whether the strategy knows the on/off state of the switch. - def knows? definition; raise; end - - # Given the state is known, whether it is on or off. - def on? definition; raise; end - - # Whether the feature can be switched on and off at runtime. - # If true, the strategy must also respond to switch! and delete! - def switchable? - false - end - - def switch! key, on; raise; end - def delete! key; raise; end - - end -end diff --git a/lib/flipflop/cacheable.rb b/lib/flipflop/cacheable.rb deleted file mode 100644 index 8163a00..0000000 --- a/lib/flipflop/cacheable.rb +++ /dev/null @@ -1,25 +0,0 @@ -module FlipFlop - module Cacheable - - def use_feature_cache=(value) - @use_feature_cache = value - end - - def use_feature_cache - @use_feature_cache - end - - def start_feature_cache - @use_feature_cache = true - @features = nil - end - - def feature_cache - return @features if @features - @features = {} - all.each { |f| @features[f.key] = f } - @features - end - - end -end diff --git a/lib/flipflop/controller_filters.rb b/lib/flipflop/controller_filters.rb index 3606e8d..f429a96 100644 --- a/lib/flipflop/controller_filters.rb +++ b/lib/flipflop/controller_filters.rb @@ -1,21 +1,19 @@ -module FlipFlop - module ControllerFilters +module Flipflop + class Forbidden < StandardError + def initialize(feature) + super("Feature '#{feature}' required") + end + end + module ControllerFilters extend ActiveSupport::Concern module ClassMethods - - def require_feature key, options = {} - before_filter options do - flipflop_feature_disabled key unless FlipFlop.on? key + def require_feature(feature, **options) + before_filter(options) do + raise Flipflop::Forbidden.new(feature) unless Flipflop.enabled?(feature) end end - - end - - def flipflop_feature_disabled key - raise FlipFlop::Forbidden.new(key) end - end end diff --git a/lib/flipflop/cookie_strategy.rb b/lib/flipflop/cookie_strategy.rb deleted file mode 100644 index 0df2a40..0000000 --- a/lib/flipflop/cookie_strategy.rb +++ /dev/null @@ -1,67 +0,0 @@ -# Uses cookie to determine feature state. -module FlipFlop - class CookieStrategy < AbstractStrategy - - def description - "Uses cookies to apply only to your session." - end - - def knows? definition - cookies.key? cookie_name(definition) - end - - def on? definition - cookie = cookies[cookie_name(definition)] - cookie_value = cookie.is_a?(Hash) ? cookie['value'] : cookie - cookie_value === 'true' - end - - def switchable? - true - end - - def switch! key, on - cookies[cookie_name(key)] = { - 'value' => (on ? "true" : "false"), - 'domain' => :all - } - end - - def delete! key - cookies.delete cookie_name(key), domain: :all - end - - def self.cookies= cookies - @cookies = cookies - end - - def cookie_name(definition) - definition = definition.key unless definition.is_a? Symbol - "flipflop_#{definition}" - end - - private - - def cookies - self.class.instance_variable_get(:@cookies) || {} - end - - # Include in ApplicationController to push cookies into CookieStrategy. - # Users before_filter and after_filter rather than around_filter to - # avoid pointlessly adding to stack depth. - module Loader - extend ActiveSupport::Concern - included do - before_filter :flipflop_cookie_strategy_before - after_filter :flipflop_cookie_strategy_after - end - def flipflop_cookie_strategy_before - CookieStrategy.cookies = cookies - end - def flipflop_cookie_strategy_after - CookieStrategy.cookies = nil - end - end - - end -end diff --git a/lib/flipflop/database_strategy.rb b/lib/flipflop/database_strategy.rb deleted file mode 100644 index aab72a8..0000000 --- a/lib/flipflop/database_strategy.rb +++ /dev/null @@ -1,46 +0,0 @@ -# Database backed system-wide -module FlipFlop - class DatabaseStrategy < AbstractStrategy - - def initialize(model_klass = Feature) - @klass = model_klass - end - - def description - "Database backed, applies to all users." - end - - def knows? definition - !!feature(definition) - end - - def on? definition - feature(definition).enabled? - end - - def switchable? - true - end - - def switch! key, enable - record = @klass.where(key: key.to_s).first_or_initialize - record.enabled = enable - record.save! - end - - def delete! key - @klass.where(key: key.to_s).first.try(:destroy) - end - - private - - def feature(definition) - if @klass.respond_to?(:use_feature_cache) && @klass.use_feature_cache - @klass.feature_cache[definition.key.to_s] - else - @klass.where(key: definition.key.to_s).first - end - end - - end -end diff --git a/lib/flipflop/declarable.rb b/lib/flipflop/declarable.rb index 03593c7..01cc4a3 100644 --- a/lib/flipflop/declarable.rb +++ b/lib/flipflop/declarable.rb @@ -1,24 +1,17 @@ -module FlipFlop +module Flipflop module Declarable - - def self.extended(base) - FeatureSet.reset - end - - # Adds a new feature definition, creates predicate method. - def feature(key, options = {}) - FeatureSet.instance << FlipFlop::Definition.new(key, options) + class << self + def extended(base) + FeatureSet.reset! + end end - # Adds a strategy for determining feature status. - def strategy(strategy) - FeatureSet.instance.add_strategy strategy + def feature(feature, *options) + FeatureSet.instance.add(Flipflop::FeatureDefinition.new(feature, *options)) end - # The default response, boolean or a Proc to be called. - def default(default) - FeatureSet.instance.default = default + def strategy(strategy, *options) + FeatureSet.instance.use(strategy.is_a?(Class) ? strategy.new(*options) : strategy) end - end end diff --git a/lib/flipflop/declaration_strategy.rb b/lib/flipflop/declaration_strategy.rb deleted file mode 100644 index 55dee61..0000000 --- a/lib/flipflop/declaration_strategy.rb +++ /dev/null @@ -1,20 +0,0 @@ -# Uses :default option passed to feature declaration. -# May be boolean or a Proc to be passed the definition. -module FlipFlop - class DeclarationStrategy < AbstractStrategy - - def description - "The default status declared with the feature." - end - - def knows? definition - !definition.options[:default].nil? - end - - def on? definition - default = definition.options[:default] - default.is_a?(Proc) ? default.call(definition) : default - end - - end -end diff --git a/lib/flipflop/definition.rb b/lib/flipflop/definition.rb deleted file mode 100644 index d927a08..0000000 --- a/lib/flipflop/definition.rb +++ /dev/null @@ -1,21 +0,0 @@ -module FlipFlop - class Definition - - attr_accessor :key - attr_accessor :options - - def initialize(key, options = {}) - @key = key - @options = options.reverse_merge \ - description: key.to_s.humanize + "." - end - - alias :name :key - alias :to_s :key - - def description - options[:description] - end - - end -end diff --git a/lib/flipflop/engine.rb b/lib/flipflop/engine.rb index a2a055e..b4fd511 100644 --- a/lib/flipflop/engine.rb +++ b/lib/flipflop/engine.rb @@ -1,9 +1,17 @@ -module FlipFlop +module Flipflop class Engine < ::Rails::Engine - isolate_namespace FlipFlop + isolate_namespace Flipflop - initializer "flipflop.blarg" do - ActionController::Base.send(:include, FlipFlop::CookieStrategy::Loader) + initializer "flipflop.configure_precompile_assets" do + config.assets.precompile += ["flipflop.css"] + end + + initializer "flipflop.load_features" do + ActiveSupport::Dependencies.load_missing_constant(Object, :Feature) rescue nil + end + + initializer "flipflop.load_cookie_strategy" do + ActionController::Base.send(:include, Flipflop::CookieStrategy::Loader) end end end diff --git a/lib/flipflop/facade.rb b/lib/flipflop/facade.rb index 9ae19d6..435fc47 100644 --- a/lib/flipflop/facade.rb +++ b/lib/flipflop/facade.rb @@ -1,18 +1,26 @@ -module FlipFlop +module Flipflop module Facade - - def on?(feature) - FeatureSet.instance.on? feature + def enabled?(feature) + FeatureSet.instance.enabled?(feature) end + alias_method :on?, :enabled? - def reset - FeatureSet.reset + def reset! + FeatureSet.reset! end - def method_missing(method, *parameters) - super unless method =~ %r{^(.*)\?$} - FeatureSet.instance.on? $1.to_sym + private + + def respond_to_missing?(method, include_private = false) + method[-1] == "?" end + def method_missing(method, *args) + if method[-1] == "?" + FeatureSet.instance.enabled?(method[0..-2].to_sym) + else + super + end + end end end diff --git a/lib/flipflop/feature_definition.rb b/lib/flipflop/feature_definition.rb new file mode 100644 index 0000000..04ba114 --- /dev/null +++ b/lib/flipflop/feature_definition.rb @@ -0,0 +1,15 @@ +module Flipflop + class FeatureDefinition + attr_reader :key, :default, :description + + def initialize(key, **options) + @key = key + @default = !!options[:default] || false + @description = options[:description] || key.to_s.humanize + "." + end + + def name + key.to_s + end + end +end diff --git a/lib/flipflop/feature_set.rb b/lib/flipflop/feature_set.rb index 4cb05fd..e9a2337 100644 --- a/lib/flipflop/feature_set.rb +++ b/lib/flipflop/feature_set.rb @@ -1,57 +1,59 @@ -module FlipFlop +require "thread" + +module Flipflop class FeatureSet + class << self + @@mutex = Mutex.new - def self.instance - @instance ||= self.new - end + def instance + @instance or @@mutex.synchronize do + @instance ||= new + end + end - def self.reset - @instance = nil - end + def reset! + @@mutex.synchronize do + @instance = nil + end + end - # Sets the default for definitions which fall through the strategies. - # Accepts boolean or a Proc to be called. - attr_writer :default + private :new + end def initialize - @definitions = Hash.new { |_, k| raise "No feature declared with key #{k.inspect}" } - @strategies = Hash.new { |_, k| raise "No strategy named #{k}" } - @default = false + @features = Hash.new { |_, k| raise "Feature '#{k}' unknown" } + @strategies = Hash.new { |_, k| raise "Strategy '#{k}' unknown" } end - # Whether the given feature is switched on. - def on? key - d = @definitions[key] - @strategies.each_value { |s| return s.on?(d) if s.knows?(d) } - default_for d + def enabled?(feature) + @strategies.each_value do |strategy| + return strategy.enabled?(feature) if strategy.knows?(feature) + end + @features[feature].default end - # Adds a feature definition to the set. - def << definition - @definitions[definition.key] = definition + def add(feature) + @features[feature.key] = feature end - # Adds a strategy for determing feature status. - def add_strategy(strategy) - strategy = strategy.new if strategy.is_a? Class - @strategies[strategy.name] = strategy + def use(strategy) + @strategies[strategy.key] = strategy end - def strategy(klass) - @strategies[klass] + def feature(feature) + @features[feature] end - def default_for(definition) - @default.is_a?(Proc) ? @default.call(definition) : @default + def features + @features.values end - def definitions - @definitions.values + def strategy(strategy) + @strategies[strategy] end def strategies @strategies.values end - end end diff --git a/lib/flipflop/forbidden.rb b/lib/flipflop/forbidden.rb deleted file mode 100644 index 5cc5139..0000000 --- a/lib/flipflop/forbidden.rb +++ /dev/null @@ -1,7 +0,0 @@ -module FlipFlop - class Forbidden < StandardError - def initialize(key) - super("requires :#{key} feature") - end - end -end diff --git a/lib/flipflop/strategies/abstract_strategy.rb b/lib/flipflop/strategies/abstract_strategy.rb new file mode 100644 index 0000000..aa1be34 --- /dev/null +++ b/lib/flipflop/strategies/abstract_strategy.rb @@ -0,0 +1,50 @@ +module Flipflop + module Strategies + class AbstractStrategy + class << self + def default_name + return "anonymous" unless name + name.split("::").last.gsub(/Strategy$/, "").underscore + end + + def default_description + end + end + + attr_reader :name, :description + + def initialize(**options) + @name = options[:name] || self.class.default_name + @description = options[:description] || self.class.default_description + end + + def key + object_id.to_s + end + + def switchable? + false + end + + def knows?(feature) + raise NotImplementedError + end + + def enabled?(feature) + raise NotImplementedError + end + + def switch!(feature, enabled) + raise NotImplementedError + end + + def clear!(feature) + raise NotImplementedError + end + + def reset! + raise NotImplementedError + end + end + end +end diff --git a/lib/flipflop/strategies/active_record_strategy.rb b/lib/flipflop/strategies/active_record_strategy.rb new file mode 100644 index 0000000..5c632dc --- /dev/null +++ b/lib/flipflop/strategies/active_record_strategy.rb @@ -0,0 +1,44 @@ +module Flipflop + module Strategies + class ActiveRecordStrategy < AbstractStrategy + class << self + def default_description + "Stores features in database. Applies to all users." + end + end + + def initialize(**options) + super + @class = options[:class] || Feature + end + + def switchable? + true + end + + def knows?(feature) + !!find_feature(feature) + end + + def enabled?(feature) + find_feature(feature).enabled? + end + + def switch!(feature, enabled) + record = @class.where(key: feature.to_s).first_or_initialize + record.enabled = enabled + record.save! + end + + def clear!(feature) + @class.where(key: feature.to_s).first.try(:destroy) + end + + private + + def find_feature(feature) + @class.where(key: feature.to_s).first + end + end + end +end diff --git a/lib/flipflop/strategies/cookie_strategy.rb b/lib/flipflop/strategies/cookie_strategy.rb new file mode 100644 index 0000000..5a6517b --- /dev/null +++ b/lib/flipflop/strategies/cookie_strategy.rb @@ -0,0 +1,62 @@ +module Flipflop + module Strategies + class CookieStrategy < AbstractStrategy + class << self + def default_description + "Stores features in a browser cookie. Applies to current user." + end + + attr_accessor :cookies + end + + def switchable? + true + end + + def knows?(feature) + cookies.key?(cookie_name(feature)) + end + + def enabled?(feature) + cookie = cookies[cookie_name(feature)] + cookie_value = cookie.is_a?(Hash) ? cookie["value"] : cookie + cookie_value === "1" + end + + def switch!(feature, enabled) + cookies[cookie_name(feature)] = { + value: (enabled ? "1" : "0"), + domain: :all, + } + end + + def clear!(feature) + cookies.delete(cookie_name(feature), domain: :all) + end + + def cookie_name(feature) + :"flipflop_#{feature}" + end + + private + + def cookies + self.class.cookies || {} + end + + module Loader + extend ActiveSupport::Concern + + included do + before_filter do + CookieStrategy.cookies = cookies + end + + after_filter do + CookieStrategy.cookies = nil + end + end + end + end + end +end diff --git a/lib/flipflop/strategies/default_strategy.rb b/lib/flipflop/strategies/default_strategy.rb new file mode 100644 index 0000000..ab43fc5 --- /dev/null +++ b/lib/flipflop/strategies/default_strategy.rb @@ -0,0 +1,19 @@ +module Flipflop + module Strategies + class DefaultStrategy < AbstractStrategy + class << self + def default_description + "Uses feature default status." + end + end + + def knows?(feature) + true + end + + def enabled?(feature) + FeatureSet.instance.feature(feature).default + end + end + end +end diff --git a/lib/flipflop/strategies/test_strategy.rb b/lib/flipflop/strategies/test_strategy.rb new file mode 100644 index 0000000..3dd822e --- /dev/null +++ b/lib/flipflop/strategies/test_strategy.rb @@ -0,0 +1,34 @@ +module Flipflop + module Strategies + class TestStrategy < AbstractStrategy + def initialize(**options) + super + @features = {} + end + + def switchable? + true + end + + def knows?(feature) + @features.has_key?(feature) + end + + def enabled?(feature) + @features[feature] + end + + def switch!(feature, enabled) + @features[feature] = enabled + end + + def clear!(feature) + @features.delete(feature) + end + + def reset! + @features.clear + end + end + end +end diff --git a/lib/flipflop/version.rb b/lib/flipflop/version.rb index fe443fd..086eb12 100644 --- a/lib/flipflop/version.rb +++ b/lib/flipflop/version.rb @@ -1,3 +1,3 @@ -module FlipFlop +module Flipflop VERSION = "1.0.2" end diff --git a/lib/generators/flipflop/install/install_generator.rb b/lib/generators/flipflop/install/install_generator.rb index 2c6d2bd..8465039 100644 --- a/lib/generators/flipflop/install/install_generator.rb +++ b/lib/generators/flipflop/install/install_generator.rb @@ -1,9 +1,11 @@ -class FlipFlop::InstallGenerator < Rails::Generators::Base +require "generators/flipflop/migration/migration_generator" +require "generators/flipflop/model/model_generator" +require "generators/flipflop/routes/routes_generator" +class Flipflop::InstallGenerator < Rails::Generators::Base def invoke_generators - %w{ model migration routes }.each do |name| - generate "flipflop:#{name}" - end + Flipflop::MigrationGenerator.new([], options).invoke_all + Flipflop::ModelGenerator.new([], options).invoke_all + Flipflop::RoutesGenerator.new([], options).invoke_all end - end diff --git a/lib/generators/flipflop/migration/migration_generator.rb b/lib/generators/flipflop/migration/migration_generator.rb index 3f43ed9..a7d54f1 100644 --- a/lib/generators/flipflop/migration/migration_generator.rb +++ b/lib/generators/flipflop/migration/migration_generator.rb @@ -1,22 +1,21 @@ require "rails/generators/migration" -class FlipFlop::MigrationGenerator < Rails::Generators::Base +class Flipflop::MigrationGenerator < Rails::Generators::Base include Rails::Generators::Migration source_root File.expand_path("../templates", __FILE__) def create_migration_file - migration_template "create_features.rb", "db/migrate/create_features.rb" + migration_template("create_features.rb", "db/migrate/create_features.rb") end # Stubbed in railties/lib/rails/generators/migration.rb # - # This implementation a simplified version of: + # This implementation is a simplified version of: # activerecord/lib/rails/generators/active_record/migration.rb # # See: http://www.ruby-forum.com/topic/203205 def self.next_migration_number(dirname) Time.now.utc.strftime("%Y%m%d%H%M%S") end - end diff --git a/lib/generators/flipflop/model/USAGE b/lib/generators/flipflop/model/USAGE index c45372e..7e01985 100644 --- a/lib/generators/flipflop/model/USAGE +++ b/lib/generators/flipflop/model/USAGE @@ -1,5 +1,5 @@ Description: - Generates the Feature database model class for FlipFlop. + Generates the Feature database model class for Flipflop. Example: rails generate flipflop:model diff --git a/lib/generators/flipflop/model/model_generator.rb b/lib/generators/flipflop/model/model_generator.rb index a52482f..dab4ea1 100644 --- a/lib/generators/flipflop/model/model_generator.rb +++ b/lib/generators/flipflop/model/model_generator.rb @@ -1,8 +1,7 @@ -class FlipFlop::ModelGenerator < Rails::Generators::Base - source_root File.expand_path('../templates', __FILE__) +class Flipflop::ModelGenerator < Rails::Generators::Base + source_root File.expand_path("../templates", __FILE__) def copy_feature_model_file copy_file "feature.rb", "app/models/feature.rb" end - end diff --git a/lib/generators/flipflop/model/templates/feature.rb b/lib/generators/flipflop/model/templates/feature.rb index 8ee2169..483e831 100644 --- a/lib/generators/flipflop/model/templates/feature.rb +++ b/lib/generators/flipflop/model/templates/feature.rb @@ -1,15 +1,13 @@ class Feature < ActiveRecord::Base - extend FlipFlop::Declarable + extend Flipflop::Declarable - strategy FlipFlop::CookieStrategy - strategy FlipFlop::DatabaseStrategy - strategy FlipFlop::DeclarationStrategy - default false + strategy Flipflop::CookieStrategy + strategy Flipflop::ActiveRecordStrategy + strategy Flipflop::DefaultStrategy # Declare your features here, e.g: # # feature :world_domination, # default: true, # description: "Take over the world." - end diff --git a/lib/generators/flipflop/routes/USAGE b/lib/generators/flipflop/routes/USAGE index 0b63ebd..59ae44d 100644 --- a/lib/generators/flipflop/routes/USAGE +++ b/lib/generators/flipflop/routes/USAGE @@ -1,5 +1,5 @@ Description: - Add routes for the FlipFlop control page. + Add routes for the Flipflop dashboard. Example: rails generate flipflop:routes diff --git a/lib/generators/flipflop/routes/routes_generator.rb b/lib/generators/flipflop/routes/routes_generator.rb index 1224493..7010a8f 100644 --- a/lib/generators/flipflop/routes/routes_generator.rb +++ b/lib/generators/flipflop/routes/routes_generator.rb @@ -1,7 +1,7 @@ -class FlipFlop::RoutesGenerator < Rails::Generators::Base +class Flipflop::RoutesGenerator < Rails::Generators::Base def add_route - route %{mount FlipFlop::Engine => "/flipflop"} + route %{mount Flipflop::Engine => "/flipflop"} end end diff --git a/spec/abstract_strategy_spec.rb b/spec/abstract_strategy_spec.rb deleted file mode 100644 index 6244660..0000000 --- a/spec/abstract_strategy_spec.rb +++ /dev/null @@ -1,11 +0,0 @@ -require "spec_helper" - -# Perhaps this is silly, but it provides some -# coverage to an important base class. -describe FlipFlop::AbstractStrategy do - - its(:name) { should == "abstract" } - its(:description) { should == "" } - it { should_not be_switchable } - -end diff --git a/spec/cacheable_spec.rb b/spec/cacheable_spec.rb deleted file mode 100644 index f210410..0000000 --- a/spec/cacheable_spec.rb +++ /dev/null @@ -1,49 +0,0 @@ -require "spec_helper" - -describe FlipFlop::Cacheable do - - subject(:model_class) do - class Sample - attr_accessor :key - end - - Class.new do - extend FlipFlop::Declarable - extend FlipFlop::Cacheable - - strategy FlipFlop::DeclarationStrategy - default false - - feature :one - feature :two, description: "Second one." - feature :three, default: true - - def self.all - list = [] - i = 65 - 3.times do - list << Sample.new - list.last.key = i.chr() - i += 1 - end - list - end - end - end - - describe "with feature cache" do - context "initial context" do - it { should respond_to(:use_feature_cache) } - it { should respond_to(:start_feature_cache) } - it { should respond_to(:feature_cache) } - specify { model_class.use_feature_cache.should be_nil } - end - - context "after a cache clear" do - before { model_class.start_feature_cache } - specify { model_class.use_feature_cache.should eq true } - specify { model_class.feature_cache.size == 3} - end - end - -end diff --git a/spec/controller_filters_spec.rb b/spec/controller_filters_spec.rb deleted file mode 100644 index 0e67cb2..0000000 --- a/spec/controller_filters_spec.rb +++ /dev/null @@ -1,27 +0,0 @@ -require "spec_helper" - -class ControllerWithFlipFlopFilters - include FlipFlop::ControllerFilters -end - -describe ControllerWithFlipFlopFilters do - - describe ".require_feature" do - - it "adds before_filter without options" do - ControllerWithFlipFlopFilters.tap do |klass| - klass.should_receive(:before_filter).with({}) - klass.send(:require_feature, :testable) - end - end - - it "adds before_filter with options" do - ControllerWithFlipFlopFilters.tap do |klass| - klass.should_receive(:before_filter).with({ only: [ :show ] }) - klass.send(:require_feature, :testable, only: [ :show ]) - end - end - - end - -end diff --git a/spec/cookie_strategy_spec.rb b/spec/cookie_strategy_spec.rb deleted file mode 100644 index 7010f3a..0000000 --- a/spec/cookie_strategy_spec.rb +++ /dev/null @@ -1,122 +0,0 @@ -require "spec_helper" -require "action_dispatch" -require "rack" - -class ControllerWithoutCookieStrategy; end -class ControllerWithCookieStrategy - def self.before_filter(_); end - def self.after_filter(_); end - def cookies; cookie_jar; end - include FlipFlop::CookieStrategy::Loader -end - -def cookie_jar - env = Rack::MockRequest.env_for("/example") - request = ActionDispatch::TestRequest.new(env) - ActionDispatch::Cookies::CookieJar.build(request) -end - -describe FlipFlop::CookieStrategy do - - let(:cookies) do - cookie_jar.tap do |jar| - jar[strategy.cookie_name(:one)] = "true" - jar[strategy.cookie_name(:two)] = "false" - end - end - let(:strategy) do - FlipFlop::CookieStrategy.new.tap do |s| - s.stub(:cookies) { cookies } - end - end - - its(:description) { should be_present } - it { should be_switchable } - - describe "cookie interrogration" do - context "enabled feature" do - specify "#knows? is true" do - strategy.knows?(:one).should be true - end - specify "#on? is true" do - strategy.on?(:one).should be true - end - end - context "disabled feature" do - specify "#knows? is true" do - strategy.knows?(:two).should be true - end - specify "#on? is false" do - strategy.on?(:two).should be false - end - end - context "feature with no cookie present" do - specify "#knows? is false" do - strategy.knows?(:three).should be false - end - specify "#on? is false" do - strategy.on?(:three).should be false - end - end - end - - describe "cookie manipulation" do - it "can switch known features on" do - strategy.switch! :one, true - strategy.on?(:one).should be true - end - it "can switch unknown features on" do - strategy.switch! :three, true - strategy.on?(:three).should be true - end - it "can switch features off" do - strategy.switch! :two, false - strategy.on?(:two).should be false - end - it "can delete knowledge of a feature" do - strategy.delete! :one - strategy.on?(:one).should be false - strategy.knows?(:one).should be false - end - end - -end - -describe FlipFlop::CookieStrategy::Loader do - - it "adds filters when included in controller" do - ControllerWithoutCookieStrategy.tap do |klass| - klass.should_receive(:before_filter).with(:flipflop_cookie_strategy_before) - klass.should_receive(:after_filter).with(:flipflop_cookie_strategy_after) - klass.send :include, FlipFlop::CookieStrategy::Loader - end - end - - describe "filter methods" do - let(:strategy) { FlipFlop::CookieStrategy.new } - let(:controller) { ControllerWithCookieStrategy.new } - describe "#flipflop_cookie_strategy_before" do - it "passes controller cookies to CookieStrategy" do - controller.should_receive(:cookies).and_return(strategy.cookie_name(:test) => "true") - expect { - controller.flipflop_cookie_strategy_before - }.to change { - [ strategy.knows?(:test), strategy.on?(:test) ] - }.from([false, false]).to([true, true]) - end - end - describe "#flipflop_cookie_strategy_after" do - before do - FlipFlop::CookieStrategy.cookies = { strategy.cookie_name(:test) => "true" } - end - it "passes controller cookies to CookieStrategy" do - expect { - controller.flipflop_cookie_strategy_after - }.to change { - [ strategy.knows?(:test), strategy.on?(:test) ] - }.from([true, true]).to([false, false]) - end - end - end - -end diff --git a/spec/database_strategy_spec.rb b/spec/database_strategy_spec.rb deleted file mode 100644 index 9446d99..0000000 --- a/spec/database_strategy_spec.rb +++ /dev/null @@ -1,110 +0,0 @@ -require "spec_helper" - -describe FlipFlop::DatabaseStrategy do - - let(:definition) { double("definition", key: "one") } - let(:strategy) { FlipFlop::DatabaseStrategy.new(model_klass) } - let(:model_klass) do - class Sample - attr_accessor :key - - def enabled? - true - end - end - - Class.new do - extend FlipFlop::Cacheable - extend FlipFlop::Declarable - feature :one - feature :two, description: "Second one." - feature :three, default: true - - def self.all - list = [] - keys = ['one', 'two', 'three'] - 3.times do |i| - list << Sample.new - list.last.key = keys[i] - end - list - end - end - end - - let(:enabled_record) { model_klass.new.tap { |m| m.stub(:enabled?) { true } } } - let(:disabled_record) { model_klass.new.tap { |m| m.stub(:enabled?) { false } } } - - subject { strategy } - - its(:switchable?) { should be true } - its(:description) { should be_present } - - let(:db_result) { [] } - before do - allow(model_klass).to(receive(:where).with(key: "one").and_return(db_result)) - end - - describe "#knows?" do - context "for unknown key" do - it "returns true" do - expect(strategy.knows?(definition)).to eq(false) - end - end - context "for known key" do - let(:db_result) { [disabled_record] } - it "returns false" do - expect(strategy.knows?(definition)).to eq(true) - end - end - end - - describe "#on? with feature cache" do - before { model_klass.start_feature_cache } - context "for an enabled record" do - let(:db_result) { [enabled_record] } - it "returns true" do - expect(strategy.on?(definition)).to eq(true) - end - end - end - - describe "#on?" do - context "for an enabled record" do - let(:db_result) { [enabled_record] } - it "returns true" do - expect(strategy.on?(definition)).to eq(true) - end - end - context "for a disabled record" do - let(:db_result) { [disabled_record] } - it "returns true" do - expect(strategy.on?(definition)).to eq(false) - end - end - end - - describe "#switch!" do - it "can switch a feature on" do - expect(db_result).to receive(:first_or_initialize).and_return(disabled_record) - expect(disabled_record).to receive(:enabled=).with(true) - expect(disabled_record).to receive(:save!) - strategy.switch! :one, true - end - it "can switch a feature off" do - expect(db_result).to receive(:first_or_initialize).and_return(enabled_record) - expect(enabled_record).to receive(:enabled=).with(false) - expect(enabled_record).to receive(:save!) - strategy.switch! :one, false - end - end - - describe "#delete!" do - let(:db_result) { [enabled_record] } - it "can delete a feature record" do - enabled_record.should_receive(:try).with(:destroy) - strategy.delete! :one - end - end - -end diff --git a/spec/declarable_spec.rb b/spec/declarable_spec.rb deleted file mode 100644 index 760134e..0000000 --- a/spec/declarable_spec.rb +++ /dev/null @@ -1,32 +0,0 @@ -require "spec_helper" - -describe FlipFlop::Declarable do - - let!(:model_class) do - Class.new do - extend FlipFlop::Declarable - - strategy FlipFlop::DeclarationStrategy - default false - - feature :one - feature :two, description: "Second one." - feature :three, default: true - end - end - - subject { FlipFlop::FeatureSet.instance } - - describe "the .on? class method" do - context "with default set to false" do - it { should_not be_on(:one) } - it { should be_on(:three) } - end - context "with default set to true" do - before { model_class.send(:default, true) } - it { should be_on(:one) } - it { should be_on(:three) } - end - end - -end diff --git a/spec/declaration_strategy_spec.rb b/spec/declaration_strategy_spec.rb deleted file mode 100644 index de3a053..0000000 --- a/spec/declaration_strategy_spec.rb +++ /dev/null @@ -1,39 +0,0 @@ -require "spec_helper" - -describe FlipFlop::DeclarationStrategy do - - def definition(default) - FlipFlop::Definition.new :feature, default: default - end - - describe "#knows?" do - it "does not know definition with no default specified" do - subject.knows?(FlipFlop::Definition.new :feature).should be false - end - it "does not know definition with default of nil" do - subject.knows?(definition(nil)).should be false - end - it "knows definition with default set to true" do - subject.knows?(definition(true)).should be true - end - it "knows definition with default set to false" do - subject.knows?(definition(false)).should be true - end - end - - describe "#on? for FlipFlop::Definition" do - subject { FlipFlop::DeclarationStrategy.new.on? definition(default) } - [ - { default: true, result: true }, - { default: false, result: false }, - { default: proc { true }, result: true, name: "proc returning true" }, - { default: proc { false }, result: false, name: "proc returning false" }, - ].each do |parameters| - context "with default of #{parameters[:name] || parameters[:default]}" do - let(:default) { parameters[:default] } - it { should == parameters[:result] } - end - end - end - -end diff --git a/spec/definition_spec.rb b/spec/definition_spec.rb deleted file mode 100644 index 20ac921..0000000 --- a/spec/definition_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -require "spec_helper" - -describe FlipFlop::Definition do - - subject { FlipFlop::Definition.new :the_key, description: "The description" } - - [:key, :name, :to_s].each do |method| - its(method) { should == :the_key } - end - - its(:description) { should == "The description" } - its(:options) { should == { description: "The description" } } - - context "without description specified" do - subject { FlipFlop::Definition.new :the_key } - its(:description) { should == "The key." } - end - -end diff --git a/spec/feature_set_spec.rb b/spec/feature_set_spec.rb deleted file mode 100644 index 47be9a0..0000000 --- a/spec/feature_set_spec.rb +++ /dev/null @@ -1,67 +0,0 @@ -require "spec_helper" - -class NullStrategy < FlipFlop::AbstractStrategy - def knows?(d); false; end -end - -class TrueStrategy < FlipFlop::AbstractStrategy - def knows?(d); true; end - def on?(d); true; end -end - -describe FlipFlop::FeatureSet do - - let :feature_set_with_null_strategy do - FlipFlop::FeatureSet.new.tap do |s| - s << FlipFlop::Definition.new(:feature) - s.add_strategy NullStrategy - end - end - - let :feature_set_with_null_then_true_strategies do - feature_set_with_null_strategy.tap do |s| - s.add_strategy TrueStrategy - end - end - - describe ".instance" do - it "returns a singleton instance" do - FlipFlop::FeatureSet.instance.should equal(FlipFlop::FeatureSet.instance) - end - it "can be reset" do - instance_before_reset = FlipFlop::FeatureSet.instance - FlipFlop::FeatureSet.reset - FlipFlop::FeatureSet.instance.should_not equal(instance_before_reset) - end - it "can be reset multiple times without error" do - 2.times { FlipFlop::FeatureSet.reset } - end - end - - describe "#default= and #on? with null strategy" do - subject { feature_set_with_null_strategy } - it "defaults to false" do - subject.on?(:feature).should be false - end - it "can default to true" do - subject.default = true - subject.on?(:feature).should be true - end - it "accepts a proc returning true" do - subject.default = proc { true } - subject.on?(:feature).should be true - end - it "accepts a proc returning false" do - subject.default = proc { false } - subject.on?(:feature).should be false - end - end - - describe "feature set with null strategy then always-true strategy" do - subject { feature_set_with_null_then_true_strategies } - it "returns true due to second strategy" do - subject.on?(:feature).should be true - end - end - -end diff --git a/spec/flipflop_spec.rb b/spec/flipflop_spec.rb deleted file mode 100644 index 902e7b1..0000000 --- a/spec/flipflop_spec.rb +++ /dev/null @@ -1,33 +0,0 @@ -require "spec_helper" - -describe FlipFlop do - - before(:all) do - Class.new do - extend FlipFlop::Declarable - strategy FlipFlop::DeclarationStrategy - default false - feature :one, default: true - feature :two, default: false - end - end - - after(:all) do - FlipFlop.reset - end - - describe ".on?" do - it "returns true for enabled features" do - FlipFlop.on?(:one).should be true - end - it "returns false for disabled features" do - FlipFlop.on?(:two).should be false - end - end - - describe "dynamic predicate methods" do - its(:one?) { should be true } - its(:two?) { should be false } - end - -end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb deleted file mode 100644 index f87da30..0000000 --- a/spec/spec_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -require 'flipflop' -require 'rspec/its' diff --git a/test/integration/features_test.rb b/test/integration/features_test.rb new file mode 100644 index 0000000..6dc1dca --- /dev/null +++ b/test/integration/features_test.rb @@ -0,0 +1,184 @@ +require File.expand_path("../../test_helper", __FILE__) + +require "capybara" + +class TestApp + class << self + def instance + @app ||= new.tap do |instance| + instance.create! + instance.load! + instance.migrate! + end + end + end + + def create! + require "rails/generators/rails/app/app_generator" + require "generators/flipflop/install/install_generator" + + FileUtils.rm_rf("tmp/app") + + Rails::Generators::AppGenerator.new(["tmp/app"], + quiet: true, + skip_active_job: true, + skip_bundle: true, + skip_gemfile: true, + skip_git: true, + skip_javascript: true, + skip_keeps: true, + skip_spring: true, + skip_test_unit: true, + skip_turbolinks: true, + ).invoke_all + + Flipflop::InstallGenerator.new([], + quiet: true, + ).invoke_all + end + + def load! + ENV["RAILS_ENV"] = "test" + require "rails" + require "flipflop/engine" + require File.expand_path("../../../tmp/app/config/environment", __FILE__) + ActiveSupport::Dependencies.mechanism = :load + require "capybara/rails" + end + + def migrate! + ActiveRecord::Migration.verbose = false + ActiveRecord::Migrator.migrate(Rails.application.paths["db/migrate"].to_a) + end +end + +describe Flipflop do + include Capybara::DSL + + before do + TestApp.instance + ActiveSupport::Dependencies.remove_constant("Feature") + Feature + end + + subject do + TestApp.instance + end + + describe "without features" do + before do + visit "/flipflop" + end + + it "should show feature table with header" do + assert_equal ["Cookie", "Active record", "Default"], + all("thead th").map(&:text)[3..-1] + end + + it "should show no features" do + assert all("tbody tr").empty? + end + end + + describe "with features" do + before do + Feature.class_eval do + feature :world_domination, description: "Try and take over the world!" + feature :shiny_things, default: true + end + + Capybara.current_session.driver.browser.clear_cookies + Feature.delete_all + + visit "/flipflop" + end + + it "should show feature rows" do + assert_equal ["World domination", "Shiny things"], + all("tr[data-feature] td.name").map(&:text) + end + + it "should show feature descriptions" do + assert_equal ["Try and take over the world!", "Shiny things."], + all("tr[data-feature] td.description").map(&:text) + end + + describe "with cookie strategy" do + it "should enable feature" do + within("tr[data-feature=world-domination] td[data-strategy=cookie]") do + click_on "on" + end + + within("tr[data-feature=world-domination]") do + assert_equal "on", first("td.status").text + assert_equal "on", first("td[data-strategy=cookie] input.btn-primary[type=submit]").value + end + end + + it "should disable feature" do + within("tr[data-feature=world-domination] td[data-strategy=cookie]") do + click_on "off" + end + + within("tr[data-feature=world-domination]") do + assert_equal "off", first("td.status").text + assert_equal "off", first("td[data-strategy=cookie] input.btn-primary[type=submit]").value + end + end + + it "should enable and clear feature" do + within("tr[data-feature=world-domination] td[data-strategy=cookie]") do + click_on "on" + end + + within("tr[data-feature=world-domination] td[data-strategy=cookie]") do + click_on "clear" + end + + within("tr[data-feature=world-domination]") do + assert_equal "off", first("td.status").text + refute has_selector?("td[data-strategy=cookie] input.btn-primary[type=submit]") + end + end + end + + describe "with active record strategy" do + it "should enable feature" do + within("tr[data-feature=world-domination] td[data-strategy=active-record]") do + click_on "on" + end + + within("tr[data-feature=world-domination]") do + assert_equal "on", first("td.status").text + assert_equal "on", first("td[data-strategy=active-record] input.btn-primary[type=submit]").value + end + end + + it "should disable feature" do + within("tr[data-feature=world-domination] td[data-strategy=active-record]") do + click_on "off" + end + + within("tr[data-feature=world-domination]") do + assert_equal "off", first("td.status").text + assert_equal "off", first("td[data-strategy=active-record] input.btn-primary[type=submit]").value + end + end + + it "should enable and clear feature" do + within("tr[data-feature=world-domination] td[data-strategy=active-record]") do + click_on "on" + end + + within("tr[data-feature=world-domination] td[data-strategy=active-record]") do + click_on "clear" + end + + within("tr[data-feature=world-domination]") do + assert_equal "off", first("td.status").text + refute has_selector?("td[data-strategy=active-record] input.btn-primary[type=submit]") + end + end + end + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 0000000..8c05db4 --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,3 @@ +require "bundler/setup" +require "flipflop" +require "minitest/autorun" diff --git a/test/unit/controller_filters_test.rb b/test/unit/controller_filters_test.rb new file mode 100644 index 0000000..dff76e2 --- /dev/null +++ b/test/unit/controller_filters_test.rb @@ -0,0 +1,61 @@ +require File.expand_path("../../test_helper", __FILE__) + +require "action_controller" + +describe Flipflop::ControllerFilters do + subject do + Class.new do + extend Flipflop::Declarable + strategy Flipflop::TestStrategy + feature :test + end + + Class.new(ActionController::Metal) do + include AbstractController::Callbacks + include Flipflop::ControllerFilters + + def index + end + + def show + end + end + end + + describe "require_feature" do + describe "with defaults" do + it "should block action without feature" do + subject.send(:require_feature, :test) + assert_raises Flipflop::Forbidden do + subject.action(:index).call({}) + end + end + + it "should allow action with feature" do + subject.send(:require_feature, :test) + Flipflop::FeatureSet.instance.strategies.first.switch!(:test, true) + assert_equal 200, subject.action(:index).call({}).first + end + end + + describe "with options" do + it "should block action without feature" do + subject.send(:require_feature, :test, only: [:show]) + assert_raises Flipflop::Forbidden do + subject.action(:show).call({}) + end + end + + it "should allow action with feature" do + subject.send(:require_feature, :test, only: [:show]) + Flipflop::FeatureSet.instance.strategies.first.switch!(:test, true) + assert_equal 200, subject.action(:show).call({}).first + end + + it "should allow other actions without feature" do + subject.send(:require_feature, :test, only: [:show]) + assert_equal 200, subject.action(:index).call({}).first + end + end + end +end diff --git a/test/unit/declarable_test.rb b/test/unit/declarable_test.rb new file mode 100644 index 0000000..3c455c9 --- /dev/null +++ b/test/unit/declarable_test.rb @@ -0,0 +1,75 @@ +require File.expand_path("../../test_helper", __FILE__) + +describe Flipflop::Declarable do + subject do + Class.new do + extend Flipflop::Declarable + end + end + + describe "included" do + it "should reset feature set" do + subject.feature(:one, default: true) + Class.new do + extend Flipflop::Declarable + end + + assert_equal [], Flipflop::FeatureSet.instance.features + end + end + + describe "feature" do + it "should append feature definition" do + subject.feature(:one, default: true) + subject.feature(:two, default: false) + + assert_equal [:one, :two], + Flipflop::FeatureSet.instance.features.map(&:key) + end + + it "should append feature definition with default" do + subject.feature(:one, default: true) + subject.feature(:two, default: false) + + assert_equal [true, false], + Flipflop::FeatureSet.instance.features.map(&:default) + end + end + + describe "strategy" do + it "should append strategy classes" do + strategies = [ + Class.new(Flipflop::AbstractStrategy), + Class.new(Flipflop::AbstractStrategy), + ] + + subject.strategy(strategies[0]) + subject.strategy(strategies[1]) + + assert_equal strategies, Flipflop::FeatureSet.instance.strategies.map(&:class) + end + + it "should append strategy objects" do + strategy_class = Class.new(Flipflop::AbstractStrategy) + strategies = [ + strategy_class.new, + strategy_class.new, + ] + + subject.strategy(strategies[0]) + subject.strategy(strategies[1]) + + assert_equal strategies, Flipflop::FeatureSet.instance.strategies + end + + it "should append strategy classes with options" do + strategy_class = Class.new(Flipflop::AbstractStrategy) + + subject.strategy(strategy_class, name: "my strategy") + subject.strategy(strategy_class, name: "awesome strategy") + + assert_equal ["my strategy", "awesome strategy"], + Flipflop::FeatureSet.instance.strategies.map(&:name) + end + end +end diff --git a/test/unit/feature_definition_test.rb b/test/unit/feature_definition_test.rb new file mode 100644 index 0000000..f045ff0 --- /dev/null +++ b/test/unit/feature_definition_test.rb @@ -0,0 +1,42 @@ +require File.expand_path("../../test_helper", __FILE__) + +describe Flipflop::FeatureDefinition do + describe "with defaults" do + subject do + Flipflop::FeatureDefinition.new(:my_key) + end + + it "should have specified key" do + assert_equal :my_key, subject.key + end + + it "should have humanized description" do + assert_equal "My key.", subject.description + end + + it "should default to false" do + assert_equal false, subject.default + end + end + + describe "with options" do + subject do + Flipflop::FeatureDefinition.new(:my_key, + default: true, + description: "Awesome feature", + ) + end + + it "should have specified key" do + assert_equal :my_key, subject.key + end + + it "should have specified description" do + assert_equal "Awesome feature", subject.description + end + + it "should have specified default" do + assert_equal true, subject.default + end + end +end diff --git a/test/unit/feature_set_test.rb b/test/unit/feature_set_test.rb new file mode 100644 index 0000000..0e0809e --- /dev/null +++ b/test/unit/feature_set_test.rb @@ -0,0 +1,52 @@ +require File.expand_path("../../test_helper", __FILE__) + +class NullStrategy < Flipflop::AbstractStrategy + def knows?(feature) + false + end +end + +class TrueStrategy < Flipflop::AbstractStrategy + def knows?(feature) + true + end + + def enabled?(feature) + true + end +end + +describe Flipflop::FeatureSet do + subject do + Flipflop::FeatureSet.reset! + Flipflop::FeatureSet.instance.tap do |set| + set.add(Flipflop::FeatureDefinition.new(:one)) + end + end + + describe "instance" do + it "returns singleton instance" do + instance = subject + assert_equal Flipflop::FeatureSet.instance, instance + end + + it "returns new instance if reset" do + instance = subject + Flipflop::FeatureSet.reset! + refute_equal instance, Flipflop::FeatureSet.instance + end + end + + describe "enabled" do + it "should return false by default" do + subject.use(NullStrategy.new) + assert_equal false, subject.enabled?(:one) + end + + it "should return value of next strategy if unknown" do + subject.use(NullStrategy.new) + subject.use(TrueStrategy.new) + assert_equal true, subject.enabled?(:one) + end + end +end diff --git a/test/unit/flipflop_test.rb b/test/unit/flipflop_test.rb new file mode 100644 index 0000000..0c04ac1 --- /dev/null +++ b/test/unit/flipflop_test.rb @@ -0,0 +1,53 @@ +require File.expand_path("../../test_helper", __FILE__) + +describe Flipflop do + before do + Class.new do + extend Flipflop::Declarable + + feature :one, default: true + feature :two, default: false + end + end + + describe "enabled?" do + it "returns true for enabled features" do + assert_equal true, Flipflop.on?(:one) + end + + it "returns false for disabled features" do + assert_equal false, Flipflop.on?(:two) + end + end + + describe "reset!" do + it "should clear features" do + Flipflop.reset! + assert_equal [], Flipflop::FeatureSet.instance.features + end + end + + describe "dynamic predicate method" do + it "should respond to feature predicate" do + assert Flipflop.respond_to?(:one?) + end + + it "should not respond to incorrectly formatted predicate" do + refute Flipflop.respond_to?(:foobar!) + end + + it "returns true for enabled features" do + assert_equal true, Flipflop.one? + end + + it "returns false for disabled features" do + assert_equal false, Flipflop.two? + end + + it "raises error for incorrectly formatted predicate" do + assert_raises NoMethodError do + Flipflop.foobar! + end + end + end +end diff --git a/test/unit/strategies/abstract_strategy_test.rb b/test/unit/strategies/abstract_strategy_test.rb new file mode 100644 index 0000000..22f24d3 --- /dev/null +++ b/test/unit/strategies/abstract_strategy_test.rb @@ -0,0 +1,39 @@ +require File.expand_path("../../../test_helper", __FILE__) + +describe Flipflop::AbstractStrategy do + describe "with defaults" do + subject do + Flipflop::AbstractStrategy.new + end + + it "should have default name" do + assert_equal "abstract", subject.name + end + + it "should have no default description" do + assert_nil subject.description + end + + it "should not be switchable" do + assert_equal false, subject.switchable? + end + + it "should have unique key" do + assert_match /^\d+$/, subject.key + end + end + + describe "with options" do + subject do + Flipflop::AbstractStrategy.new(name: "strategy", description: "my strategy") + end + + it "should have specified name" do + assert_equal "strategy", subject.name + end + + it "should have specified description" do + assert_equal "my strategy", subject.description + end + end +end diff --git a/test/unit/strategies/active_record_strategy_test.rb b/test/unit/strategies/active_record_strategy_test.rb new file mode 100644 index 0000000..14edb8c --- /dev/null +++ b/test/unit/strategies/active_record_strategy_test.rb @@ -0,0 +1,130 @@ +require File.expand_path("../../../test_helper", __FILE__) + +class ResultSet + def initialize(key, results = []) + @key, @results = key, results + end + + def first_or_initialize + @results.first or MyFeature.new(@key, false) + end + + def first + @results.first + end +end + +class MyFeature < Struct.new(:key, :enabled) + class << self + attr_accessor :results + + def where(conditions) + results[conditions[:key].to_sym] + end + end + + alias_method :enabled?, :enabled + + def destroy + MyFeature.results[key] = ResultSet.new(key) + end + + def save! + MyFeature.results[key] = ResultSet.new(key, [self]) + end +end + +describe Flipflop::ActiveRecordStrategy do + describe "with defaults" do + subject do + Flipflop::ActiveRecordStrategy.new(class: MyFeature) + end + + it "should have default name" do + assert_equal "active_record", subject.name + end + + it "should have default description" do + assert_equal "Stores features in database. Applies to all users.", + subject.description + end + + it "should be switchable" do + assert_equal true, subject.switchable? + end + + it "should have unique key" do + assert_match /^\d+$/, subject.key + end + + describe "with enabled feature" do + before do + MyFeature.results = { + one: ResultSet.new(:one, [MyFeature.new(:one, true)]), + } + end + + it "should know feature" do + assert_equal true, subject.knows?(:one) + end + + it "should have feature enabled" do + assert_equal true, subject.enabled?(:one) + end + + it "should be able to switch feature off" do + subject.switch!(:one, false) + assert_equal false, subject.enabled?(:one) + end + + it "should be able to clear feature" do + subject.clear!(:one) + assert_equal false, subject.knows?(:one) + end + end + + describe "with disabled feature" do + before do + MyFeature.results = { + two: ResultSet.new(:two, [MyFeature.new(:two, false)]), + } + end + + it "should know feature" do + assert_equal true, subject.knows?(:two) + end + + it "should not have feature enabled" do + assert_equal false, subject.enabled?(:two) + end + + it "should be able to switch feature on" do + subject.switch!(:two, true) + assert_equal true, subject.enabled?(:two) + end + + it "should be able to clear feature" do + subject.clear!(:two) + assert_equal false, subject.knows?(:two) + end + end + + describe "with unsaved feature" do + before do + MyFeature.results = { + three: ResultSet.new(:three), + } + end + + it "should not know feature" do + assert_equal false, subject.knows?(:three) + end + + it "should be able to switch feature on" do + subject.switch!(:three, true) + assert_equal true, subject.enabled?(:three) + assert_equal true, subject.knows?(:three) + end + end + end +end diff --git a/test/unit/strategies/cookie_strategy_test.rb b/test/unit/strategies/cookie_strategy_test.rb new file mode 100644 index 0000000..ebf483d --- /dev/null +++ b/test/unit/strategies/cookie_strategy_test.rb @@ -0,0 +1,144 @@ +require File.expand_path("../../../test_helper", __FILE__) + +require "action_controller" + +describe Flipflop::CookieStrategy do + def create_cookie_jar + env = Rack::MockRequest.env_for("/example") + request = ActionDispatch::TestRequest.new(env) + ActionDispatch::Cookies::CookieJar.build(request) + end + + describe "with defaults" do + subject do + Flipflop::CookieStrategy.new + end + + before do + subject.class.cookies = create_cookie_jar + end + + after do + subject.class.cookies = nil + end + + it "should have default name" do + assert_equal "cookie", subject.name + end + + it "should have default description" do + assert_equal "Stores features in a browser cookie. Applies to current user.", + subject.description + end + + it "should be switchable" do + assert_equal true, subject.switchable? + end + + it "should have unique key" do + assert_match /^\d+$/, subject.key + end + + describe "with enabled feature" do + before do + subject.class.cookies[subject.cookie_name(:one)] = "1" + end + + it "should know feature" do + assert_equal true, subject.knows?(:one) + end + + it "should have feature enabled" do + assert_equal true, subject.enabled?(:one) + end + + it "should be able to switch feature off" do + subject.switch!(:one, false) + assert_equal false, subject.enabled?(:one) + end + + it "should be able to clear feature" do + subject.clear!(:one) + assert_equal false, subject.knows?(:one) + end + end + + describe "with disabled feature" do + before do + subject.class.cookies[subject.cookie_name(:two)] = "0" + end + + it "should know feature" do + assert_equal true, subject.knows?(:two) + end + + it "should not have feature enabled" do + assert_equal false, subject.enabled?(:two) + end + + it "should be able to switch feature on" do + subject.switch!(:two, true) + assert_equal true, subject.enabled?(:two) + end + + it "should be able to clear feature" do + subject.clear!(:two) + assert_equal false, subject.knows?(:two) + end + end + + describe "with uncookied feature" do + it "should not know feature" do + assert_equal false, subject.knows?(:three) + end + + it "should be able to switch feature on" do + subject.switch!(:three, true) + assert_equal true, subject.enabled?(:three) + assert_equal true, subject.knows?(:three) + end + end + end +end + +describe Flipflop::CookieStrategy::Loader do + subject do + # Force loading of cookie logic. + ActionDispatch::Cookies + + Class.new(ActionController::Metal) do + class << self + attr_accessor :cookies + end + + include ActionController::Helpers + include ActionController::Cookies + include AbstractController::Callbacks + include Flipflop::CookieStrategy::Loader + + def index + self.class.cookies = Flipflop::CookieStrategy.cookies + end + end + end + + it "should add before filter to controller" do + filters = subject._process_action_callbacks.select { |f| f.kind == :before } + assert_equal 1, filters.length + end + + it "should add after filter to controller" do + filters = subject._process_action_callbacks.select { |f| f.kind == :after } + assert_equal 1, filters.length + end + + it "should set cookies" do + subject.action(:index).call({}) + assert_instance_of ActionDispatch::Cookies::CookieJar, subject.cookies + end + + it "should clear cookies" do + subject.action(:index).call({}) + assert_nil Flipflop::CookieStrategy.cookies + end +end diff --git a/test/unit/strategies/default_strategy_test.rb b/test/unit/strategies/default_strategy_test.rb new file mode 100644 index 0000000..e170682 --- /dev/null +++ b/test/unit/strategies/default_strategy_test.rb @@ -0,0 +1,54 @@ +require File.expand_path("../../../test_helper", __FILE__) + +describe Flipflop::DefaultStrategy do + before do + Class.new do + extend Flipflop::Declarable + + feature :one, default: true + feature :two + end + end + + describe "with defaults" do + subject do + Flipflop::DefaultStrategy.new + end + + it "should have default name" do + assert_equal "default", subject.name + end + + it "should have no default description" do + assert_equal "Uses feature default status.", subject.description + end + + it "should not be switchable" do + assert_equal false, subject.switchable? + end + + it "should have unique key" do + assert_match /^\d+$/, subject.key + end + + describe "with explicitly defaulted feature" do + it "should know feature" do + assert_equal true, subject.knows?(:one) + end + + it "should have feature enabled" do + assert_equal true, subject.enabled?(:one) + end + end + + describe "with implicitly defaulted feature" do + it "should know feature" do + assert_equal true, subject.knows?(:two) + end + + it "should not have feature enabled" do + assert_equal false, subject.enabled?(:two) + end + end + end +end diff --git a/test/unit/strategies/test_strategy_test.rb b/test/unit/strategies/test_strategy_test.rb new file mode 100644 index 0000000..72053bf --- /dev/null +++ b/test/unit/strategies/test_strategy_test.rb @@ -0,0 +1,85 @@ +require File.expand_path("../../../test_helper", __FILE__) + +describe Flipflop::TestStrategy do + describe "with defaults" do + subject do + Flipflop::TestStrategy.new + end + + it "should have default name" do + assert_equal "test", subject.name + end + + it "should not have default description" do + assert_nil subject.description + end + + it "should be switchable" do + assert_equal true, subject.switchable? + end + + it "should have unique key" do + assert_match /^\d+$/, subject.key + end + + describe "with enabled feature" do + before do + subject.switch!(:one, true) + end + + it "should know feature" do + assert_equal true, subject.knows?(:one) + end + + it "should have feature enabled" do + assert_equal true, subject.enabled?(:one) + end + + it "should be able to switch feature off" do + subject.switch!(:one, false) + assert_equal false, subject.enabled?(:one) + end + + it "should be able to clear feature" do + subject.clear!(:one) + assert_equal false, subject.knows?(:one) + end + end + + describe "with disabled feature" do + before do + subject.switch!(:two, false) + end + + it "should know feature" do + assert_equal true, subject.knows?(:two) + end + + it "should not have feature enabled" do + assert_equal false, subject.enabled?(:two) + end + + it "should be able to switch feature on" do + subject.switch!(:two, true) + assert_equal true, subject.enabled?(:two) + end + + it "should be able to clear feature" do + subject.clear!(:two) + assert_equal false, subject.knows?(:two) + end + end + + describe "with unsaved feature" do + it "should not know feature" do + assert_equal false, subject.knows?(:three) + end + + it "should be able to switch feature on" do + subject.switch!(:three, true) + assert_equal true, subject.enabled?(:three) + assert_equal true, subject.knows?(:three) + end + end + end +end