From d3eaf2f58f8547c43b2949d00c9edda530ab73a3 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Fri, 13 Sep 2024 14:00:02 +1200 Subject: [PATCH] Modernize gem (#79) * Migrate/update tests to `sus`. * Fix test matrix. --- .editorconfig | 4 + .github/workflows/development.yml | 89 -------- .github/workflows/documentation-coverage.yaml | 25 +++ .github/workflows/documentation.yaml | 58 +++++ .github/workflows/rubocop.yaml | 24 ++ .github/workflows/test-coverage.yaml | 58 +++++ .github/workflows/test-external.yaml | 37 +++ .github/workflows/test-proxy.yaml | 55 +++++ .github/workflows/test.yaml | 51 +++++ .gitignore | 15 +- .mailmap | 6 + .rspec | 4 - .rubocop.yml | 53 +++++ cloudflare.gemspec | 19 +- config/sus.rb | 7 + fixtures/cloudflare/a_connection.rb | 63 ++++++ gems.rb | 27 ++- lib/cloudflare.rb | 52 ++--- lib/cloudflare/accounts.rb | 34 +-- lib/cloudflare/connection.rb | 54 ++--- .../custom_hostname/ssl_attribute.rb | 11 +- .../custom_hostname/ssl_attribute/settings.rb | 14 +- lib/cloudflare/custom_hostnames.rb | 105 +++++---- lib/cloudflare/dns.rb | 102 +++++---- lib/cloudflare/firewall.rb | 73 +++--- lib/cloudflare/kv/namespaces.rb | 96 +++++--- lib/cloudflare/kv/rest_wrapper.rb | 44 ---- lib/cloudflare/kv/wrapper.rb | 38 ++++ lib/cloudflare/logs.rb | 30 +-- lib/cloudflare/paginate.rb | 54 ++--- lib/cloudflare/representation.rb | 148 +++++------- lib/cloudflare/rspec/connection.rb | 57 ----- lib/cloudflare/user.rb | 29 +-- lib/cloudflare/version.rb | 28 +-- lib/cloudflare/zones.rb | 86 +++---- license.md | 41 ++++ README.md => readme.md | 44 ++-- release.cert | 28 +++ spec/cloudflare/accounts_spec.rb | 24 -- .../ssl_attribute/settings_spec.rb | 54 ----- .../custom_hostname/ssl_attribute_spec.rb | 73 ------ spec/cloudflare/custom_hostnames_spec.rb | 211 ------------------ spec/cloudflare/dns_spec.rb | 50 ----- spec/cloudflare/firewall_spec.rb | 49 ---- spec/cloudflare/kv/namespaces_spec.rb | 71 ------ spec/cloudflare/zone_spec.rb | 27 --- spec/spec_helper.rb | 86 ------- test/cloudflare/accounts.rb | 39 ++++ test/cloudflare/connection.rb | 21 ++ .../custom_hostname/ssl_attribute.rb | 80 +++++++ .../custom_hostname/ssl_attribute/settings.rb | 59 +++++ test/cloudflare/custom_hostnames.rb | 210 +++++++++++++++++ test/cloudflare/dns.rb | 61 +++++ test/cloudflare/firewall.rb | 54 +++++ test/cloudflare/kv/namespaces.rb | 82 +++++++ test/cloudflare/logs.rb | 17 ++ test/cloudflare/zones.rb | 49 ++++ 57 files changed, 1694 insertions(+), 1386 deletions(-) delete mode 100644 .github/workflows/development.yml create mode 100644 .github/workflows/documentation-coverage.yaml create mode 100644 .github/workflows/documentation.yaml create mode 100644 .github/workflows/rubocop.yaml create mode 100644 .github/workflows/test-coverage.yaml create mode 100644 .github/workflows/test-external.yaml create mode 100644 .github/workflows/test-proxy.yaml create mode 100644 .github/workflows/test.yaml create mode 100644 .mailmap delete mode 100644 .rspec create mode 100644 .rubocop.yml create mode 100644 config/sus.rb create mode 100644 fixtures/cloudflare/a_connection.rb delete mode 100644 lib/cloudflare/kv/rest_wrapper.rb create mode 100644 lib/cloudflare/kv/wrapper.rb delete mode 100644 lib/cloudflare/rspec/connection.rb create mode 100644 license.md rename README.md => readme.md (58%) create mode 100644 release.cert delete mode 100644 spec/cloudflare/accounts_spec.rb delete mode 100644 spec/cloudflare/custom_hostname/ssl_attribute/settings_spec.rb delete mode 100644 spec/cloudflare/custom_hostname/ssl_attribute_spec.rb delete mode 100644 spec/cloudflare/custom_hostnames_spec.rb delete mode 100644 spec/cloudflare/dns_spec.rb delete mode 100644 spec/cloudflare/firewall_spec.rb delete mode 100644 spec/cloudflare/kv/namespaces_spec.rb delete mode 100644 spec/cloudflare/zone_spec.rb delete mode 100644 spec/spec_helper.rb create mode 100644 test/cloudflare/accounts.rb create mode 100644 test/cloudflare/connection.rb create mode 100644 test/cloudflare/custom_hostname/ssl_attribute.rb create mode 100644 test/cloudflare/custom_hostname/ssl_attribute/settings.rb create mode 100644 test/cloudflare/custom_hostnames.rb create mode 100644 test/cloudflare/dns.rb create mode 100644 test/cloudflare/firewall.rb create mode 100644 test/cloudflare/kv/namespaces.rb create mode 100644 test/cloudflare/logs.rb create mode 100644 test/cloudflare/zones.rb diff --git a/.editorconfig b/.editorconfig index 538ba2b..a6e7d26 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3,3 +3,7 @@ root = true [*] indent_style = tab indent_size = 2 + +[*.{yml,yaml}] +indent_style = space +indent_size = 2 diff --git a/.github/workflows/development.yml b/.github/workflows/development.yml deleted file mode 100644 index ed2a9bf..0000000 --- a/.github/workflows/development.yml +++ /dev/null @@ -1,89 +0,0 @@ -name: Development - -on: [push, pull_request] - -jobs: - test: - runs-on: ${{matrix.os}}-latest - continue-on-error: ${{matrix.experimental}} - - strategy: - matrix: - os: - - ubuntu - - macos - - ruby: - - "2.6" - - "2.7" - - "3.0" - - experimental: [false] - env: [""] - - include: - - os: ubuntu - ruby: truffleruby - experimental: true - - os: ubuntu - ruby: jruby - experimental: true - - os: ubuntu - ruby: head - experimental: true - - steps: - - uses: actions/checkout@v2 - - uses: ruby/setup-ruby@v1 - with: - ruby-version: ${{matrix.ruby}} - bundler-cache: true - - - name: Run tests - timeout-minutes: 5 - env: - CLOUDFLARE_TEST_ZONE_MANAGEMENT: true - CLOUDFLARE_EMAIL: ${{secrets.CLOUDFLARE_EMAIL}} - CLOUDFLARE_KEY: ${{secrets.CLOUDFLARE_KEY}} - run: ${{matrix.env}} bundle exec rspec - - test-with-proxy: - runs-on: ${{matrix.os}}-latest - continue-on-error: ${{matrix.experimental}} - - strategy: - matrix: - os: - - ubuntu - ruby: - - 2.7 - - experimental: [false] - env: [""] - - proxy: - - http://localhost:3128 - - steps: - - uses: actions/checkout@v2 - - uses: ruby/setup-ruby@v1 - with: - ruby-version: ${{matrix.ruby}} - - - name: Install dependencies - run: ${{matrix.env}} bundle install - - - name: Prepare squid - if: matrix.proxy - run: | - sudo apt-get install squid - sudo systemctl start squid - - - name: Run tests - timeout-minutes: 5 - env: - CLOUDFLARE_TEST_ZONE_MANAGEMENT: true - CLOUDFLARE_PROXY: ${{matrix.proxy}} - CLOUDFLARE_EMAIL: ${{secrets.CLOUDFLARE_EMAIL}} - CLOUDFLARE_KEY: ${{secrets.CLOUDFLARE_KEY}} - run: ${{matrix.env}} bundle exec rspec diff --git a/.github/workflows/documentation-coverage.yaml b/.github/workflows/documentation-coverage.yaml new file mode 100644 index 0000000..b3bac9a --- /dev/null +++ b/.github/workflows/documentation-coverage.yaml @@ -0,0 +1,25 @@ +name: Documentation Coverage + +on: [push, pull_request] + +permissions: + contents: read + +env: + CONSOLE_OUTPUT: XTerm + COVERAGE: PartialSummary + +jobs: + validate: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.3" + bundler-cache: true + + - name: Validate coverage + timeout-minutes: 5 + run: bundle exec bake decode:index:coverage lib diff --git a/.github/workflows/documentation.yaml b/.github/workflows/documentation.yaml new file mode 100644 index 0000000..f5f553a --- /dev/null +++ b/.github/workflows/documentation.yaml @@ -0,0 +1,58 @@ +name: Documentation + +on: + push: + branches: + - main + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages: +permissions: + contents: read + pages: write + id-token: write + +# Allow one concurrent deployment: +concurrency: + group: "pages" + cancel-in-progress: true + +env: + CONSOLE_OUTPUT: XTerm + BUNDLE_WITH: maintenance + +jobs: + generate: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.3" + bundler-cache: true + + - name: Installing packages + run: sudo apt-get install wget + + - name: Generate documentation + timeout-minutes: 5 + run: bundle exec bake utopia:project:static --force no + + - name: Upload documentation artifact + uses: actions/upload-pages-artifact@v3 + with: + path: docs + + deploy: + runs-on: ubuntu-latest + + environment: + name: github-pages + url: ${{steps.deployment.outputs.page_url}} + + needs: generate + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/rubocop.yaml b/.github/workflows/rubocop.yaml new file mode 100644 index 0000000..287c06d --- /dev/null +++ b/.github/workflows/rubocop.yaml @@ -0,0 +1,24 @@ +name: RuboCop + +on: [push, pull_request] + +permissions: + contents: read + +env: + CONSOLE_OUTPUT: XTerm + +jobs: + check: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ruby + bundler-cache: true + + - name: Run RuboCop + timeout-minutes: 10 + run: bundle exec rubocop diff --git a/.github/workflows/test-coverage.yaml b/.github/workflows/test-coverage.yaml new file mode 100644 index 0000000..eb155e1 --- /dev/null +++ b/.github/workflows/test-coverage.yaml @@ -0,0 +1,58 @@ +name: Test Coverage + +on: [push, pull_request] + +permissions: + contents: read + +env: + CONSOLE_OUTPUT: XTerm + COVERAGE: PartialSummary + CLOUDFLARE_EMAIL: ${{secrets.CLOUDFLARE_EMAIL}} + CLOUDFLARE_KEY: ${{secrets.CLOUDFLARE_KEY}} + +jobs: + test: + name: ${{matrix.ruby}} on ${{matrix.os}} + runs-on: ${{matrix.os}}-latest + + strategy: + matrix: + os: + - ubuntu + + ruby: + - "3.3" + + steps: + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{matrix.ruby}} + bundler-cache: true + + - name: Run tests + timeout-minutes: 5 + run: bundle exec bake test + + - uses: actions/upload-artifact@v4 + with: + name: coverage-${{matrix.os}}-${{matrix.ruby}} + path: .covered.db + + validate: + needs: test + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.3" + bundler-cache: true + + - uses: actions/download-artifact@v4 + + - name: Validate coverage + timeout-minutes: 5 + run: bundle exec bake covered:validate --paths */.covered.db \; diff --git a/.github/workflows/test-external.yaml b/.github/workflows/test-external.yaml new file mode 100644 index 0000000..4606575 --- /dev/null +++ b/.github/workflows/test-external.yaml @@ -0,0 +1,37 @@ +name: Test External + +on: [push, pull_request] + +permissions: + contents: read + +env: + CONSOLE_OUTPUT: XTerm + CLOUDFLARE_EMAIL: ${{secrets.CLOUDFLARE_EMAIL}} + CLOUDFLARE_KEY: ${{secrets.CLOUDFLARE_KEY}} + +jobs: + test: + name: ${{matrix.ruby}} on ${{matrix.os}} + runs-on: ${{matrix.os}}-latest + + strategy: + matrix: + os: + - ubuntu + + ruby: + - "3.1" + - "3.2" + - "3.3" + + steps: + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{matrix.ruby}} + bundler-cache: true + + - name: Run tests + timeout-minutes: 10 + run: bundle exec bake test:external diff --git a/.github/workflows/test-proxy.yaml b/.github/workflows/test-proxy.yaml new file mode 100644 index 0000000..28d1a75 --- /dev/null +++ b/.github/workflows/test-proxy.yaml @@ -0,0 +1,55 @@ +name: Test + +on: [push, pull_request] + +permissions: + contents: read + +env: + CONSOLE_OUTPUT: XTerm + CLOUDFLARE_PROXY: http://localhost:3128 + CLOUDFLARE_EMAIL: ${{secrets.CLOUDFLARE_EMAIL}} + CLOUDFLARE_KEY: ${{secrets.CLOUDFLARE_KEY}} + +jobs: + test: + name: ${{matrix.ruby}} on ${{matrix.os}} + runs-on: ${{matrix.os}}-latest + continue-on-error: ${{matrix.experimental}} + + strategy: + matrix: + os: + - ubuntu + + ruby: + - "3.3" + + experimental: [false] + + include: + - os: ubuntu + ruby: truffleruby + experimental: true + - os: ubuntu + ruby: jruby + experimental: true + - os: ubuntu + ruby: head + experimental: true + + steps: + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{matrix.ruby}} + bundler-cache: true + + - name: Prepare squid + run: | + sudo apt-get install squid + sudo systemctl start squid + + - name: Run tests + timeout-minutes: 10 + run: bundle exec bake test diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..012ee57 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,51 @@ +name: Test + +on: [push, pull_request] + +permissions: + contents: read + +env: + CONSOLE_OUTPUT: XTerm + CLOUDFLARE_EMAIL: ${{secrets.CLOUDFLARE_EMAIL}} + CLOUDFLARE_KEY: ${{secrets.CLOUDFLARE_KEY}} + +jobs: + test: + name: ${{matrix.ruby}} on ${{matrix.os}} + runs-on: ${{matrix.os}}-latest + continue-on-error: ${{matrix.experimental}} + + strategy: + matrix: + os: + - ubuntu + + ruby: + - "3.1" + - "3.2" + - "3.3" + + experimental: [false] + + include: + - os: ubuntu + ruby: truffleruby + experimental: true + - os: ubuntu + ruby: jruby + experimental: true + - os: ubuntu + ruby: head + experimental: true + + steps: + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{matrix.ruby}} + bundler-cache: true + + - name: Run tests + timeout-minutes: 10 + run: bundle exec bake test diff --git a/.gitignore b/.gitignore index 88ac4bc..6c32510 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,7 @@ /.bundle/ -/.yardoc -/gems.locked -/_yardoc/ -/coverage/ -/doc/ /pkg/ -/spec/reports/ -/tmp/ +/gems.locked +/.covered.db +/external -# rspec failure tracking -.rspec_status -.covered.db -.env +/.env diff --git a/.mailmap b/.mailmap new file mode 100644 index 0000000..2fb94e8 --- /dev/null +++ b/.mailmap @@ -0,0 +1,6 @@ +Eric McKay +Denis Sadomowski +莫粒 +Sherman Koa +Fedishin Nazar +Akinori Musha diff --git a/.rspec b/.rspec deleted file mode 100644 index 3db1ba3..0000000 --- a/.rspec +++ /dev/null @@ -1,4 +0,0 @@ ---format documentation ---backtrace ---warnings ---require spec_helper \ No newline at end of file diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..3b8d476 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,53 @@ +AllCops: + DisabledByDefault: true + +Layout/IndentationStyle: + Enabled: true + EnforcedStyle: tabs + +Layout/InitialIndentation: + Enabled: true + +Layout/IndentationWidth: + Enabled: true + Width: 1 + +Layout/IndentationConsistency: + Enabled: true + EnforcedStyle: normal + +Layout/BlockAlignment: + Enabled: true + +Layout/EndAlignment: + Enabled: true + EnforcedStyleAlignWith: start_of_line + +Layout/BeginEndAlignment: + Enabled: true + EnforcedStyleAlignWith: start_of_line + +Layout/ElseAlignment: + Enabled: true + +Layout/DefEndAlignment: + Enabled: true + +Layout/CaseIndentation: + Enabled: true + +Layout/CommentIndentation: + Enabled: true + +Layout/EmptyLinesAroundClassBody: + Enabled: true + +Layout/EmptyLinesAroundModuleBody: + Enabled: true + +Style/FrozenStringLiteralComment: + Enabled: true + +Style/StringLiterals: + Enabled: true + EnforcedStyle: double_quotes diff --git a/cloudflare.gemspec b/cloudflare.gemspec index d52acc3..def98be 100644 --- a/cloudflare.gemspec +++ b/cloudflare.gemspec @@ -1,3 +1,4 @@ +# frozen_string_literal: true require_relative "lib/cloudflare/version" @@ -6,19 +7,21 @@ Gem::Specification.new do |spec| spec.version = Cloudflare::VERSION spec.summary = "A Ruby wrapper for the Cloudflare API." - spec.authors = ["Marcin Prokop", "Samuel Williams"] + spec.authors = ["Samuel Williams", "Marcin Prokop", "Leonhardt Wille", "Rob Widmer", "Akinori Musha", "Sherman Koa", "Michael Kalygin", "Denis Sadomowski", "Eric McKay", "Fedishin Nazar", "Casey Lopez", "David Wegman", "Greg Retkowski", "Guillaume Leseur", "Jason Green", "Kugayama Nana", "Kyle Corbitt", "Mike Perham", "Olle Jonsson", "Terry Kerr", "莫粒"] spec.license = "MIT" + spec.cert_chain = ["release.cert"] + spec.signing_key = File.expand_path("~/.gem/release.pem") + spec.homepage = "https://github.com/socketry/cloudflare" - spec.files = Dir.glob('{lib}/**/*', File::FNM_DOTMATCH, base: __dir__) + spec.metadata = { + "source_code_uri" => "https://github.com/socketry/cloudflare.git", + } - spec.required_ruby_version = ">= 2.5" + spec.files = Dir.glob(["{lib}/**/*", "*.md"], File::FNM_DOTMATCH, base: __dir__) - spec.add_dependency "async-rest", "~> 0.12.3" + spec.required_ruby_version = ">= 3.1" - spec.add_development_dependency "async-rspec" - spec.add_development_dependency "bundler" - spec.add_development_dependency "covered" - spec.add_development_dependency "rspec", "~> 3.6" + spec.add_dependency "async-rest", "~> 0.18" end diff --git a/config/sus.rb b/config/sus.rb new file mode 100644 index 0000000..f99b9c2 --- /dev/null +++ b/config/sus.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2024, by Samuel Williams. + +require "covered/sus" +include Covered::Sus diff --git a/fixtures/cloudflare/a_connection.rb b/fixtures/cloudflare/a_connection.rb new file mode 100644 index 0000000..833bd91 --- /dev/null +++ b/fixtures/cloudflare/a_connection.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2024, by Samuel Williams. + +require "cloudflare" +require "sus/fixtures/async/reactor_context" +require "async/http/proxy" + +module Cloudflare + AUTH_EMAIL = ENV["CLOUDFLARE_EMAIL"] + AUTH_KEY = ENV["CLOUDFLARE_KEY"] + PROXY_URL = ENV["CLOUDFLARE_PROXY"] + + ACCOUNT_ID = ENV["CLOUDFLARE_ACCOUNT_ID"] + + ZONE_NAMES = %w{alligator ant bear bee bird camel cat cheetah chicken chimpanzee cow crocodile deer dog dolphin duck eagle elephant fish fly fox frog giraffe goat goldfish hamster hippopotamus horse kangaroo kitten lion lobster monkey octopus owl panda pig puppy rabbit rat scorpion seal shark sheep snail snake spider squirrel tiger turtle wolf zebra} + + JOB_ID = ENV.fetch("INVOCATION_ID", "testing").hash + + ZONE_NAME = ENV["CLOUDFLARE_ZONE_NAME"] || "#{ZONE_NAMES[JOB_ID % ZONE_NAMES.size]}.com" + + if AUTH_EMAIL.nil? || AUTH_EMAIL.empty? || AUTH_KEY.nil? || AUTH_KEY.empty? + $stderr.puts <<~EOF + Please make sure you have defined CLOUDFLARE_EMAIL and CLOUDFLARE_KEY in your environment. You can also specify CLOUDFLARE_ZONE_NAME to test with your own zone and CLOUDFLARE_ACCOUNT_ID to use a specific account + EOF + end + + AConnection = Sus::Shared("a connection") do + include Sus::Fixtures::Async::ReactorContext + + let(:connection) do + if proxy_url = PROXY_URL + proxy_endpoint = Async::HTTP::Endpoint.parse(proxy_url) + @client = Async::HTTP::Client.new(proxy_endpoint) + @connection = Cloudflare.connect(@client.proxied_endpoint(Connection::ENDPOINT), email: AUTH_EMAIL, key: AUTH_KEY) + else + @client = nil + @connection = Cloudflare.connect(email: AUTH_EMAIL, key: AUTH_KEY) + end + end + + let(:account) do + if ACCOUNT_ID + connection.accounts.find_by_id(ACCOUNT_ID) + else + connection.accounts.first + end + end + + let(:job_id) {JOB_ID} + let(:zone_names) {ZONE_NAMES} + let(:zone_name) {ZONE_NAME} + + let(:zones) {connection.zones} + let(:zone) {zones.find_by_name(zone_name) || zones.create(zone_name, account)} + + after do + @connection&.close + @client&.close + end + end +end diff --git a/gems.rb b/gems.rb index 68877f6..7859daf 100644 --- a/gems.rb +++ b/gems.rb @@ -1,22 +1,33 @@ # frozen_string_literal: true -source 'https://rubygems.org' +# Released under the MIT License. +# Copyright, 2014-2024, by Samuel Williams. +# Copyright, 2014, by Marcin Prokop. +# Copyright, 2018, by Leonhardt Wille. + +source "https://rubygems.org" # Specify your gem's dependencies in cloudflare.gemspec gemspec -gem 'async-http', '~> 0.48', '>= 0.48.2' - group :maintenance, optional: true do - gem "bake-bundler" + gem "bake-gem" gem "bake-modernize" gem "utopia-project" end group :test do - gem 'coveralls', require: false - gem 'simplecov' - gem 'sinatra' - gem 'webmock' + gem "sus" + gem "covered" + gem "decode" + gem "rubocop" + + gem "sus-fixtures-async" + + gem "sinatra" + gem "webmock" + + gem "bake-test" + gem "bake-test-external" end diff --git a/lib/cloudflare.rb b/lib/cloudflare.rb index 8b44929..e9e9788 100644 --- a/lib/cloudflare.rb +++ b/lib/cloudflare.rb @@ -1,47 +1,31 @@ # frozen_string_literal: true -# Copyright, 2012, by Marcin Prokop. -# Copyright, 2017, by Samuel G. D. Williams. -# -# 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. +# Released under the MIT License. +# Copyright, 2012-2016, by Marcin Prokop. +# Copyright, 2013, by Eric McKay. +# Copyright, 2014, by Jason Green. +# Copyright, 2014-2024, by Samuel Williams. +# Copyright, 2014, by Greg Retkowski. +# Copyright, 2018, by Leonhardt Wille. +# Copyright, 2019, by Akinori Musha. -require 'async' -require_relative 'cloudflare/connection' +require "async" +require_relative "cloudflare/connection" module Cloudflare - DEFAULT_ENDPOINT = Async::HTTP::Endpoint.parse('https://api.cloudflare.com/client/v4') - - def self.connect(endpoint = DEFAULT_ENDPOINT, **auth_info) - representation = Connection.for(endpoint) + def self.connect(*arguments, **auth_info) + connection = Connection.open(*arguments) if !auth_info.empty? - representation = representation.authenticated(**auth_info) + connection = connection.authenticated(**auth_info) end - return representation unless block_given? + return connection unless block_given? - Async do - begin - yield representation - ensure - representation.close - end + Sync do + yield connection + ensure + connection.close end end end diff --git a/lib/cloudflare/accounts.rb b/lib/cloudflare/accounts.rb index b3f23a6..7151d00 100644 --- a/lib/cloudflare/accounts.rb +++ b/lib/cloudflare/accounts.rb @@ -1,39 +1,21 @@ # frozen_string_literal: true -# Copyright, 2012, by Marcin Prokop. -# Copyright, 2017, by Samuel G. D. Williams. -# Copyright, 2017, by David Rosenbloom. -# -# 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. +# Released under the MIT License. +# Copyright, 2018-2024, by Samuel Williams. +# Copyright, 2019, by Rob Widmer. -require_relative 'representation' -require_relative 'paginate' -require_relative 'kv/namespaces' +require_relative "representation" +require_relative "paginate" +require_relative "kv/namespaces" module Cloudflare class Account < Representation def id - value[:id] + result[:id] end def kv_namespaces - self.with(KV::Namespaces, path: 'storage/kv/namespaces') + self.with(KV::Namespaces, path: "storage/kv/namespaces") end end diff --git a/lib/cloudflare/connection.rb b/lib/cloudflare/connection.rb index 86e9ad2..20eb721 100644 --- a/lib/cloudflare/connection.rb +++ b/lib/cloudflare/connection.rb @@ -1,44 +1,36 @@ # frozen_string_literal: true -# Copyright, 2018, by Samuel G. D. Williams. -# -# 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. +# Released under the MIT License. +# Copyright, 2014-2016, by Marcin Prokop. +# Copyright, 2014-2024, by Samuel Williams. +# Copyright, 2015, by Kyle Corbitt. +# Copyright, 2015, by Guillaume Leseur. +# Copyright, 2018, by Leonhardt Wille. +# Copyright, 2018, by Michael Kalygin. +# Copyright, 2018, by Sherman Koa. +# Copyright, 2019, by Akinori Musha. -require_relative 'representation' +require "async/rest/resource" -require_relative 'zones' -require_relative 'accounts' -require_relative 'user' +require_relative "zones" +require_relative "accounts" +require_relative "user" module Cloudflare - class Connection < Representation + class Connection < Async::REST::Resource + ENDPOINT = Async::HTTP::Endpoint.parse("https://api.cloudflare.com/client/v4/") + def authenticated(token: nil, key: nil, email: nil) headers = {} if token - headers['Authorization'] = "Bearer #{token}" + headers["authorization"] = "bearer #{token}" elsif key if email - headers['X-Auth-Key'] = key - headers['X-Auth-Email'] = email + headers["x-auth-key"] = key + headers["x-auth-email"] = email else - headers['X-Auth-User-Service-Key'] = key + headers["x-auth-user-service-key"] = key end end @@ -46,15 +38,15 @@ def authenticated(token: nil, key: nil, email: nil) end def zones - self.with(Zones, path: 'zones') + Zones.new(self.with(path: "zones/")) end def accounts - self.with(Accounts, path: 'accounts') + Accounts.new(self.with(path: "accounts")) end def user - self.with(User, path: 'user') + User.new(self.with(path: "user")) end end end diff --git a/lib/cloudflare/custom_hostname/ssl_attribute.rb b/lib/cloudflare/custom_hostname/ssl_attribute.rb index 87ff188..573c5c8 100644 --- a/lib/cloudflare/custom_hostname/ssl_attribute.rb +++ b/lib/cloudflare/custom_hostname/ssl_attribute.rb @@ -1,6 +1,11 @@ # frozen_string_literal: true -require_relative './ssl_attribute/settings' +# Released under the MIT License. +# Copyright, 2019, by Rob Widmer. +# Copyright, 2019-2024, by Samuel Williams. + +require_relative "ssl_attribute/settings" +require_relative "../representation" module Cloudflare class CustomHostname < Representation @@ -10,7 +15,7 @@ def initialize(params) end def active? - status == 'active' + status == "active" end def cname @@ -34,7 +39,7 @@ def method end def pending_validation? - status == 'pending_validation' + status == "pending_validation" end # Wraps the settings hash if it exists or initializes the settings hash and then wraps it diff --git a/lib/cloudflare/custom_hostname/ssl_attribute/settings.rb b/lib/cloudflare/custom_hostname/ssl_attribute/settings.rb index 53824a8..c0b5ce4 100644 --- a/lib/cloudflare/custom_hostname/ssl_attribute/settings.rb +++ b/lib/cloudflare/custom_hostname/ssl_attribute/settings.rb @@ -1,10 +1,16 @@ # frozen_string_literal: true +# Released under the MIT License. +# Copyright, 2019, by Rob Widmer. +# Copyright, 2019-2024, by Samuel Williams. + +require_relative "../../representation" + module Cloudflare class CustomHostname < Representation class SSLAttribute class Settings - def initialize(settings) + def initialize(settings = {}) @settings = settings end @@ -25,7 +31,7 @@ def http2 # Always coerce into a boolean, if the key is not # provided, this value may not be accurate def http2? - http2 == 'on' + http2 == "on" end def http2=(value) @@ -49,7 +55,7 @@ def tls_1_3 # Always coerce into a boolean, if the key is not # provided, this value may not be accurate def tls_1_3? - tls_1_3 == 'on' + tls_1_3 == "on" end def tls_1_3=(value) @@ -62,7 +68,7 @@ def process_boolean(key, value) if value.nil? @settings.delete(key) else - @settings[key] = !value || value == 'off' ? 'off' : 'on' + @settings[key] = !value || value == "off" ? "off" : "on" end end end diff --git a/lib/cloudflare/custom_hostnames.rb b/lib/cloudflare/custom_hostnames.rb index 3ad550b..3e94b9b 100644 --- a/lib/cloudflare/custom_hostnames.rb +++ b/lib/cloudflare/custom_hostnames.rb @@ -1,78 +1,101 @@ # frozen_string_literal: true -# This implements the Custom Hostname API -# https://api.cloudflare.com/#custom-hostname-for-a-zone-properties +# Released under the MIT License. +# Copyright, 2019, by Rob Widmer. +# Copyright, 2019-2024, by Samuel Williams. -require_relative 'custom_hostname/ssl_attribute' -require_relative 'paginate' -require_relative 'representation' +require_relative "custom_hostname/ssl_attribute" +require_relative "paginate" +require_relative "representation" module Cloudflare class CustomHostname < Representation + include Async::REST::Representation::Mutable + # Only available if enabled for your zone def custom_origin - value[:custom_origin_server] + result[:custom_origin_server] end - + # Only available if enabled for your zone def custom_metadata - value[:custom_metadata] + result[:custom_metadata] end - + def hostname - value[:hostname] + result[:hostname] end - + def id - value[:id] + result[:id] end - + def ssl - @ssl ||= SSLAttribute.new(value[:ssl]) + @ssl ||= SSLAttribute.new(result[:ssl]) end - + # Check if the cert has been validated # passing true will send a request to Cloudflare to try to validate the cert def ssl_active?(force_update = false) - send_patch(ssl: { method: ssl.method, type: ssl.type }) if force_update && ssl.pending_validation? - ssl.active? + if force_update && ssl.pending_validation? + self.patch(ssl: {method: ssl.method, type: ssl.type}) + end + + return ssl.active? end - + def update_settings(metadata: nil, origin: nil, ssl: nil) - attrs = {} - attrs[:custom_metadata] = metadata if metadata - attrs[:custom_origin_server] = origin if origin - attrs[:ssl] = ssl if ssl - - send_patch(attrs) + payload = {} + + payload[:custom_metadata] = metadata if metadata + payload[:custom_origin_server] = origin if origin + payload[:ssl] = ssl if ssl + + self.patch(payload) end - + alias :to_s :hostname - + private - - def send_patch(data) - response = patch(data) - - @ssl = nil # Kill off our cached version of the ssl object so it will be regenerated from the response - @value = response.result + + def patch(payload) + self.class.patch(@resource, payload) do |resource, response| + value = response.read + + if value[:sucess] + @ssl = nil + @value = value + else + raise RequestError.new(@resource, value) + end + end end end class CustomHostnames < Representation include Paginate - + def representation CustomHostname end - - # initializes a custom hostname object and yields it for customization before saving - def create(hostname, metadata: nil, origin: nil, ssl: {}, &block) - attrs = { hostname: hostname, ssl: { method: 'http', type: 'dv' }.merge(ssl) } - attrs[:custom_metadata] = metadata if metadata - attrs[:custom_origin_server] = origin if origin - - represent_message(self.post(attrs)) + + def create(hostname, metadata: nil, origin: nil, ssl: {}, **options) + payload = {hostname: hostname, ssl: {method: "http", type: "dv"}.merge(ssl), **options} + + payload[:custom_metadata] = metadata if metadata + payload[:custom_origin_server] = origin if origin + + CustomHostname.post(@resource, payload) do |resource, response| + value = response.read + result = value[:result] + metadata = response.headers + + if id = result[:id] + resource = resource.with(path: id) + end + + CustomHostname.new(resource, value: value, metadata: metadata) + end end def find_by_hostname(hostname) diff --git a/lib/cloudflare/dns.rb b/lib/cloudflare/dns.rb index 0c4567c..f5e24cb 100644 --- a/lib/cloudflare/dns.rb +++ b/lib/cloudflare/dns.rb @@ -1,86 +1,84 @@ # frozen_string_literal: true -# Copyright, 2012, by Marcin Prokop. -# Copyright, 2017, by Samuel G. D. Williams. -# Copyright, 2017, by David Rosenbloom. -# -# 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. +# Released under the MIT License. +# Copyright, 2019-2024, by Samuel Williams. +# Copyright, 2019, by Rob Widmer. +# Copyright, 2019, by David Wegman. -require_relative 'representation' -require_relative 'paginate' +require_relative "representation" +require_relative "paginate" module Cloudflare module DNS class Record < Representation - def initialize(url, record = nil, **options) - super(url, **options) - - @record = record || get.result - end - + include Async::REST::Representation::Mutable + def update_content(content, **options) - response = put( - type: @record[:type], - name: @record[:name], + self.class.put(@resource, { + type: self.type, + name: self.name, content: content, **options - ) - - @value = response.result + }) do |resource, response| + if response.success? + @value = response.read + @metadata = response.headers + else + raise RequestError.new(resource, response.read) + end + + self + end end - + def type - value[:type] + result[:type] end - + def name - value[:name] + result[:name] end - + def content - value[:content] + result[:content] end - - def proxied - value[:proxied] + + def proxied? + result[:proxied] end - + + alias proxied proxied? + def to_s - "#{@record[:name]} #{@record[:type]} #{@record[:content]}" + "#{self.name} #{self.type} #{self.content}" end end - + class Records < Representation include Paginate - + def representation Record end - - TTL_AUTO = 1 def create(type, name, content, **options) - represent_message(self.post(type: type, name: name, content: content, **options)) + payload = {type: type, name: name, content: content, **options} + + Record.post(@resource, payload) do |resource, response| + value = response.read + result = value[:result] + metadata = response.headers + + if id = result[:id] + resource = resource.with(path: id) + end + + Record.new(resource, value: value, metadata: metadata) + end end def find_by_name(name) - each(name: name).first + each(name: name).find{|record| record.name == name} end end end diff --git a/lib/cloudflare/firewall.rb b/lib/cloudflare/firewall.rb index 40ba423..3858dbf 100644 --- a/lib/cloudflare/firewall.rb +++ b/lib/cloudflare/firewall.rb @@ -1,43 +1,29 @@ # frozen_string_literal: true -# Copyright, 2018, by Samuel G. D. Williams. -# -# 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. +# Released under the MIT License. +# Copyright, 2019-2024, by Samuel Williams. +# Copyright, 2019, by Rob Widmer. -require_relative 'representation' -require_relative 'paginate' +require_relative "representation" +require_relative "paginate" module Cloudflare module Firewall class Rule < Representation + include Async::REST::Representation::Mutable + def mode - value[:mode] + result[:mode] end - + def notes - value[:notes] + result[:notes] end - + def configuration - value[:configuration] + result[:configuration] end - + def to_s "#{configuration[:value]} - #{mode} - #{notes}" end @@ -45,26 +31,29 @@ def to_s class Rules < Representation include Paginate - + def representation Rule end - - def set(mode, value, notes: nil, target: 'ip') + + def set(mode, value, notes: nil, target: "ip") notes ||= "cloudflare gem [#{mode}] #{Time.now.strftime('%m/%d/%y')}" - - message = self.post({ - mode: mode.to_s, - notes: notes, - configuration: { - target: target, - value: value.to_s, - } - }) - - represent_message(message) + + payload = {mode: mode.to_s, notes: notes, configuration: {target: target, value: value.to_s}} + + Rule.post(@resource, payload) do |resource, response| + value = response.read + result = value[:result] + metadata = response.headers + + if id = result[:id] + resource = resource.with(path: id) + end + + Rule.new(resource, value: value, metadata: metadata) + end end - + def each_by_value(value, &block) each(configuration_value: value, &block) end diff --git a/lib/cloudflare/kv/namespaces.rb b/lib/cloudflare/kv/namespaces.rb index 059e737..a62f936 100644 --- a/lib/cloudflare/kv/namespaces.rb +++ b/lib/cloudflare/kv/namespaces.rb @@ -1,77 +1,111 @@ # frozen_string_literal: true -# This implements the Worker KV Store API -# https://api.cloudflare.com/#workers-kv-namespace-properties +# Released under the MIT License. +# Copyright, 2019, by Rob Widmer. +# Copyright, 2019-2024, by Samuel Williams. +# Copyright, 2021, by Terry Kerr. -require_relative '../paginate' -require_relative '../representation' -require_relative 'rest_wrapper' +require_relative "../paginate" +require_relative "../representation" +require_relative "wrapper" module Cloudflare module KV class Key < Representation def name - value[:name] + result[:name] end end - + + class Value < Representation[Wrapper] + include Async::REST::Representation::Mutable + + def put(value) + self.class.put(@resource, value) do |resource, response| + value = response.read + + return value[:success] + end + end + end + class Keys < Representation include Paginate - + def representation Key end end - + class Namespace < Representation + include Async::REST::Representation::Mutable + def delete_value(name) value_representation(name).delete.success? end - + def id - value[:id] + result[:id] end - + def keys - self.with(Keys, path: 'keys') + self.with(Keys, path: "keys") end - + def read_value(name) value_representation(name).value end - + def rename(new_title) - put(title: new_title) - value[:title] = new_title + self.class.put(@resource, title: new_title) do |resource, response| + value = response.read + + if value[:success] + result[:title] = new_title + else + raise RequestError.new(resource, value) + end + end end - + def title - value[:title] + result[:title] end - + def write_value(name, value) - value_representation(name).put(value).success? + value_representation(name).put(value) end - + private - + def value_representation(name) - @representation_class ||= Representation[RESTWrapper] - self.with(@representation_class, path: "values/#{name}") + self.with(Value, path: "values/#{name}/") end end - + class Namespaces < Representation include Paginate - + def representation Namespace end - - def create(title) - represent_message(post(title: title)) + + def create(title, **options) + payload = {title: title, **options} + + Namespace.post(@resource, payload) do |resource, response| + value = response.read + result = value[:result] + metadata = response.headers + + if id = result[:id] + resource = resource.with(path: id) + end + + Namespace.new(resource, value: value, metadata: metadata) + end end - + def find_by_title(title) each.find {|namespace| namespace.title == title } end diff --git a/lib/cloudflare/kv/rest_wrapper.rb b/lib/cloudflare/kv/rest_wrapper.rb deleted file mode 100644 index bccc44e..0000000 --- a/lib/cloudflare/kv/rest_wrapper.rb +++ /dev/null @@ -1,44 +0,0 @@ -# frozen_string_literal: true - -require 'json' - -module Cloudflare - module KV - class RESTWrapper < Async::REST::Wrapper::Generic - APPLICATION_OCTET_STREAM = 'application/octet-stream' - APPLICATION_JSON = 'application/json' - ACCEPT_HEADER = "#{APPLICATION_JSON}, #{APPLICATION_OCTET_STREAM}" - - def prepare_request(payload, headers) - headers['accept'] ||= ACCEPT_HEADER - - if payload - headers['content-type'] = APPLICATION_OCTET_STREAM - ::Protocol::HTTP::Body::Buffered.new([payload.to_s]) - end - end - - def parser_for(response) - if response.headers['content-type'].start_with?(APPLICATION_OCTET_STREAM) - OctetParser - elsif response.headers['content-type'].start_with?(APPLICATION_JSON) - JsonParser - else - Async::REST::Wrapper::Generic::Unsupported - end - end - - class OctetParser < ::Protocol::HTTP::Body::Wrapper - def join - super - end - end - - class JsonParser < ::Protocol::HTTP::Body::Wrapper - def join - JSON.parse(super, symbolize_names: true) - end - end - end - end -end diff --git a/lib/cloudflare/kv/wrapper.rb b/lib/cloudflare/kv/wrapper.rb new file mode 100644 index 0000000..ffa4572 --- /dev/null +++ b/lib/cloudflare/kv/wrapper.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2021, by Terry Kerr. +# Copyright, 2024, by Samuel Williams. + +require "json" + +module Cloudflare + module KV + class Wrapper < Cloudflare::Wrapper + APPLICATION_OCTET_STREAM = "application/octet-stream" + def prepare_request(request, payload) + request.headers.add("accept", APPLICATION_OCTET_STREAM) + + if payload + request.headers["content-type"] = APPLICATION_OCTET_STREAM + + request.body = ::Protocol::HTTP::Body::Buffered.new([payload.to_s]) + end + end + + def parser_for(response) + if response.headers["content-type"].start_with?(APPLICATION_OCTET_STREAM) + OctetParser + else + super + end + end + + class OctetParser < ::Protocol::HTTP::Body::Wrapper + def join + super.force_encoding(Encoding::BINARY) + end + end + end + end +end diff --git a/lib/cloudflare/logs.rb b/lib/cloudflare/logs.rb index 2ec4161..e20be9d 100644 --- a/lib/cloudflare/logs.rb +++ b/lib/cloudflare/logs.rb @@ -1,31 +1,16 @@ -# Copyright, 2019, by Samuel G. D. Williams. -# -# 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. +# frozen_string_literal: true -require_relative 'representation' -require_relative 'paginate' +# Released under the MIT License. +# Copyright, 2019-2024, by Samuel Williams. + +require_relative "representation" +require_relative "paginate" module Cloudflare module Logs class Entry < Representation def to_s - "#{value[:rayid]}-#{value[:ClientRequestURI]}" + "#{result[:rayid]}-#{result[:ClientRequestURI]}" end end @@ -38,4 +23,3 @@ def representation end end end - diff --git a/lib/cloudflare/paginate.rb b/lib/cloudflare/paginate.rb index e2d087f..25af9a0 100644 --- a/lib/cloudflare/paginate.rb +++ b/lib/cloudflare/paginate.rb @@ -1,52 +1,40 @@ -# Copyright, 2018, by Samuel G. D. Williams. -# -# 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. +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2018-2024, by Samuel Williams. +# Copyright, 2019, by Rob Widmer. module Cloudflare module Paginate include Enumerable - + def each(page: 1, per_page: 50, **parameters) return to_enum(:each, page: page, per_page: per_page, **parameters) unless block_given? - + while true - zones = @resource.get(self.class, page: page, per_page: per_page, **parameters) - - break if zones.empty? - - Array(zones.value).each do |attributes| - yield represent(zones.metadata, attributes) + resource = @resource.with(parameters: {page: page, per_page: per_page, **parameters}) + + response = self.class.get(resource) + + break if response.empty? + + response.results.each do |attributes| + yield represent(response.metadata, attributes) end - + page += 1 - + # Was this the last page? - break if zones.value.size < per_page + break if response.results.size < per_page end end - + def empty? self.value.empty? end - + def find_by_id(id) - representation.new(@resource.with(path: id)) + representation.new(@resource.with(path: "#{id}/")) end end end diff --git a/lib/cloudflare/representation.rb b/lib/cloudflare/representation.rb index c64f317..30c1a4f 100644 --- a/lib/cloudflare/representation.rb +++ b/lib/cloudflare/representation.rb @@ -1,111 +1,87 @@ # frozen_string_literal: true -# Copyright, 2012, by Marcin Prokop. -# Copyright, 2017, by Samuel G. D. Williams. -# -# 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. +# Released under the MIT License. +# Copyright, 2017-2024, by Samuel Williams. +# Copyright, 2018, by Leonhardt Wille. +# Copyright, 2019, by Rob Widmer. -require 'json' +require "json" -require 'async/rest/representation' +require "async/rest/representation" +require "async/rest/wrapper/json" module Cloudflare class RequestError < StandardError - def initialize(resource, errors) - super("#{resource}: #{errors.map{|attributes| attributes[:message]}.join(', ')}") - - @representation = representation + def initialize(request, value) + if error = value[:error] + super("#{request}: #{error}") + elsif errors = value[:errors] + super("#{request}: #{errors.map{|attributes| attributes[:message]}.join(', ')}") + else + super("#{request}: #{value.inspect}") + end + + @value = value end - - attr_reader :representation + + attr :value end - - class Message - def initialize(response) - @response = response - @body = response.read - - # Some endpoints return the value instead of a message object (like KV reads) - @body = { success: true, result: @body } unless @body.is_a?(Hash) - end - - attr :response - attr :body - - def headers - @response.headers - end - - def result - @body[:result] - end - - def read - @body[:result] - end - - def results - Array(result) - end - - def errors - @body[:errors] - end - - def messages - @body[:messages] - end - - def success? - @body[:success] + + class Wrapper < Async::REST::Wrapper::JSON + def process_response(request, response) + super + + if response.failure? + raise RequestError.new(request, response.read) + end end end - + class Representation < Async::REST::Representation - def process_response(*) - message = Message.new(super) - - unless message.success? - raise RequestError.new(@resource, message.errors) - end - - return message - end - + WRAPPER = Wrapper.new + def representation Representation end - + def represent(metadata, attributes) resource = @resource.with(path: attributes[:id]) - - representation.new(resource, metadata: metadata, value: attributes) + + representation.new(resource, metadata: metadata, value: { + success: true, result: attributes + }) end - + def represent_message(message) represent(message.headers, message.result) end - + + def result + value[:result] + end + def to_hash - if value.is_a?(Hash) - return value - end + result + end + + def to_id + {id: result[:id]} + end + + def results + Array(result) + end + + def errors + value[:errors] + end + + def messages + value[:messages] + end + + def success? + value[:success] end end end diff --git a/lib/cloudflare/rspec/connection.rb b/lib/cloudflare/rspec/connection.rb deleted file mode 100644 index 506b767..0000000 --- a/lib/cloudflare/rspec/connection.rb +++ /dev/null @@ -1,57 +0,0 @@ -# frozen_string_literal: true - -# Copyright, 2017, by Samuel G. D. Williams. -# -# 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. - -require 'async/rspec' -require 'async/http/proxy' - -require_relative '../../cloudflare' - -module Cloudflare - module RSpec - module Connection - end - - RSpec.shared_context Connection do - include_context Async::RSpec::Reactor - - # You must specify these in order for the tests to run. - let(:email) {ENV['CLOUDFLARE_EMAIL']} - let(:key) {ENV['CLOUDFLARE_KEY']} - - let(:connection) do - if proxy_url = ENV['CLOUDFLARE_PROXY'] - proxy_endpoint = Async::HTTP::Endpoint.parse(proxy_url) - @client = Async::HTTP::Client.new(proxy_endpoint) - @connection = Cloudflare.connect(@client.proxied_endpoint(DEFAULT_ENDPOINT), key: key, email: email) - else - @client = nil - @connection = Cloudflare.connect(key: key, email: email) - end - end - - after do - @connection&.close - @client&.close - end - end - end -end diff --git a/lib/cloudflare/user.rb b/lib/cloudflare/user.rb index 82d0543..a02ea1e 100644 --- a/lib/cloudflare/user.rb +++ b/lib/cloudflare/user.rb @@ -1,36 +1,19 @@ # frozen_string_literal: true -# Copyright, 2012, by Marcin Prokop. -# Copyright, 2017, by Samuel G. D. Williams. -# -# 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. +# Released under the MIT License. +# Copyright, 2017-2024, by Samuel Williams. +# Copyright, 2018, by Leonhardt Wille. -require_relative 'representation' +require_relative "representation" module Cloudflare class User < Representation def id - value[:id] + result[:id] end def email - value[:email] + result[:email] end end end diff --git a/lib/cloudflare/version.rb b/lib/cloudflare/version.rb index 59c0a6e..35fb796 100644 --- a/lib/cloudflare/version.rb +++ b/lib/cloudflare/version.rb @@ -1,26 +1,12 @@ # frozen_string_literal: true -# Copyright, 2012, by Marcin Prokop. -# Copyright, 2017, by Samuel G. D. Williams. -# -# 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. +# Released under the MIT License. +# Copyright, 2014-2016, by Marcin Prokop. +# Copyright, 2014-2024, by Samuel Williams. +# Copyright, 2015, by Kyle Corbitt. +# Copyright, 2018, by Leonhardt Wille. +# Copyright, 2018, by Casey Lopez. module Cloudflare - VERSION = '4.3.0' + VERSION = "4.3.0" end diff --git a/lib/cloudflare/zones.rb b/lib/cloudflare/zones.rb index 69a4e5d..419502f 100644 --- a/lib/cloudflare/zones.rb +++ b/lib/cloudflare/zones.rb @@ -1,65 +1,59 @@ # frozen_string_literal: true -# Copyright, 2012, by Marcin Prokop. -# Copyright, 2017, by Samuel G. D. Williams. -# Copyright, 2017, by David Rosenbloom. -# -# 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. +# Released under the MIT License. +# Copyright, 2017-2024, by Samuel Williams. +# Copyright, 2017, by Denis Sadomowski. +# Copyright, 2017, by 莫粒. +# Copyright, 2018, by Leonhardt Wille. +# Copyright, 2018, by Michael Kalygin. +# Copyright, 2018, by Sherman Koa. +# Copyright, 2018, by Kugayama Nana. +# Copyright, 2018, by Casey Lopez. +# Copyright, 2019, by Akinori Musha. +# Copyright, 2019, by Rob Widmer. -require_relative 'representation' -require_relative 'paginate' +require_relative "representation" +require_relative "paginate" -require_relative 'custom_hostnames' -require_relative 'firewall' -require_relative 'dns' -require_relative 'logs' +require_relative "custom_hostnames" +require_relative "firewall" +require_relative "dns" +require_relative "logs" module Cloudflare class Zone < Representation + include Async::REST::Representation::Mutable + def custom_hostnames - self.with(CustomHostnames, path: 'custom_hostnames') + self.with(CustomHostnames, path: "custom_hostnames") end def dns_records - self.with(DNS::Records, path: 'dns_records') + self.with(DNS::Records, path: "dns_records") end def firewall_rules - self.with(Firewall::Rules, path: 'firewall/access_rules/rules') + self.with(Firewall::Rules, path: "firewall/access_rules/rules") end def logs - self.with(Logs::Received, path: 'logs/received') + self.with(Logs::Received, path: "logs/received") end - DEFAULT_PURGE_CACHE_PARAMS = { + DEFAULT_PURGE_CACHE_PARAMETERS = { purge_everything: true }.freeze - def purge_cache(parameters = DEFAULT_PURGE_CACHE_PARAMS) - self.with(Zone, path: 'purge_cache').post(parameters) + def purge_cache(**options) + if options.empty? + options = DEFAULT_PURGE_CACHE_PARAMETERS + end - return self + self.class.post(@resource.with(path: "purge_cache"), options) end def name - value[:name] + result[:name] end alias to_s name @@ -72,12 +66,24 @@ def representation Zone end - def create(name, account, jump_start = false) - represent_message(self.post(name: name, account: account.to_hash, jump_start: jump_start)) + def create(name, account, jump_start: false, **options) + payload = {name: name, account: account.to_id, jump_start: jump_start, **options} + + Zone.post(@resource, payload) do |resource, response| + value = response.read + result = value[:result] + metadata = response.headers + + if id = result[:id] + resource = resource.with(path: id) + end + + Zone.new(resource, value: value, metadata: metadata) + end end - + def find_by_name(name) - each(name: name).first + each(name: name).find{|zone| zone.name == name} end end end diff --git a/license.md b/license.md new file mode 100644 index 0000000..2fe77a4 --- /dev/null +++ b/license.md @@ -0,0 +1,41 @@ +# MIT License + +Copyright, 2012-2017, by Marcin Prokop. +Copyright, 2013, by Eric McKay. +Copyright, 2014, by Jason Green. +Copyright, 2014-2024, by Samuel Williams. +Copyright, 2014, by Greg Retkowski. +Copyright, 2015, by Kyle Corbitt. +Copyright, 2015, by Guillaume Leseur. +Copyright, 2017, by Denis Sadomowski. +Copyright, 2017, by 莫粒. +Copyright, 2018, by Leonhardt Wille. +Copyright, 2018, by Mike Perham. +Copyright, 2018, by Michael Kalygin. +Copyright, 2018, by Sherman Koa. +Copyright, 2018, by Kugayama Nana. +Copyright, 2018, by Casey Lopez. +Copyright, 2019, by Akinori Musha. +Copyright, 2019, by Rob Widmer. +Copyright, 2019, by Fedishin Nazar. +Copyright, 2019, by David Wegman. +Copyright, 2020, by Olle Jonsson. +Copyright, 2021, by Terry Kerr. + +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 similarity index 58% rename from README.md rename to readme.md index 8545b56..9181b8d 100644 --- a/README.md +++ b/readme.md @@ -2,7 +2,7 @@ It is a Ruby wrapper for the Cloudflare V4 API. It provides a light weight wrapper using `RestClient::Resource`. The wrapper functionality is limited to zones and DNS records at this time, *PRs welcome*. -[![Development Status](https://github.com/socketry/cloudflare/workflows/Development/badge.svg)](https://github.com/socketry/cloudflare/actions?workflow=Development) +[![Development Status](https://github.com/socketry/cloudflare/workflows/Test/badge.svg)](https://github.com/socketry/cloudflare/actions?workflow=Test) ## Installation @@ -86,39 +86,23 @@ end ## Contributing -1. Fork it -2. Create your feature branch (`git checkout -b my-new-feature`) -3. Commit your changes (`git commit -am 'Add some feature'`) -4. Push to the branch (`git push origin my-new-feature`) -5. Create new Pull Request +We welcome contributions to this project. -## See Also - - - [Cloudflare::DNS::Update](https://github.com/ioquatix/cloudflare-dns-update) - A dynamic DNS updater based on this gem. - - [Rubyflare](https://github.com/trev/rubyflare) - Another implementation. +1. Fork it. +2. Create your feature branch (`git checkout -b my-new-feature`). +3. Commit your changes (`git commit -am 'Add some feature'`). +4. Push to the branch (`git push origin my-new-feature`). +5. Create new Pull Request. -## License +### Developer Certificate of Origin -Released under the MIT license. +In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed. -Copyright, 2018, by [Samuel G. D. Williams](http://www.codeotaku.com/samuel-williams). -Copyright, 2017, by [David Rosenbloom](http://artifactory.com). -Copyright, 2012, 2014, by [Marcin Prokop](https://github.com/b4k3r). +### Community Guidelines -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: +This project is best served by a collaborative and respectful environment. Treat each other professionally, respect differing viewpoints, and engage constructively. Harassment, discrimination, or harmful behavior is not tolerated. Communicate clearly, listen actively, and support one another. If any issues arise, please inform the project maintainers. -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +## See Also -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. + - [Cloudflare::DNS::Update](https://github.com/ioquatix/cloudflare-dns-update) - A dynamic DNS updater based on this gem. + - [Rubyflare](https://github.com/trev/rubyflare) - Another implementation. diff --git a/release.cert b/release.cert new file mode 100644 index 0000000..d98e595 --- /dev/null +++ b/release.cert @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIE2DCCA0CgAwIBAgIBATANBgkqhkiG9w0BAQsFADBhMRgwFgYDVQQDDA9zYW11 +ZWwud2lsbGlhbXMxHTAbBgoJkiaJk/IsZAEZFg1vcmlvbnRyYW5zZmVyMRIwEAYK +CZImiZPyLGQBGRYCY28xEjAQBgoJkiaJk/IsZAEZFgJuejAeFw0yMjA4MDYwNDUz +MjRaFw0zMjA4MDMwNDUzMjRaMGExGDAWBgNVBAMMD3NhbXVlbC53aWxsaWFtczEd +MBsGCgmSJomT8ixkARkWDW9yaW9udHJhbnNmZXIxEjAQBgoJkiaJk/IsZAEZFgJj +bzESMBAGCgmSJomT8ixkARkWAm56MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIB +igKCAYEAomvSopQXQ24+9DBB6I6jxRI2auu3VVb4nOjmmHq7XWM4u3HL+pni63X2 +9qZdoq9xt7H+RPbwL28LDpDNflYQXoOhoVhQ37Pjn9YDjl8/4/9xa9+NUpl9XDIW +sGkaOY0eqsQm1pEWkHJr3zn/fxoKPZPfaJOglovdxf7dgsHz67Xgd/ka+Wo1YqoE +e5AUKRwUuvaUaumAKgPH+4E4oiLXI4T1Ff5Q7xxv6yXvHuYtlMHhYfgNn8iiW8WN +XibYXPNP7NtieSQqwR/xM6IRSoyXKuS+ZNGDPUUGk8RoiV/xvVN4LrVm9upSc0ss +RZ6qwOQmXCo/lLcDUxJAgG95cPw//sI00tZan75VgsGzSWAOdjQpFM0l4dxvKwHn +tUeT3ZsAgt0JnGqNm2Bkz81kG4A2hSyFZTFA8vZGhp+hz+8Q573tAR89y9YJBdYM +zp0FM4zwMNEUwgfRzv1tEVVUEXmoFCyhzonUUw4nE4CFu/sE3ffhjKcXcY//qiSW +xm4erY3XAgMBAAGjgZowgZcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0O +BBYEFO9t7XWuFf2SKLmuijgqR4sGDlRsMC4GA1UdEQQnMCWBI3NhbXVlbC53aWxs +aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MC4GA1UdEgQnMCWBI3NhbXVlbC53aWxs +aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MA0GCSqGSIb3DQEBCwUAA4IBgQB5sxkE +cBsSYwK6fYpM+hA5B5yZY2+L0Z+27jF1pWGgbhPH8/FjjBLVn+VFok3CDpRqwXCl +xCO40JEkKdznNy2avOMra6PFiQyOE74kCtv7P+Fdc+FhgqI5lMon6tt9rNeXmnW/ +c1NaMRdxy999hmRGzUSFjozcCwxpy/LwabxtdXwXgSay4mQ32EDjqR1TixS1+smp +8C/NCWgpIfzpHGJsjvmH2wAfKtTTqB9CVKLCWEnCHyCaRVuKkrKjqhYCdmMBqCws +JkxfQWC+jBVeG9ZtPhQgZpfhvh+6hMhraUYRQ6XGyvBqEUe+yo6DKIT3MtGE2+CP +eX9i9ZWBydWb8/rvmwmX2kkcBbX0hZS1rcR593hGc61JR6lvkGYQ2MYskBveyaxt +Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8 +voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg= +-----END CERTIFICATE----- diff --git a/spec/cloudflare/accounts_spec.rb b/spec/cloudflare/accounts_spec.rb deleted file mode 100644 index 8f0c209..0000000 --- a/spec/cloudflare/accounts_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe Cloudflare::Accounts, order: :defined, timeout: 30 do - include_context Cloudflare::Account - - before do - account.id # Force a fetch if it hasn't happened yet - end - - it 'can list existing accounts' do - accounts = connection.accounts.to_a - expect(accounts.any? {|a| a.id == account.id }).to be true - end - - it 'can get a specific account' do - expect(connection.accounts.find_by_id(account.id).id).to eq account.id - end - - it 'can generate a representation for the KV namespace endpoint' do - ns = connection.accounts.find_by_id(account.id).kv_namespaces - expect(ns).to be_kind_of(Cloudflare::KV::Namespaces) - expect(ns.resource.reference.path).to end_with("/#{account.id}/storage/kv/namespaces") - end -end diff --git a/spec/cloudflare/custom_hostname/ssl_attribute/settings_spec.rb b/spec/cloudflare/custom_hostname/ssl_attribute/settings_spec.rb deleted file mode 100644 index a96be10..0000000 --- a/spec/cloudflare/custom_hostname/ssl_attribute/settings_spec.rb +++ /dev/null @@ -1,54 +0,0 @@ -RSpec.describe Cloudflare::CustomHostname::SSLAttribute::Settings do - - subject { described_class.new({}) } - - it 'has an accessor for ciphers' do - ciphers = double - expect(subject.ciphers).to be_nil - subject.ciphers = ciphers - expect(subject.ciphers).to be ciphers - end - - it 'has a boolean accessor for http2' do - expect(subject.http2).to be_nil - expect(subject.http2?).to be false - subject.http2 = true - expect(subject.http2).to eq 'on' - expect(subject.http2?).to be true - subject.http2 = false - expect(subject.http2).to eq 'off' - expect(subject.http2?).to be false - subject.http2 = 'on' - expect(subject.http2).to eq 'on' - expect(subject.http2?).to be true - subject.http2 = 'off' - expect(subject.http2).to eq 'off' - expect(subject.http2?).to be false - end - - it 'has an accessor for min_tls_version' do - tls_version = double - expect(subject.min_tls_version).to be_nil - subject.min_tls_version = tls_version - expect(subject.min_tls_version).to be tls_version - end - - it 'has a boolean accessor for tls_1_3' do - expect(subject.tls_1_3).to be_nil - expect(subject.tls_1_3?).to be false - subject.tls_1_3 = true - expect(subject.tls_1_3).to eq 'on' - expect(subject.tls_1_3?).to be true - subject.tls_1_3 = false - expect(subject.tls_1_3).to eq 'off' - expect(subject.tls_1_3?).to be false - subject.tls_1_3 = 'on' - expect(subject.tls_1_3).to eq 'on' - expect(subject.tls_1_3?).to be true - subject.tls_1_3 = 'off' - expect(subject.tls_1_3).to eq 'off' - expect(subject.tls_1_3?).to be false - end - - -end diff --git a/spec/cloudflare/custom_hostname/ssl_attribute_spec.rb b/spec/cloudflare/custom_hostname/ssl_attribute_spec.rb deleted file mode 100644 index dace23f..0000000 --- a/spec/cloudflare/custom_hostname/ssl_attribute_spec.rb +++ /dev/null @@ -1,73 +0,0 @@ -RSpec.describe Cloudflare::CustomHostname::SSLAttribute do - - accessors = [:cname, :cname_target, :http_body, :http_url, :method, :status, :type, :validation_errors] - - let(:original_hash) { {} } - - subject { described_class.new(original_hash) } - - accessors.each do |key| - - it "has an accessor for the #{key} value" do - test_value = double - expect(subject.send(key)).to be_nil - original_hash[key] = test_value - expect(subject.send(key)).to be test_value - end - - end - - it '#active? returns true when the status is "active" and false otherwise' do - expect(subject.active?).to be false - original_hash[:status] = 'initializing' - expect(subject.active?).to be false - original_hash[:status] = 'pending_validation' - expect(subject.active?).to be false - original_hash[:status] = 'pending_deployment' - expect(subject.active?).to be false - original_hash[:status] = 'active' - expect(subject.active?).to be true - end - - it '#pending_validation? returns true when the status is "pending_validation" and false otherwise' do - expect(subject.pending_validation?).to be false - original_hash[:status] = 'initializing' - expect(subject.pending_validation?).to be false - original_hash[:status] = 'active' - expect(subject.pending_validation?).to be false - original_hash[:status] = 'pending_deployment' - expect(subject.pending_validation?).to be false - original_hash[:status] = 'pending_validation' - expect(subject.pending_validation?).to be true - end - - describe '#settings' do - - it 'should return a Settings object' do - expect(subject.settings).to be_kind_of Cloudflare::CustomHostname::SSLAttribute::Settings - end - - it 'initailizes the settings object with the value from the settings key' do - settings = { min_tls_version: double } - original_hash[:settings] = settings - expect(subject.settings.min_tls_version).to be settings[:min_tls_version] - end - - it 'initializes the settings object with a new hash if the settings key does not exist' do - expected_value = double - expect(original_hash[:settings]).to be_nil - expect(subject.settings.min_tls_version).to be_nil - expect(original_hash[:settings]).not_to be_nil - original_hash[:settings][:min_tls_version] = expected_value - expect(subject.settings.min_tls_version).to be expected_value - end - - it 'updates the stored hash with values set on the settings object' do - expected_value = double - expect(subject.settings.min_tls_version).to be_nil - subject.settings.min_tls_version = expected_value - expect(original_hash[:settings][:min_tls_version]).to be expected_value - end - end - -end diff --git a/spec/cloudflare/custom_hostnames_spec.rb b/spec/cloudflare/custom_hostnames_spec.rb deleted file mode 100644 index c8edf44..0000000 --- a/spec/cloudflare/custom_hostnames_spec.rb +++ /dev/null @@ -1,211 +0,0 @@ - -RSpec.xdescribe Cloudflare::CustomHostnames, order: :defined, timeout: 30 do - include_context Cloudflare::Zone - - let(:domain) { "www-#{job_id}.example.com" } - - let(:record) { @record = zone.custom_hostnames.create(domain) } - - let(:custom_origin) do - subdomain = "origin-#{job_id}" - @dns_record = zone.dns_records.create("A", subdomain, "1.2.3.4") # This needs to exist or the calls will fail - "#{subdomain}.#{zone.name}" - end - - after do - if defined? @record - expect(@record.delete).to be_success - end - - if defined? @dns_record - expect(@dns_record.delete).to be_success - end - end - - it 'can create a custom hostname record' do - expect(record).to be_kind_of Cloudflare::CustomHostname - expect(record.custom_metadata).to be_nil - expect(record.hostname).to eq domain - expect(record.custom_origin).to be_nil - expect(record.ssl.method).to eq 'http' - expect(record.ssl.type).to eq 'dv' - end - - it 'can create a custom hostname record with a custom origin' do - begin - @record = zone.custom_hostnames.create(domain, origin: custom_origin) - - expect(@record).to be_kind_of Cloudflare::CustomHostname - expect(@record.custom_metadata).to be_nil - expect(@record.hostname).to eq domain - expect(@record.custom_origin).to eq custom_origin - expect(@record.ssl.method).to eq 'http' - expect(@record.ssl.type).to eq 'dv' - rescue Cloudflare::RequestError => e - if e.message.include?('custom origin server has not been granted') - skip(e.message) # This currently doesn't work but might start eventually: https://github.com/socketry/async-rspec/issues/7 - else - raise - end - end - end - - it 'can create a custom hostname record with different ssl options' do - @record = zone.custom_hostnames.create(domain, ssl: { method: 'cname' }) - - expect(@record).to be_kind_of Cloudflare::CustomHostname - expect(@record.custom_metadata).to be_nil - expect(@record.hostname).to eq domain - expect(@record.custom_origin).to be_nil - expect(@record.ssl.method).to eq 'cname' - expect(@record.ssl.type).to eq 'dv' - end - - it 'can create a custom hostname record with additional metadata' do - metadata = { a: rand(1..10) } - - begin - @record = zone.custom_hostnames.create(domain, metadata: metadata) - - expect(@record).to be_kind_of Cloudflare::CustomHostname - expect(@record.custom_metadata).to eq metadata - expect(@record.hostname).to eq domain - expect(@record.custom_origin).to be_nil - expect(@record.ssl.method).to eq 'http' - expect(@record.ssl.type).to eq 'dv' - rescue Cloudflare::RequestError => e - if e.message.include?('No custom metadata access has been allocated for this zone') - skip(e.message) # This currently doesn't work but might start eventually: https://github.com/socketry/async-rspec/issues/7 - else - raise - end - end - end - - it 'can look up an existing custom hostname by the hostname or id' do - expect(zone.custom_hostnames.find_by_hostname(record.hostname).id).to eq record.id - expect(zone.custom_hostnames.find_by_id(record.id).id).to eq record.id - end - - context 'with existing record' do - - it 'returns the hostname when calling #to_s' do - expect(record.to_s).to eq domain - end - - it 'can update metadata' do - metadata = { c: rand(1..10) } - - expect(record.custom_metadata).to be_nil - - begin - record.update_settings(metadata: metadata) - - # Make sure the existing object is updated - expect(record.custom_metadata).to eq metadata - - # Verify that the server has the changes - found_record = zone.custom_hostnames.find_by_id(record.id) - - expect(found_record.custom_metadata).to eq metadata - expect(found_record.hostname).to eq domain - expect(found_record.custom_origin).to be_nil - rescue Cloudflare::RequestError => e - if e.message.include?('No custom metadata access has been allocated for this zone') - skip(e.message) # This currently doesn't work but might start eventually: https://github.com/socketry/async-rspec/issues/7 - else - raise - end - end - end - - it 'can update the custom origin' do - expect(record.custom_origin).to be_nil - - begin - record.update_settings(origin: custom_origin) - - # Make sure the existing object is updated - expect(record.custom_origin).to eq custom_origin - - # Verify that the server has the changes - found_record = zone.custom_hostnames.find_by_id(record.id) - - expect(found_record.custom_metadata).to be_nil - expect(found_record.hostname).to eq domain - expect(found_record.custom_origin).to eq custom_origin - rescue Cloudflare::RequestError => e - if e.message.include?('custom origin server has not been granted') - skip(e.message) # This currently doesn't work but might start eventually: https://github.com/socketry/async-rspec/issues/7 - else - raise - end - end - end - - it 'can update ssl information' do - expect(record.ssl.method).to eq 'http' - - record.update_settings(ssl: { method: 'cname', type: 'dv' }) - - # Make sure the existing object is updated - expect(record.ssl.method).to eq 'cname' - - # Verify that the server has the changes - found_record = zone.custom_hostnames.find_by_id(record.id) - - expect(found_record.custom_metadata).to be_nil - expect(found_record.hostname).to eq domain - expect(found_record.custom_origin).to be_nil - expect(found_record.ssl.method).to eq 'cname' - end - - context 'has an ssl section' do - - it 'wraps it in an SSLAttributes object' do - expect(record.ssl).to be_kind_of Cloudflare::CustomHostname::SSLAttribute - end - - it 'has some helpers for commonly used keys' do - # Make sure our values exist before we check to make sure that they are returned correctly - expect(record.value[:ssl].values_at(:method, :http_body, :http_url).compact).not_to be_empty - expect(record.ssl.method).to be record.value[:ssl][:method] - expect(record.ssl.http_body).to be record.value[:ssl][:http_body] - expect(record.ssl.http_url).to be record.value[:ssl][:http_url] - end - - end - - describe '#ssl_active?' do - - it 'returns the result of calling ssl.active?' do - expected_value = double - expect(record.ssl).to receive(:active?).and_return(expected_value) - expect(record).not_to receive(:send_patch) - expect(record.ssl_active?).to be expected_value - end - - it 'returns the result of calling ssl.active? without triggering an update if force_update is true and the ssl is not in the pending_validation state' do - expected_value = double - expect(record.ssl).to receive(:active?).and_return(expected_value) - expect(record.ssl.method).not_to be_nil - expect(record.ssl.type).not_to be_nil - expect(record.ssl.pending_validation?).to be false - expect(record).not_to receive(:send_patch).with(ssl: { method: record.ssl.method, type: record.ssl.type }) - expect(record.ssl_active?(true)).to be expected_value - end - - it 'returns the result of calling ssl.active? after triggering an update if force_update is true and the ssl is in the pending_validation state' do - expected_value = double - expect(record.ssl).to receive(:active?).and_return(expected_value) - expect(record.ssl.method).not_to be_nil - expect(record.ssl.type).not_to be_nil - record.value[:ssl][:status] = 'pending_validation' - expect(record).to receive(:send_patch).with(ssl: { method: record.ssl.method, type: record.ssl.type }) - expect(record.ssl_active?(true)).to be expected_value - end - - end - - end -end diff --git a/spec/cloudflare/dns_spec.rb b/spec/cloudflare/dns_spec.rb deleted file mode 100644 index 1c76e62..0000000 --- a/spec/cloudflare/dns_spec.rb +++ /dev/null @@ -1,50 +0,0 @@ - -require 'cloudflare/rspec/connection' - -RSpec.describe Cloudflare::DNS, order: :defined, timeout: 30 do - include_context Cloudflare::Zone - - let(:subdomain) {"www-#{job_id}"} - - after do - if defined? @record - expect(@record.delete).to be_success - end - end - - context "new record" do - it "can create dns record" do - @record = zone.dns_records.create("A", subdomain, "1.2.3.4") - expect(@record.type).to be == "A" - expect(@record.name).to be_start_with subdomain - expect(@record.content).to be == "1.2.3.4" - end - - it "can create dns record with proxied option" do - @record = zone.dns_records.create("A", subdomain, "1.2.3.4", proxied: true) - expect(@record.type).to be == "A" - expect(@record.name).to be_start_with subdomain - expect(@record.content).to be == "1.2.3.4" - expect(@record.proxied).to be_truthy - end - end - - context "with existing record" do - let(:record) {@record = zone.dns_records.create("A", subdomain, "1.2.3.4")} - it "can update dns content" do - record.update_content("4.3.2.1") - expect(record.content).to be == "4.3.2.1" - - fetched_record = zone.dns_records.find_by_name(record.name) - expect(fetched_record.content).to be == record.content - end - - it "can update dns content with proxied option" do - record.update_content("4.3.2.1", proxied: true) - expect(record.proxied).to be_truthy - - fetched_record = zone.dns_records.find_by_name(record.name) - expect(fetched_record.proxied).to be_truthy - end - end -end diff --git a/spec/cloudflare/firewall_spec.rb b/spec/cloudflare/firewall_spec.rb deleted file mode 100644 index 06f0f6e..0000000 --- a/spec/cloudflare/firewall_spec.rb +++ /dev/null @@ -1,49 +0,0 @@ - -require 'cloudflare/rspec/connection' - -RSpec.describe Cloudflare::Firewall, order: :defined, timeout: 30 do - include_context Cloudflare::Zone - - let(:notes) {'gemtest'} - - context "with several rules" do - let(:allow_ip) {'123.123.123.123'} - let(:block_ip) {'123.123.123.124'} - - before do - zone.firewall_rules.each do |rule| - rule.delete - end - - zone.firewall_rules.set('whitelist', allow_ip) - zone.firewall_rules.set('block', block_ip) - end - - it 'should get all rules' do - rules = zone.firewall_rules.to_a - - expect(rules.size).to be >= 2 - end - - it 'should get rules with specific value' do - rules = zone.firewall_rules.each_by_value(allow_ip).to_a - - expect(rules.size).to be == 1 - end - end - - %w[block challenge whitelist].each_with_index do |mode, index| - it "should create a #{mode} rule" do - value = "1.2.3.#{index}" - rule = zone.firewall_rules.set(mode, value, notes: notes) - - expect(rule.mode).to be == mode - expect(rule.configuration[:value]).to be == value - expect(rule.configuration[:target]).to be == 'ip' - expect(rule.notes).to be == notes - - ensure - rule&.delete - end - end -end diff --git a/spec/cloudflare/kv/namespaces_spec.rb b/spec/cloudflare/kv/namespaces_spec.rb deleted file mode 100644 index 5fa2551..0000000 --- a/spec/cloudflare/kv/namespaces_spec.rb +++ /dev/null @@ -1,71 +0,0 @@ - -RSpec.describe Cloudflare::KV::Namespaces, kv_spec: true, order: :defined, timeout: 30 do - include_context Cloudflare::Account - - let(:namespace) { @namespace = account.kv_namespaces.create(namespace_title) } - let(:namespace_title) { "Test NS ##{rand(1..100)}" } - - after do - if defined? @namespace - expect(@namespace.delete).to be_success - end - end - - it 'can create a namespace' do - expect(namespace).to be_kind_of Cloudflare::KV::Namespace - expect(namespace.id).not_to be_nil - expect(namespace.title).to eq namespace_title - end - - it 'can find a namespace by title' do - namespace # Call this so that the namespace gets created - expect(account.kv_namespaces.find_by_title(namespace_title).id).to eq namespace.id - end - - it 'can rename the namespace' do - new_title = "#{namespace_title}-#{rand(1..100)}" - namespace.rename(new_title) - expect(namespace.title).to eq new_title - expect(account.kv_namespaces.find_by_title(new_title).id).to eq namespace.id - expect(account.kv_namespaces.find_by_title(namespace_title)).to be_nil - end - - it 'can store a key/value, read it back' do - key = "key-#{rand(1..100)}" - value = rand(100..999) - namespace.write_value(key, value) - expect(account.kv_namespaces.find_by_id(namespace.id).read_value(key)).to eq value.to_s - end - - it 'can read a previously stored key' do - key = "key-#{rand(1..100)}" - value = rand(100..999) - expect(account.kv_namespaces.find_by_id(namespace.id).write_value(key, value)).to be true - expect(namespace.read_value(key)).to eq value.to_s - end - - it 'can delete keys' do - key = "key-#{rand(1..100)}" - value = rand(100..999) - expect(namespace.write_value(key, value)).to be true - expect(namespace.read_value(key)).to eq value.to_s - expect(namespace.delete_value(key)).to be true - expect do - account.kv_namespaces.find_by_id(namespace.id).read_value(key) - end.to raise_error(Cloudflare::RequestError) - end - - it 'can get the keys that exist in the namespace' do - counter = 0 - keys = Array.new(rand(1..9)) { "key-#{counter += 1}" } # Keep this single digits so ordering works - keys.each_with_index do |key, i| - namespace.write_value(key, i) - end - - saved_keys = account.kv_namespaces.find_by_id(namespace.id).keys.to_a - expect(saved_keys.length).to eq keys.length - saved_keys.each_with_index do |key, i| - expect(key.name).to eq keys[i] - end - end -end diff --git a/spec/cloudflare/zone_spec.rb b/spec/cloudflare/zone_spec.rb deleted file mode 100644 index 3c73a97..0000000 --- a/spec/cloudflare/zone_spec.rb +++ /dev/null @@ -1,27 +0,0 @@ - -RSpec.describe Cloudflare::Zones, order: :defined, timeout: 30 do - include_context Cloudflare::Zone - - if ENV['CLOUDFLARE_TEST_ZONE_MANAGEMENT'] == 'true' - it "can delete existing domain if exists" do - if zone = zones.find_by_name(name) - expect(zone.delete).to be_success - end - end - - it "can create a zone" do - zone = zones.create(name, account) - expect(zone.value).to include(:id) - end - end - - it "can list zones" do - matching_zones = zones.select{|zone| zone.name == name} - expect(matching_zones).to_not be_empty - end - - it "can get zone by name" do - found_zone = zones.find_by_name(name) - expect(found_zone.name).to be == name - end -end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb deleted file mode 100644 index 95093e3..0000000 --- a/spec/spec_helper.rb +++ /dev/null @@ -1,86 +0,0 @@ -# frozen_string_literal: true - -AUTH_EMAIL = ENV['CLOUDFLARE_EMAIL'] -AUTH_KEY = ENV['CLOUDFLARE_KEY'] - -if AUTH_EMAIL.nil? || AUTH_EMAIL.empty? || AUTH_KEY.nil? || AUTH_KEY.empty? - puts 'Please make sure you have defined CLOUDFLARE_EMAIL and CLOUDFLARE_KEY in your environment' - puts 'You can also specify CLOUDFLARE_ZONE_NAME to test with your own zone and' - puts 'CLOUDFLARE_ACCOUNT_ID to use a specific account' - exit(1) -end - -ACCOUNT_ID = ENV['CLOUDFLARE_ACCOUNT_ID'] -NAMES = %w{alligator ant bear bee bird camel cat cheetah chicken chimpanzee cow crocodile deer dog dolphin duck eagle elephant fish fly fox frog giraffe goat goldfish hamster hippopotamus horse kangaroo kitten lion lobster monkey octopus owl panda pig puppy rabbit rat scorpion seal shark sheep snail snake spider squirrel tiger turtle wolf zebra} -JOB_ID = ENV.fetch('INVOCATION_ID', 'testing').hash -ZONE_NAME = ENV['CLOUDFLARE_ZONE_NAME'] || "#{NAMES[JOB_ID % NAMES.size]}.com" - -$stderr.puts "Using zone name: #{ZONE_NAME}" - -require 'covered/rspec' -require 'async/rspec' - -require 'cloudflare/rspec/connection' -require 'cloudflare/zones' - -RSpec.shared_context Cloudflare::Account do - include_context Cloudflare::RSpec::Connection - - let(:account) do - if ACCOUNT_ID - connection.accounts.find_by_id(ACCOUNT_ID) - else - connection.accounts.first - end - end -end - -RSpec.shared_context Cloudflare::Zone do - include_context Cloudflare::Account - - let(:job_id) {JOB_ID} - let(:names) {NAMES.dup} - let(:name) {ZONE_NAME.dup} - - let(:zones) {connection.zones} - - let(:zone) {@zone = zones.find_by_name(name) || zones.create(name, account)} - - # after do - # if defined? @zone - # @zone.delete - # end - # end -end - -RSpec.configure do |config| - # Enable flags like --only-failures and --next-failure - config.example_status_persistence_file_path = '.rspec_status' - - config.expect_with :rspec do |c| - c.syntax = :expect - end - - disabled_specs = {} - - # Check for features the current account has enabled - Cloudflare.connect(key: AUTH_KEY, email: AUTH_EMAIL) do |conn| - begin - account = if ACCOUNT_ID - conn.accounts.find_by_id(ACCOUNT_ID) - else - conn.accounts.first - end - account.kv_namespaces.to_a - rescue Cloudflare::RequestError => e - if e.message.include?('your account is not entitled') - puts 'Disabling KV specs due to no access' - disabled_specs[:kv_spec] = true - else - raise - end - end - end - - config.filter_run_excluding disabled_specs unless disabled_specs.empty? -end diff --git a/test/cloudflare/accounts.rb b/test/cloudflare/accounts.rb new file mode 100644 index 0000000..d6064a1 --- /dev/null +++ b/test/cloudflare/accounts.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2019, by Rob Widmer. +# Copyright, 2024, by Samuel Williams. + +require "cloudflare/accounts" +require "cloudflare/a_connection" + +describe Cloudflare::Accounts do + include_context Cloudflare::AConnection + + before do + # Force a fetch if it hasn't happened yet: + account.id + end + + it "can list existing accounts" do + accounts = connection.accounts.to_a + + expect(accounts).to have_value(have_attributes( + id: be == account.id + )) + end + + it "can get a specific account" do + fetched_account = connection.accounts.find_by_id(account.id) + + expect(fetched_account.id).to be == account.id + end + + it "can generate a representation for the KV namespace endpoint" do + namespace = connection.accounts.find_by_id(account.id).kv_namespaces + + expect(namespace).to be_a(Cloudflare::KV::Namespaces) + + expect(namespace.resource.reference.path).to be(:end_with?, "/#{account.id}/storage/kv/namespaces") + end +end diff --git a/test/cloudflare/connection.rb b/test/cloudflare/connection.rb new file mode 100644 index 0000000..163e821 --- /dev/null +++ b/test/cloudflare/connection.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2024, by Samuel Williams. + +require "cloudflare/accounts" +require "cloudflare/a_connection" + +describe Cloudflare::Connection do + include_context Cloudflare::AConnection + + with "#user" do + it "can get the current user" do + user = connection.user + + expect(user).to have_attributes( + id: be =~ /\A[a-f0-9]{32}\z/, + ) + end + end +end diff --git a/test/cloudflare/custom_hostname/ssl_attribute.rb b/test/cloudflare/custom_hostname/ssl_attribute.rb new file mode 100644 index 0000000..c091344 --- /dev/null +++ b/test/cloudflare/custom_hostname/ssl_attribute.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2019, by Rob Widmer. +# Copyright, 2024, by Samuel Williams. + +require "cloudflare/custom_hostname/ssl_attribute" + +ACCESSORS = [:cname, :cname_target, :http_body, :http_url, :method, :status, :type, :validation_errors] + +describe Cloudflare::CustomHostname::SSLAttribute do + let(:original_hash) {Hash.new} + let(:attribute) {subject.new(original_hash)} + + ACCESSORS.each do |key| + it "has an accessor for the #{key} value", unique: key do + test_value = Object.new + expect(attribute.send(key)).to be_nil + + original_hash[key] = test_value + expect(attribute.send(key)).to be == test_value + end + end + + it '#active? returns true when the status is "active" and false otherwise' do + expect(attribute.active?).to be == false + original_hash[:status] = "initializing" + expect(attribute.active?).to be == false + original_hash[:status] = "pending_validation" + expect(attribute.active?).to be == false + original_hash[:status] = "pending_deployment" + expect(attribute.active?).to be == false + original_hash[:status] = "active" + expect(attribute.active?).to be == true + end + + it '#pending_validation? returns true when the status is "pending_validation" and false otherwise' do + expect(attribute.pending_validation?).to be == false + original_hash[:status] = "initializing" + expect(attribute.pending_validation?).to be == false + original_hash[:status] = "active" + expect(attribute.pending_validation?).to be == false + original_hash[:status] = "pending_deployment" + expect(attribute.pending_validation?).to be == false + original_hash[:status] = "pending_validation" + expect(attribute.pending_validation?).to be == true + end + + with "#settings" do + it "should return a Settings object" do + expect(attribute.settings).to be_a Cloudflare::CustomHostname::SSLAttribute::Settings + end + + it "initailizes the settings object with the value from the settings key" do + settings = {min_tls_version: Object.new} + + original_hash[:settings] = settings + + expect(attribute.settings.min_tls_version).to be == settings[:min_tls_version] + end + + it "initializes the settings object with a new hash if the settings key does not exist" do + expected_value = Object.new + + expect(original_hash[:settings]).to be_nil + expect(attribute.settings.min_tls_version).to be_nil + expect(original_hash[:settings]).not.to be_nil + original_hash[:settings][:min_tls_version] = expected_value + expect(attribute.settings.min_tls_version).to be == expected_value + end + + it "updates the stored hash with values set on the settings object" do + expected_value = Object.new + + expect(attribute.settings.min_tls_version).to be_nil + attribute.settings.min_tls_version = expected_value + expect(original_hash[:settings][:min_tls_version]).to be == expected_value + end + end +end diff --git a/test/cloudflare/custom_hostname/ssl_attribute/settings.rb b/test/cloudflare/custom_hostname/ssl_attribute/settings.rb new file mode 100644 index 0000000..893fc84 --- /dev/null +++ b/test/cloudflare/custom_hostname/ssl_attribute/settings.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2019, by Rob Widmer. +# Copyright, 2024, by Samuel Williams. + +require "cloudflare/custom_hostname/ssl_attribute/settings" + +describe Cloudflare::CustomHostname::SSLAttribute::Settings do + let(:settings) {subject.new} + + it "has an accessor for ciphers" do + ciphers = Object.new + expect(settings.ciphers).to be_nil + settings.ciphers = ciphers + expect(settings.ciphers).to be == ciphers + end + + it "has a boolean accessor for http2" do + expect(settings.http2).to be_nil + expect(settings.http2?).to be == false + settings.http2 = true + expect(settings.http2).to be == "on" + expect(settings.http2?).to be == true + settings.http2 = false + expect(settings.http2).to be == "off" + expect(settings.http2?).to be == false + settings.http2 = "on" + expect(settings.http2).to be == "on" + expect(settings.http2?).to be == true + settings.http2 = "off" + expect(settings.http2).to be == "off" + expect(settings.http2?).to be == false + end + + it "has an accessor for min_tls_version" do + tls_version = Object.new + expect(settings.min_tls_version).to be_nil + settings.min_tls_version = tls_version + expect(settings.min_tls_version).to be == tls_version + end + + it "has a boolean accessor for tls_1_3" do + expect(settings.tls_1_3).to be_nil + expect(settings.tls_1_3?).to be == false + settings.tls_1_3 = true + expect(settings.tls_1_3).to be == "on" + expect(settings.tls_1_3?).to be == true + settings.tls_1_3 = false + expect(settings.tls_1_3).to be == "off" + expect(settings.tls_1_3?).to be == false + settings.tls_1_3 = "on" + expect(settings.tls_1_3).to be == "on" + expect(settings.tls_1_3?).to be == true + settings.tls_1_3 = "off" + expect(settings.tls_1_3).to be == "off" + expect(settings.tls_1_3?).to be == false + end +end diff --git a/test/cloudflare/custom_hostnames.rb b/test/cloudflare/custom_hostnames.rb new file mode 100644 index 0000000..86a51c6 --- /dev/null +++ b/test/cloudflare/custom_hostnames.rb @@ -0,0 +1,210 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2019, by Rob Widmer. +# Copyright, 2019-2024, by Samuel Williams. + +require "cloudflare/custom_hostnames" +require "cloudflare/a_connection" + +describe Cloudflare::CustomHostnames do + include_context Cloudflare::AConnection + + let(:domain) {"www-#{job_id}.#{zone_name}"} + let(:record) {zone.custom_hostnames.create(domain)} + + let(:subdomain) {"origin-#{job_id}"} + let(:dns_record) {zone.dns_records.create("A", subdomain, "1.2.3.4")} + + let(:custom_origin) {"#{subdomain}.#{zone.name}"} + + after do + @record&.delete + @dns_record&.delete + end + + # it "can create a custom hostname record" do + # expect(record).to be_a Cloudflare::CustomHostname + # expect(record.custom_metadata).to be_nil + # expect(record.hostname).to be == domain + # expect(record.custom_origin).to be_nil + # expect(record.ssl.method).to be == "http" + # expect(record.ssl.type).to be == "dv" + # end + + # it "can create a custom hostname record with a custom origin" do + # begin + # @record = zone.custom_hostnames.create(domain, origin: custom_origin) + + # expect(@record).to be_kind_of Cloudflare::CustomHostname + # expect(@record.custom_metadata).to be_nil + # expect(@record.hostname).to eq domain + # expect(@record.custom_origin).to eq custom_origin + # expect(@record.ssl.method).to eq "http" + # expect(@record.ssl.type).to eq "dv" + # rescue Cloudflare::RequestError => e + # if e.message.include?("custom origin server has not been granted") + # skip(e.message) # This currently doesn't work but might start eventually: https://github.com/socketry/async-rspec/issues/7 + # else + # raise + # end + # end + # end + + # it "can create a custom hostname record with different ssl options" do + # @record = zone.custom_hostnames.create(domain, ssl: { method: "cname" }) + + # expect(@record).to be_kind_of Cloudflare::CustomHostname + # expect(@record.custom_metadata).to be_nil + # expect(@record.hostname).to eq domain + # expect(@record.custom_origin).to be_nil + # expect(@record.ssl.method).to eq "cname" + # expect(@record.ssl.type).to eq "dv" + # end + + # it "can create a custom hostname record with additional metadata" do + # metadata = { a: rand(1..10) } + + # begin + # @record = zone.custom_hostnames.create(domain, metadata: metadata) + + # expect(@record).to be_kind_of Cloudflare::CustomHostname + # expect(@record.custom_metadata).to eq metadata + # expect(@record.hostname).to eq domain + # expect(@record.custom_origin).to be_nil + # expect(@record.ssl.method).to eq "http" + # expect(@record.ssl.type).to eq "dv" + # rescue Cloudflare::RequestError => e + # if e.message.include?("No custom metadata access has been allocated for this zone") + # skip(e.message) # This currently doesn't work but might start eventually: https://github.com/socketry/async-rspec/issues/7 + # else + # raise + # end + # end + # end + + # it "can look up an existing custom hostname by the hostname or id" do + # expect(zone.custom_hostnames.find_by_hostname(record.hostname).id).to eq record.id + # expect(zone.custom_hostnames.find_by_id(record.id).id).to eq record.id + # end + + # context "with existing record" do + + # it "returns the hostname when calling #to_s" do + # expect(record.to_s).to eq domain + # end + + # it "can update metadata" do + # metadata = { c: rand(1..10) } + + # expect(record.custom_metadata).to be_nil + + # begin + # record.update_settings(metadata: metadata) + + # # Make sure the existing object is updated + # expect(record.custom_metadata).to eq metadata + + # # Verify that the server has the changes + # found_record = zone.custom_hostnames.find_by_id(record.id) + + # expect(found_record.custom_metadata).to eq metadata + # expect(found_record.hostname).to eq domain + # expect(found_record.custom_origin).to be_nil + # rescue Cloudflare::RequestError => e + # if e.message.include?("No custom metadata access has been allocated for this zone") + # skip(e.message) # This currently doesn't work but might start eventually: https://github.com/socketry/async-rspec/issues/7 + # else + # raise + # end + # end + # end + + # it "can update the custom origin" do + # expect(record.custom_origin).to be_nil + + # begin + # record.update_settings(origin: custom_origin) + + # # Make sure the existing object is updated + # expect(record.custom_origin).to eq custom_origin + + # # Verify that the server has the changes + # found_record = zone.custom_hostnames.find_by_id(record.id) + + # expect(found_record.custom_metadata).to be_nil + # expect(found_record.hostname).to eq domain + # expect(found_record.custom_origin).to eq custom_origin + # rescue Cloudflare::RequestError => e + # if e.message.include?("custom origin server has not been granted") + # skip(e.message) # This currently doesn't work but might start eventually: https://github.com/socketry/async-rspec/issues/7 + # else + # raise + # end + # end + # end + + # it "can update ssl information" do + # expect(record.ssl.method).to eq "http" + + # record.update_settings(ssl: { method: "cname", type: "dv" }) + + # # Make sure the existing object is updated + # expect(record.ssl.method).to eq "cname" + + # # Verify that the server has the changes + # found_record = zone.custom_hostnames.find_by_id(record.id) + + # expect(found_record.custom_metadata).to be_nil + # expect(found_record.hostname).to eq domain + # expect(found_record.custom_origin).to be_nil + # expect(found_record.ssl.method).to eq "cname" + # end + + # context "has an ssl section" do + + # it "wraps it in an SSLAttributes object" do + # expect(record.ssl).to be_kind_of Cloudflare::CustomHostname::SSLAttribute + # end + + # it "has some helpers for commonly used keys" do + # # Make sure our values exist before we check to make sure that they are returned correctly + # expect(record.result[:ssl].values_at(:method, :http_body, :http_url).compact).not_to be_empty + # expect(record.ssl.method).to be record.result[:ssl][:method] + # expect(record.ssl.http_body).to be record.result[:ssl][:http_body] + # expect(record.ssl.http_url).to be record.result[:ssl][:http_url] + # end + + # end + + # describe "#ssl_active?" do + + # it "returns the result of calling ssl.active?" do + # expected_value = double + # expect(record.ssl).to receive(:active?).and_return(expected_value) + # expect(record).not_to receive(:send_patch) + # expect(record.ssl_active?).to be expected_value + # end + + # it "returns the result of calling ssl.active? without triggering an update if force_update is true and the ssl is not in the pending_validation state" do + # expected_value = double + # expect(record.ssl).to receive(:active?).and_return(expected_value) + # expect(record.ssl.method).not_to be_nil + # expect(record.ssl.type).not_to be_nil + # expect(record.ssl.pending_validation?).to be false + # expect(record).not_to receive(:send_patch).with(ssl: { method: record.ssl.method, type: record.ssl.type }) + # expect(record.ssl_active?(true)).to be expected_value + # end + + # it "returns the result of calling ssl.active? after triggering an update if force_update is true and the ssl is in the pending_validation state" do + # expected_value = double + # expect(record.ssl).to receive(:active?).and_return(expected_value) + # expect(record.ssl.method).not_to be_nil + # expect(record.ssl.type).not_to be_nil + # record.result[:ssl][:status] = "pending_validation" + # expect(record).to receive(:send_patch).with(ssl: { method: record.ssl.method, type: record.ssl.type }) + # expect(record.ssl_active?(true)).to be expected_value + # end + # end + # end +end diff --git a/test/cloudflare/dns.rb b/test/cloudflare/dns.rb new file mode 100644 index 0000000..7f3bf45 --- /dev/null +++ b/test/cloudflare/dns.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2019-2024, by Samuel Williams. +# Copyright, 2019, by David Wegman. + +require "cloudflare/dns" +require "cloudflare/a_connection" + +describe Cloudflare::DNS do + include_context Cloudflare::AConnection + + let(:subdomain) {"www-#{job_id || SecureRandom.hex(4)}"} + + with "new record" do + it "can create dns record" do + record = zone.dns_records.create("A", subdomain, "1.2.3.4") + + expect(record.type).to be == "A" + expect(record.name).to be(:start_with?, subdomain) + expect(record.content).to be == "1.2.3.4" + ensure + record&.delete + end + + it "can create dns record with proxied option" do + record = zone.dns_records.create("A", subdomain, "1.2.3.4", proxied: true) + + expect(record.type).to be == "A" + expect(record.name).to be(:start_with?, subdomain) + expect(record.content).to be == "1.2.3.4" + expect(record.proxied).to be_truthy + ensure + record&.delete + end + end + + with "existing record" do + let(:record) {zone.dns_records.create("A", subdomain, "1.2.3.4")} + + after do + @record&.delete + end + + it "can update dns content" do + record.update_content("4.3.2.1") + expect(record.content).to be == "4.3.2.1" + + fetched_record = zone.dns_records.find_by_name(record.name) + expect(fetched_record.content).to be == record.content + end + + it "can update dns content with proxied option" do + record.update_content("4.3.2.1", proxied: true) + expect(record).to be(:proxied?) + + fetched_record = zone.dns_records.find_by_name(record.name) + expect(fetched_record).to be(:proxied?) + end + end +end diff --git a/test/cloudflare/firewall.rb b/test/cloudflare/firewall.rb new file mode 100644 index 0000000..15f59d7 --- /dev/null +++ b/test/cloudflare/firewall.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2019-2024, by Samuel Williams. + +require "cloudflare/firewall" +require "cloudflare/a_connection" + +describe Cloudflare::Firewall do + include_context Cloudflare::AConnection + + let(:notes) {"gemtest"} + + with "several rules" do + let(:allow_ip) {"123.123.123.123"} + let(:block_ip) {"123.123.123.124"} + + before do + zone.firewall_rules.each do |rule| + rule.delete + end + + zone.firewall_rules.set("whitelist", allow_ip) + zone.firewall_rules.set("block", block_ip) + end + + it "should get all rules" do + rules = zone.firewall_rules.to_a + + expect(rules.size).to be >= 2 + end + + it "should get rules with specific value" do + rules = zone.firewall_rules.each_by_value(allow_ip).to_a + + expect(rules.size).to be == 1 + end + end + + %w[block challenge whitelist].each_with_index do |mode, index| + it "should create a #{mode} rule", unique: mode do + value = "1.2.3.#{index}" + rule = zone.firewall_rules.set(mode, value, notes: notes) + + expect(rule.mode).to be == mode + expect(rule.configuration[:value]).to be == value + expect(rule.configuration[:target]).to be == "ip" + expect(rule.notes).to be == notes + + ensure + rule&.delete + end + end +end diff --git a/test/cloudflare/kv/namespaces.rb b/test/cloudflare/kv/namespaces.rb new file mode 100644 index 0000000..61f9bf0 --- /dev/null +++ b/test/cloudflare/kv/namespaces.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2019, by Rob Widmer. +# Copyright, 2019-2024, by Samuel Williams. + +require "cloudflare/kv/namespaces" +require "cloudflare/a_connection" + +describe Cloudflare::KV::Namespaces do + include_context Cloudflare::AConnection + + let(:namespace_title) {"Test Worker #{SecureRandom.hex(4)}"} + let(:namespace) {account.kv_namespaces.create(namespace_title) } + + after do + @namespace&.delete + end + + it "can create a namespace" do + expect(namespace).to be_a(Cloudflare::KV::Namespace) + expect(namespace.id).to be =~ /\A[a-f0-9]{32}\z/ + expect(namespace.title).to be == namespace_title + + fetched_namespace = account.kv_namespaces.find_by_title(namespace_title) + expect(fetched_namespace).to have_attributes( + id: be == namespace.id + ) + end + + it "can rename the namespace" do + new_title = namespace_title + " Renamed" + + namespace.rename(new_title) + + expect(namespace.title).to be == new_title + + fetched_namespace = account.kv_namespaces.find_by_title(new_title) + expect(fetched_namespace).to have_attributes( + id: be == namespace.id + ) + end + + it "can store a key/value, read it back" do + key = "key-#{rand(1..100)}" + value = rand(100..999) + + expect(namespace.write_value(key, value)).to be == true + + fetched_namespace = account.kv_namespaces.find_by_id(namespace.id) + expect(fetched_namespace.read_value(key)).to be == value.to_s + end + + it "can delete keys" do + key = "key-#{SecureRandom.hex(8)}" + value = SecureRandom.hex(32) + + namespace.write_value(key, value) + expect(namespace.read_value(key)).to be == value.to_s + expect(namespace.delete_value(key)).to be == true + + # This doesn't always reliably fail, so we can't test it: + # + # fetched_namespace = account.kv_namespaces.find_by_id(namespace.id) + # + # expect do + # fetched_namespace.read_value(key) + # end.to raise_exception(Cloudflare::RequestError) + end + + it "can get the keys that exist in the namespace" do + keys = 10.times.map{|index| "key-#{index}"} + + keys.each do |key| + namespace.write_value(key, key) + end + + fetched_keys = account.kv_namespaces.find_by_id(namespace.id).keys.map(&:name) + + expect(fetched_keys.sort).to be == keys.sort + end +end diff --git a/test/cloudflare/logs.rb b/test/cloudflare/logs.rb new file mode 100644 index 0000000..9886974 --- /dev/null +++ b/test/cloudflare/logs.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2024, by Samuel Williams. + +require "cloudflare/logs" +require "cloudflare/a_connection" + +describe Cloudflare::Logs do + include_context Cloudflare::AConnection + + # it "can list logs" do + # logs = zone.logs.first(10) + + # expect(logs).not.to be(:empty?) + # end +end diff --git a/test/cloudflare/zones.rb b/test/cloudflare/zones.rb new file mode 100644 index 0000000..2e23529 --- /dev/null +++ b/test/cloudflare/zones.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2017-2024, by Samuel Williams. +# Copyright, 2018, by Leonhardt Wille. +# Copyright, 2018, by Michael Kalygin. +# Copyright, 2018, by Sherman Koa. +# Copyright, 2019, by Rob Widmer. + +require "cloudflare/zones" +require "cloudflare/a_connection" + +describe Cloudflare::Zones do + include_context Cloudflare::AConnection + + with "temporary zone" do + let(:temporary_zone_name) {"#{SecureRandom.hex(8)}-testing.com"} + + it "can create and destroy zone" do + temporary_zone = zones.create(temporary_zone_name, account) + + fetched_zone = zones.find_by_name(temporary_zone_name) + expect(fetched_zone.name).to be == temporary_zone_name + + fetched_zone.delete + end + end + + with "test zone" do + before do + # Ensure the zone exists: + self.zone + end + + it "can list zones" do + matching_zones = zones.select{|zone| zone.name == zone_name} + + expect(matching_zones).not.to be(:empty?) + end + + it "can get zone by name" do + found_zone = zones.find_by_name(zone_name) + + expect(found_zone).to have_attributes( + name: be == zone_name + ) + end + end +end