From f53d8cee1b8dbf0510209ac6e72bc819f3ad9b89 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Fri, 13 Sep 2024 12:55:24 +1200 Subject: [PATCH] Migrate/update tests. --- .gitignore | 5 +- .rspec | 4 - cloudflare.gemspec | 2 +- config/sus.rb | 7 + fixtures/cloudflare/a_connection.rb | 62 +++++ gems.rb | 4 +- lib/cloudflare.rb | 24 +- lib/cloudflare/accounts.rb | 2 +- lib/cloudflare/connection.rb | 8 +- .../custom_hostname/ssl_attribute.rb | 3 +- .../custom_hostname/ssl_attribute/settings.rb | 4 +- lib/cloudflare/custom_hostnames.rb | 94 +++++--- lib/cloudflare/dns.rb | 73 +++--- lib/cloudflare/firewall.rb | 45 ++-- lib/cloudflare/kv/namespaces.rb | 86 ++++--- lib/cloudflare/kv/rest_wrapper.rb | 48 ---- lib/cloudflare/kv/wrapper.rb | 38 +++ lib/cloudflare/logs.rb | 3 +- lib/cloudflare/paginate.rb | 4 +- lib/cloudflare/representation.rb | 48 ++-- lib/cloudflare/rspec/connection.rb | 41 ---- lib/cloudflare/user.rb | 4 +- lib/cloudflare/zones.rb | 42 ++-- spec/cloudflare/accounts_spec.rb | 28 --- .../ssl_attribute/settings_spec.rb | 60 ----- .../custom_hostname/ssl_attribute_spec.rb | 79 ------- spec/cloudflare/custom_hostnames_spec.rb | 216 ------------------ spec/cloudflare/dns_spec.rb | 55 ----- spec/cloudflare/kv/namespaces_spec.rb | 76 ------ spec/cloudflare/zone_spec.rb | 35 --- spec/spec_helper.rb | 93 -------- 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 +++++ .../cloudflare/firewall.rb | 11 +- test/cloudflare/kv/namespaces.rb | 80 +++++++ test/cloudflare/logs.rb | 17 ++ test/cloudflare/zones.rb | 49 ++++ 41 files changed, 1001 insertions(+), 919 deletions(-) delete mode 100644 .rspec 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 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/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 rename spec/cloudflare/firewall_spec.rb => test/cloudflare/firewall.rb (82%) 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/.gitignore b/.gitignore index 5bc8957..6c32510 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,4 @@ /.covered.db /external -# rspec failure tracking -.rspec_status -.covered.db -.env +/.env 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/cloudflare.gemspec b/cloudflare.gemspec index 7d13b3e..def98be 100644 --- a/cloudflare.gemspec +++ b/cloudflare.gemspec @@ -23,5 +23,5 @@ Gem::Specification.new do |spec| spec.required_ruby_version = ">= 3.1" - spec.add_dependency "async-rest", "~> 0.12" + 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..cb26a51 --- /dev/null +++ b/fixtures/cloudflare/a_connection.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2024, by Samuel Williams. + +require "cloudflare" +require "sus/fixtures/async/reactor_context" + +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(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 2853f3f..7859daf 100644 --- a/gems.rb +++ b/gems.rb @@ -18,12 +18,12 @@ end group :test do - gem "rspec" + gem "sus" gem "covered" gem "decode" gem "rubocop" - gem "async-rspec" + gem "sus-fixtures-async" gem "sinatra" gem "webmock" diff --git a/lib/cloudflare.rb b/lib/cloudflare.rb index c018dec..e9e9788 100644 --- a/lib/cloudflare.rb +++ b/lib/cloudflare.rb @@ -14,20 +14,18 @@ module Cloudflare def self.connect(*arguments, **auth_info) + connection = Connection.open(*arguments) + + if !auth_info.empty? + connection = connection.authenticated(**auth_info) + end + + return connection unless block_given? + Sync do - connection = Connection.open(*arguments) - - if !auth_info.empty? - connection = connection.authenticated(**auth_info) - end - - return connection unless block_given? - - begin - yield connection - ensure - connection.close - end + yield connection + ensure + connection.close end end end diff --git a/lib/cloudflare/accounts.rb b/lib/cloudflare/accounts.rb index 23bce50..7151d00 100644 --- a/lib/cloudflare/accounts.rb +++ b/lib/cloudflare/accounts.rb @@ -11,7 +11,7 @@ module Cloudflare class Account < Representation def id - value[:id] + result[:id] end def kv_namespaces diff --git a/lib/cloudflare/connection.rb b/lib/cloudflare/connection.rb index 8704538..20eb721 100644 --- a/lib/cloudflare/connection.rb +++ b/lib/cloudflare/connection.rb @@ -24,13 +24,13 @@ 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 diff --git a/lib/cloudflare/custom_hostname/ssl_attribute.rb b/lib/cloudflare/custom_hostname/ssl_attribute.rb index f7479af..573c5c8 100644 --- a/lib/cloudflare/custom_hostname/ssl_attribute.rb +++ b/lib/cloudflare/custom_hostname/ssl_attribute.rb @@ -4,7 +4,8 @@ # Copyright, 2019, by Rob Widmer. # Copyright, 2019-2024, by Samuel Williams. -require_relative "./ssl_attribute/settings" +require_relative "ssl_attribute/settings" +require_relative "../representation" module Cloudflare class CustomHostname < Representation diff --git a/lib/cloudflare/custom_hostname/ssl_attribute/settings.rb b/lib/cloudflare/custom_hostname/ssl_attribute/settings.rb index 5747867..c0b5ce4 100644 --- a/lib/cloudflare/custom_hostname/ssl_attribute/settings.rb +++ b/lib/cloudflare/custom_hostname/ssl_attribute/settings.rb @@ -4,11 +4,13 @@ # 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 diff --git a/lib/cloudflare/custom_hostnames.rb b/lib/cloudflare/custom_hostnames.rb index 8d3e1bd..3e94b9b 100644 --- a/lib/cloudflare/custom_hostnames.rb +++ b/lib/cloudflare/custom_hostnames.rb @@ -10,70 +10,92 @@ 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 6697172..f5e24cb 100644 --- a/lib/cloudflare/dns.rb +++ b/lib/cloudflare/dns.rb @@ -11,59 +11,74 @@ 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 7a42de7..3858dbf 100644 --- a/lib/cloudflare/firewall.rb +++ b/lib/cloudflare/firewall.rb @@ -10,18 +10,20 @@ 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 @@ -29,26 +31,29 @@ def to_s class Rules < Representation include Paginate - + def representation Rule end - + 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 2570ba1..a62f936 100644 --- a/lib/cloudflare/kv/namespaces.rb +++ b/lib/cloudflare/kv/namespaces.rb @@ -7,73 +7,105 @@ require_relative "../paginate" require_relative "../representation" -require_relative "rest_wrapper" +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 70c34a6..0000000 --- a/lib/cloudflare/kv/rest_wrapper.rb +++ /dev/null @@ -1,48 +0,0 @@ -# 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 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 58aefb6..e20be9d 100644 --- a/lib/cloudflare/logs.rb +++ b/lib/cloudflare/logs.rb @@ -10,7 +10,7 @@ module Cloudflare module Logs class Entry < Representation def to_s - "#{value[:rayid]}-#{value[:ClientRequestURI]}" + "#{result[:rayid]}-#{result[:ClientRequestURI]}" end end @@ -23,4 +23,3 @@ def representation end end end - diff --git a/lib/cloudflare/paginate.rb b/lib/cloudflare/paginate.rb index 6013927..25af9a0 100644 --- a/lib/cloudflare/paginate.rb +++ b/lib/cloudflare/paginate.rb @@ -12,7 +12,9 @@ def each(page: 1, per_page: 50, **parameters) return to_enum(:each, page: page, per_page: per_page, **parameters) unless block_given? while true - response = self.class.get(@resource, {page: page, per_page: per_page, **parameters}) + resource = @resource.with(parameters: {page: page, per_page: per_page, **parameters}) + + response = self.class.get(resource) break if response.empty? diff --git a/lib/cloudflare/representation.rb b/lib/cloudflare/representation.rb index 1431921..30c1a4f 100644 --- a/lib/cloudflare/representation.rb +++ b/lib/cloudflare/representation.rb @@ -12,30 +12,34 @@ module Cloudflare class RequestError < StandardError - def initialize(resource, errors) - super("#{resource}: #{errors.map{|attributes| attributes[:message]}.join(', ')}") + 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 - @representation = representation + @value = value end - attr_reader :representation + attr :value end 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 WRAPPER = Wrapper.new - def initialize(...) - super(...) - - # Some endpoints return the value instead of a message object (like KV reads) - unless @value.is_a?(Hash) - @value = {success: true, result: @value} - end - end - def representation Representation end @@ -43,25 +47,25 @@ def representation 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 to_hash - if value.is_a?(Hash) - return value - end - end - def result value[:result] end - def read - value[:result] + def to_hash + result + end + + def to_id + {id: result[:id]} end def results diff --git a/lib/cloudflare/rspec/connection.rb b/lib/cloudflare/rspec/connection.rb deleted file mode 100644 index 0c1a180..0000000 --- a/lib/cloudflare/rspec/connection.rb +++ /dev/null @@ -1,41 +0,0 @@ -# frozen_string_literal: true - -# Released under the MIT License. -# Copyright, 2017-2024, by Samuel Williams. -# Copyright, 2018, by Leonhardt Wille. - -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 dde3d18..a02ea1e 100644 --- a/lib/cloudflare/user.rb +++ b/lib/cloudflare/user.rb @@ -9,11 +9,11 @@ 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/zones.rb b/lib/cloudflare/zones.rb index 73a12a3..419502f 100644 --- a/lib/cloudflare/zones.rb +++ b/lib/cloudflare/zones.rb @@ -22,34 +22,38 @@ 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 @@ -62,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/spec/cloudflare/accounts_spec.rb b/spec/cloudflare/accounts_spec.rb deleted file mode 100644 index c2700ff..0000000 --- a/spec/cloudflare/accounts_spec.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -# Released under the MIT License. -# Copyright, 2019, by Rob Widmer. -# Copyright, 2024, by Samuel Williams. - -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 7297a00..0000000 --- a/spec/cloudflare/custom_hostname/ssl_attribute/settings_spec.rb +++ /dev/null @@ -1,60 +0,0 @@ -# frozen_string_literal: true - -# Released under the MIT License. -# Copyright, 2019, by Rob Widmer. -# Copyright, 2024, by Samuel Williams. - -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 84653bb..0000000 --- a/spec/cloudflare/custom_hostname/ssl_attribute_spec.rb +++ /dev/null @@ -1,79 +0,0 @@ -# frozen_string_literal: true - -# Released under the MIT License. -# Copyright, 2019, by Rob Widmer. -# Copyright, 2024, by Samuel Williams. - -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 11e9411..0000000 --- a/spec/cloudflare/custom_hostnames_spec.rb +++ /dev/null @@ -1,216 +0,0 @@ -# frozen_string_literal: true - -# Released under the MIT License. -# Copyright, 2019, by Rob Widmer. -# Copyright, 2019-2024, by Samuel Williams. - -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 28f03a6..0000000 --- a/spec/cloudflare/dns_spec.rb +++ /dev/null @@ -1,55 +0,0 @@ -# frozen_string_literal: true - -# Released under the MIT License. -# Copyright, 2019-2024, by Samuel Williams. -# Copyright, 2019, by David Wegman. - -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/kv/namespaces_spec.rb b/spec/cloudflare/kv/namespaces_spec.rb deleted file mode 100644 index ce69086..0000000 --- a/spec/cloudflare/kv/namespaces_spec.rb +++ /dev/null @@ -1,76 +0,0 @@ -# frozen_string_literal: true - -# Released under the MIT License. -# Copyright, 2019, by Rob Widmer. -# Copyright, 2019-2024, by Samuel Williams. - -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 3bf4013..0000000 --- a/spec/cloudflare/zone_spec.rb +++ /dev/null @@ -1,35 +0,0 @@ -# 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. - -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 5586ca8..0000000 --- a/spec/spec_helper.rb +++ /dev/null @@ -1,93 +0,0 @@ -# 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. - -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/spec/cloudflare/firewall_spec.rb b/test/cloudflare/firewall.rb similarity index 82% rename from spec/cloudflare/firewall_spec.rb rename to test/cloudflare/firewall.rb index 3296c49..15f59d7 100644 --- a/spec/cloudflare/firewall_spec.rb +++ b/test/cloudflare/firewall.rb @@ -3,14 +3,15 @@ # Released under the MIT License. # Copyright, 2019-2024, by Samuel Williams. -require "cloudflare/rspec/connection" +require "cloudflare/firewall" +require "cloudflare/a_connection" -RSpec.describe Cloudflare::Firewall, order: :defined, timeout: 30 do - include_context Cloudflare::Zone +describe Cloudflare::Firewall do + include_context Cloudflare::AConnection let(:notes) {"gemtest"} - context "with several rules" do + with "several rules" do let(:allow_ip) {"123.123.123.123"} let(:block_ip) {"123.123.123.124"} @@ -37,7 +38,7 @@ end %w[block challenge whitelist].each_with_index do |mode, index| - it "should create a #{mode} rule" do + it "should create a #{mode} rule", unique: mode do value = "1.2.3.#{index}" rule = zone.firewall_rules.set(mode, value, notes: notes) diff --git a/test/cloudflare/kv/namespaces.rb b/test/cloudflare/kv/namespaces.rb new file mode 100644 index 0000000..ea11eaa --- /dev/null +++ b/test/cloudflare/kv/namespaces.rb @@ -0,0 +1,80 @@ +# 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 + + 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