flipper-cloud 0.28.3 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/flipper/version.rb +1 -1
- metadata +14 -53
- data/docs/images/flipper_cloud.png +0 -0
- data/examples/cloud/app.ru +0 -12
- data/examples/cloud/basic.rb +0 -22
- data/examples/cloud/cloud_setup.rb +0 -4
- data/examples/cloud/forked.rb +0 -31
- data/examples/cloud/import.rb +0 -17
- data/examples/cloud/threaded.rb +0 -36
- data/flipper-cloud.gemspec +0 -28
- data/lib/flipper/cloud/configuration.rb +0 -189
- data/lib/flipper/cloud/dsl.rb +0 -27
- data/lib/flipper/cloud/engine.rb +0 -29
- data/lib/flipper/cloud/instrumenter.rb +0 -48
- data/lib/flipper/cloud/message_verifier.rb +0 -95
- data/lib/flipper/cloud/middleware.rb +0 -63
- data/lib/flipper/cloud/routes.rb +0 -13
- data/lib/flipper/cloud.rb +0 -57
- data/spec/flipper/cloud/configuration_spec.rb +0 -261
- data/spec/flipper/cloud/dsl_spec.rb +0 -82
- data/spec/flipper/cloud/engine_spec.rb +0 -95
- data/spec/flipper/cloud/message_verifier_spec.rb +0 -104
- data/spec/flipper/cloud/middleware_spec.rb +0 -289
- data/spec/flipper/cloud_spec.rb +0 -179
| @@ -1,63 +0,0 @@ | |
| 1 | 
            -
            # frozen_string_literal: true
         | 
| 2 | 
            -
             | 
| 3 | 
            -
            require "flipper/cloud/message_verifier"
         | 
| 4 | 
            -
             | 
| 5 | 
            -
            module Flipper
         | 
| 6 | 
            -
              module Cloud
         | 
| 7 | 
            -
                class Middleware
         | 
| 8 | 
            -
                  # Internal: The path to match for webhook requests.
         | 
| 9 | 
            -
                  WEBHOOK_PATH = %r{\A/webhooks\/?\Z}
         | 
| 10 | 
            -
                  # Internal: The root path to match for requests.
         | 
| 11 | 
            -
                  ROOT_PATH = %r{\A/\Z}
         | 
| 12 | 
            -
             | 
| 13 | 
            -
                  def initialize(app, options = {})
         | 
| 14 | 
            -
                    @app = app
         | 
| 15 | 
            -
                    @env_key = options.fetch(:env_key, 'flipper')
         | 
| 16 | 
            -
                  end
         | 
| 17 | 
            -
             | 
| 18 | 
            -
                  def call(env)
         | 
| 19 | 
            -
                    dup.call!(env)
         | 
| 20 | 
            -
                  end
         | 
| 21 | 
            -
             | 
| 22 | 
            -
                  def call!(env)
         | 
| 23 | 
            -
                    request = Rack::Request.new(env)
         | 
| 24 | 
            -
                    if request.post? && (request.path_info.match(ROOT_PATH) || request.path_info.match(WEBHOOK_PATH))
         | 
| 25 | 
            -
                      status = 200
         | 
| 26 | 
            -
                      headers = {
         | 
| 27 | 
            -
                        "Content-Type" => "application/json",
         | 
| 28 | 
            -
                      }
         | 
| 29 | 
            -
                      body = "{}"
         | 
| 30 | 
            -
                      payload = request.body.read
         | 
| 31 | 
            -
                      signature = request.env["HTTP_FLIPPER_CLOUD_SIGNATURE"]
         | 
| 32 | 
            -
                      flipper = env.fetch(@env_key)
         | 
| 33 | 
            -
             | 
| 34 | 
            -
                      begin
         | 
| 35 | 
            -
                        message_verifier = MessageVerifier.new(secret: flipper.sync_secret)
         | 
| 36 | 
            -
                        if message_verifier.verify(payload, signature)
         | 
| 37 | 
            -
                          begin
         | 
| 38 | 
            -
                            flipper.sync
         | 
| 39 | 
            -
                            body = JSON.generate({
         | 
| 40 | 
            -
                              groups: Flipper.group_names.map { |name| {name: name}}
         | 
| 41 | 
            -
                            })
         | 
| 42 | 
            -
                          rescue Flipper::Adapters::Http::Error => error
         | 
| 43 | 
            -
                            status = error.response.code.to_i == 402 ? 402 : 500
         | 
| 44 | 
            -
                            headers["Flipper-Cloud-Response-Error-Class"] = error.class.name
         | 
| 45 | 
            -
                            headers["Flipper-Cloud-Response-Error-Message"] = error.message
         | 
| 46 | 
            -
                          rescue => error
         | 
| 47 | 
            -
                            status = 500
         | 
| 48 | 
            -
                            headers["Flipper-Cloud-Response-Error-Class"] = error.class.name
         | 
| 49 | 
            -
                            headers["Flipper-Cloud-Response-Error-Message"] = error.message
         | 
| 50 | 
            -
                          end
         | 
| 51 | 
            -
                        end
         | 
| 52 | 
            -
                      rescue MessageVerifier::InvalidSignature
         | 
| 53 | 
            -
                        status = 400
         | 
| 54 | 
            -
                      end
         | 
| 55 | 
            -
             | 
| 56 | 
            -
                      [status, headers, [body]]
         | 
| 57 | 
            -
                    else
         | 
| 58 | 
            -
                      @app.call(env)
         | 
| 59 | 
            -
                    end
         | 
| 60 | 
            -
                  end
         | 
| 61 | 
            -
                end
         | 
| 62 | 
            -
              end
         | 
| 63 | 
            -
            end
         | 
    
        data/lib/flipper/cloud/routes.rb
    DELETED
    
    | @@ -1,13 +0,0 @@ | |
| 1 | 
            -
            # Default routes loaded by Flipper::Cloud::Engine
         | 
| 2 | 
            -
            Rails.application.routes.draw do
         | 
| 3 | 
            -
              if ENV["FLIPPER_CLOUD_TOKEN"] && ENV["FLIPPER_CLOUD_SYNC_SECRET"]
         | 
