Merge branch 'discovery'

This commit is contained in:
Benjamin Neff 2015-07-29 01:52:15 +02:00
commit d5ffc61448
72 changed files with 2476 additions and 273 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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?

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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 "<EntityName>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

View file

@ -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

View file

@ -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

View file

@ -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}

View file

@ -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 "<EntityName>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"

View file

@ -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
# @{<full_name> ; <diaspora_id>}
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

View file

@ -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

View file

@ -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
# @{<full_name> ; <diaspora_id>}
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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -1,4 +0,0 @@
#!/bin/sh
command="bundle exec rake --trace"
exec $command

View file

@ -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 "<Subject>acct:alice@localhost:3000</Subject>"
end

View file

@ -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

View file

@ -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

View file

@ -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
<div id="content">
<h1>#{account}</h1>
<div id="content_inner">
<div class="entity_profile vcard author" id="i">
<h2>User profile</h2>
<dl class="entity_nickname">
<dt>Nickname</dt>
<dd>
<a class="nickname url uid" href="#{alice.url}" rel="me"></a>
</dd>
</dl>
<dl class="entity_given_name">
<dt>First name</dt>
<dd>
<span class="given_name"></span>
</dd>
</dl>
<dl class="entity_family_name">
<dt>Family name</dt>
<dd>
<span class="family_name"></span>
</dd>
</dl>
<dl class="entity_fn">
<dt>Full name</dt>
<dd>
<span class="fn"></span>
</dd>
</dl>
<dl class="entity_url">
<dt>URL</dt>
<dd>
<a class="url" href="#{alice.url}" id="pod_location" rel="me">#{alice.url}</a>
</dd>
</dl>
<dl class="entity_photo">
<dt>Photo</dt>
<dd>
<img class="photo avatar" height="300px" src="#{default_image}" width="300px">
</dd>
</dl>
<dl class="entity_photo_medium">
<dt>Photo</dt>
<dd>
<img class="photo avatar" height="100px" src="#{default_image}" width="100px">
</dd>
</dl>
<dl class="entity_photo_small">
<dt>Photo</dt>
<dd>
<img class="photo avatar" height="50px" src="#{default_image}" width="50px">
</dd>
</dl>
<dl class="entity_searchable">
<dt>Searchable</dt>
<dd>
<span class="searchable">true</span>
</dd>
</dl>
</div>
</div>
</div>
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

View file

@ -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
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
@ -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
<div id="content">
@ -218,7 +236,7 @@ HTML
</div>
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
<span class="fn">#{person.full_name}</span>
</div>
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

View file

@ -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
</XRD>
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
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
</XRD>
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
<Link rel="lrdd" type="application/xrd+xml" template="#{base_url}webfinger?q="/>
</XRD>
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

View file

@ -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
<?xml version="1.0" encoding="UTF-8"?>
@ -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
</XRD>
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
<?xml version="1.0" encoding="UTF-8"?>
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
<Subject>#{acct}</Subject>
<Alias>#{person.alias_url}</Alias>
<Link rel="http://microformats.org/profile/hcard" type="text/html" href="#{person.hcard_url}"/>
<Link rel="http://joindiaspora.com/seed_location" type="text/html" href="#{person.url}"/>
<Link rel="http://webfinger.net/rel/profile-page" type="text/html" href="#{person.profile_url}"/>
<Link rel="http://schemas.google.com/g/2010#updates-from" type="application/atom+xml" href="#{person.atom_url}"/>
<Link rel="salmon" href="#{person.salmon_url}"/>
</XRD>
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
invalid_xml = <<-XML
<?xml version="1.0" encoding="UTF-8"?>
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
</XRD>
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

View file

@ -1,5 +1,5 @@
module DiasporaFederation
describe WebFinger::XrdDocument do
describe Discovery::XrdDocument do
let(:xml) {
<<XML
<?xml version="1.0" encoding="UTF-8"?>
@ -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("<html></html>") }.to raise_error WebFinger::InvalidDocument
expect { Discovery::XrdDocument.xml_data("<html></html>") }.to raise_error Discovery::InvalidDocument
end
end
end

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -0,0 +1,5 @@
class RenameDiasporaHandleToDiasporaId < ActiveRecord::Migration
def change
rename_column :people, :diaspora_handle, :diaspora_id
end
end

View file

@ -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