diff --git a/.travis.yml b/.travis.yml index 5e55165..1e8e3a0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,4 +16,4 @@ branches: before_install: gem install bundler bundler_args: "--deployment --without development --jobs=3 --retry=3" -script: "./script/ci/travis.sh" +script: bundle exec rake --trace diff --git a/Gemfile b/Gemfile index 862f93f..c9d145e 100644 --- a/Gemfile +++ b/Gemfile @@ -42,6 +42,7 @@ group :test do gem "fixture_builder", "~> 0.4.1" gem "factory_girl_rails", "~> 4.5.0" gem "rspec-collection_matchers", "~> 1.1.2" + gem "webmock", "~> 1.21.0" end group :development, :test do diff --git a/Gemfile.lock b/Gemfile.lock index ffd57ed..d1f992c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,7 +2,11 @@ PATH remote: . specs: diaspora_federation (0.0.3) + faraday (~> 0.9.0) + faraday_middleware (~> 0.9.0) nokogiri (~> 1.6, >= 1.6.6) + typhoeus (~> 0.7.0) + valid (~> 0.5.0) diaspora_federation-rails (0.0.3) diaspora_federation (= 0.0.3) rails (~> 4.2) @@ -45,6 +49,7 @@ GEM minitest (~> 5.1) thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) + addressable (2.3.8) arel (6.0.0) ast (2.0.0) astrolabe (1.3.0) @@ -56,14 +61,22 @@ GEM simplecov (>= 0.7.1, < 1.0.0) coderay (1.1.0) columnize (0.9.0) + crack (0.4.2) + safe_yaml (~> 1.0.0) diff-lcs (1.2.5) docile (1.1.5) erubis (2.7.0) + ethon (0.7.4) + ffi (>= 1.3.0) factory_girl (4.5.0) activesupport (>= 3.0.0) factory_girl_rails (4.5.0) factory_girl (~> 4.5.0) railties (>= 3.0.0) + faraday (0.9.1) + multipart-post (>= 1.2, < 3) + faraday_middleware (0.9.2) + faraday (>= 0.7.4, < 0.10) ffi (1.9.10) fixture_builder (0.4.1) activerecord (>= 2) @@ -111,6 +124,7 @@ GEM mini_portile (0.6.2) minitest (5.7.0) multi_json (1.11.1) + multipart-post (2.0.0) nenv (0.2.0) nokogiri (1.6.6.2) mini_portile (~> 0.6.0) @@ -193,6 +207,7 @@ GEM rainbow (>= 1.99.1, < 3.0) ruby-progressbar (~> 1.4) ruby-progressbar (1.7.5) + safe_yaml (1.0.4) shellany (0.0.1) simplecov (0.10.0) docile (~> 1.1.0) @@ -218,10 +233,16 @@ GEM systemu (2.6.5) thor (0.19.1) thread_safe (0.3.5) + typhoeus (0.7.2) + ethon (>= 0.7.4) tzinfo (1.2.2) thread_safe (~> 0.1) uuid (2.3.8) macaddr (~> 1.0) + valid (0.5.0) + webmock (1.21.0) + addressable (>= 2.3.6) + crack (>= 0.3.2) yard (0.8.7.6) PLATFORMS @@ -250,6 +271,7 @@ DEPENDENCIES spring-watcher-listen sqlite3 (~> 1.3.10) uuid (~> 2.3.8) + webmock (~> 1.21.0) yard BUNDLED WITH diff --git a/README.md b/README.md index a7818ed..73b5f5b 100644 --- a/README.md +++ b/README.md @@ -43,10 +43,10 @@ DiasporaFederation.configure do |config| config.server_uri = AppConfig.pod_uri config.define_callbacks do - on :person_webfinger_fetch do |handle| - person = Person.find_local_by_diaspora_handle(handle) + on :person_webfinger_fetch do |diaspora_id| + person = Person.find_local_by_diaspora_id(diaspora_id) if person - DiasporaFederation::WebFinger::WebFinger.new( + DiasporaFederation::Discovery::WebFinger.new( # ... ) end diff --git a/app/controllers/diaspora_federation/h_card_controller.rb b/app/controllers/diaspora_federation/h_card_controller.rb index 70e8821..65e700d 100644 --- a/app/controllers/diaspora_federation/h_card_controller.rb +++ b/app/controllers/diaspora_federation/h_card_controller.rb @@ -9,10 +9,12 @@ module DiasporaFederation def hcard person_hcard = DiasporaFederation.callbacks.trigger(:person_hcard_fetch, params[:guid]) - return render nothing: true, status: 404 if person_hcard.nil? - - logger.info "hcard profile request for: #{person_hcard.nickname}:#{person_hcard.guid}" - render html: person_hcard.to_html.html_safe + if person_hcard.nil? + render nothing: true, status: 404 if person_hcard.nil? + else + logger.info "hcard profile request for: #{person_hcard.nickname}:#{person_hcard.guid}" + render html: person_hcard.to_html.html_safe + end end end end diff --git a/app/controllers/diaspora_federation/webfinger_controller.rb b/app/controllers/diaspora_federation/webfinger_controller.rb index 3343e1c..ac9fd5a 100644 --- a/app/controllers/diaspora_federation/webfinger_controller.rb +++ b/app/controllers/diaspora_federation/webfinger_controller.rb @@ -37,10 +37,12 @@ module DiasporaFederation def legacy_webfinger person_wf = find_person_webfinger(params[:q]) if params[:q] - return render nothing: true, status: 404 if person_wf.nil? - - logger.info "webfinger profile request for: #{person_wf.acct_uri}" - render body: person_wf.to_xml, content_type: "application/xrd+xml" + if person_wf.nil? + render nothing: true, status: 404 + else + logger.info "webfinger profile request for: #{person_wf.acct_uri}" + render body: person_wf.to_xml, content_type: "application/xrd+xml" + end end private @@ -48,11 +50,11 @@ module DiasporaFederation # creates the host-meta xml with the configured server_uri and caches it # @return [String] XML string def self.host_meta_xml - @host_meta_xml ||= WebFinger::HostMeta.from_base_url(DiasporaFederation.server_uri.to_s).to_xml + @host_meta_xml ||= Discovery::HostMeta.from_base_url(DiasporaFederation.server_uri.to_s).to_xml end def find_person_webfinger(query) - DiasporaFederation.callbacks.trigger(:person_webfinger_fetch, query.strip.downcase.gsub("acct:", "")) + DiasporaFederation.callbacks.trigger(:person_webfinger_fetch, query.strip.downcase.sub("acct:", "")) end end end diff --git a/diaspora_federation.gemspec b/diaspora_federation.gemspec index 77d9274..6e58eae 100644 --- a/diaspora_federation.gemspec +++ b/diaspora_federation.gemspec @@ -21,4 +21,8 @@ Gem::Specification.new do |s| s.required_ruby_version = "~> 2.0" s.add_dependency "nokogiri", "~> 1.6", ">= 1.6.6" + s.add_dependency "faraday", "~> 0.9.0" + s.add_dependency "faraday_middleware", "~> 0.9.0" + s.add_dependency "typhoeus", "~> 0.7.0" + s.add_dependency "valid", "~> 0.5.0" end diff --git a/lib/diaspora_federation.rb b/lib/diaspora_federation.rb index 1580b65..ebbb1ef 100644 --- a/lib/diaspora_federation.rb +++ b/lib/diaspora_federation.rb @@ -3,8 +3,13 @@ require "diaspora_federation/logging" require "diaspora_federation/callbacks" require "diaspora_federation/properties_dsl" require "diaspora_federation/entity" +require "diaspora_federation/validators" -require "diaspora_federation/web_finger" +require "diaspora_federation/fetcher" + +require "diaspora_federation/entities" + +require "diaspora_federation/discovery" # diaspora* federation library module DiasporaFederation @@ -30,6 +35,12 @@ module DiasporaFederation # config.server_uri = AppConfig.pod_uri attr_accessor :server_uri + # Set the bundle of certificate authorities (CA) certificates + # + # @example + # config.certificate_authorities = AppConfig.environment.certificate_authorities.get + attr_accessor :certificate_authorities + # configure the federation library # # @example @@ -63,10 +74,17 @@ module DiasporaFederation # called from after_initialize # @raise [ConfigurationError] if the configuration is incomplete or invalid def validate_config - configuration_error "Missing server_uri" unless @server_uri.respond_to? :host + configuration_error "server_uri: Missing or invalid" unless @server_uri.respond_to? :host + + configuration_error "certificate_authorities: Not configured" if @certificate_authorities.nil? + unless File.file? @certificate_authorities + configuration_error "certificate_authorities: File not found: #{@certificate_authorities}" + end + unless @callbacks.definition_complete? configuration_error "Missing handlers for #{@callbacks.missing_handlers.join(', ')}" end + logger.info "successfully configured the federation library" end diff --git a/lib/diaspora_federation/discovery.rb b/lib/diaspora_federation/discovery.rb new file mode 100644 index 0000000..95cd7e9 --- /dev/null +++ b/lib/diaspora_federation/discovery.rb @@ -0,0 +1,14 @@ +module DiasporaFederation + # This module provides the namespace for the various classes implementing + # WebFinger and other protocols used for metadata discovery on remote servers + # in the Diaspora* network. + module Discovery + end +end + +require "diaspora_federation/discovery/exceptions" +require "diaspora_federation/discovery/xrd_document" +require "diaspora_federation/discovery/host_meta" +require "diaspora_federation/discovery/web_finger" +require "diaspora_federation/discovery/h_card" +require "diaspora_federation/discovery/discovery" diff --git a/lib/diaspora_federation/discovery/discovery.rb b/lib/diaspora_federation/discovery/discovery.rb new file mode 100644 index 0000000..5ecb1d6 --- /dev/null +++ b/lib/diaspora_federation/discovery/discovery.rb @@ -0,0 +1,91 @@ +module DiasporaFederation + module Discovery + # This class contains the logic to fetch all data for the given diaspora ID + class Discovery + include DiasporaFederation::Logging + + # @return [String] the diaspora ID of the account + attr_reader :diaspora_id + + # @param [String] diaspora_id the diaspora id to discover + def initialize(diaspora_id) + @diaspora_id = clean_diaspora_id(diaspora_id) + end + + # fetch all metadata for the account + # @return [Person] + def fetch + logger.info "Fetch data for #{diaspora_id}" + + unless diaspora_id == clean_diaspora_id(webfinger.acct_uri) + raise DiscoveryError, "Diaspora ID does not match: Wanted #{diaspora_id} but got" \ + " #{clean_diaspora_id(webfinger.acct_uri)}" + end + + person + end + + private + + def clean_diaspora_id(diaspora_id) + diaspora_id.strip.sub("acct:", "").to_s.downcase + end + + def get(url, http_fallback=false) + logger.info "Fetching #{url} for #{diaspora_id}" + response = Fetcher.get(url) + raise "Failed to fetch #{url}: #{response.status}" unless response.success? + response.body + rescue => e + if http_fallback && url.start_with?("https://") + logger.warn "Retry with http: #{url} for #{diaspora_id}: #{e.class}: #{e.message}" + url.sub!("https://", "http://") + retry + else + raise DiscoveryError, "Failed to fetch #{url} for #{diaspora_id}: #{e.class}: #{e.message}" + end + end + + def host_meta_url + domain = diaspora_id.split("@")[1] + "https://#{domain}/.well-known/host-meta" + end + + def legacy_webfinger_url_from_host_meta + # this tries the xrd url with https first, then falls back to http + host_meta = HostMeta.from_xml get(host_meta_url, true) + host_meta.webfinger_template_url.gsub("{uri}", "acct:#{diaspora_id}") + end + + def webfinger + @webfinger ||= WebFinger.from_xml get(legacy_webfinger_url_from_host_meta) + end + + def hcard + @hcard ||= HCard.from_html get(webfinger.hcard_url) + end + + def person + Entities::Person.new( + guid: hcard.guid || webfinger.guid, + diaspora_id: diaspora_id, + url: webfinger.seed_url, + exported_key: hcard.public_key || webfinger.public_key, + profile: profile + ) + end + + def profile + Entities::Profile.new( + diaspora_id: diaspora_id, + first_name: hcard.first_name, + last_name: hcard.last_name, + image_url: hcard.photo_large_url, + image_url_medium: hcard.photo_medium_url, + image_url_small: hcard.photo_small_url, + searchable: hcard.searchable + ) + end + end + end +end diff --git a/lib/diaspora_federation/web_finger/exceptions.rb b/lib/diaspora_federation/discovery/exceptions.rb similarity index 79% rename from lib/diaspora_federation/web_finger/exceptions.rb rename to lib/diaspora_federation/discovery/exceptions.rb index 5883dc7..38c536a 100644 --- a/lib/diaspora_federation/web_finger/exceptions.rb +++ b/lib/diaspora_federation/discovery/exceptions.rb @@ -1,5 +1,5 @@ module DiasporaFederation - module WebFinger + module Discovery # Raised, if the XML structure is invalid class InvalidDocument < RuntimeError end @@ -11,5 +11,9 @@ module DiasporaFederation # * if the html passed to {HCard.from_html} in some way is malformed, invalid or incomplete. class InvalidData < RuntimeError end + + # Raised, if there is an error while discover a new person + class DiscoveryError < RuntimeError + end end end diff --git a/lib/diaspora_federation/web_finger/h_card.rb b/lib/diaspora_federation/discovery/h_card.rb similarity index 88% rename from lib/diaspora_federation/web_finger/h_card.rb rename to lib/diaspora_federation/discovery/h_card.rb index 76025e9..e9222f5 100644 --- a/lib/diaspora_federation/web_finger/h_card.rb +++ b/lib/diaspora_federation/discovery/h_card.rb @@ -1,5 +1,5 @@ module DiasporaFederation - module WebFinger + module Discovery # This class provides the means of generating an parsing account data to and # from the hCard format. # hCard is based on +RFC 2426+ (vCard) which got superseded by +RFC 6350+. @@ -15,17 +15,17 @@ module DiasporaFederation # # @example Creating a hCard document from a person hash # hc = HCard.new( - # guid: "0123456789abcdef", - # nickname: "user", - # full_name: "User Name", - # seed_url: "https://server.example/", - # photo_large_url: "https://server.example/uploads/l.jpg", - # photo_medium_url: "https://server.example/uploads/m.jpg", - # photo_small_url: "https://server.example/uploads/s.jpg", - # serialized_public_key: "-----BEGIN PUBLIC KEY-----\nABCDEF==\n-----END PUBLIC KEY-----", - # searchable: true, - # first_name: "User", - # last_name: "Name" + # guid: "0123456789abcdef", + # nickname: "user", + # full_name: "User Name", + # seed_url: "https://server.example/", + # photo_large_url: "https://server.example/uploads/l.jpg", + # photo_medium_url: "https://server.example/uploads/m.jpg", + # photo_small_url: "https://server.example/uploads/s.jpg", + # public_key: "-----BEGIN PUBLIC KEY-----\nABCDEF==\n-----END PUBLIC KEY-----", + # searchable: true, + # first_name: "User", + # last_name: "Name" # ) # html_string = hc.to_html # @@ -48,12 +48,12 @@ module DiasporaFederation property :guid # @!attribute [r] nickname - # the first part of the diaspora handle + # the first part of the diaspora ID # @return [String] nickname property :nickname # @!attribute [r] full_name - # @return [String] display name of the user + # @return [String] display name of the user property :full_name # @!attribute [r] url @@ -70,10 +70,8 @@ module DiasporaFederation # DER-encoded PKCS#1 key beginning with the text # "-----BEGIN PUBLIC KEY-----" and ending with "-----END PUBLIC KEY-----". # - # @note the public key is new in the hcard and is optional now. - # # @return [String] public key - property :public_key, default: nil + property :public_key # @!attribute [r] photo_large_url # @return [String] url to the big avatar (300x300) @@ -163,8 +161,8 @@ module DiasporaFederation def self.from_html(html_string) doc = parse_html_and_validate(html_string) - data = { - guid: content_from_doc(doc, :uid), + new( + guid: guid_from_doc(doc), nickname: content_from_doc(doc, :nickname), full_name: content_from_doc(doc, :fn), url: element_from_doc(doc, :url)["href"], @@ -172,15 +170,13 @@ module DiasporaFederation photo_medium_url: photo_from_doc(doc, :photo_medium), photo_small_url: photo_from_doc(doc, :photo_small), searchable: (content_from_doc(doc, :searchable) == "true"), + # TODO: public key is new and can be missing + public_key: (content_from_doc(doc, :key) unless element_from_doc(doc, :key).nil?), - # TODO: change me! ################### + # TODO: remove first_name and last_name! first_name: content_from_doc(doc, :given_name), last_name: content_from_doc(doc, :family_name) - ####################################### - } - # TODO: public key is new and can be missing - data[:public_key] = content_from_doc(doc, :key) unless element_from_doc(doc, :key).nil? - new(data) + ) end private @@ -288,6 +284,14 @@ module DiasporaFederation element_from_doc(doc, photo_selector)["src"] end private_class_method :photo_from_doc + + # @deprecated hack for old hcard + # @todo remove this when all pods have the new generator + def self.guid_from_doc(doc) + uid_element = element_from_doc(doc, :uid) + uid_element.content unless uid_element[:class].include? "nickname" + end + private_class_method :guid_from_doc end end end diff --git a/lib/diaspora_federation/web_finger/host_meta.rb b/lib/diaspora_federation/discovery/host_meta.rb similarity index 86% rename from lib/diaspora_federation/web_finger/host_meta.rb rename to lib/diaspora_federation/discovery/host_meta.rb index 444c16f..9346b6e 100644 --- a/lib/diaspora_federation/web_finger/host_meta.rb +++ b/lib/diaspora_federation/discovery/host_meta.rb @@ -1,6 +1,6 @@ module DiasporaFederation - module WebFinger + module Discovery # Generates and parses Host Meta documents. # # This is a minimal implementation of the standard, only to the degree of what @@ -19,8 +19,13 @@ module DiasporaFederation class HostMeta private_class_method :new + # @param [String] webfinger_url the webfinger-url + def initialize(webfinger_url) + @webfinger_url = webfinger_url + end + # URL fragment to append to the base URL - WEBFINGER_SUFFIX = "webfinger?q={uri}" + WEBFINGER_SUFFIX = "/webfinger?q={uri}" # Returns the WebFinger URL that was used to build this instance (either from # xml or by giving a base URL). @@ -42,18 +47,14 @@ module DiasporaFederation # Builds a new HostMeta instance and constructs the WebFinger URL from the # given base URL by appending HostMeta::WEBFINGER_SUFFIX. + # @param [String, URL] base_url the base-url for the webfinger-url # @return [HostMeta] # @raise [InvalidData] if the webfinger url is malformed def self.from_base_url(base_url) - raise ArgumentError, "base_url is not a String" unless base_url.instance_of?(String) - - base_url += "/" unless base_url.end_with?("/") - webfinger_url = base_url + WEBFINGER_SUFFIX + webfinger_url = "#{base_url.to_s.chomp('/')}#{WEBFINGER_SUFFIX}" raise InvalidData, "invalid webfinger url: #{webfinger_url}" unless webfinger_url_valid?(webfinger_url) - hm = allocate - hm.instance_variable_set(:@webfinger_url, webfinger_url) - hm + new(webfinger_url) end # Reads the given Host Meta XML document string and populates the @@ -67,16 +68,14 @@ module DiasporaFederation webfinger_url = webfinger_url_from_xrd(data) raise InvalidData, "invalid webfinger url: #{webfinger_url}" unless webfinger_url_valid?(webfinger_url) - hm = allocate - hm.instance_variable_set(:@webfinger_url, webfinger_url) - hm + new(webfinger_url) end # Applies some basic sanity-checking to the given URL # @param [String] url validation subject # @return [Boolean] validation result def self.webfinger_url_valid?(url) - !url.nil? && url.instance_of?(String) && url =~ %r{^https?:\/\/.*\{uri\}}i + !url.nil? && url.instance_of?(String) && url =~ %r{^https?:\/\/.*\/.*\{uri\}.*}i end private_class_method :webfinger_url_valid? diff --git a/lib/diaspora_federation/web_finger/web_finger.rb b/lib/diaspora_federation/discovery/web_finger.rb similarity index 86% rename from lib/diaspora_federation/web_finger/web_finger.rb rename to lib/diaspora_federation/discovery/web_finger.rb index a0ddc7a..d8759a8 100644 --- a/lib/diaspora_federation/web_finger/web_finger.rb +++ b/lib/diaspora_federation/discovery/web_finger.rb @@ -1,5 +1,5 @@ module DiasporaFederation - module WebFinger + module Discovery # The WebFinger document used for Diaspora* user discovery is based on an older # draft of the specification you can find in the wiki of the "webfinger" project # on {http://code.google.com/p/webfinger/wiki/WebFingerProtocol Google Code} @@ -147,20 +147,23 @@ module DiasporaFederation def self.from_xml(webfinger_xml) data = parse_xml_and_validate(webfinger_xml) - hcard_url, seed_url, guid, profile_url, atom_url, salmon_url, public_key = parse_links(data) + links = data[:links] + + # TODO: remove! public key is deprecated in webfinger + public_key = parse_link(links, REL_PUBKEY) new( acct_uri: data[:subject], alias_url: data[:aliases].first, - hcard_url: hcard_url, - seed_url: seed_url, - profile_url: profile_url, - atom_url: atom_url, - salmon_url: salmon_url, + hcard_url: parse_link(links, REL_HCARD), + seed_url: parse_link(links, REL_SEED), + profile_url: parse_link(links, REL_PROFILE), + atom_url: parse_link(links, REL_ATOM), + salmon_url: parse_link(links, REL_SALMON), # TODO: remove me! ########## - guid: guid, - public_key: Base64.strict_decode64(public_key) + guid: parse_link(links, REL_GUID), + public_key: (Base64.strict_decode64(public_key) if public_key) ) end @@ -172,10 +175,10 @@ module DiasporaFederation # @return [Hash] data XML data # @raise [InvalidData] if the given XML string is invalid or incomplete def self.parse_xml_and_validate(webfinger_xml) - data = XrdDocument.xml_data(webfinger_xml) - valid = data.key?(:subject) && data.key?(:aliases) && data.key?(:links) - raise InvalidData, "webfinger xml is incomplete" unless valid - data + XrdDocument.xml_data(webfinger_xml).tap do |data| + valid = data.key?(:subject) && data.key?(:aliases) && data.key?(:links) + raise InvalidData, "webfinger xml is incomplete" unless valid + end end private_class_method :parse_xml_and_validate @@ -209,22 +212,9 @@ module DiasporaFederation ################################## end - def self.parse_links(data) - links = data[:links] - hcard = parse_link(links, REL_HCARD) - seed = parse_link(links, REL_SEED) - guid = parse_link(links, REL_GUID) - profile = parse_link(links, REL_PROFILE) - atom = parse_link(links, REL_ATOM) - salmon = parse_link(links, REL_SALMON) - pubkey = parse_link(links, REL_PUBKEY) - raise InvalidData, "webfinger xml is incomplete" unless [hcard, seed, guid, profile, atom, salmon, pubkey].all? - [hcard[:href], seed[:href], guid[:href], profile[:href], atom[:href], salmon[:href], pubkey[:href]] - end - private_class_method :parse_links - def self.parse_link(links, rel) - links.find {|l| l[:rel] == rel } + element = links.find {|l| l[:rel] == rel } + element ? element[:href] : nil end private_class_method :parse_link end diff --git a/lib/diaspora_federation/web_finger/xrd_document.rb b/lib/diaspora_federation/discovery/xrd_document.rb similarity index 92% rename from lib/diaspora_federation/web_finger/xrd_document.rb rename to lib/diaspora_federation/discovery/xrd_document.rb index d35fbe2..7e1a22a 100644 --- a/lib/diaspora_federation/web_finger/xrd_document.rb +++ b/lib/diaspora_federation/discovery/xrd_document.rb @@ -1,5 +1,5 @@ module DiasporaFederation - module WebFinger + module Discovery # This class implements basic handling of XRD documents as far as it is # necessary in the context of the protocols used with Diaspora* federation. # @@ -92,19 +92,18 @@ module DiasporaFederation # @raise [InvalidDocument] if the XRD is malformed def self.xml_data(xrd_doc) doc = parse_xrd_document(xrd_doc) - data = {} - exp_elem = doc.at_xpath("xrd:XRD/xrd:Expires", NS) - data[:expires] = DateTime.strptime(exp_elem.content, DATETIME_FORMAT) unless exp_elem.nil? + {}.tap do |data| + exp_elem = doc.at_xpath("xrd:XRD/xrd:Expires", NS) + data[:expires] = DateTime.strptime(exp_elem.content, DATETIME_FORMAT) unless exp_elem.nil? - subj_elem = doc.at_xpath("xrd:XRD/xrd:Subject", NS) - data[:subject] = subj_elem.content unless subj_elem.nil? + subj_elem = doc.at_xpath("xrd:XRD/xrd:Subject", NS) + data[:subject] = subj_elem.content unless subj_elem.nil? - parse_aliases_from_xml_doc(doc, data) - parse_properties_from_xml_doc(doc, data) - parse_links_from_xml_doc(doc, data) - - data + parse_aliases_from_xml_doc(doc, data) + parse_properties_from_xml_doc(doc, data) + parse_links_from_xml_doc(doc, data) + end end private diff --git a/lib/diaspora_federation/entities.rb b/lib/diaspora_federation/entities.rb new file mode 100644 index 0000000..3665875 --- /dev/null +++ b/lib/diaspora_federation/entities.rb @@ -0,0 +1,12 @@ +module DiasporaFederation + # This namespace contains all the entities used to encapsulate data that is + # passed around in the Diaspora* network as part of the federation protocol. + # + # All entities must be defined in this namespace. otherwise the XML + # de-serialization will fail. + module Entities + end +end + +require "diaspora_federation/entities/profile" +require "diaspora_federation/entities/person" diff --git a/lib/diaspora_federation/entities/person.rb b/lib/diaspora_federation/entities/person.rb new file mode 100644 index 0000000..32a9080 --- /dev/null +++ b/lib/diaspora_federation/entities/person.rb @@ -0,0 +1,33 @@ +module DiasporaFederation + module Entities + # this entity contains the base data of a person + # + # @see Validators::PersonValidator + class Person < Entity + # @!attribute [r] guid + # @see HCard#guid + # @return [String] guid + property :guid + + # @!attribute [r] diaspora_id + # The diaspora ID of the person + # @return [String] diaspora ID + property :diaspora_id, xml_name: :diaspora_handle + + # @!attribute [r] url + # @see WebFinger#seed_url + # @return [String] link to the pod + property :url + + # @!attribute [r] profile + # all profile data of the person + # @return [Profile] the profile of the person + entity :profile, Entities::Profile + + # @!attribute [r] exported_key + # @see HCard#public_key + # @return [String] public key + property :exported_key + end + end +end diff --git a/lib/diaspora_federation/entities/profile.rb b/lib/diaspora_federation/entities/profile.rb new file mode 100644 index 0000000..1ecdd35 --- /dev/null +++ b/lib/diaspora_federation/entities/profile.rb @@ -0,0 +1,54 @@ +module DiasporaFederation + module Entities + # this entity contains all the profile data of a person + # + # @see Validators::ProfileValidator + class Profile < Entity + # @!attribute [r] diaspora_id + # The diaspora ID of the person + # @see Person#diaspora_id + # @return [String] diaspora ID + property :diaspora_id, xml_name: :diaspora_handle + + # @!attribute [r] first_name + # @deprecated + # @see #full_name + # @see HCard#first_name + # @return [String] first name + property :first_name, default: nil + + # @!attribute [r] last_name + # @deprecated + # @see #full_name + # @see HCard#last_name + # @return [String] last name + property :last_name, default: nil + + # @!attribute [r] image_url + # @see HCard#photo_large_url + # @return [String] url to the big avatar (300x300) + property :image_url, default: nil + # @!attribute [r] image_url_medium + # @see HCard#photo_medium_url + # @return [String] url to the medium avatar (100x100) + property :image_url_medium, default: nil + # @!attribute [r] image_url_small + # @see HCard#photo_small_url + # @return [String] url to the small avatar (50x50) + property :image_url_small, default: nil + + property :birthday, default: nil + property :gender, default: nil + property :bio, default: nil + property :location, default: nil + + # @!attribute [r] searchable + # @see HCard#searchable + # @return [Boolean] searchable flag + property :searchable, default: true + + property :nsfw, default: false + property :tag_string, default: nil + end + end +end diff --git a/lib/diaspora_federation/entity.rb b/lib/diaspora_federation/entity.rb index fa3793f..86f980b 100644 --- a/lib/diaspora_federation/entity.rb +++ b/lib/diaspora_federation/entity.rb @@ -15,6 +15,7 @@ module DiasporaFederation # property :prop # property :optional, default: false # property :dynamic_default, default: -> { Time.now } + # property :another_prop, xml_name: :another_name # entity :nested, NestedEntity # entity :multiple, [OtherEntity] # end @@ -37,6 +38,12 @@ module DiasporaFederation # Initializes the Entity with the given attribute hash and freezes the created # instance it returns. # + # After creation, the entity is validated against a Validator, if one is defined. + # The Validator needs to be in the {DiasporaFederation::Validators} namespace and + # named like "Validator". Only valid entities can be created. + # + # @see DiasporaFederation::Validators + # # @note Attributes not defined as part of the class definition ({PropertiesDSL#property}, # {PropertiesDSL#entity}) get discarded silently. # @@ -50,16 +57,18 @@ module DiasporaFederation end self.class.default_values.merge(data).each do |k, v| - instance_variable_set("@#{k}", v) if setable?(k, v) + instance_variable_set("@#{k}", nilify(v)) if setable?(k, v) end + freeze + validate end # Returns a Hash representing this Entity (attributes => values) # @return [Hash] entity data (mostly equal to the hash used for initialization). def to_h self.class.class_prop_names.each_with_object({}) do |prop, hash| - hash[prop] = send(prop) + hash[prop] = public_send(prop) end end @@ -76,16 +85,12 @@ module DiasporaFederation # some of this is from Rails "Inflector.demodulize" and "Inflector.undersore" def self.entity_name - word = name.dup - i = word.rindex("::") - word = word[(i + 2)..-1] if i - - word.gsub!("::", "/") - word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2') - word.gsub!(/([a-z\d])([A-Z])/, '\1_\2') - word.tr!("-", "_") - word.downcase! - word + name.rpartition("::").last.tap do |word| + word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2') + word.gsub!(/([a-z\d])([A-Z])/, '\1_\2') + word.tr!("-", "_") + word.downcase! + end end private @@ -113,34 +118,61 @@ module DiasporaFederation val.all? {|v| v.instance_of?(t.first) }) end + def nilify(value) + return nil if value.respond_to?(:empty?) && value.empty? + value + end + + def validate + validator_name = "DiasporaFederation::Validators::#{self.class.name.split('::').last}Validator" + return unless Validators.const_defined? validator_name + + validator_class = Validators.const_get validator_name + validator = validator_class.new self + raise ValidationError, error_message(validator) unless validator.valid? + end + + def error_message(validator) + errors = validator.errors.map do |prop, rule| + "property: #{prop}, value: #{public_send(prop).inspect}, rule: #{rule[:rule]}, with params: #{rule[:params]}" + end + "Failed validation for properties: #{errors.join(' | ')}" + end + # Serialize the Entity into XML elements # @return [Nokogiri::XML::Element] root node def entity_xml doc = Nokogiri::XML::DocumentFragment.new(Nokogiri::XML::Document.new) - root_element = Nokogiri::XML::Element.new(self.class.entity_name, doc) - - self.class.class_props.each do |prop_def| - name = prop_def[:name] - type = prop_def[:type] - if type == String - root_element << simple_node(doc, name) - else - # call #to_xml for each item and append to root - [*send(name)].compact.each do |item| - root_element << item.to_xml - end + Nokogiri::XML::Element.new(self.class.entity_name, doc).tap do |root_element| + self.class.class_props.each do |prop_def| + add_property_to_xml(doc, prop_def, root_element) end end + end - root_element + def add_property_to_xml(doc, prop_def, root_element) + property = prop_def[:name] + type = prop_def[:type] + if type == String + root_element << simple_node(doc, prop_def[:xml_name], property) + else + # call #to_xml for each item and append to root + [*public_send(property)].compact.each do |item| + root_element << item.to_xml + end + end end # create simple node, fill it with text and append to root - def simple_node(doc, name) - node = Nokogiri::XML::Element.new(name.to_s, doc) - data = send(name).to_s - node.content = data unless data.empty? - node + def simple_node(doc, name, property) + Nokogiri::XML::Element.new(name.to_s, doc).tap do |node| + data = public_send(property).to_s + node.content = data unless data.empty? + end + end + + # Raised, if entity is not valid + class ValidationError < RuntimeError end end end diff --git a/lib/diaspora_federation/fetcher.rb b/lib/diaspora_federation/fetcher.rb new file mode 100644 index 0000000..4ece8eb --- /dev/null +++ b/lib/diaspora_federation/fetcher.rb @@ -0,0 +1,42 @@ +require "faraday" +require "faraday_middleware/response/follow_redirects" +require "typhoeus/adapters/faraday" + +module DiasporaFederation + # A wrapper for {https://github.com/lostisland/faraday Faraday} used for + # fetching + # + # @see Discovery::Discovery + class Fetcher + # Perform a GET request + # + # @param [String] uri the URI + # @return [Faraday::Response] the response + def self.get(uri) + connection.get(uri) + end + + # gets the Faraday connection + # + # @return [Faraday::Connection] the response + def self.connection + create_default_connection unless @connection + @connection.dup + end + + def self.create_default_connection + options = { + request: {timeout: 30}, + ssl: {ca_file: DiasporaFederation.certificate_authorities} + } + + @connection = Faraday::Connection.new(options) do |builder| + builder.use FaradayMiddleware::FollowRedirects, limit: 4 + builder.adapter :typhoeus + end + + @connection.headers["User-Agent"] = "DiasporaFederation/#{DiasporaFederation::VERSION}" + end + private_class_method :create_default_connection + end +end diff --git a/lib/diaspora_federation/logging.rb b/lib/diaspora_federation/logging.rb index 125415c..615e0e2 100644 --- a/lib/diaspora_federation/logging.rb +++ b/lib/diaspora_federation/logging.rb @@ -13,10 +13,12 @@ module DiasporaFederation # use logging-gem if available return ::Logging::Logger[self] if defined?(::Logging::Logger) + # use rails logger if running in rails and no logging-gem is available + return ::Rails.logger if defined?(::Rails) + # fallback logger @logger = Logger.new(STDOUT) - loglevel = defined?(::Rails) ? ::Rails.configuration.log_level.to_s.upcase : "INFO" - @logger.level = Logger.const_get(loglevel) + @logger.level = Logger::INFO @logger end end diff --git a/lib/diaspora_federation/properties_dsl.rb b/lib/diaspora_federation/properties_dsl.rb index abfb9fb..12525d7 100644 --- a/lib/diaspora_federation/properties_dsl.rb +++ b/lib/diaspora_federation/properties_dsl.rb @@ -6,6 +6,7 @@ module DiasporaFederation # property :prop # property :optional, default: false # property :dynamic_default, default: -> { Time.now } + # property :another_prop, xml_name: :another_name # entity :nested, NestedEntity # entity :multiple, [OtherEntity] module PropertiesDSL @@ -19,6 +20,7 @@ module DiasporaFederation # @param [Hash] opts further options # @option opts [Object, #call] :default a default value, making the # property optional + # @option opts [Symbol] :xml_name another name used for xml generation def property(name, opts={}) define_property name, String, opts end @@ -69,7 +71,14 @@ module DiasporaFederation def define_property(name, type, opts={}) raise InvalidName unless name_valid?(name) - class_props << {name: name, type: type} + xml_name = name + if opts.has_key? :xml_name + raise ArgumentError, "xml_name is not supported for nested entities" unless type == String + xml_name = opts[:xml_name] + raise InvalidName, "invalid xml_name" unless name_valid?(xml_name) + end + + class_props << {name: name, xml_name: xml_name, type: type} default_props[name] = opts[:default] if opts.has_key? :default instance_eval { attr_reader name } @@ -79,7 +88,7 @@ module DiasporaFederation # @param [String, Symbol] name the name to check # @return [Boolean] def name_valid?(name) - name.instance_of?(Symbol) || name.instance_of?(String) + name.instance_of?(Symbol) end # checks if the type extends {Entity} diff --git a/lib/diaspora_federation/validators.rb b/lib/diaspora_federation/validators.rb new file mode 100644 index 0000000..4371747 --- /dev/null +++ b/lib/diaspora_federation/validators.rb @@ -0,0 +1,38 @@ +require "validation" +require "validation/rule/regular_expression" +require "validation/rule/not_empty" +require "validation/rule/uri" + +# +valid+ gem namespace +module Validation + # This module contains custom validation rules for various data field types. + # That includes types for which there are no provided rules by the +valid+ gem + # or types that are very specific to Diaspora* federation and need special handling. + # The rules are used inside the {DiasporaFederation::Validators validator classes} + # to perform basic sanity-checks on {DiasporaFederation::Entities federation entities}. + module Rule + end +end + +require "diaspora_federation/validators/rules/birthday" +require "diaspora_federation/validators/rules/boolean" +require "diaspora_federation/validators/rules/diaspora_id" +require "diaspora_federation/validators/rules/guid" +require "diaspora_federation/validators/rules/nilable_uri" +require "diaspora_federation/validators/rules/not_nil" +require "diaspora_federation/validators/rules/public_key" +require "diaspora_federation/validators/rules/tag_count" + +module DiasporaFederation + # Validators to perform basic sanity-checks on {DiasporaFederation::Entities federation entities}. + # + # The Validators are mapped with the entities by name. The naming schema + # is "Validator". + module Validators + end +end + +require "diaspora_federation/validators/h_card_validator" +require "diaspora_federation/validators/person_validator" +require "diaspora_federation/validators/profile_validator" +require "diaspora_federation/validators/web_finger_validator" diff --git a/lib/diaspora_federation/validators/h_card_validator.rb b/lib/diaspora_federation/validators/h_card_validator.rb new file mode 100644 index 0000000..029f244 --- /dev/null +++ b/lib/diaspora_federation/validators/h_card_validator.rb @@ -0,0 +1,30 @@ +module DiasporaFederation + module Validators + # This validates a {Discovery::HCard} + # + # @todo activate guid and public key validation after all pod have it in + # the hcard. + # + # @note + class HCardValidator < Validation::Validator + include Validation + + # rule :guid, :guid + + # the name must not contain a semicolon because of mentions + # @{ ; } + rule :full_name, regular_expression: {regex: /\A[^;]{,70}\z/} + rule :first_name, regular_expression: {regex: /\A[^;]{,32}\z/} + rule :last_name, regular_expression: {regex: /\A[^;]{,32}\z/} + + # this urls can be relative + rule :photo_large_url, [:not_nil, nilableURI: [:path]] + rule :photo_medium_url, [:not_nil, nilableURI: [:path]] + rule :photo_small_url, [:not_nil, nilableURI: [:path]] + + # rule :exported_key, :public_key + + rule :searchable, :boolean + end + end +end diff --git a/lib/diaspora_federation/validators/person_validator.rb b/lib/diaspora_federation/validators/person_validator.rb new file mode 100644 index 0000000..f344f9e --- /dev/null +++ b/lib/diaspora_federation/validators/person_validator.rb @@ -0,0 +1,18 @@ +module DiasporaFederation + module Validators + # This validates a {Entities::Person} + class PersonValidator < Validation::Validator + include Validation + + rule :guid, :guid + + rule :diaspora_id, :diaspora_id + + rule :url, %i(not_nil nilableURI) + + rule :profile, :not_nil + + rule :exported_key, :public_key + end + end +end diff --git a/lib/diaspora_federation/validators/profile_validator.rb b/lib/diaspora_federation/validators/profile_validator.rb new file mode 100644 index 0000000..773ee97 --- /dev/null +++ b/lib/diaspora_federation/validators/profile_validator.rb @@ -0,0 +1,33 @@ +module DiasporaFederation + module Validators + # This validates a {Entities::Profile} + class ProfileValidator < Validation::Validator + include Validation + + rule :diaspora_id, :diaspora_id + + # the name must not contain a semicolon because of mentions + # @{ ; } + rule :first_name, regular_expression: {regex: /\A[^;]{,32}\z/} + rule :last_name, regular_expression: {regex: /\A[^;]{,32}\z/} + + # this urls can be relative + rule :image_url, nilableURI: [:path] + rule :image_url_medium, nilableURI: [:path] + rule :image_url_small, nilableURI: [:path] + + rule :birthday, :birthday + + # TODO: replace regex with "length: {maximum: xxx}" but this rule doesn't allow nil now. + rule :gender, regular_expression: {regex: /\A.{,255}\z/} + rule :bio, regular_expression: {regex: /\A.{,65535}\z/} + rule :location, regular_expression: {regex: /\A.{,255}\z/} + + rule :searchable, :boolean + + rule :nsfw, :boolean + + rule :tag_string, tag_count: {maximum: 5} + end + end +end diff --git a/lib/diaspora_federation/validators/rules/birthday.rb b/lib/diaspora_federation/validators/rules/birthday.rb new file mode 100644 index 0000000..682719c --- /dev/null +++ b/lib/diaspora_federation/validators/rules/birthday.rb @@ -0,0 +1,38 @@ +require "date" + +module Validation + module Rule + # Birthday validation rule + # + # Valid is: + # * nil or an empty +String+ + # * a +Date+ object + # * a +String+ with the format "yyyy-mm-dd" and is a valid +Date+, example: 2015-07-25 + class Birthday + # The error key for this rule + # @return [Symbol] error key + def error_key + :birthday + end + + # Determines if value is a valid birthday date + def valid_value?(value) + return true if value.nil? || (value.is_a?(String) && value.empty?) + return true if value.is_a? Date + + if value =~ /[0-9]{4}\-[0-9]{2}\-[0-9]{2}/ + date_field = value.split("-").map(&:to_i) + return Date.valid_civil?(date_field[0], date_field[1], date_field[2]) + end + + false + end + + # This rule has no params + # @return [Hash] params + def params + {} + end + end + end +end diff --git a/lib/diaspora_federation/validators/rules/boolean.rb b/lib/diaspora_federation/validators/rules/boolean.rb new file mode 100644 index 0000000..f2f4918 --- /dev/null +++ b/lib/diaspora_federation/validators/rules/boolean.rb @@ -0,0 +1,38 @@ +module Validation + module Rule + # Boolean validation rule + # + # Valid is: + # * a +String+: "true", "false", "t", "f", "yes", "no", "y", "n", "1", "0" + # * a +Fixnum+: 1 or 0 + # * a +Boolean+: true or false + class Boolean + # The error key for this rule + # @return [Symbol] error key + def error_key + :boolean + end + + # Determines if value is a valid +boolean+ + def valid_value?(value) + return false if value.nil? + + if value.is_a?(String) + true if value =~ /\A(true|false|t|f|yes|no|y|n|1|0)\z/i + elsif value.is_a?(Fixnum) + true if value == 1 || value == 0 + elsif [true, false].include? value + true + else + false + end + end + + # This rule has no params + # @return [Hash] params + def params + {} + end + end + end +end diff --git a/lib/diaspora_federation/validators/rules/diaspora_id.rb b/lib/diaspora_federation/validators/rules/diaspora_id.rb new file mode 100644 index 0000000..591c621 --- /dev/null +++ b/lib/diaspora_federation/validators/rules/diaspora_id.rb @@ -0,0 +1,46 @@ +module Validation + module Rule + # Diaspora ID validation rule + # + # This rule is based on https://github.com/zombor/Validator/blob/master/lib/validation/rule/email.rb + # which was adapted from https://github.com/emmanuel/aequitas/blob/master/lib/aequitas/rule/format/email_address.rb + class DiasporaId + # The Regex for a valid diaspora ID + DIASPORA_ID = begin + letter = "a-zA-Z" + digit = "0-9" + username = "[#{letter}#{digit}\-\_\.]+" + atext = "[#{letter}#{digit}+\=\-\_]" + dot_atom = "#{atext}+([.]#{atext}*)*" + no_ws_ctl = '\x01-\x08\x11\x12\x14-\x1f\x7f' + text = '[\x01-\x09\x11\x12\x14-\x7f]' + quoted_pair = "(\\x5c#{text})" + dtext = "[#{no_ws_ctl}\\x21-\\x5a\\x5e-\\x7e]" + dcontent = "(?:#{dtext}|#{quoted_pair})" + domain_literal = "\\[#{dcontent}+\\]" + domain = "(?:#{dot_atom}|#{domain_literal})" + port = "(:[#{digit}]+)?" + addr_spec = "#{username}\@#{domain}#{port}" + + /\A#{addr_spec}\z/u + end + + # The error key for this rule + # @return [Symbol] error key + def error_key + :diaspora_id + end + + # Determines if value is a valid diaspora ID + def valid_value?(value) + !DIASPORA_ID.match(value).nil? + end + + # This rule has no params + # @return [Hash] params + def params + {} + end + end + end +end diff --git a/lib/diaspora_federation/validators/rules/guid.rb b/lib/diaspora_federation/validators/rules/guid.rb new file mode 100644 index 0000000..73077fa --- /dev/null +++ b/lib/diaspora_federation/validators/rules/guid.rb @@ -0,0 +1,28 @@ +module Validation + module Rule + # GUID validation rule + # + # Valid is a +String+ that is at least 16 chars long and contains only: + # * Letters: a-z + # * Numbers: 0-9 + # * Special chars: '-', '_', '@', '.' and ':' + class Guid + # The error key for this rule + # @return [Symbol] error key + def error_key + :guid + end + + # Determines if value is a valid +GUID+ + def valid_value?(value) + value.is_a?(String) && value.downcase =~ /\A[0-9a-z\-_@.:]{16,}\z/ + end + + # This rule has no params + # @return [Hash] params + def params + {} + end + end + end +end diff --git a/lib/diaspora_federation/validators/rules/nilable_uri.rb b/lib/diaspora_federation/validators/rules/nilable_uri.rb new file mode 100644 index 0000000..727d460 --- /dev/null +++ b/lib/diaspora_federation/validators/rules/nilable_uri.rb @@ -0,0 +1,19 @@ +module Validation + module Rule + # URI validation rule + + # It allows +nil+, so maybe add an additional {Rule::NotNil} rule. + class NilableURI < Validation::Rule::URI + # The error key for this rule + # @return [Symbol] error key + def error_key + :nilableURI + end + + # Determines if value is a valid URI + def valid_value?(uri_string) + uri_string.nil? || super + end + end + end +end diff --git a/lib/diaspora_federation/validators/rules/not_nil.rb b/lib/diaspora_federation/validators/rules/not_nil.rb new file mode 100644 index 0000000..8d37d7e --- /dev/null +++ b/lib/diaspora_federation/validators/rules/not_nil.rb @@ -0,0 +1,23 @@ +module Validation + module Rule + # Validates that a property is not +nil+ + class NotNil + # The error key for this rule + # @return [Symbol] error key + def error_key + :not_nil + end + + # Determines if value is not nil + def valid_value?(value) + !value.nil? + end + + # This rule has no params + # @return [Hash] params + def params + {} + end + end + end +end diff --git a/lib/diaspora_federation/validators/rules/public_key.rb b/lib/diaspora_federation/validators/rules/public_key.rb new file mode 100644 index 0000000..8601fc9 --- /dev/null +++ b/lib/diaspora_federation/validators/rules/public_key.rb @@ -0,0 +1,33 @@ +module Validation + module Rule + # Public key validation rule + # + # A valid key must: + # * start with "-----BEGIN PUBLIC KEY-----" and end with "-----END PUBLIC KEY-----" + # or + # * start with "-----BEGIN RSA PUBLIC KEY-----" and end with "-----END RSA PUBLIC KEY-----" + class PublicKey + # The error key for this rule + # @return [Symbol] error key + def error_key + :public_key + end + + # Determines if value is a valid public key + def valid_value?(value) + !value.nil? && ( + (value.strip.start_with?("-----BEGIN PUBLIC KEY-----") && + value.strip.end_with?("-----END PUBLIC KEY-----")) || + (value.strip.start_with?("-----BEGIN RSA PUBLIC KEY-----") && + value.strip.end_with?("-----END RSA PUBLIC KEY-----")) + ) + end + + # This rule has no params + # @return [Hash] params + def params + {} + end + end + end +end diff --git a/lib/diaspora_federation/validators/rules/tag_count.rb b/lib/diaspora_federation/validators/rules/tag_count.rb new file mode 100644 index 0000000..6f100a8 --- /dev/null +++ b/lib/diaspora_federation/validators/rules/tag_count.rb @@ -0,0 +1,33 @@ +module Validation + module Rule + # Rule for validating the number of tags in a string. + # Only the "#" characters will be counted. + # The string can be nil. + class TagCount + # This rule must have a +maximum+ param + # @return [Hash] params + attr_reader :params + + # @param [Hash] params + # @option params [Fixnum] :maximum maximum allowed tag count + def initialize(params) + unless params.include?(:maximum) && params[:maximum].is_a?(Fixnum) + raise ArgumentError, "A number has to be specified for :maximum" + end + + @params = params + end + + # The error key for this rule + # @return [Symbol] error key + def error_key + :tag_count + end + + # Determines if value doesn't have more than +maximum+ tags + def valid_value?(value) + value.nil? || value.count("#") <= params[:maximum] + end + end + end +end diff --git a/lib/diaspora_federation/validators/web_finger_validator.rb b/lib/diaspora_federation/validators/web_finger_validator.rb new file mode 100644 index 0000000..a6aa397 --- /dev/null +++ b/lib/diaspora_federation/validators/web_finger_validator.rb @@ -0,0 +1,20 @@ +module DiasporaFederation + module Validators + # This validates a {Discovery::WebFinger} + # + # @note it does not validate the guid and public key, because it will be + # removed in the webfinger + class WebFingerValidator < Validation::Validator + include Validation + + rule :acct_uri, :not_empty + + rule :alias_url, [:not_nil, nilableURI: %i(host path)] + rule :hcard_url, [:not_nil, nilableURI: %i(host path)] + rule :seed_url, %i(not_nil nilableURI) + rule :profile_url, [:not_nil, nilableURI: %i(host path)] + rule :atom_url, [:not_nil, nilableURI: %i(host path)] + rule :salmon_url, [:not_nil, nilableURI: %i(host path)] + end + end +end diff --git a/lib/diaspora_federation/web_finger.rb b/lib/diaspora_federation/web_finger.rb deleted file mode 100644 index f0d6222..0000000 --- a/lib/diaspora_federation/web_finger.rb +++ /dev/null @@ -1,13 +0,0 @@ -module DiasporaFederation - # This module provides the namespace for the various classes implementing - # WebFinger and other protocols used for metadata discovery on remote servers - # in the Diaspora* network. - module WebFinger - end -end - -require "diaspora_federation/web_finger/exceptions" -require "diaspora_federation/web_finger/xrd_document" -require "diaspora_federation/web_finger/host_meta" -require "diaspora_federation/web_finger/web_finger" -require "diaspora_federation/web_finger/h_card" diff --git a/script/ci/travis.sh b/script/ci/travis.sh deleted file mode 100755 index 39cb575..0000000 --- a/script/ci/travis.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh - -command="bundle exec rake --trace" -exec $command diff --git a/spec/controllers/diaspora_federation/webfinger_controller_spec.rb b/spec/controllers/diaspora_federation/webfinger_controller_spec.rb index 62b42cb..98d5949 100644 --- a/spec/controllers/diaspora_federation/webfinger_controller_spec.rb +++ b/spec/controllers/diaspora_federation/webfinger_controller_spec.rb @@ -24,13 +24,13 @@ module DiasporaFederation expect(response.header["Content-Type"]).to include "application/xrd+xml" end - it "calls WebFinger::HostMeta.from_base_url with the base url" do - expect(WebFinger::HostMeta).to receive(:from_base_url).with("http://localhost:3000/").and_call_original + it "calls Discovery::HostMeta.from_base_url with the base url" do + expect(Discovery::HostMeta).to receive(:from_base_url).with("http://localhost:3000/").and_call_original get :host_meta end it "caches the xml" do - expect(WebFinger::HostMeta).to receive(:from_base_url).exactly(1).times.and_call_original + expect(Discovery::HostMeta).to receive(:from_base_url).exactly(1).times.and_call_original get :host_meta get :host_meta end @@ -48,7 +48,7 @@ module DiasporaFederation expect(response).to be_success end - it "contains the diaspora handle" do + it "contains the diaspora id" do get :legacy_webfinger, "q" => "acct:alice@localhost:3000" expect(response.body).to include "acct:alice@localhost:3000" end diff --git a/spec/entities.rb b/spec/entities.rb index 486a8a6..2ecb901 100644 --- a/spec/entities.rb +++ b/spec/entities.rb @@ -1,22 +1,39 @@ -module Entities - class TestEntity < DiasporaFederation::Entity - property :test +module DiasporaFederation + module Entities + class TestEntity < DiasporaFederation::Entity + property :test + end + + class TestDefaultEntity < DiasporaFederation::Entity + property :test1 + property :test2 + property :test3, default: true + property :test4, default: -> { true } + end + + class OtherEntity < DiasporaFederation::Entity + property :asdf + end + + class TestNestedEntity < DiasporaFederation::Entity + property :asdf + entity :test, TestEntity + entity :multi, [OtherEntity] + end + + class TestEntityWithXmlName < DiasporaFederation::Entity + property :test + property :qwer, xml_name: :asdf + end end - class TestDefaultEntity < DiasporaFederation::Entity - property :test1 - property :test2 - property :test3, default: true - property :test4, default: -> { true } - end + module Validators + class TestDefaultEntityValidator < Validation::Validator + include Validation - class OtherEntity < DiasporaFederation::Entity - property :asdf - end - - class TestNestedEntity < DiasporaFederation::Entity - property :asdf - entity :test, TestEntity - entity :multi, [OtherEntity] + rule :test1, regular_expression: {regex: /\A[^;]{,32}\z/} + rule :test2, :not_nil + rule :test3, :boolean + end end end diff --git a/spec/factories.rb b/spec/factories.rb index 88173c4..8d53271 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -5,12 +5,69 @@ def r_str end FactoryGirl.define do + sequence(:guid) { UUID.generate :compact } + sequence(:diaspora_id) {|n| "person-#{n}-#{r_str}@localhost:3000" } + sequence(:public_key) { OpenSSL::PKey::RSA.generate(1024).public_key.export } + factory :person do - sequence(:diaspora_handle) {|n| "person-#{n}-#{r_str}@localhost:3000" } + diaspora_id url "http://localhost:3000/" - serialized_public_key OpenSSL::PKey::RSA.generate(1024).public_key.export + serialized_public_key { generate(:public_key) } after(:create) do |u| u.save end end + + factory :webfinger, class: DiasporaFederation::Discovery::WebFinger do + guid + acct_uri { "acct:#{generate(:diaspora_id)}" } + alias_url "http://localhost:3000/people/0123456789abcdef" + hcard_url "http://localhost:3000/hcard/users/user" + seed_url "http://localhost:3000/" + profile_url "http://localhost:3000/u/user" + atom_url "http://localhost:3000/public/user.atom" + salmon_url "http://localhost:3000/receive/users/0123456789abcdef" + public_key + end + + factory :h_card, class: DiasporaFederation::Discovery::HCard do + guid + nickname "some_name" + full_name "my name" + first_name "my name" + last_name nil + url "http://localhost:3000/" + public_key + photo_large_url "/assets/user/default.png" + photo_medium_url "/assets/user/default.png" + photo_small_url "/assets/user/default.png" + searchable true + end + + factory :person_entity, class: DiasporaFederation::Entities::Person do + guid + diaspora_id + url "http://localhost:3000/" + exported_key { generate(:public_key) } + profile { + DiasporaFederation::Entities::Profile.new( + FactoryGirl.attributes_for(:profile_entity, diaspora_id: diaspora_id)) + } + end + + factory :profile_entity, class: DiasporaFederation::Entities::Profile do + diaspora_id + first_name "my name" + last_name nil + image_url "/assets/user/default.png" + image_url_medium "/assets/user/default.png" + image_url_small "/assets/user/default.png" + birthday "1988-07-15" + gender "Male" + bio "some text about me" + location "github" + searchable true + nsfw false + tag_string "#i #love #tags" + end end diff --git a/spec/lib/diaspora_federation/discovery/discovery_spec.rb b/spec/lib/diaspora_federation/discovery/discovery_spec.rb new file mode 100644 index 0000000..e70c933 --- /dev/null +++ b/spec/lib/diaspora_federation/discovery/discovery_spec.rb @@ -0,0 +1,192 @@ +module DiasporaFederation + describe Discovery::Discovery do + let(:host_meta_xrd) { FixtureGeneration.load_fixture("host-meta") } + let(:webfinger_xrd) { FixtureGeneration.load_fixture("legacy-webfinger") } + let(:hcard_html) { FixtureGeneration.load_fixture("hcard") } + let(:account) { alice.diaspora_id } + let(:default_image) { "http://localhost:3000/assets/user/default.png" } + + describe "#intialize" do + it "sets diaspora id" do + discovery = Discovery::Discovery.new("some_user@example.com") + expect(discovery.diaspora_id).to eq("some_user@example.com") + end + + it "downcases account and strips whitespace, and sub 'acct:'" do + discovery = Discovery::Discovery.new("acct:BIGBOY@Example.Com ") + expect(discovery.diaspora_id).to eq("bigboy@example.com") + end + end + + describe ".fetch" do + it "fetches the userdata and returns a person object" do + stub_request(:get, "https://localhost:3000/.well-known/host-meta") + .to_return(status: 200, body: host_meta_xrd) + stub_request(:get, "http://localhost:3000/webfinger?q=acct:#{account}") + .to_return(status: 200, body: webfinger_xrd) + stub_request(:get, "http://localhost:3000/hcard/users/#{alice.guid}") + .to_return(status: 200, body: hcard_html) + + person = Discovery::Discovery.new(account).fetch + + expect(person.guid).to eq(alice.guid) + expect(person.diaspora_id).to eq(account) + expect(person.url).to eq(alice.url) + expect(person.exported_key).to eq(alice.serialized_public_key) + + profile = person.profile + + expect(profile.diaspora_id).to eq(alice.diaspora_id) + expect(profile.first_name).to eq("Dummy") + expect(profile.last_name).to eq("User") + + expect(profile.image_url).to eq(default_image) + expect(profile.image_url_medium).to eq(default_image) + expect(profile.image_url_small).to eq(default_image) + end + + it "falls back to http if https fails with 404" do + stub_request(:get, "https://localhost:3000/.well-known/host-meta") + .to_return(status: 404) + stub_request(:get, "http://localhost:3000/.well-known/host-meta") + .to_return(status: 200, body: host_meta_xrd) + stub_request(:get, "http://localhost:3000/webfinger?q=acct:#{account}") + .to_return(status: 200, body: webfinger_xrd) + stub_request(:get, "http://localhost:3000/hcard/users/#{alice.guid}") + .to_return(status: 200, body: hcard_html) + + person = Discovery::Discovery.new(account).fetch + + expect(person.guid).to eq(alice.guid) + expect(person.diaspora_id).to eq(account) + end + + it "falls back to http if https fails with ssl error" do + stub_request(:get, "https://localhost:3000/.well-known/host-meta") + .to_raise(OpenSSL::SSL::SSLError) + stub_request(:get, "http://localhost:3000/.well-known/host-meta") + .to_return(status: 200, body: host_meta_xrd) + stub_request(:get, "http://localhost:3000/webfinger?q=acct:#{account}") + .to_return(status: 200, body: webfinger_xrd) + stub_request(:get, "http://localhost:3000/hcard/users/#{alice.guid}") + .to_return(status: 200, body: hcard_html) + + person = Discovery::Discovery.new(account).fetch + + expect(person.guid).to eq(alice.guid) + expect(person.diaspora_id).to eq(account) + end + + it "fails if the diaspora id does not match" do + modified_webfinger = webfinger_xrd.gsub(account, "anonther_user@example.com") + + stub_request(:get, "https://localhost:3000/.well-known/host-meta") + .to_return(status: 200, body: host_meta_xrd) + stub_request(:get, "http://localhost:3000/webfinger?q=acct:#{account}") + .to_return(status: 200, body: modified_webfinger) + + expect { Discovery::Discovery.new(account).fetch }.to raise_error Discovery::DiscoveryError + end + + it "fails if the diaspora id was not found" do + stub_request(:get, "https://localhost:3000/.well-known/host-meta") + .to_return(status: 200, body: host_meta_xrd) + stub_request(:get, "http://localhost:3000/webfinger?q=acct:#{account}") + .to_return(status: 404) + + expect { Discovery::Discovery.new(account).fetch }.to raise_error Discovery::DiscoveryError + end + + it "reads old hcard without guid and public key" do + historic_hcard_html = <<-HTML +
+

#{account}

+
+
+

User profile

+
+
Nickname
+
+ +
+
+
+
First name
+
+ +
+
+
+
Family name
+
+ +
+
+
+
Full name
+
+ +
+
+
+
URL
+
+#{alice.url} +
+
+
+
Photo
+
+ +
+
+
+
Photo
+
+ +
+
+
+
Photo
+
+ +
+
+
+
Searchable
+
+true +
+
+
+
+
+ HTML + + stub_request(:get, "https://localhost:3000/.well-known/host-meta") + .to_return(status: 200, body: host_meta_xrd) + stub_request(:get, "http://localhost:3000/webfinger?q=acct:#{account}") + .to_return(status: 200, body: webfinger_xrd) + stub_request(:get, "http://localhost:3000/hcard/users/#{alice.guid}") + .to_return(status: 200, body: historic_hcard_html) + + person = Discovery::Discovery.new(account).fetch + + expect(person.guid).to eq(alice.guid) + expect(person.diaspora_id).to eq(account) + expect(person.url).to eq(alice.url) + expect(person.exported_key).to eq(alice.serialized_public_key) + + profile = person.profile + + expect(profile.diaspora_id).to eq(alice.diaspora_id) + expect(profile.first_name).to be_nil + expect(profile.last_name).to be_nil + + expect(profile.image_url).to eq(default_image) + expect(profile.image_url_medium).to eq(default_image) + expect(profile.image_url_small).to eq(default_image) + end + end + end +end diff --git a/spec/lib/diaspora_federation/web_finger/h_card_spec.rb b/spec/lib/diaspora_federation/discovery/h_card_spec.rb similarity index 78% rename from spec/lib/diaspora_federation/web_finger/h_card_spec.rb rename to spec/lib/diaspora_federation/discovery/h_card_spec.rb index c7e124a..687645a 100644 --- a/spec/lib/diaspora_federation/web_finger/h_card_spec.rb +++ b/spec/lib/diaspora_federation/discovery/h_card_spec.rb @@ -1,10 +1,27 @@ module DiasporaFederation - describe WebFinger::HCard do + describe Discovery::HCard do let(:person) { FactoryGirl.create(:person) } let(:photo_large_url) { "#{person.url}/upload/large.png" } let(:photo_medium_url) { "#{person.url}/upload/medium.png" } let(:photo_small_url) { "#{person.url}/upload/small.png" } + let(:data) { + { + guid: person.guid, + nickname: person.nickname, + full_name: person.full_name, + url: person.url, + photo_large_url: photo_large_url, + photo_medium_url: photo_medium_url, + photo_small_url: photo_small_url, + public_key: person.serialized_public_key, + searchable: person.searchable, + first_name: person.first_name, + last_name: person.last_name + } + } + let(:klass) { Discovery::HCard } + let(:html) { <<-HTML @@ -92,36 +109,18 @@ module DiasporaFederation HTML } - it "must not create blank instances" do - expect { WebFinger::HCard.new({}) }.to raise_error ArgumentError - end + it_behaves_like "an Entity subclass" context "generation" do it "creates an instance from a data hash" do - hcard = WebFinger::HCard.new( - guid: person.guid, - nickname: person.nickname, - full_name: person.full_name, - url: person.url, - photo_large_url: photo_large_url, - photo_medium_url: photo_medium_url, - photo_small_url: photo_small_url, - public_key: person.serialized_public_key, - searchable: person.searchable, - first_name: person.first_name, - last_name: person.last_name - ) + hcard = Discovery::HCard.new(data) expect(hcard.to_html).to eq(html) end - - it "fails if nil was given" do - expect { WebFinger::HCard.new(nil) }.to raise_error ArgumentError, "expected a Hash" - end end context "parsing" do it "reads its own output" do - hcard = WebFinger::HCard.from_html(html) + hcard = Discovery::HCard.from_html(html) expect(hcard.guid).to eq(person.guid) expect(hcard.nickname).to eq(person.nickname) expect(hcard.full_name).to eq(person.full_name) @@ -137,7 +136,7 @@ HTML end it "is frozen after parsing" do - hcard = WebFinger::HCard.from_html(html) + hcard = Discovery::HCard.from_html(html) expect(hcard).to be_frozen end @@ -147,11 +146,30 @@ HTML "class=\"searchable\"><" ) - hcard = WebFinger::HCard.from_html(changed_html) + hcard = Discovery::HCard.from_html(changed_html) expect(hcard.searchable).to eq(false) end + it "name is nil if empty" do + changed_html = html.sub( + "class=\"fn\">#{person.full_name}<", + "class=\"fn\"><" + ).sub( + "class=\"given_name\">#{person.first_name}<", + "class=\"given_name\"><" + ).sub( + "class=\"family_name\">#{person.last_name}<", + "class=\"family_name\"><" + ) + + hcard = Discovery::HCard.from_html(changed_html) + + expect(hcard.full_name).to be_nil + expect(hcard.first_name).to be_nil + expect(hcard.last_name).to be_nil + end + it "reads old-style HTML" do historic_html = <<-HTML
@@ -218,7 +236,7 @@ HTML
HTML - hcard = WebFinger::HCard.from_html(historic_html) + hcard = Discovery::HCard.from_html(historic_html) expect(hcard.url).to eq(person.url) expect(hcard.photo_large_url).to eq(photo_large_url) expect(hcard.photo_medium_url).to eq(photo_medium_url) @@ -227,6 +245,9 @@ HTML expect(hcard.first_name).to eq(person.first_name) expect(hcard.last_name).to eq(person.last_name) + + expect(hcard.guid).to be_nil + expect(hcard.public_key).to be_nil end it "fails if the document is incomplete" do @@ -235,11 +256,11 @@ HTML #{person.full_name} HTML - expect { WebFinger::HCard.from_html(invalid_html) }.to raise_error WebFinger::InvalidData + expect { Discovery::HCard.from_html(invalid_html) }.to raise_error Discovery::InvalidData end it "fails if the document is not HTML" do - expect { WebFinger::HCard.from_html("") }.to raise_error WebFinger::InvalidData + expect { Discovery::HCard.from_html("") }.to raise_error Discovery::InvalidData end end end diff --git a/spec/lib/diaspora_federation/web_finger/host_meta_spec.rb b/spec/lib/diaspora_federation/discovery/host_meta_spec.rb similarity index 67% rename from spec/lib/diaspora_federation/web_finger/host_meta_spec.rb rename to spec/lib/diaspora_federation/discovery/host_meta_spec.rb index 0bb9545..66e7fc8 100644 --- a/spec/lib/diaspora_federation/web_finger/host_meta_spec.rb +++ b/spec/lib/diaspora_federation/discovery/host_meta_spec.rb @@ -1,5 +1,5 @@ module DiasporaFederation - describe WebFinger::HostMeta do + describe Discovery::HostMeta do let(:base_url) { "https://pod.example.tld/" } let(:xml) { <<-XML @@ -11,28 +11,33 @@ XML } it "must not create blank instances" do - expect { WebFinger::HostMeta.new }.to raise_error NoMethodError + expect { Discovery::HostMeta.new }.to raise_error NoMethodError end context "generation" do it "creates a nice XML document" do - hm = WebFinger::HostMeta.from_base_url(base_url) + hm = Discovery::HostMeta.from_base_url(base_url) + expect(hm.to_xml).to eq(xml) + end + + it "converts object to string" do + hm = Discovery::HostMeta.from_base_url(URI(base_url)) expect(hm.to_xml).to eq(xml) end it "appends a '/' if necessary" do - hm = WebFinger::HostMeta.from_base_url("https://pod.example.tld") + hm = Discovery::HostMeta.from_base_url("https://pod.example.tld") expect(hm.to_xml).to eq(xml) end it "fails if the base_url was omitted" do - expect { WebFinger::HostMeta.from_base_url("") }.to raise_error WebFinger::InvalidData + expect { Discovery::HostMeta.from_base_url("") }.to raise_error Discovery::InvalidData end end context "parsing" do it "parses its own output" do - hm = WebFinger::HostMeta.from_xml(xml) + hm = Discovery::HostMeta.from_xml(xml) expect(hm.webfinger_template_url).to eq("#{base_url}webfinger?q={uri}") end @@ -49,7 +54,7 @@ XML XML - hm = WebFinger::HostMeta.from_xml(historic_xml) + hm = Discovery::HostMeta.from_xml(historic_xml) expect(hm.webfinger_template_url).to eq("#{base_url}webfinger?q={uri}") end @@ -59,7 +64,7 @@ XML XML - expect { WebFinger::HostMeta.from_xml(invalid_xml) }.to raise_error WebFinger::InvalidData + expect { Discovery::HostMeta.from_xml(invalid_xml) }.to raise_error Discovery::InvalidData end it "fails if the document contains a malformed webfinger url" do @@ -69,11 +74,11 @@ XML XML - expect { WebFinger::HostMeta.from_xml(invalid_xml) }.to raise_error WebFinger::InvalidData + expect { Discovery::HostMeta.from_xml(invalid_xml) }.to raise_error Discovery::InvalidData end it "fails if the document is invalid" do - expect { WebFinger::HostMeta.from_xml("") }.to raise_error WebFinger::InvalidDocument + expect { Discovery::HostMeta.from_xml("") }.to raise_error Discovery::InvalidDocument end end end diff --git a/spec/lib/diaspora_federation/web_finger/web_finger_spec.rb b/spec/lib/diaspora_federation/discovery/web_finger_spec.rb similarity index 60% rename from spec/lib/diaspora_federation/web_finger/web_finger_spec.rb rename to spec/lib/diaspora_federation/discovery/web_finger_spec.rb index e5313b7..79d3e4f 100644 --- a/spec/lib/diaspora_federation/web_finger/web_finger_spec.rb +++ b/spec/lib/diaspora_federation/discovery/web_finger_spec.rb @@ -1,9 +1,24 @@ module DiasporaFederation - describe WebFinger::WebFinger do + describe Discovery::WebFinger do let(:person) { FactoryGirl.create(:person) } - let(:acct) { "acct:#{person.diaspora_handle}" } + let(:acct) { "acct:#{person.diaspora_id}" } let(:public_key_base64) { Base64.strict_encode64(person.serialized_public_key) } + let(:data) { + { + acct_uri: "acct:#{person.diaspora_id}", + alias_url: person.alias_url, + hcard_url: person.hcard_url, + seed_url: person.url, + profile_url: person.profile_url, + atom_url: person.atom_url, + salmon_url: person.salmon_url, + guid: person.guid, + public_key: person.serialized_public_key + } + } + let(:klass) { Discovery::WebFinger } + let(:xml) { <<-XML @@ -21,34 +36,18 @@ module DiasporaFederation XML } - it "must not create blank instances" do - expect { WebFinger::WebFinger.new({}) }.to raise_error ArgumentError - end + it_behaves_like "an Entity subclass" context "generation" do it "creates a nice XML document" do - wf = WebFinger::WebFinger.new( - acct_uri: "acct:#{person.diaspora_handle}", - alias_url: person.alias_url, - hcard_url: person.hcard_url, - seed_url: person.url, - profile_url: person.profile_url, - atom_url: person.atom_url, - salmon_url: person.salmon_url, - guid: person.guid, - public_key: person.serialized_public_key - ) + wf = Discovery::WebFinger.new(data) expect(wf.to_xml).to eq(xml) end - - it "fails if nil was given" do - expect { WebFinger::WebFinger.new(nil) }.to raise_error ArgumentError, "expected a Hash" - end end context "parsing" do it "reads its own output" do - wf = WebFinger::WebFinger.from_xml(xml) + wf = Discovery::WebFinger.from_xml(xml) expect(wf.acct_uri).to eq(acct) expect(wf.alias_url).to eq(person.alias_url) expect(wf.hcard_url).to eq(person.hcard_url) @@ -62,7 +61,7 @@ XML end it "is frozen after parsing" do - wf = WebFinger::WebFinger.from_xml(xml) + wf = Discovery::WebFinger.from_xml(xml) expect(wf).to be_frozen end @@ -84,7 +83,7 @@ XML XML - wf = WebFinger::WebFinger.from_xml(historic_xml) + wf = Discovery::WebFinger.from_xml(historic_xml) expect(wf.acct_uri).to eq(acct) expect(wf.alias_url).to eq(person.alias_url) expect(wf.hcard_url).to eq(person.hcard_url) @@ -97,17 +96,44 @@ XML expect(wf.public_key).to eq(person.serialized_public_key) end + it "reads future XML without guid and public key" do + future_xml = <<-XML + + + #{acct} + #{person.alias_url} + + + + + + +XML + + wf = Discovery::WebFinger.from_xml(future_xml) + expect(wf.acct_uri).to eq(acct) + expect(wf.alias_url).to eq(person.alias_url) + expect(wf.hcard_url).to eq(person.hcard_url) + expect(wf.seed_url).to eq(person.url) + expect(wf.profile_url).to eq(person.profile_url) + expect(wf.atom_url).to eq(person.atom_url) + expect(wf.salmon_url).to eq(person.salmon_url) + + expect(wf.guid).to be_nil + expect(wf.public_key).to be_nil + end + it "fails if the document is empty" do - invalid_xml = < XML - expect { WebFinger::WebFinger.from_xml(invalid_xml) }.to raise_error WebFinger::InvalidData + expect { Discovery::WebFinger.from_xml(invalid_xml) }.to raise_error Discovery::InvalidData end it "fails if the document is not XML" do - expect { WebFinger::WebFinger.from_xml("") }.to raise_error WebFinger::InvalidDocument + expect { Discovery::WebFinger.from_xml("") }.to raise_error Discovery::InvalidDocument end end end diff --git a/spec/lib/diaspora_federation/web_finger/xrd_document_spec.rb b/spec/lib/diaspora_federation/discovery/xrd_document_spec.rb similarity index 87% rename from spec/lib/diaspora_federation/web_finger/xrd_document_spec.rb rename to spec/lib/diaspora_federation/discovery/xrd_document_spec.rb index 18d566d..42ba34f 100644 --- a/spec/lib/diaspora_federation/web_finger/xrd_document_spec.rb +++ b/spec/lib/diaspora_federation/discovery/xrd_document_spec.rb @@ -1,5 +1,5 @@ module DiasporaFederation - describe WebFinger::XrdDocument do + describe Discovery::XrdDocument do let(:xml) { < @@ -49,7 +49,7 @@ XML context "generation" do it "creates the xml document" do - doc = WebFinger::XrdDocument.new + doc = Discovery::XrdDocument.new doc.expires = data[:expires] doc.subject = data[:subject] @@ -71,16 +71,16 @@ XML context "parsing" do it "reads the xml document" do - doc = WebFinger::XrdDocument.xml_data(xml) + doc = Discovery::XrdDocument.xml_data(xml) expect(doc).to eq(data) end it "raises InvalidDocument if the xml is empty" do - expect { WebFinger::XrdDocument.xml_data("") }.to raise_error WebFinger::InvalidDocument + expect { Discovery::XrdDocument.xml_data("") }.to raise_error Discovery::InvalidDocument end it "raises InvalidDocument if the xml is no XRD document" do - expect { WebFinger::XrdDocument.xml_data("") }.to raise_error WebFinger::InvalidDocument + expect { Discovery::XrdDocument.xml_data("") }.to raise_error Discovery::InvalidDocument end end end diff --git a/spec/lib/diaspora_federation/entities/person_spec.rb b/spec/lib/diaspora_federation/entities/person_spec.rb new file mode 100644 index 0000000..f4091ec --- /dev/null +++ b/spec/lib/diaspora_federation/entities/person_spec.rb @@ -0,0 +1,8 @@ +module DiasporaFederation + describe Entities::Person do + let(:data) { FactoryGirl.attributes_for(:person_entity) } + let(:klass) { Entities::Person } + + it_behaves_like "an Entity subclass" + end +end diff --git a/spec/lib/diaspora_federation/entities/profile_spec.rb b/spec/lib/diaspora_federation/entities/profile_spec.rb new file mode 100644 index 0000000..398f601 --- /dev/null +++ b/spec/lib/diaspora_federation/entities/profile_spec.rb @@ -0,0 +1,8 @@ +module DiasporaFederation + describe Entities::Profile do + let(:data) { FactoryGirl.attributes_for(:profile_entity) } + let(:klass) { Entities::Profile } + + it_behaves_like "an Entity subclass" + end +end diff --git a/spec/lib/diaspora_federation/entity_spec.rb b/spec/lib/diaspora_federation/entity_spec.rb index 10192e5..085bfb1 100644 --- a/spec/lib/diaspora_federation/entity_spec.rb +++ b/spec/lib/diaspora_federation/entity_spec.rb @@ -18,19 +18,50 @@ module DiasporaFederation }.to raise_error ArgumentError, "missing required properties: test1, test2" end - it "sets the defaults" do - entity = Entities::TestDefaultEntity.new(test1: 1, test2: 2) - expect(entity.to_h[:test3]).to be_truthy + context "defaults" do + it "sets the defaults" do + entity = Entities::TestDefaultEntity.new(test1: "1", test2: "2") + expect(entity.test3).to be_truthy + end + + it "handles callable defaults" do + entity = Entities::TestDefaultEntity.new(test1: "1", test2: "2") + expect(entity.test4).to be_truthy + end + + it "uses provided values over defaults" do + entity = Entities::TestDefaultEntity.new(data) + expect(entity.test3).to be_falsey + expect(entity.test4).to be_falsey + end end - it "handles callable defaults" do - entity = Entities::TestDefaultEntity.new(test1: 1, test2: 2) - expect(entity.to_h[:test4]).to be_truthy - end - - it "uses provided values over defaults" do + it "sets nil if string is empty" do + data[:test1] = "" entity = Entities::TestDefaultEntity.new(data) - expect(entity.to_h[:test3]).to be_falsey + expect(entity.test1).to be_nil + end + + context "validation" do + let(:invalid_data) { {test1: "as;df", test2: nil, test3: "no boolean"} } + + it "validates the entity and raise an error with failed properties if not valid" do + expect { + Entities::TestDefaultEntity.new(invalid_data) + }.to raise_error Entity::ValidationError, /Failed validation for properties:.*test1.*\|.*test2.*\|.*test3/ + end + + it "contains the failed rule" do + expect { + Entities::TestDefaultEntity.new(invalid_data) + }.to raise_error Entity::ValidationError, /property: test2, value: nil, rule: not_nil, with params: \{\}/ + end + + it "contains the params of the failed rule" do + expect { + Entities::TestDefaultEntity.new(invalid_data) + }.to raise_error Entity::ValidationError, /rule: regular_expression, with params: \{:regex=>.*\}/ + end end end @@ -54,7 +85,9 @@ module DiasporaFederation it "contains nodes for each of the properties" do entity = Entities::TestDefaultEntity.new(data) - entity.to_xml.children.each do |node| + xml_children = entity.to_xml.children + expect(xml_children).to have_exactly(4).items + xml_children.each do |node| expect(%w(test1 test2 test3 test4)).to include(node.name) end end @@ -85,6 +118,7 @@ module DiasporaFederation it "gets xml-ified by #to_xml" do entity = Entities::TestNestedEntity.new(nested_data) xml = entity.to_xml + expect(xml.children).to have_exactly(4).items xml.children.each do |node| expect(%w(asdf test_entity other_entity)).to include(node.name) end @@ -92,5 +126,23 @@ module DiasporaFederation expect(xml.xpath("other_entity")).to have_exactly(2).items end end + + context "xml_name" do + let(:hash) { {test: "test", qwer: "qwer"} } + + it "uses xml_name for the #to_xml" do + entity = Entities::TestEntityWithXmlName.new(hash) + xml_children = entity.to_xml.children + expect(xml_children).to have_exactly(2).items + xml_children.each do |node| + expect(%w(test asdf)).to include(node.name) + end + end + + it "should not use the xml_name for the #to_h" do + entity = Entities::TestEntityWithXmlName.new(hash) + expect(entity.to_h).to eq(hash) + end + end end end diff --git a/spec/lib/diaspora_federation/fetcher_spec.rb b/spec/lib/diaspora_federation/fetcher_spec.rb new file mode 100644 index 0000000..a84d819 --- /dev/null +++ b/spec/lib/diaspora_federation/fetcher_spec.rb @@ -0,0 +1,56 @@ +module DiasporaFederation + describe Fetcher do + describe ".get" do + it "gets the url" do + stub_request(:get, "http://www.example.com") + .to_return(body: "foobar", status: 200) + + response = Fetcher.get("http://www.example.com") + expect(response.body).to eq("foobar") + end + + it "follows redirects" do + stub_request(:get, "http://www.example.com") + .to_return(status: 302, headers: {"Location" => "http://www.example.com/redirected"}) + stub_request(:get, "http://www.example.com/redirected") + .to_return(body: "foobar", status: 200) + + response = Fetcher.get("http://www.example.com") + expect(response.body).to eq("foobar") + end + + it "follows redirects 4 times" do + stub_request(:get, "http://www.example.com") + .to_return(status: 302, headers: {"Location" => "http://www.example.com"}).times(4) + .to_return(status: 200) + + Fetcher.get("http://www.example.com") + end + + it "follows redirects not more than 4 times" do + stub_request(:get, "http://www.example.com") + .to_return(status: 302, headers: {"Location" => "http://www.example.com"}) + + expect { Fetcher.get("http://www.example.com") }.to raise_error FaradayMiddleware::RedirectLimitReached + end + + it "uses the gem name as User-Agent" do + stub_request(:get, "http://www.example.com") + .with(headers: {"User-Agent" => "DiasporaFederation/#{DiasporaFederation::VERSION}"}) + + Fetcher.get("http://www.example.com") + end + end + + describe ".connection" do + it "returns a new connection every time" do + expect(Fetcher.connection).to be_a Faraday::Connection + end + + it "returns a new connection every time" do + connection1 = Fetcher.connection + expect(Fetcher.connection).to_not be(connection1) + end + end + end +end diff --git a/spec/lib/diaspora_federation/properties_dsl_spec.rb b/spec/lib/diaspora_federation/properties_dsl_spec.rb index 8a225f2..cd8a287 100644 --- a/spec/lib/diaspora_federation/properties_dsl_spec.rb +++ b/spec/lib/diaspora_federation/properties_dsl_spec.rb @@ -8,19 +8,12 @@ module DiasporaFederation properties = dsl.class_props expect(properties).to have(1).item expect(properties.first[:name]).to eq(:test) - expect(properties.first[:type]).to eq(String) - end - - it "can name simple properties by string" do - dsl.property "test" - properties = dsl.class_props - expect(properties).to have(1).item - expect(properties.first[:name]).to eq("test") + expect(properties.first[:xml_name]).to eq(:test) expect(properties.first[:type]).to eq(String) end it "will not accept other types for names" do - [1234, true, {}].each do |val| + ["test", 1234, true, {}].each do |val| expect { dsl.property val }.to raise_error PropertiesDSL::InvalidName @@ -34,8 +27,26 @@ module DiasporaFederation properties = dsl.class_props expect(properties).to have(3).items expect(properties.map {|e| e[:name] }).to include(:test, :asdf, :zzzz) + expect(properties.map {|e| e[:xml_name] }).to include(:test, :asdf, :zzzz) properties.each {|e| expect(e[:type]).to eq(String) } end + + it "can add an xml name to simple properties with a symbol" do + dsl.property :test, xml_name: :xml_test + properties = dsl.class_props + expect(properties).to have(1).item + expect(properties.first[:name]).to eq(:test) + expect(properties.first[:xml_name]).to eq(:xml_test) + expect(properties.first[:type]).to eq(String) + end + + it "will not accept other types for xml names" do + ["test", 1234, true, {}].each do |val| + expect { + dsl.property :test, xml_name: val + }.to raise_error PropertiesDSL::InvalidName, "invalid xml_name" + end + end end context "nested entities" do @@ -75,6 +86,12 @@ module DiasporaFederation }.to raise_error PropertiesDSL::InvalidType end end + + it "can not add an xml name to a nested entity" do + expect { + dsl.entity :other, Entities::TestEntity, xml_name: :other_name + }.to raise_error ArgumentError, "xml_name is not supported for nested entities" + end end describe ".default_values" do diff --git a/spec/lib/diaspora_federation/validators/h_card_validator_spec.rb b/spec/lib/diaspora_federation/validators/h_card_validator_spec.rb new file mode 100644 index 0000000..a23887b --- /dev/null +++ b/spec/lib/diaspora_federation/validators/h_card_validator_spec.rb @@ -0,0 +1,55 @@ +module DiasporaFederation + describe Validators::HCardValidator do + let(:entity) { :h_card } + + def hcard_stub(data={}) + OpenStruct.new(FactoryGirl.attributes_for(:h_card).merge(data)) + end + + it "validates a well-formed instance" do + validator = Validators::HCardValidator.new(hcard_stub) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + + describe "#full_name" do + it_behaves_like "a name validator" do + let(:property) { :full_name } + let(:length) { 70 } + end + end + + %i(first_name last_name).each do |prop| + describe "##{prop}" do + it_behaves_like "a name validator" do + let(:property) { prop } + let(:length) { 32 } + end + end + end + + %i(photo_large_url photo_medium_url photo_small_url).each do |prop| + describe "##{prop}" do + it "must not be nil or empty" do + [nil, ""].each do |val| + validator = Validators::HCardValidator.new(hcard_stub(prop => val)) + + expect(validator).not_to be_valid + expect(validator.errors).to include(prop) + end + end + + it_behaves_like "a url path validator" do + let(:property) { prop } + end + end + end + + describe "#searchable" do + it_behaves_like "a boolean validator" do + let(:property) { :searchable } + end + end + end +end diff --git a/spec/lib/diaspora_federation/validators/person_validator_spec.rb b/spec/lib/diaspora_federation/validators/person_validator_spec.rb new file mode 100644 index 0000000..4f62ce0 --- /dev/null +++ b/spec/lib/diaspora_federation/validators/person_validator_spec.rb @@ -0,0 +1,41 @@ +module DiasporaFederation + describe Validators::PersonValidator do + let(:entity) { :person_entity } + + it "validates a well-formed instance" do + instance = OpenStruct.new(FactoryGirl.attributes_for(:person_entity)) + validator = Validators::PersonValidator.new(instance) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + + it_behaves_like "a diaspora id validator" do + let(:property) { :diaspora_id } + end + + it_behaves_like "a guid validator" do + let(:property) { :guid } + end + + context "#url" do + it_behaves_like "a url validator without path" do + let(:property) { :url } + end + end + + context "#profile" do + it "fails if profile is nil" do + instance = OpenStruct.new(FactoryGirl.attributes_for(:person_entity, profile: nil)) + validator = Validators::PersonValidator.new(instance) + + expect(validator).not_to be_valid + expect(validator.errors).to include(:profile) + end + end + + it_behaves_like "a public key validator" do + let(:property) { :exported_key } + end + end +end diff --git a/spec/lib/diaspora_federation/validators/profile_validator_spec.rb b/spec/lib/diaspora_federation/validators/profile_validator_spec.rb new file mode 100644 index 0000000..9f50174 --- /dev/null +++ b/spec/lib/diaspora_federation/validators/profile_validator_spec.rb @@ -0,0 +1,112 @@ +module DiasporaFederation + describe Validators::ProfileValidator do + let(:entity) { :profile_entity } + + def profile_stub(data={}) + OpenStruct.new(FactoryGirl.attributes_for(:profile_entity).merge(data)) + end + + it "validates a well-formed instance" do + validator = Validators::ProfileValidator.new(profile_stub) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + + it_behaves_like "a diaspora id validator" do + let(:property) { :diaspora_id } + end + + %i(first_name last_name).each do |prop| + describe "##{prop}" do + it_behaves_like "a name validator" do + let(:property) { prop } + let(:length) { 32 } + end + end + end + + %i(image_url image_url_medium image_url_small).each do |prop| + describe "##{prop}" do + it "is allowed to be nil" do + validator = Validators::ProfileValidator.new(profile_stub(prop => nil)) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + + it_behaves_like "a url path validator" do + let(:property) { prop } + end + end + end + + describe "#gender" do + it_behaves_like "a length validator" do + let(:property) { :gender } + let(:length) { 255 } + end + end + + describe "#bio" do + it_behaves_like "a length validator" do + let(:property) { :bio } + let(:length) { 65_535 } + end + end + + describe "#location" do + it_behaves_like "a length validator" do + let(:property) { :location } + let(:length) { 255 } + end + end + + describe "#birthday" do + it "may be empty or nil" do + [nil, ""].each do |val| + validator = Validators::ProfileValidator.new(profile_stub(birthday: val)) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + end + + it "may be a Date or date string" do + [Date.parse("2013-06-29"), "2013-06-29"].each do |val| + validator = Validators::ProfileValidator.new(profile_stub(birthday: val)) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + end + + it "must not be an arbitrary string or other object" do + ["asdf asdf", true, 1234].each do |val| + validator = Validators::ProfileValidator.new(profile_stub(birthday: val)) + + expect(validator).not_to be_valid + expect(validator.errors).to include(:birthday) + end + end + end + + %i(searchable nsfw).each do |prop| + describe "##{prop}" do + it_behaves_like "a boolean validator" do + let(:property) { prop } + end + end + end + + describe "#tag_string" do + it "must not contain more than 5 tags" do + validator = Validators::ProfileValidator.new( + profile_stub(tag_string: "#i #have #too #many #tags #in #my #profile")) + + expect(validator).not_to be_valid + expect(validator.errors).to include(:tag_string) + end + end + end +end diff --git a/spec/lib/diaspora_federation/validators/rules/birthday_spec.rb b/spec/lib/diaspora_federation/validators/rules/birthday_spec.rb new file mode 100644 index 0000000..6a65a6b --- /dev/null +++ b/spec/lib/diaspora_federation/validators/rules/birthday_spec.rb @@ -0,0 +1,54 @@ +describe Validation::Rule::Birthday do + it "will not accept parameters" do + validator = Validation::Validator.new({}) + expect { + validator.rule(:birthday, birthday: {param: true}) + }.to raise_error ArgumentError + end + + it "has an error key" do + expect(described_class.new.error_key).to eq(:birthday) + end + + context "validation" do + it "validates a date object" do + validator = Validation::Validator.new(OpenStruct.new(birthday: Date.new)) + validator.rule(:birthday, :birthday) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + + it "validates a string" do + validator = Validation::Validator.new(OpenStruct.new(birthday: "2015-07-19")) + validator.rule(:birthday, :birthday) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + + it "validates an empty string" do + validator = Validation::Validator.new(OpenStruct.new(birthday: "")) + validator.rule(:birthday, :birthday) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + + it "validates nil" do + validator = Validation::Validator.new(OpenStruct.new(birthday: nil)) + validator.rule(:birthday, :birthday) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + + it "fails for invalid date string" do + validator = Validation::Validator.new(OpenStruct.new(birthday: "i'm no date")) + validator.rule(:birthday, :birthday) + + expect(validator).not_to be_valid + expect(validator.errors).to include(:birthday) + end + end +end diff --git a/spec/lib/diaspora_federation/validators/rules/boolean_spec.rb b/spec/lib/diaspora_federation/validators/rules/boolean_spec.rb new file mode 100644 index 0000000..74dc2e5 --- /dev/null +++ b/spec/lib/diaspora_federation/validators/rules/boolean_spec.rb @@ -0,0 +1,74 @@ +describe Validation::Rule::Boolean do + it "will not accept parameters" do + validator = Validation::Validator.new({}) + expect { + validator.rule(:number, numeric: {param: true}) + }.to raise_error ArgumentError + end + + it "has an error key" do + expect(described_class.new.error_key).to eq(:boolean) + end + + context "validation" do + context "strings" do + it "validates boolean-esque strings" do + %w(true false yes no t f y n 1 0).each do |str| + validator = Validation::Validator.new(OpenStruct.new(boolean: str)) + validator.rule(:boolean, :boolean) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + end + + it "fails for non-boolean-esque strings" do + validator = Validation::Validator.new(OpenStruct.new(boolean: "asdf")) + validator.rule(:boolean, :boolean) + + expect(validator).not_to be_valid + expect(validator.errors).to include(:boolean) + end + end + + context "numbers" do + it "validates 0 and 1 to boolean" do + [0, 1].each do |num| + validator = Validation::Validator.new(OpenStruct.new(boolean: num)) + validator.rule(:boolean, :boolean) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + end + + it "fails for all other numbers" do + validator = Validation::Validator.new(OpenStruct.new(boolean: 1234)) + validator.rule(:boolean, :boolean) + + expect(validator).not_to be_valid + expect(validator.errors).to include(:boolean) + end + end + + context "boolean types" do + it "validates true and false" do + [true, false].each do |bln| + validator = Validation::Validator.new(OpenStruct.new(boolean: bln)) + validator.rule(:boolean, :boolean) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + end + end + + it "fails for nil" do + validator = Validation::Validator.new(OpenStruct.new(boolean: nil)) + validator.rule(:boolean, :boolean) + + expect(validator).not_to be_valid + expect(validator.errors).to include(:boolean) + end + end +end diff --git a/spec/lib/diaspora_federation/validators/rules/diaspora_id_spec.rb b/spec/lib/diaspora_federation/validators/rules/diaspora_id_spec.rb new file mode 100644 index 0000000..072004e --- /dev/null +++ b/spec/lib/diaspora_federation/validators/rules/diaspora_id_spec.rb @@ -0,0 +1,78 @@ +describe Validation::Rule::DiasporaId do + it "will not accept parameters" do + validator = Validation::Validator.new({}) + expect { + validator.rule(:diaspora_id, diaspora_id: {param: true}) + }.to raise_error ArgumentError + end + + it "has an error key" do + expect(described_class.new.error_key).to eq(:diaspora_id) + end + + context "validation" do + it "validates a normal diaspora id" do + validator = Validation::Validator.new(OpenStruct.new(diaspora_id: "some_user@example.com")) + validator.rule(:diaspora_id, :diaspora_id) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + + it "validates a diaspora id with localhost" do + validator = Validation::Validator.new(OpenStruct.new(diaspora_id: "some_user@localhost")) + validator.rule(:diaspora_id, :diaspora_id) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + + it "validates a diaspora id with port" do + validator = Validation::Validator.new(OpenStruct.new(diaspora_id: "some_user@example.com:3000")) + validator.rule(:diaspora_id, :diaspora_id) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + + it "validates a diaspora id with IPv4 address" do + validator = Validation::Validator.new(OpenStruct.new(diaspora_id: "some_user@123.45.67.89")) + validator.rule(:diaspora_id, :diaspora_id) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + + it "validates a diaspora id with IPv6 address" do + validator = Validation::Validator.new(OpenStruct.new(diaspora_id: "some_user@[2001:1234:5678:90ab:cdef::1]")) + validator.rule(:diaspora_id, :diaspora_id) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + + it "validates a diaspora id with . and -" do + validator = Validation::Validator.new(OpenStruct.new(diaspora_id: "some-fancy.user@example.com")) + validator.rule(:diaspora_id, :diaspora_id) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + + it "fails if the diaspora id contains a / in the domain-name" do + validator = Validation::Validator.new(OpenStruct.new(diaspora_id: "some_user@example.com/friendica")) + validator.rule(:diaspora_id, :diaspora_id) + + expect(validator).not_to be_valid + expect(validator.errors).to include(:diaspora_id) + end + + it "fails if the diaspora id contains a special-chars in the username" do + validator = Validation::Validator.new(OpenStruct.new(diaspora_id: "some_user$^%@example.com")) + validator.rule(:diaspora_id, :diaspora_id) + + expect(validator).not_to be_valid + expect(validator.errors).to include(:diaspora_id) + end + end +end diff --git a/spec/lib/diaspora_federation/validators/rules/guid_spec.rb b/spec/lib/diaspora_federation/validators/rules/guid_spec.rb new file mode 100644 index 0000000..272adfb --- /dev/null +++ b/spec/lib/diaspora_federation/validators/rules/guid_spec.rb @@ -0,0 +1,63 @@ +describe Validation::Rule::Guid do + it "will not accept parameters" do + validator = Validation::Validator.new({}) + expect { + validator.rule(:guid, guid: {param: true}) + }.to raise_error ArgumentError + end + + it "has an error key" do + expect(described_class.new.error_key).to eq(:guid) + end + + context "validation" do + it "validates a string at least 16 chars long, consisting of [0-9a-f] (diaspora)" do + validator = Validation::Validator.new(OpenStruct.new(guid: "abcdef0123456789")) + validator.rule(:guid, :guid) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + + it "validates a long string with random characters and [-_@.:] (redmatrix)" do + validator = Validation::Validator.new( + OpenStruct.new(guid: "1234567890ABCDefgh_ijkl-mnopqrSTUVwxyz@example.com:3000")) + validator.rule(:guid, :guid) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + + it "fails if the string is too short" do + validator = Validation::Validator.new(OpenStruct.new(guid: "012345")) + validator.rule(:guid, :guid) + + expect(validator).not_to be_valid + expect(validator.errors).to include(:guid) + end + + it "fails if the string contains invalid chars" do + validator = Validation::Validator.new(OpenStruct.new(guid: "ghijklmnopqrstuvwxyz++")) + validator.rule(:guid, :guid) + + expect(validator).not_to be_valid + expect(validator.errors).to include(:guid) + end + + it "fails if the string is empty" do + validator = Validation::Validator.new(OpenStruct.new(guid: "")) + validator.rule(:guid, :guid) + + expect(validator).not_to be_valid + expect(validator.errors).to include(:guid) + end + + it "fails if the string is nil" do + validator = Validation::Validator.new(OpenStruct.new(guid: nil)) + validator.rule(:guid, :guid) + + expect(validator).not_to be_valid + expect(validator.errors).to include(:guid) + end + end +end diff --git a/spec/lib/diaspora_federation/validators/rules/nilable_uri_spec.rb b/spec/lib/diaspora_federation/validators/rules/nilable_uri_spec.rb new file mode 100644 index 0000000..691e4fd --- /dev/null +++ b/spec/lib/diaspora_federation/validators/rules/nilable_uri_spec.rb @@ -0,0 +1,57 @@ +describe Validation::Rule::NilableURI do + it "has an error key" do + expect(described_class.new.error_key).to eq(:nilableURI) + end + + context "validation" do + it "validates a valid uri" do + validator = Validation::Validator.new(OpenStruct.new(uri: "http://example.com")) + validator.rule(:uri, :nilableURI) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + + it "validates nil" do + validator = Validation::Validator.new(OpenStruct.new(uri: nil)) + validator.rule(:uri, :nilableURI) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + + it "fails when given an invalid uri" do + validator = Validation::Validator.new(OpenStruct.new(uri: "foo:/%urim")) + validator.rule(:uri, :nilableURI) + + expect(validator).not_to be_valid + expect(validator.errors).to include(:uri) + end + + context "part validation" do + it "fails to validate when given a uri without a host" do + validator = Validation::Validator.new(OpenStruct.new(uri: "http:foo@")) + validator.rule(:uri, :nilableURI) + + expect(validator).not_to be_valid + expect(validator.errors).to include(:uri) + end + + it "fails to validate when given a uri without a scheme" do + validator = Validation::Validator.new(OpenStruct.new(uri: "example.com")) + validator.rule(:uri, :nilableURI) + + expect(validator).not_to be_valid + expect(validator.errors).to include(:uri) + end + + it "fails to validate when given a uri without a path" do + validator = Validation::Validator.new(OpenStruct.new(uri: "http://example.com")) + validator.rule(:uri, nilableURI: %i(host path)) + + expect(validator).not_to be_valid + expect(validator.errors).to include(:uri) + end + end + end +end diff --git a/spec/lib/diaspora_federation/validators/rules/not_nil_spec.rb b/spec/lib/diaspora_federation/validators/rules/not_nil_spec.rb new file mode 100644 index 0000000..ae5d1e6 --- /dev/null +++ b/spec/lib/diaspora_federation/validators/rules/not_nil_spec.rb @@ -0,0 +1,38 @@ +describe Validation::Rule::NotNil do + it "will not accept parameters" do + validator = Validation::Validator.new({}) + expect { + validator.rule(:not_nil, not_nil: {param: true}) + }.to raise_error ArgumentError + end + + it "has an error key" do + expect(described_class.new.error_key).to eq(:not_nil) + end + + context "validation" do + it "validates a string " do + validator = Validation::Validator.new(OpenStruct.new(not_nil: "abcd")) + validator.rule(:not_nil, :not_nil) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + + it "validates a object " do + validator = Validation::Validator.new(OpenStruct.new(not_nil: Object.new)) + validator.rule(:not_nil, :not_nil) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + + it "fails if it is nil" do + validator = Validation::Validator.new(OpenStruct.new(not_nil: nil)) + validator.rule(:not_nil, :not_nil) + + expect(validator).not_to be_valid + expect(validator.errors).to include(:not_nil) + end + end +end diff --git a/spec/lib/diaspora_federation/validators/rules/public_key_spec.rb b/spec/lib/diaspora_federation/validators/rules/public_key_spec.rb new file mode 100644 index 0000000..f4eb9ca --- /dev/null +++ b/spec/lib/diaspora_federation/validators/rules/public_key_spec.rb @@ -0,0 +1,71 @@ +describe Validation::Rule::PublicKey do + it "will not accept parameters" do + validator = Validation::Validator.new({}) + expect { + validator.rule(:key, public_key: {param: true}) + }.to raise_error ArgumentError + end + + it "has an error key" do + expect(described_class.new.error_key).to eq(:public_key) + end + + context "validation" do + ["PUBLIC KEY", "RSA PUBLIC KEY"].each do |key_type| + context key_type do + let(:prefix) { "-----BEGIN #{key_type}-----" } + let(:suffix) { "-----END #{key_type}-----" } + + let(:key) { "#{prefix}\nAAAAAA==\n#{suffix}\n" } + + it "validates an exported RSA key" do + validator = Validation::Validator.new(OpenStruct.new(key: key)) + validator.rule(:key, :public_key) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + + it "strips whitespace" do + validator = Validation::Validator.new(OpenStruct.new(key: " \n #{key}\n \n ")) + validator.rule(:key, :public_key) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + + it "fails if the prefix is missing" do + validator = Validation::Validator.new(OpenStruct.new(key: "\nAAAAAA==\n#{suffix}\n")) + validator.rule(:key, :public_key) + + expect(validator).not_to be_valid + expect(validator.errors).to include(:key) + end + + it "fails if the suffix is missing" do + validator = Validation::Validator.new(OpenStruct.new(key: "#{prefix}\nAAAAAA==\n\n")) + validator.rule(:key, :public_key) + + expect(validator).not_to be_valid + expect(validator.errors).to include(:key) + end + + it "fails if the key is empty" do + validator = Validation::Validator.new(OpenStruct.new(key: "")) + validator.rule(:key, :public_key) + + expect(validator).not_to be_valid + expect(validator.errors).to include(:key) + end + + it "fails if the key is nil" do + validator = Validation::Validator.new(OpenStruct.new(key: nil)) + validator.rule(:key, :public_key) + + expect(validator).not_to be_valid + expect(validator.errors).to include(:key) + end + end + end + end +end diff --git a/spec/lib/diaspora_federation/validators/rules/tag_count_spec.rb b/spec/lib/diaspora_federation/validators/rules/tag_count_spec.rb new file mode 100644 index 0000000..a54e846 --- /dev/null +++ b/spec/lib/diaspora_federation/validators/rules/tag_count_spec.rb @@ -0,0 +1,57 @@ +describe Validation::Rule::TagCount do + it "requires a parameter" do + validator = Validation::Validator.new({}) + expect { + validator.rule(:tags, :tag_count) + }.to raise_error ArgumentError + end + + it "requires a integer as parameter" do + validator = Validation::Validator.new({}) + [nil, "", 5.5].each do |val| + expect { + validator.rule(:tags, tag_count: {maximum: val}) + }.to raise_error ArgumentError, "A number has to be specified for :maximum" + end + end + + it "has an error key" do + expect(described_class.new(maximum: 5).error_key).to eq(:tag_count) + end + + context "validation" do + let(:tag_str) { "#i #love #tags" } + + it "validates less tags" do + validator = Validation::Validator.new(OpenStruct.new(tags: tag_str)) + validator.rule(:tags, tag_count: {maximum: 5}) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + + it "validates exactly as many tags" do + validator = Validation::Validator.new(OpenStruct.new(tags: tag_str)) + validator.rule(:tags, tag_count: {maximum: 3}) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + + it "fails for too many tags" do + validator = Validation::Validator.new(OpenStruct.new(tags: tag_str)) + validator.rule(:tags, tag_count: {maximum: 1}) + + expect(validator).not_to be_valid + expect(validator.errors).to include(:tags) + end + + it "validates if tags are nil" do + validator = Validation::Validator.new(OpenStruct.new(tags: nil)) + validator.rule(:tags, tag_count: {maximum: 5}) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + end +end diff --git a/spec/lib/diaspora_federation/validators/web_finger_validator_spec.rb b/spec/lib/diaspora_federation/validators/web_finger_validator_spec.rb new file mode 100644 index 0000000..b9813c0 --- /dev/null +++ b/spec/lib/diaspora_federation/validators/web_finger_validator_spec.rb @@ -0,0 +1,45 @@ +module DiasporaFederation + describe Validators::WebFingerValidator do + let(:entity) { :webfinger } + + def webfinger_stub(data={}) + OpenStruct.new(FactoryGirl.attributes_for(:webfinger).merge(data)) + end + + it "validates a well-formed instance" do + validator = Validators::WebFingerValidator.new(webfinger_stub) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + + describe "#acct_uri" do + it "fails if it is nil or empty" do + [nil, ""].each do |val| + validator = Validators::WebFingerValidator.new(webfinger_stub(acct_uri: val)) + + expect(validator).not_to be_valid + expect(validator.errors).to include(:acct_uri) + end + end + end + + %i(alias_url hcard_url profile_url atom_url salmon_url).each do |prop| + describe "##{prop}" do + it_behaves_like "a url validator without path" do + let(:property) { prop } + end + + it_behaves_like "a url path validator" do + let(:property) { prop } + end + end + end + + describe "#seed_url" do + it_behaves_like "a url validator without path" do + let(:property) { :seed_url } + end + end + end +end diff --git a/spec/lib/diaspora_federation_spec.rb b/spec/lib/diaspora_federation_spec.rb index 72434c5..c875582 100644 --- a/spec/lib/diaspora_federation_spec.rb +++ b/spec/lib/diaspora_federation_spec.rb @@ -6,14 +6,31 @@ module DiasporaFederation DiasporaFederation.validate_config end - it "should fails if the server_uri is missing" do + it "should fail if the server_uri is missing" do temp = DiasporaFederation.server_uri DiasporaFederation.server_uri = nil - expect { DiasporaFederation.validate_config }.to raise_error ConfigurationError, "Missing server_uri" + expect { DiasporaFederation.validate_config }.to raise_error ConfigurationError, + "server_uri: Missing or invalid" DiasporaFederation.server_uri = temp end - it "should validate the config" do + it "should fail if the certificate_authorities is missing" do + temp = DiasporaFederation.certificate_authorities + DiasporaFederation.certificate_authorities = nil + expect { DiasporaFederation.validate_config }.to raise_error ConfigurationError, + "certificate_authorities: Not configured" + DiasporaFederation.certificate_authorities = temp + end + + it "should fail if the certificate_authorities is missing" do + temp = DiasporaFederation.certificate_authorities + DiasporaFederation.certificate_authorities = "/unknown" + expect { DiasporaFederation.validate_config }.to raise_error ConfigurationError, + "certificate_authorities: File not found: /unknown" + DiasporaFederation.certificate_authorities = temp + end + + it "should validate the callbacks" do expect(DiasporaFederation.callbacks).to receive(:definition_complete?).and_return(false) expect { DiasporaFederation.validate_config }.to raise_error ConfigurationError, "Missing handlers for " end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index b3e6ca4..ceb42ca 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -6,6 +6,7 @@ unless ENV["NO_COVERAGE"] == "true" SimpleCov::Formatter::RcovFormatter ] SimpleCov.start do + add_filter "lib/diaspora_federation/logging.rb" add_filter "spec" add_filter "test" end @@ -18,6 +19,7 @@ ENV["RAILS_ENV"] ||= "test" require File.join(File.dirname(__FILE__), "..", "test", "dummy", "config", "environment") require "rspec/rails" +require "webmock/rspec" # load factory girl factories require "factories" @@ -28,12 +30,9 @@ require "entities" # some helper methods def alice - @alice ||= Person.find_by(diaspora_handle: "alice@localhost:3000") + @alice ||= Person.find_by(diaspora_id: "alice@localhost:3000") end -# Force fixture rebuild -FileUtils.rm_f(Rails.root.join("tmp", "fixture_builder.yml")) - # Requires supporting files with custom matchers and macros, etc, # in ./support/ and its subdirectories. fixture_builder_file = "#{File.dirname(__FILE__)}/support/fixture_builder.rb" @@ -53,6 +52,10 @@ RSpec.configure do |config| config.include FactoryGirl::Syntax::Methods config.use_transactional_fixtures = true + # load fixtures + config.fixture_path = "#{::Rails.root}/test/fixtures" + config.global_fixtures = :all + config.mock_with :rspec do |mocks| # Prevents you from mocking or stubbing a method that does not exist on # a real object. This is generally recommended, and will default to diff --git a/spec/support/fixture_builder.rb b/spec/support/fixture_builder.rb index dbb7f41..a737a58 100644 --- a/spec/support/fixture_builder.rb +++ b/spec/support/fixture_builder.rb @@ -11,6 +11,6 @@ FixtureBuilder.configure do |fbuilder| # now declare objects fbuilder.factory do - FactoryGirl.create(:person, diaspora_handle: "alice@localhost:3000") + FactoryGirl.create(:person, diaspora_id: "alice@localhost:3000") end end diff --git a/spec/support/fixture_generation.rb b/spec/support/fixture_generation.rb index d7081e7..e06dec1 100644 --- a/spec/support/fixture_generation.rb +++ b/spec/support/fixture_generation.rb @@ -9,6 +9,12 @@ module FixtureGeneration file.puts(markup) end end + + def self.load_fixture(name, fixture_path=nil) + fixture_path = Rails.root.join("tmp", "fixtures") unless fixture_path + fixture_file = fixture_path.join("#{name}.fixture.html") + File.open(fixture_file).read + end end RSpec::Rails::ControllerExampleGroup.class_eval do diff --git a/spec/support/shared_entity_specs.rb b/spec/support/shared_entity_specs.rb new file mode 100644 index 0000000..8946cee --- /dev/null +++ b/spec/support/shared_entity_specs.rb @@ -0,0 +1,33 @@ +shared_examples "an Entity subclass" do + it "should be an Entity" do + expect(klass).to be < DiasporaFederation::Entity + end + + it "has its properties set" do + expect(klass.class_prop_names).to include(*data.keys) + end + + context "behaviour" do + let(:instance) { klass.new(data) } + + describe "initialize" do + it "must not create blank instances" do + expect { klass.new({}) }.to raise_error ArgumentError + end + + it "fails if nil was given" do + expect { klass.new(nil) }.to raise_error ArgumentError, "expected a Hash" + end + + it "should be frozen" do + expect(instance).to be_frozen + end + end + + describe "#to_h" do + it "should resemble the input data" do + expect(instance.to_h).to eq(data) + end + end + end +end diff --git a/spec/support/shared_validator_specs.rb b/spec/support/shared_validator_specs.rb new file mode 100644 index 0000000..74a12da --- /dev/null +++ b/spec/support/shared_validator_specs.rb @@ -0,0 +1,211 @@ +def entity_stub(entity, property, val) + instance = OpenStruct.new(FactoryGirl.attributes_for(entity)) + instance.public_send("#{property}=", val) + instance +end + +ALPHANUMERIC_RANGE = [*"0".."9", *"A".."Z", *"a".."z"] + +def alphanumeric_string(length) + Array.new(length) { ALPHANUMERIC_RANGE.sample }.join +end + +shared_examples "a diaspora id validator" do + it "must not be nil or empty" do + [nil, ""].each do |val| + validator = described_class.new(entity_stub(entity, property, val)) + + expect(validator).not_to be_valid + expect(validator.errors).to include(property) + end + end + + it "must be a valid diaspora id" do + validator = described_class.new(entity_stub(entity, property, "i am a weird diaspora id @@@ ### 12345")) + + expect(validator).not_to be_valid + expect(validator.errors).to include(property) + end +end + +shared_examples "a guid validator" do + it "validates a well-formed guid from redmatrix" do + validator = described_class.new(entity_stub(entity, property, "1234567890ABCDefgh_ijkl-mnopQR@example.com:3000")) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + + it "must be at least 16 chars" do + validator = described_class.new(entity_stub(entity, property, "aaaaaa")) + + expect(validator).not_to be_valid + expect(validator.errors).to include(property) + end + + it "must only contain [0-9a-z-_@.:]" do + validator = described_class.new(entity_stub(entity, property, "zzz+-#*$$")) + + expect(validator).not_to be_valid + expect(validator.errors).to include(property) + end + + it "must not be nil or empty" do + [nil, ""].each do |val| + validator = described_class.new(entity_stub(entity, property, val)) + + expect(validator).not_to be_valid + expect(validator.errors).to include(property) + end + end +end + +shared_examples "a boolean validator" do + it "validates a well-formed boolean" do + [true, "true", false, "false"].each do |val| + validator = described_class.new(entity_stub(entity, property, val)) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + end + + it "must not be an arbitrary string or other object" do + ["asdf", Time.zone.today, 1234].each do |val| + validator = described_class.new(entity_stub(entity, property, val)) + + expect(validator).not_to be_valid + expect(validator.errors).to include(property) + end + end +end + +shared_examples "a public key validator" do + it "fails for malformed rsa key" do + validator = described_class.new(entity_stub(entity, property, "ASDF")) + + expect(validator).not_to be_valid + expect(validator.errors).to include(property) + end + + it "must not be nil or empty" do + [nil, ""].each do |val| + validator = described_class.new(entity_stub(entity, property, val)) + + expect(validator).not_to be_valid + expect(validator.errors).to include(property) + end + end +end + +shared_examples "a name validator" do + it "is allowed to be nil or empty" do + [nil, ""].each do |val| + validator = described_class.new(entity_stub(entity, property, val)) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + end + + it "is allowed to contain special chars" do + validator = described_class.new(entity_stub(entity, property, "cool name ©")) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + + it "validates the maximum number of chars" do + validator = described_class.new(entity_stub(entity, property, alphanumeric_string(length))) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + + it "must not exceed the maximum number of chars" do + validator = described_class.new(entity_stub(entity, property, alphanumeric_string(length + 1))) + + expect(validator).not_to be_valid + expect(validator.errors).to include(property) + end + + it "must not contain semicolons" do + validator = described_class.new(entity_stub(entity, property, "asdf;qwer;yxcv")) + + expect(validator).not_to be_valid + expect(validator.errors).to include(property) + end +end + +shared_examples "a length validator" do + it "is allowed to be nil or empty" do + [nil, ""].each do |val| + validator = described_class.new(entity_stub(entity, property, val)) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + end + + it "is allowed to contain special chars" do + validator = described_class.new(entity_stub(entity, property, "cool name ©;:#%")) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + + it "validates the maximum number of chars" do + validator = described_class.new(entity_stub(entity, property, alphanumeric_string(length))) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + + it "must not exceed the maximum number of chars" do + validator = described_class.new(entity_stub(entity, property, alphanumeric_string(length + 1))) + + expect(validator).not_to be_valid + expect(validator.errors).to include(property) + end +end + +shared_examples "a url validator without path" do + it "must not be nil or empty" do + [nil, ""].each do |val| + validator = described_class.new(entity_stub(entity, property, val)) + + expect(validator).not_to be_valid + expect(validator.errors).to include(property) + end + end + + it "fails for url with special chars" do + validator = described_class.new(entity_stub(entity, property, "https://asdf$%.com")) + + expect(validator).not_to be_valid + expect(validator.errors).to include(property) + end + + it "fails for url without scheme" do + validator = described_class.new(entity_stub(entity, property, "example.com")) + + expect(validator).not_to be_valid + expect(validator.errors).to include(property) + end +end + +shared_examples "a url path validator" do + it "fails for url with special chars" do + validator = described_class.new(entity_stub(entity, property, "https://asdf$%.com/some/path")) + + expect(validator).not_to be_valid + expect(validator.errors).to include(property) + end + + it "fails for url without path" do + validator = described_class.new(entity_stub(entity, property, "https://example.com")) + + expect(validator).not_to be_valid + expect(validator.errors).to include(property) + end +end diff --git a/test/dummy/app/models/person.rb b/test/dummy/app/models/person.rb index a46a163..4fce4cd 100644 --- a/test/dummy/app/models/person.rb +++ b/test/dummy/app/models/person.rb @@ -7,7 +7,7 @@ class Person < ActiveRecord::Base def atom_url; "#{url}public/#{nickname}.atom" end def salmon_url; "#{url}receive/users/#{guid}" end - def nickname; diaspora_handle.split("@")[0] end + def nickname; diaspora_id.split("@")[0] end def photo_default_url; "#{url}assets/user/default.png" end diff --git a/test/dummy/config/initializers/diaspora_federation.rb b/test/dummy/config/initializers/diaspora_federation.rb index a41a7c5..d7883f1 100644 --- a/test/dummy/config/initializers/diaspora_federation.rb +++ b/test/dummy/config/initializers/diaspora_federation.rb @@ -1,16 +1,26 @@ -require "diaspora_federation/web_finger" +require "diaspora_federation/discovery" + +if File.file?("/etc/ssl/certs/ca-certificates.crt") + # For Debian, Ubuntu, Archlinux, Gentoo + ca_file = "/etc/ssl/certs/ca-certificates.crt" +else + # For CentOS, Fedora + ca_file = "/etc/pki/tls/certs/ca-bundle.crt" +end # configure the federation engine DiasporaFederation.configure do |config| # the pod url config.server_uri = URI("http://localhost:3000/") + config.certificate_authorities = ca_file + config.define_callbacks do - on :person_webfinger_fetch do |handle| - person = Person.find_by(diaspora_handle: handle) + on :person_webfinger_fetch do |diaspora_id| + person = Person.find_by(diaspora_id: diaspora_id) if person - DiasporaFederation::WebFinger::WebFinger.new( - acct_uri: "acct:#{person.diaspora_handle}", + DiasporaFederation::Discovery::WebFinger.new( + acct_uri: "acct:#{person.diaspora_id}", alias_url: person.alias_url, hcard_url: person.hcard_url, seed_url: person.url, @@ -26,7 +36,7 @@ DiasporaFederation.configure do |config| on :person_hcard_fetch do |guid| person = Person.find_by(guid: guid) if person - DiasporaFederation::WebFinger::HCard.new( + DiasporaFederation::Discovery::HCard.new( guid: person.guid, nickname: person.nickname, full_name: person.full_name, diff --git a/test/dummy/db/migrate/20150722224751_rename_diaspora_handle_to_diaspora_id.rb b/test/dummy/db/migrate/20150722224751_rename_diaspora_handle_to_diaspora_id.rb new file mode 100644 index 0000000..e997558 --- /dev/null +++ b/test/dummy/db/migrate/20150722224751_rename_diaspora_handle_to_diaspora_id.rb @@ -0,0 +1,5 @@ +class RenameDiasporaHandleToDiasporaId < ActiveRecord::Migration + def change + rename_column :people, :diaspora_handle, :diaspora_id + end +end diff --git a/test/dummy/db/schema.rb b/test/dummy/db/schema.rb index 7167574..dfcda3a 100644 --- a/test/dummy/db/schema.rb +++ b/test/dummy/db/schema.rb @@ -11,12 +11,12 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20150614014411) do +ActiveRecord::Schema.define(version: 20150722224751) do create_table "people", force: :cascade do |t| t.string "guid", null: false t.text "url", null: false - t.string "diaspora_handle", null: false + t.string "diaspora_id", null: false t.text "serialized_public_key", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false