| 4 | 
            -
                config = Rails.application.config.flipper
         | 
| 5 | 
            -
             | 
| 6 | 
            -
                cloud_app = Flipper::Cloud.app(nil,
         | 
| 7 | 
            -
                  env_key: config.env_key,
         | 
| 8 | 
            -
                  memoizer_options: { preload: config.preload }
         | 
| 9 | 
            -
                )
         | 
| 10 | 
            -
             | 
| 11 | 
            -
                mount cloud_app, at: config.cloud_path
         | 
| 12 | 
            -
              end
         | 
| 13 | 
            -
            end
         | 
    
        data/lib/flipper/cloud.rb
    DELETED
    
    | @@ -1,57 +0,0 @@ | |
| 1 | 
            -
            require "flipper"
         | 
| 2 | 
            -
            require "flipper/middleware/setup_env"
         | 
| 3 | 
            -
            require "flipper/middleware/memoizer"
         | 
| 4 | 
            -
            require "flipper/cloud/configuration"
         | 
| 5 | 
            -
            require "flipper/cloud/dsl"
         | 
| 6 | 
            -
            require "flipper/cloud/middleware"
         | 
| 7 | 
            -
            require "flipper/cloud/engine" if defined?(Rails::Engine)
         | 
| 8 | 
            -
             | 
| 9 | 
            -
            module Flipper
         | 
| 10 | 
            -
              module Cloud
         | 
| 11 | 
            -
                # Public: Returns a new Flipper instance with an http adapter correctly
         | 
| 12 | 
            -
                # configured for flipper cloud.
         | 
| 13 | 
            -
                #
         | 
| 14 | 
            -
                # token - The String token for the environment from the website.
         | 
| 15 | 
            -
                # options - The Hash of options. See Flipper::Cloud::Configuration.
         | 
| 16 | 
            -
                # block - The block that configuration will be yielded to allowing you to
         | 
| 17 | 
            -
                #         customize this cloud instance and its adapter.
         | 
| 18 | 
            -
                def self.new(options = {})
         | 
| 19 | 
            -
                  configuration = Configuration.new(options)
         | 
| 20 | 
            -
                  yield configuration if block_given?
         | 
| 21 | 
            -
                  DSL.new(configuration)
         | 
| 22 | 
            -
                end
         | 
| 23 | 
            -
             | 
| 24 | 
            -
                def self.app(flipper = nil, options = {})
         | 
| 25 | 
            -
                  env_key = options.fetch(:env_key, 'flipper')
         | 
| 26 | 
            -
                  memoizer_options = options.fetch(:memoizer_options, {})
         | 
| 27 | 
            -
             | 
| 28 | 
            -
                  app = ->(_) { [404, { 'Content-Type'.freeze => 'application/json'.freeze }, ['{}'.freeze]] }
         | 
| 29 | 
            -
                  builder = Rack::Builder.new
         | 
| 30 | 
            -
                  yield builder if block_given?
         | 
| 31 | 
            -
                  builder.use Flipper::Middleware::SetupEnv, flipper, env_key: env_key
         | 
| 32 | 
            -
                  builder.use Flipper::Middleware::Memoizer, memoizer_options.merge(env_key: env_key)
         | 
| 33 | 
            -
                  builder.use Flipper::Cloud::Middleware, env_key: env_key
         | 
| 34 | 
            -
                  builder.run app
         | 
| 35 | 
            -
                  klass = self
         | 
| 36 | 
            -
                  app = builder.to_app
         | 
| 37 | 
            -
                  app.define_singleton_method(:inspect) { klass.inspect } # pretty rake routes output
         | 
| 38 | 
            -
                  app
         | 
| 39 | 
            -
                end
         | 
| 40 | 
            -
             | 
| 41 | 
            -
                # Private: Configure Flipper to use Cloud by default
         | 
| 42 | 
            -
                def self.set_default
         | 
| 43 | 
            -
                  Flipper.configure do |config|
         | 
| 44 | 
            -
                    config.default do
         | 
| 45 | 
            -
                      if ENV["FLIPPER_CLOUD_TOKEN"]
         | 
| 46 | 
            -
                        self.new(local_adapter: config.adapter)
         | 
| 47 | 
            -
                      else
         | 
| 48 | 
            -
                        warn "Missing FLIPPER_CLOUD_TOKEN environment variable. Disabling Flipper::Cloud."
         | 
| 49 | 
            -
                        Flipper.new(config.adapter)
         | 
| 50 | 
            -
                      end
         | 
| 51 | 
            -
                    end
         | 
| 52 | 
            -
                  end
         | 
| 53 | 
            -
                end
         | 
| 54 | 
            -
              end
         | 
| 55 | 
            -
            end
         | 
| 56 | 
            -
             | 
| 57 | 
            -
            Flipper::Cloud.set_default
         | 
| @@ -1,261 +0,0 @@ | |
| 1 | 
            -
            require 'flipper/cloud/configuration'
         | 
| 2 | 
            -
            require 'flipper/adapters/instrumented'
         | 
| 3 | 
            -
             | 
| 4 | 
            -
            RSpec.describe Flipper::Cloud::Configuration do
         | 
| 5 | 
            -
              let(:required_options) do
         | 
| 6 | 
            -
                { token: "asdf" }
         | 
| 7 | 
            -
              end
         | 
| 8 | 
            -
             | 
| 9 | 
            -
              it "can set token" do
         | 
| 10 | 
            -
                instance = described_class.new(required_options)
         | 
| 11 | 
            -
                expect(instance.token).to eq(required_options[:token])
         | 
| 12 | 
            -
              end
         | 
| 13 | 
            -
             | 
| 14 | 
            -
              it "can set token from ENV var" do
         | 
| 15 | 
            -
                ENV["FLIPPER_CLOUD_TOKEN"] = "from_env"
         | 
| 16 | 
            -
                instance = described_class.new(required_options.reject { |k, v| k == :token })
         | 
| 17 | 
            -
                expect(instance.token).to eq("from_env")
         | 
| 18 | 
            -
              end
         | 
| 19 | 
            -
             | 
| 20 | 
            -
              it "can set instrumenter" do
         | 
| 21 | 
            -
                instrumenter = Object.new
         | 
| 22 | 
            -
                instance = described_class.new(required_options.merge(instrumenter: instrumenter))
         | 
| 23 | 
            -
                expect(instance.instrumenter).to be(instrumenter)
         | 
| 24 | 
            -
              end
         | 
| 25 | 
            -
             | 
| 26 | 
            -
              it "can set read_timeout" do
         | 
| 27 | 
            -
                instance = described_class.new(required_options.merge(read_timeout: 5))
         | 
| 28 | 
            -
                expect(instance.read_timeout).to eq(5)
         | 
| 29 | 
            -
              end
         | 
| 30 | 
            -
             | 
| 31 | 
            -
              it "can set read_timeout from ENV var" do
         | 
| 32 | 
            -
                ENV["FLIPPER_CLOUD_READ_TIMEOUT"] = "9"
         | 
| 33 | 
            -
                instance = described_class.new(required_options.reject { |k, v| k == :read_timeout })
         | 
| 34 | 
            -
                expect(instance.read_timeout).to eq(9)
         | 
| 35 | 
            -
              end
         | 
| 36 | 
            -
             | 
| 37 | 
            -
              it "can set open_timeout" do
         | 
| 38 | 
            -
                instance = described_class.new(required_options.merge(open_timeout: 5))
         | 
| 39 | 
            -
                expect(instance.open_timeout).to eq(5)
         | 
| 40 | 
            -
              end
         | 
| 41 | 
            -
             | 
| 42 | 
            -
              it "can set open_timeout from ENV var" do
         | 
| 43 | 
            -
                ENV["FLIPPER_CLOUD_OPEN_TIMEOUT"] = "9"
         | 
| 44 | 
            -
                instance = described_class.new(required_options.reject { |k, v| k == :open_timeout })
         | 
| 45 | 
            -
                expect(instance.open_timeout).to eq(9)
         | 
| 46 | 
            -
              end
         | 
| 47 | 
            -
             | 
| 48 | 
            -
              it "can set write_timeout" do
         | 
| 49 | 
            -
                instance = described_class.new(required_options.merge(write_timeout: 5))
         | 
| 50 | 
            -
                expect(instance.write_timeout).to eq(5)
         | 
| 51 | 
            -
              end
         | 
| 52 | 
            -
             | 
| 53 | 
            -
              it "can set write_timeout from ENV var" do
         | 
| 54 | 
            -
                ENV["FLIPPER_CLOUD_WRITE_TIMEOUT"] = "9"
         | 
| 55 | 
            -
                instance = described_class.new(required_options.reject { |k, v| k == :write_timeout })
         | 
| 56 | 
            -
                expect(instance.write_timeout).to eq(9)
         | 
| 57 | 
            -
              end
         | 
| 58 | 
            -
             | 
| 59 | 
            -
              it "can set sync_interval" do
         | 
| 60 | 
            -
                instance = described_class.new(required_options.merge(sync_interval: 1))
         | 
| 61 | 
            -
                expect(instance.sync_interval).to eq(1)
         | 
| 62 | 
            -
              end
         | 
| 63 | 
            -
             | 
| 64 | 
            -
              it "can set sync_interval from ENV var" do
         | 
| 65 | 
            -
                ENV["FLIPPER_CLOUD_SYNC_INTERVAL"] = "5"
         | 
| 66 | 
            -
                instance = described_class.new(required_options.reject { |k, v| k == :sync_interval })
         | 
| 67 | 
            -
                expect(instance.sync_interval).to eq(5)
         | 
| 68 | 
            -
              end
         | 
| 69 | 
            -
             | 
| 70 | 
            -
              it "passes sync_interval into sync adapter" do
         | 
| 71 | 
            -
                # The initial sync of http to local invokes this web request.
         | 
| 72 | 
            -
                stub_request(:get, /flippercloud\.io/).to_return(status: 200, body: "{}")
         | 
| 73 | 
            -
             | 
| 74 | 
            -
                instance = described_class.new(required_options.merge(sync_interval: 1))
         | 
| 75 | 
            -
                poller = instance.send(:poller)
         | 
| 76 | 
            -
                expect(poller.interval).to eq(1)
         | 
| 77 | 
            -
              end
         | 
| 78 | 
            -
             | 
| 79 | 
            -
              it "can set debug_output" do
         | 
| 80 | 
            -
                instance = described_class.new(required_options.merge(debug_output: STDOUT))
         | 
| 81 | 
            -
                expect(instance.debug_output).to eq(STDOUT)
         | 
| 82 | 
            -
              end
         | 
| 83 | 
            -
             | 
| 84 | 
            -
              it "defaults adapter block" do
         | 
| 85 | 
            -
                # The initial sync of http to local invokes this web request.
         | 
| 86 | 
            -
                stub_request(:get, /flippercloud\.io/).to_return(status: 200, body: "{}")
         | 
| 87 | 
            -
             | 
| 88 | 
            -
                instance = described_class.new(required_options)
         | 
| 89 | 
            -
                expect(instance.adapter).to be_instance_of(Flipper::Adapters::DualWrite)
         | 
| 90 | 
            -
              end
         | 
| 91 | 
            -
             | 
| 92 | 
            -
              it "can override adapter block" do
         | 
| 93 | 
            -
                # The initial sync of http to local invokes this web request.
         | 
| 94 | 
            -
                stub_request(:get, /flippercloud\.io/).to_return(status: 200, body: "{}")
         | 
| 95 | 
            -
             | 
| 96 | 
            -
                instance = described_class.new(required_options)
         | 
| 97 | 
            -
                instance.adapter do |adapter|
         | 
| 98 | 
            -
                  Flipper::Adapters::Instrumented.new(adapter)
         | 
| 99 | 
            -
                end
         | 
| 100 | 
            -
                expect(instance.adapter).to be_instance_of(Flipper::Adapters::Instrumented)
         | 
| 101 | 
            -
              end
         | 
| 102 | 
            -
             | 
| 103 | 
            -
              it "defaults url" do
         | 
| 104 | 
            -
                instance = described_class.new(required_options.reject { |k, v| k == :url })
         | 
| 105 | 
            -
                expect(instance.url).to eq("https://www.flippercloud.io/adapter")
         | 
| 106 | 
            -
              end
         | 
| 107 | 
            -
             | 
| 108 | 
            -
              it "can override url using options" do
         | 
| 109 | 
            -
                options = required_options.merge(url: "http://localhost:5000/adapter")
         | 
| 110 | 
            -
                instance = described_class.new(options)
         | 
| 111 | 
            -
                expect(instance.url).to eq("http://localhost:5000/adapter")
         | 
| 112 | 
            -
             | 
| 113 | 
            -
                instance = described_class.new(required_options)
         | 
| 114 | 
            -
                instance.url = "http://localhost:5000/adapter"
         | 
| 115 | 
            -
                expect(instance.url).to eq("http://localhost:5000/adapter")
         | 
| 116 | 
            -
              end
         | 
| 117 | 
            -
             | 
| 118 | 
            -
              it "can override URL using ENV var" do
         | 
| 119 | 
            -
                ENV["FLIPPER_CLOUD_URL"] = "https://example.com"
         | 
| 120 | 
            -
                instance = described_class.new(required_options.reject { |k, v| k == :url })
         | 
| 121 | 
            -
                expect(instance.url).to eq("https://example.com")
         | 
| 122 | 
            -
              end
         | 
| 123 | 
            -
             | 
| 124 | 
            -
              it "defaults sync_method to :poll" do
         | 
| 125 | 
            -
                instance = described_class.new(required_options)
         | 
| 126 | 
            -
             | 
| 127 | 
            -
                expect(instance.sync_method).to eq(:poll)
         | 
| 128 | 
            -
              end
         | 
| 129 | 
            -
             | 
| 130 | 
            -
              it "sets sync_method to :webhook if sync_secret provided" do
         | 
| 131 | 
            -
                instance = described_class.new(required_options.merge({
         | 
| 132 | 
            -
                  sync_secret: "secret",
         | 
| 133 | 
            -
                }))
         | 
| 134 | 
            -
             | 
| 135 | 
            -
                expect(instance.sync_method).to eq(:webhook)
         | 
| 136 | 
            -
                expect(instance.adapter).to be_instance_of(Flipper::Adapters::DualWrite)
         | 
| 137 | 
            -
              end
         | 
| 138 | 
            -
             | 
| 139 | 
            -
              it "sets sync_method to :webhook if FLIPPER_CLOUD_SYNC_SECRET set" do
         | 
| 140 | 
            -
                ENV["FLIPPER_CLOUD_SYNC_SECRET"] = "abc"
         | 
| 141 | 
            -
                instance = described_class.new(required_options)
         | 
| 142 | 
            -
             | 
| 143 | 
            -
                expect(instance.sync_method).to eq(:webhook)
         | 
| 144 | 
            -
                expect(instance.adapter).to be_instance_of(Flipper::Adapters::DualWrite)
         | 
| 145 | 
            -
              end
         | 
| 146 | 
            -
             | 
| 147 | 
            -
              it "can set sync_secret" do
         | 
| 148 | 
            -
                instance = described_class.new(required_options.merge(sync_secret: "from_config"))
         | 
| 149 | 
            -
                  expect(instance.sync_secret).to eq("from_config")
         | 
| 150 | 
            -
              end
         | 
| 151 | 
            -
             | 
| 152 | 
            -
              it "can override sync_secret using ENV var" do
         | 
| 153 | 
            -
                ENV["FLIPPER_CLOUD_SYNC_SECRET"] = "from_env"
         | 
| 154 | 
            -
                instance = described_class.new(required_options.reject { |k, v| k == :sync_secret })
         | 
| 155 | 
            -
                expect(instance.sync_secret).to eq("from_env")
         | 
| 156 | 
            -
              end
         | 
| 157 | 
            -
             | 
| 158 | 
            -
              it "can sync with cloud" do
         | 
| 159 | 
            -
                body = JSON.generate({
         | 
| 160 | 
            -
                  "features": [
         | 
| 161 | 
            -
                    {
         | 
| 162 | 
            -
                      "key": "search",
         | 
| 163 | 
            -
                      "state": "on",
         | 
| 164 | 
            -
                      "gates": [
         | 
| 165 | 
            -
                        {
         | 
| 166 | 
            -
                          "key": "boolean",
         | 
| 167 | 
            -
                          "name": "boolean",
         | 
| 168 | 
            -
                          "value": true
         | 
| 169 | 
            -
                        },
         | 
| 170 | 
            -
                        {
         | 
| 171 | 
            -
                          "key": "groups",
         | 
| 172 | 
            -
                          "name": "group",
         | 
| 173 | 
            -
                          "value": []
         | 
| 174 | 
            -
                        },
         | 
| 175 | 
            -
                        {
         | 
| 176 | 
            -
                          "key": "actors",
         | 
| 177 | 
            -
                          "name": "actor",
         | 
| 178 | 
            -
                          "value": []
         | 
| 179 | 
            -
                        },
         | 
| 180 | 
            -
                        {
         | 
| 181 | 
            -
                          "key": "percentage_of_actors",
         | 
| 182 | 
            -
                          "name": "percentage_of_actors",
         | 
| 183 | 
            -
                          "value": 0
         | 
| 184 | 
            -
                        },
         | 
| 185 | 
            -
                        {
         | 
| 186 | 
            -
                          "key": "percentage_of_time",
         | 
| 187 | 
            -
                          "name": "percentage_of_time",
         | 
| 188 | 
            -
                          "value": 0
         | 
| 189 | 
            -
                        }
         | 
| 190 | 
            -
                      ]
         | 
| 191 | 
            -
                    },
         | 
| 192 | 
            -
                    {
         | 
| 193 | 
            -
                      "key": "history",
         | 
| 194 | 
            -
                      "state": "off",
         | 
| 195 | 
            -
                      "gates": [
         | 
| 196 | 
            -
                        {
         | 
| 197 | 
            -
                          "key": "boolean",
         | 
| 198 | 
            -
                          "name": "boolean",
         | 
| 199 | 
            -
                          "value": false
         | 
| 200 | 
            -
                        },
         | 
| 201 | 
            -
                        {
         | 
| 202 | 
            -
                          "key": "groups",
         | 
| 203 | 
            -
                          "name": "group",
         | 
| 204 | 
            -
                          "value": []
         | 
| 205 | 
            -
                        },
         | 
| 206 | 
            -
                        {
         | 
| 207 | 
            -
                          "key": "actors",
         | 
| 208 | 
            -
                          "name": "actor",
         | 
| 209 | 
            -
                          "value": []
         | 
| 210 | 
            -
                        },
         | 
| 211 | 
            -
                        {
         | 
| 212 | 
            -
                          "key": "percentage_of_actors",
         | 
| 213 | 
            -
                          "name": "percentage_of_actors",
         | 
| 214 | 
            -
                          "value": 0
         | 
| 215 | 
            -
                        },
         | 
| 216 | 
            -
                        {
         | 
| 217 | 
            -
                          "key": "percentage_of_time",
         | 
| 218 | 
            -
                          "name": "percentage_of_time",
         | 
| 219 | 
            -
                          "value": 0
         | 
| 220 | 
            -
                        }
         | 
| 221 | 
            -
                      ]
         | 
| 222 | 
            -
                    }
         | 
| 223 | 
            -
                  ]
         | 
| 224 | 
            -
                })
         | 
| 225 | 
            -
                stub = stub_request(:get, "https://www.flippercloud.io/adapter/features?exclude_gate_names=true").
         | 
| 226 | 
            -
                  with({
         | 
| 227 | 
            -
                    headers: {
         | 
| 228 | 
            -
                      'Flipper-Cloud-Token'=>'asdf',
         | 
| 229 | 
            -
                    },
         | 
| 230 | 
            -
                  }).to_return(status: 200, body: body, headers: {})
         | 
| 231 | 
            -
                instance = described_class.new(required_options)
         | 
| 232 | 
            -
                instance.sync
         | 
| 233 | 
            -
             | 
| 234 | 
            -
                # Check that remote was fetched.
         | 
| 235 | 
            -
                expect(stub).to have_been_requested
         | 
| 236 | 
            -
             | 
| 237 | 
            -
                # Check that local adapter really did sync.
         | 
| 238 | 
            -
                local_adapter = instance.local_adapter
         | 
| 239 | 
            -
                all = local_adapter.get_all
         | 
| 240 | 
            -
                expect(all.keys).to eq(["search", "history"])
         | 
| 241 | 
            -
                expect(all["search"][:boolean]).to eq("true")
         | 
| 242 | 
            -
                expect(all["history"][:boolean]).to eq(nil)
         | 
| 243 | 
            -
              end
         | 
| 244 | 
            -
             | 
| 245 | 
            -
              it "can setup brow to report events to cloud" do
         | 
| 246 | 
            -
                # skip logging brow
         | 
| 247 | 
            -
                Brow.logger = Logger.new(File::NULL)
         | 
| 248 | 
            -
                brow = described_class.new(required_options).brow
         | 
| 249 | 
            -
             | 
| 250 | 
            -
                stub = stub_request(:post, "https://www.flippercloud.io/adapter/events")
         | 
| 251 | 
            -
                  .with { |request|
         | 
| 252 | 
            -
                    data = JSON.parse(request.body)
         | 
| 253 | 
            -
                    data.keys == ["uuid", "messages"] && data["messages"] == [{"n" => 1}]
         | 
| 254 | 
            -
                  }
         | 
| 255 | 
            -
                  .to_return(status: 201, body: "{}", headers: {})
         | 
| 256 | 
            -
             | 
| 257 | 
            -
                brow.push({"n" => 1})
         | 
| 258 | 
            -
                brow.worker.stop
         | 
| 259 | 
            -
                expect(stub).to have_been_requested.times(1)
         | 
| 260 | 
            -
              end
         | 
| 261 | 
            -
            end
         | 
| @@ -1,82 +0,0 @@ | |
| 1 | 
            -
            require 'flipper/cloud/configuration'
         | 
| 2 | 
            -
            require 'flipper/cloud/dsl'
         | 
| 3 | 
            -
            require 'flipper/adapters/operation_logger'
         | 
| 4 | 
            -
            require 'flipper/adapters/instrumented'
         | 
| 5 | 
            -
             | 
| 6 | 
            -
            RSpec.describe Flipper::Cloud::DSL do
         | 
| 7 | 
            -
              it 'delegates everything to flipper instance' do
         | 
| 8 | 
            -
                cloud_configuration = Flipper::Cloud::Configuration.new({
         | 
| 9 | 
            -
                  token: "asdf",
         | 
| 10 | 
            -
                  sync_secret: "tasty",
         | 
| 11 | 
            -
                })
         | 
| 12 | 
            -
                dsl = described_class.new(cloud_configuration)
         | 
| 13 | 
            -
                expect(dsl.features).to eq(Set.new)
         | 
| 14 | 
            -
                expect(dsl.enabled?(:foo)).to be(false)
         | 
| 15 | 
            -
              end
         | 
| 16 | 
            -
             | 
| 17 | 
            -
              it 'delegates sync to cloud configuration' do
         | 
| 18 | 
            -
                stub = stub_request(:get, "https://www.flippercloud.io/adapter/features?exclude_gate_names=true").
         | 
| 19 | 
            -
                  with({
         | 
| 20 | 
            -
                    headers: {
         | 
| 21 | 
            -
                      'Flipper-Cloud-Token'=>'asdf',
         | 
| 22 | 
            -
                    },
         | 
| 23 | 
            -
                  }).to_return(status: 200, body: '{"features": {}}', headers: {})
         | 
| 24 | 
            -
                cloud_configuration = Flipper::Cloud::Configuration.new({
         | 
| 25 | 
            -
                  token: "asdf",
         | 
| 26 | 
            -
                  sync_secret: "tasty",
         | 
| 27 | 
            -
                })
         | 
| 28 | 
            -
                dsl = described_class.new(cloud_configuration)
         | 
| 29 | 
            -
                dsl.sync
         | 
| 30 | 
            -
                expect(stub).to have_been_requested
         | 
| 31 | 
            -
              end
         | 
| 32 | 
            -
             | 
| 33 | 
            -
              it 'delegates sync_secret to cloud configuration' do
         | 
| 34 | 
            -
                cloud_configuration = Flipper::Cloud::Configuration.new({
         | 
| 35 | 
            -
                  token: "asdf",
         | 
| 36 | 
            -
                  sync_secret: "tasty",
         | 
| 37 | 
            -
                })
         | 
| 38 | 
            -
                dsl = described_class.new(cloud_configuration)
         | 
| 39 | 
            -
                expect(dsl.sync_secret).to eq("tasty")
         | 
| 40 | 
            -
              end
         | 
| 41 | 
            -
             | 
| 42 | 
            -
              context "when sync_method is webhook" do
         | 
| 43 | 
            -
                let(:local_adapter) do
         | 
| 44 | 
            -
                  Flipper::Adapters::OperationLogger.new Flipper::Adapters::Memory.new
         | 
| 45 | 
            -
                end
         | 
| 46 | 
            -
             | 
| 47 | 
            -
                let(:cloud_configuration) do
         | 
| 48 | 
            -
                  cloud_configuration = Flipper::Cloud::Configuration.new({
         | 
| 49 | 
            -
                    token: "asdf",
         | 
| 50 | 
            -
                    sync_secret: "tasty",
         | 
| 51 | 
            -
                    local_adapter: local_adapter
         | 
| 52 | 
            -
                  })
         | 
| 53 | 
            -
                end
         | 
| 54 | 
            -
             | 
| 55 | 
            -
                subject do
         | 
| 56 | 
            -
                  described_class.new(cloud_configuration)
         | 
| 57 | 
            -
                end
         | 
| 58 | 
            -
             | 
| 59 | 
            -
                it "sends reads to local adapter" do
         | 
| 60 | 
            -
                  subject.features
         | 
| 61 | 
            -
                  subject.enabled?(:foo)
         | 
| 62 | 
            -
                  expect(local_adapter.count(:features)).to be(1)
         | 
| 63 | 
            -
                  expect(local_adapter.count(:get)).to be(1)
         | 
| 64 | 
            -
                end
         | 
| 65 | 
            -
             | 
| 66 | 
            -
                it "sends writes to cloud and local" do
         | 
| 67 | 
            -
                  add_stub = stub_request(:post, "https://www.flippercloud.io/adapter/features").
         | 
| 68 | 
            -
                    with({headers: {'Flipper-Cloud-Token'=>'asdf'}}).
         | 
| 69 | 
            -
                    to_return(status: 200, body: '{}', headers: {})
         | 
| 70 | 
            -
                  enable_stub = stub_request(:post, "https://www.flippercloud.io/adapter/features/foo/boolean").
         | 
| 71 | 
            -
                    with(headers: {'Flipper-Cloud-Token'=>'asdf'}).
         | 
| 72 | 
            -
                    to_return(status: 200, body: '{}', headers: {})
         | 
| 73 | 
            -
             | 
| 74 | 
            -
                  subject.enable(:foo)
         | 
| 75 | 
            -
             | 
| 76 | 
            -
                  expect(local_adapter.count(:add)).to be(1)
         | 
| 77 | 
            -
                  expect(local_adapter.count(:enable)).to be(1)
         | 
| 78 | 
            -
                  expect(add_stub).to have_been_requested
         | 
| 79 | 
            -
                  expect(enable_stub).to have_been_requested
         | 
| 80 | 
            -
                end
         | 
| 81 | 
            -
              end
         | 
| 82 | 
            -
            end
         | 
| @@ -1,95 +0,0 @@ | |
| 1 | 
            -
            require 'rails'
         | 
| 2 | 
            -
            require 'flipper/cloud'
         | 
| 3 | 
            -
             | 
| 4 | 
            -
            RSpec.describe Flipper::Cloud::Engine do
         | 
| 5 | 
            -
              let(:env) do
         | 
| 6 | 
            -
                { "FLIPPER_CLOUD_TOKEN" => "test-token" }
         | 
| 7 | 
            -
              end
         | 
| 8 | 
            -
             | 
| 9 | 
            -
              let(:application) do
         | 
| 10 | 
            -
                Class.new(Rails::Application) do
         | 
| 11 | 
            -
                  config.eager_load = false
         | 
| 12 | 
            -
                  config.logger = ActiveSupport::Logger.new($stdout)
         | 
| 13 | 
            -
                end
         | 
| 14 | 
            -
              end
         | 
| 15 | 
            -
             | 
| 16 | 
            -
              # App for Rack::Test
         | 
| 17 | 
            -
              let(:app) { application.routes }
         | 
| 18 | 
            -
             | 
| 19 | 
            -
              before do
         | 
| 20 | 
            -
                Rails.application = nil
         | 
| 21 | 
            -
                ActiveSupport::Dependencies.autoload_paths = ActiveSupport::Dependencies.autoload_paths.dup
         | 
| 22 | 
            -
                ActiveSupport::Dependencies.autoload_once_paths = ActiveSupport::Dependencies.autoload_once_paths.dup
         | 
| 23 | 
            -
             | 
| 24 | 
            -
                # Force loading of flipper to configure itself
         | 
| 25 | 
            -
                load 'flipper/cloud.rb'
         | 
| 26 | 
            -
              end
         | 
| 27 | 
            -
             | 
| 28 | 
            -
              it "initializes cloud configuration" do
         | 
| 29 | 
            -
                stub_request(:get, /flippercloud\.io/).to_return(status: 200, body: "{}")
         | 
| 30 | 
            -
             | 
| 31 | 
            -
                ENV.update(env)
         | 
| 32 | 
            -
                application.initialize!
         | 
| 33 | 
            -
             | 
| 34 | 
            -
                expect(Flipper.instance).to be_a(Flipper::Cloud::DSL)
         | 
| 35 | 
            -
                expect(Flipper.instance.instrumenter).to be(ActiveSupport::Notifications)
         | 
| 36 | 
            -
              end
         | 
| 37 | 
            -
             | 
| 38 | 
            -
              context "with CLOUD_SYNC_SECRET" do
         | 
| 39 | 
            -
                before do
         | 
| 40 | 
            -
                  env.update "FLIPPER_CLOUD_SYNC_SECRET" => "test-secret"
         | 
| 41 | 
            -
                end
         | 
| 42 | 
            -
             | 
| 43 | 
            -
                let(:request_body) do
         | 
| 44 | 
            -
                  JSON.generate({
         | 
| 45 | 
            -
                    "environment_id" => 1,
         | 
| 46 | 
            -
                    "webhook_id" => 1,
         | 
| 47 | 
            -
                    "delivery_id" => SecureRandom.uuid,
         | 
| 48 | 
            -
                    "action" => "sync",
         | 
| 49 | 
            -
                  })
         | 
| 50 | 
            -
                end
         | 
| 51 | 
            -
                let(:timestamp) { Time.now }
         | 
| 52 | 
            -
                let(:signature) {
         | 
| 53 | 
            -
                  Flipper::Cloud::MessageVerifier.new(secret: env["FLIPPER_CLOUD_SYNC_SECRET"]).generate(request_body, timestamp)
         | 
| 54 | 
            -
                }
         | 
| 55 | 
            -
                let(:signature_header_value) {
         | 
| 56 | 
            -
                  Flipper::Cloud::MessageVerifier.new(secret: "").header(signature, timestamp)
         | 
| 57 | 
            -
                }
         | 
| 58 | 
            -
             | 
| 59 | 
            -
                it "configures webhook app" do
         | 
| 60 | 
            -
                  ENV.update(env)
         | 
| 61 | 
            -
                  application.initialize!
         | 
| 62 | 
            -
             | 
| 63 | 
            -
                  stub = stub_request(:get, "https://www.flippercloud.io/adapter/features?exclude_gate_names=true").with({
         | 
| 64 | 
            -
                    headers: { "Flipper-Cloud-Token" => ENV["FLIPPER_CLOUD_TOKEN"] },
         | 
| 65 | 
            -
                  }).to_return(status: 200, body: JSON.generate({ features: {} }), headers: {})
         | 
| 66 | 
            -
             | 
| 67 | 
            -
                  post "/_flipper", request_body, { "HTTP_FLIPPER_CLOUD_SIGNATURE" => signature_header_value }
         | 
| 68 | 
            -
             | 
| 69 | 
            -
                  expect(last_response.status).to eq(200)
         | 
| 70 | 
            -
                  expect(stub).to have_been_requested
         | 
| 71 | 
            -
                end
         | 
| 72 | 
            -
              end
         | 
| 73 | 
            -
             | 
| 74 | 
            -
              context "without CLOUD_SYNC_SECRET" do
         | 
| 75 | 
            -
                it "does not configure webhook app" do
         | 
| 76 | 
            -
                  ENV.update(env)
         | 
| 77 | 
            -
                  application.initialize!
         | 
| 78 | 
            -
             | 
| 79 | 
            -
                  post "/_flipper"
         | 
| 80 | 
            -
                  expect(last_response.status).to eq(404)
         | 
| 81 | 
            -
                end
         | 
| 82 | 
            -
              end
         | 
| 83 | 
            -
             | 
| 84 | 
            -
              context "without FLIPPER_CLOUD_TOKEN" do
         | 
| 85 | 
            -
                it "gracefully skips configuring webhook app" do
         | 
| 86 | 
            -
                  ENV["FLIPPER_CLOUD_TOKEN"] = nil
         | 
| 87 | 
            -
                  application.initialize!
         | 
| 88 | 
            -
                  expect(silence { Flipper.instance }).to match(/Missing FLIPPER_CLOUD_TOKEN/)
         | 
| 89 | 
            -
                  expect(Flipper.instance).to be_a(Flipper::DSL)
         | 
| 90 | 
            -
             | 
| 91 | 
            -
                  post "/_flipper"
         | 
| 92 | 
            -
                  expect(last_response.status).to eq(404)
         | 
| 93 | 
            -
                end
         | 
| 94 | 
            -
              end
         | 
| 95 | 
            -
            end
         | 
| @@ -1,104 +0,0 @@ | |
| 1 | 
            -
            require 'flipper/cloud/message_verifier'
         | 
| 2 | 
            -
             | 
| 3 | 
            -
            RSpec.describe Flipper::Cloud::MessageVerifier do
         | 
| 4 | 
            -
              let(:payload) { "some payload" }
         | 
| 5 | 
            -
              let(:secret) { "secret" }
         | 
| 6 | 
            -
              let(:timestamp) { Time.now }
         | 
| 7 | 
            -
             | 
| 8 | 
            -
              describe "#generate" do
         | 
| 9 | 
            -
                it "generates signature that can be verified" do
         | 
| 10 | 
            -
                  message_verifier = Flipper::Cloud::MessageVerifier.new(secret: secret)
         | 
| 11 | 
            -
                  signature = message_verifier.generate(payload, timestamp)
         | 
| 12 | 
            -
                  header = generate_header(timestamp: timestamp, signature: signature)
         | 
| 13 | 
            -
                  expect(message_verifier.verify(payload, header)).to be(true)
         | 
| 14 | 
            -
                end
         | 
| 15 | 
            -
              end
         | 
| 16 | 
            -
             | 
| 17 | 
            -
              describe "#header" do
         | 
| 18 | 
            -
                it "generates a header in valid format" do
         | 
| 19 | 
            -
                  version = "v1"
         | 
| 20 | 
            -
                  message_verifier = Flipper::Cloud::MessageVerifier.new(secret: secret, version: version)
         | 
| 21 | 
            -
                  signature = message_verifier.generate(payload, timestamp)
         | 
| 22 | 
            -
                  header = message_verifier.header(signature, timestamp)
         | 
| 23 | 
            -
                  expect(header).to eq("t=#{timestamp.to_i},#{version}=#{signature}")
         | 
| 24 | 
            -
                end
         | 
| 25 | 
            -
              end
         | 
| 26 | 
            -
             | 
| 27 | 
            -
              describe ".header" do
         | 
| 28 | 
            -
                it "generates a header in valid format" do
         | 
| 29 | 
            -
                  version = "v1"
         | 
| 30 | 
            -
                  message_verifier = Flipper::Cloud::MessageVerifier.new(secret: secret, version: version)
         | 
| 31 | 
            -
                  signature = message_verifier.generate(payload, timestamp)
         | 
| 32 | 
            -
             | 
| 33 | 
            -
                  header = Flipper::Cloud::MessageVerifier.header(signature, timestamp, version)
         | 
| 34 | 
            -
                  expect(header).to eq("t=#{timestamp.to_i},#{version}=#{signature}")
         | 
| 35 | 
            -
                end
         | 
| 36 | 
            -
              end
         | 
| 37 | 
            -
             | 
| 38 | 
            -
              describe "#verify" do
         | 
| 39 | 
            -
                it "raises a InvalidSignature when the header does not have the expected format" do
         | 
| 40 | 
            -
                  header = "i'm not even a real signature header"
         | 
| 41 | 
            -
                  expect {
         | 
| 42 | 
            -
                    message_verifier = Flipper::Cloud::MessageVerifier.new(secret: "secret")
         | 
| 43 | 
            -
                    message_verifier.verify(payload, header)
         | 
| 44 | 
            -
                  }.to raise_error(Flipper::Cloud::MessageVerifier::InvalidSignature, "Unable to extract timestamp and signatures from header")
         | 
| 45 | 
            -
                end
         | 
| 46 | 
            -
             | 
| 47 | 
            -
                it "raises a InvalidSignature when there are no signatures with the expected version" do
         | 
| 48 | 
            -
                  header = generate_header(version: "v0")
         | 
| 49 | 
            -
                  expect {
         | 
| 50 | 
            -
                    message_verifier = Flipper::Cloud::MessageVerifier.new(secret: "secret")
         | 
| 51 | 
            -
                    message_verifier.verify(payload, header)
         | 
| 52 | 
            -
                  }.to raise_error(Flipper::Cloud::MessageVerifier::InvalidSignature, /No signatures found with expected version/)
         | 
| 53 | 
            -
                end
         | 
| 54 | 
            -
             | 
| 55 | 
            -
                it "raises a InvalidSignature when there are no valid signatures for the payload" do
         | 
| 56 | 
            -
                  header = generate_header(signature: "bad_signature")
         | 
| 57 | 
            -
                  expect {
         | 
| 58 | 
            -
                    message_verifier = Flipper::Cloud::MessageVerifier.new(secret: "secret")
         | 
| 59 | 
            -
                    message_verifier.verify(payload, header)
         | 
| 60 | 
            -
                  }.to raise_error(Flipper::Cloud::MessageVerifier::InvalidSignature, "No signatures found matching the expected signature for payload")
         | 
| 61 | 
            -
                end
         | 
| 62 | 
            -
             | 
| 63 | 
            -
                it "raises a InvalidSignature when the timestamp is not within the tolerance" do
         | 
| 64 | 
            -
                  header = generate_header(timestamp: Time.now - 15)
         | 
| 65 | 
            -
                  expect {
         | 
| 66 | 
            -
                    message_verifier = Flipper::Cloud::MessageVerifier.new(secret: secret)
         | 
| 67 | 
            -
                    message_verifier.verify(payload, header, tolerance: 10)
         | 
| 68 | 
            -
                  }.to raise_error(Flipper::Cloud::MessageVerifier::InvalidSignature, /Timestamp outside the tolerance zone/)
         | 
| 69 | 
            -
                end
         | 
| 70 | 
            -
             | 
| 71 | 
            -
                it "returns true when the header contains a valid signature and the timestamp is within the tolerance" do
         | 
| 72 | 
            -
                  header = generate_header
         | 
| 73 | 
            -
                  message_verifier = Flipper::Cloud::MessageVerifier.new(secret: "secret")
         | 
| 74 | 
            -
                  expect(message_verifier.verify(payload, header, tolerance: 10)).to be(true)
         | 
| 75 | 
            -
                end
         | 
| 76 | 
            -
             | 
| 77 | 
            -
                it "returns true when the header contains at least one valid signature" do
         | 
| 78 | 
            -
                  header = generate_header + ",v1=bad_signature"
         | 
| 79 | 
            -
                  message_verifier = Flipper::Cloud::MessageVerifier.new(secret: secret)
         | 
| 80 | 
            -
                  expect(message_verifier.verify(payload, header, tolerance: 10)).to be(true)
         | 
| 81 | 
            -
                end
         | 
| 82 | 
            -
             | 
| 83 | 
            -
                it "returns true when the header contains a valid signature and the timestamp is off but no tolerance is provided" do
         | 
| 84 | 
            -
                  header = generate_header(timestamp: Time.at(12_345))
         | 
| 85 | 
            -
                  message_verifier = Flipper::Cloud::MessageVerifier.new(secret: secret)
         | 
| 86 | 
            -
                  expect(message_verifier.verify(payload, header)).to be(true)
         | 
| 87 | 
            -
                end
         | 
| 88 | 
            -
              end
         | 
| 89 | 
            -
             | 
| 90 | 
            -
              private
         | 
| 91 | 
            -
             | 
| 92 | 
            -
              def generate_header(options = {})
         | 
| 93 | 
            -
                options[:secret] ||= secret
         | 
| 94 | 
            -
                options[:version] ||= "v1"
         | 
| 95 | 
            -
             | 
| 96 | 
            -
                message_verifier = Flipper::Cloud::MessageVerifier.new(secret: options[:secret], version: options[:version])
         | 
| 97 | 
            -
             | 
| 98 | 
            -
                options[:timestamp] ||= timestamp
         | 
| 99 | 
            -
                options[:payload] ||= payload
         | 
| 100 | 
            -
                options[:signature] ||= message_verifier.generate(options[:payload], options[:timestamp])
         | 
| 101 | 
            -
             | 
| 102 | 
            -
                Flipper::Cloud::MessageVerifier.header(options[:signature], options[:timestamp], options[:version])
         | 
| 103 | 
            -
              end
         | 
| 104 | 
            -
            end
         |