pax_global_header00006660000000000000000000000064125602273270014520gustar00rootroot0000000000000052 comment=8178ff2dc898a8f49dd71f6eb46f2a09712462de globalid-0.3.6/000077500000000000000000000000001256022732700133035ustar00rootroot00000000000000globalid-0.3.6/.gitignore000066400000000000000000000000531256022732700152710ustar00rootroot00000000000000/.ruby-version /.ruby-gemset /Gemfile.lock globalid-0.3.6/.travis.yml000066400000000000000000000011421256022732700154120ustar00rootroot00000000000000--- language: ruby sudo: false cache: bundler rvm: - 1.9.3 - 2.0.0 - 2.1 - 2.2 - ruby-head - rbx-2 - jruby matrix: allow_failures: - rvm: rbx-2 - rvm: jruby - rvm: ruby-head fast_finish: true notifications: email: false irc: on_success: change on_failure: always channels: - "irc.freenode.org#rails-contrib" campfire: on_success: change on_failure: always rooms: - secure: "A6lU9PnAYUzvayOnJjKXA+lav6uQ4O8+bhFZs/wiEw3I/B/vUyNjxUlDu3qRhZHDKs3LX1zH82BC/TEP6CgVtP7hE0WfgFqaCouLzBxgzlyzQMdlObRdoO1PlNTglfph8Q7gQEY3Fo4dZmE9DfgOzY6Lkopnj7Esg2YnoZ1lrXI=" globalid-0.3.6/CHANGELOG.md000066400000000000000000000011151256022732700151120ustar00rootroot00000000000000* Added locate_many(ignore_missing: true) option that'll use #where instead of #find for bulk location, which via Active Record means missing records will be ignored instead of raise an exception. *DHH* * Fixed that purpose should be passed on when using Locator.locate_many_signed(strings, for: 'purpose'). *DHH* * Added #to_gid_param and #to_sgid_param as short-cuts for to_gid.to_param and to_sgid.to_param for convenience. *DHH* * Added support for Locator.locate_many and Locator.locate_many_signed to efficiently fetch many models. *Tom Ward, DHH* globalid-0.3.6/Gemfile000066400000000000000000000001111256022732700145670ustar00rootroot00000000000000source 'https://rubygems.org' gemspec gem 'activemodel' gem 'railties' globalid-0.3.6/MIT-LICENSE000066400000000000000000000020551256022732700147410ustar00rootroot00000000000000Copyright (c) 2014 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. globalid-0.3.6/README.md000066400000000000000000000076011256022732700145660ustar00rootroot00000000000000# Global ID - Reference models by URI [![Build Status](https://secure.travis-ci.org/rails/globalid.png)](https://travis-ci.org/rails/globalid) A Global ID is an app wide URI that uniquely identifies a model instance: gid://YourApp/Some::Model/id This is helpful when you need a single identifier to reference different classes of objects. One example is job scheduling. We need to reference a model object rather than serialize the object itself. We can pass a Global ID that can be used to locate the model when it's time to perform the job. The job scheduler doesn't need to know the details of model naming and IDs, just that it has a global identifier that references a model. Another example is a drop-down list of options, consisting of both Users and Groups. Normally we'd need to come up with our own ad hoc scheme to reference them. With Global IDs, we have a universal identifier that works for objects of both classes. ## Usage Mix `GlobalID::Identification` into any model with a `#find(id)` class method. Support is automatically included in Active Record. ```ruby >> person_gid = Person.find(1).to_global_id => #> person_gid.uri => #> person_gid.to_s => "gid://app/Person/1" >> GlobalID::Locator.locate person_gid => # ``` ### Signed Global IDs For added security GlobalIDs can also be signed to ensure that the data hasn't been tampered with. ```ruby >> person_gid = Person.find(1).to_signed_global_id => #> person_sgid = Person.find(1).to_sgid => #> person_sgid.to_s => "BAhJIh5naWQ6Ly9pZGluYWlkaS9Vc2VyLzM5NTk5BjoGRVQ=--81d7358dd5ee2ca33189bb404592df5e8d11420e" >> GlobalID::Locator.locate_signed person_sgid => # ``` You can even bump the security up some more by explaining what purpose a Signed Global ID is for. In this way evildoers can't reuse a sign-up form's SGID on the login page. For example. ```ruby >> signup_person_sgid = Person.find(1).to_sgid(for: 'signup_form') => #> GlobalID::Locator.locate_signed(signup_person_sgid, for: 'signup_form') => # ``` You can also have SGIDs that expire some time in the future. This is useful if there's a resource, people shouldn't have indefinite access to, like a share link. ```ruby >> expiring_sgid = Document.find(5).to_sgid(expires_in: 2.hours, for: 'sharing') => #> GlobalID::Locator.locate_signed(expiring_sgid, for: 'sharing') => # # More than 2 hours later... >> GlobalID::Locator.locate_signed(expiring_sgid, for: 'sharing') => nil >> explicit_expiring_sgid = SecretAgentMessage.find(5).to_sgid(expires_at: Time.now.advance(hours: 1)) => #> GlobalID::Locator.locate_signed explicit_expiring_sgid => nil ``` ### Custom App Locator A custom locator can be set for an app by calling `GlobalID::Locator.use` and providing an app locator to use for that app. A custom app locator is useful when different apps collaborate and reference each others' Global IDs. When finding a Global ID's model, the locator to use is based on the app name provided in the Global ID url. A custom locator can either be a block or a class. Using a block: ```ruby GlobalID::Locator.use :foo do |gid| FooRemote.const_get(gid.model_name).find(gid.model_id) end ``` Using a class: ```ruby GlobalID::Locator.use :bar, BarLocator.new class BarLocator def locate(gid) @search_client.search name: gid.model_name, id: gid.model_id end end ``` After defining locators as above, URIs like "gid://foo/Person/1" and "gid://bar/Person/1" will now use the foo block locator and `BarLocator` respectively. Other apps will still keep using the default locator. ## License GlobalID is released under the MIT license. globalid-0.3.6/Rakefile000066400000000000000000000003311256022732700147450ustar00rootroot00000000000000require 'bundler/gem_tasks' require 'rake/testtask' task :default => :test Rake::TestTask.new do |t| t.libs << 'test' t.test_files = FileList['test/cases/**/*_test.rb'] t.verbose = true t.warning = true end globalid-0.3.6/globalid.gemspec000066400000000000000000000012711256022732700164260ustar00rootroot00000000000000Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY s.name = 'globalid' s.version = '0.3.6' s.summary = 'Refer to any model with a URI: gid://app/class/id' s.description = 'URIs for your models makes it easy to pass references around.' s.required_ruby_version = '>= 1.9.3' s.license = 'MIT' s.author = 'David Heinemeier Hansson' s.email = 'david@loudthinking.com' s.homepage = 'http://www.rubyonrails.org' s.files = Dir['MIT-LICENSE', 'README.rdoc', 'lib/**/*'] s.require_path = 'lib' # TODO: may relax this dependency further s.add_runtime_dependency 'activesupport', '>= 4.1.0' s.add_development_dependency 'rake' end globalid-0.3.6/lib/000077500000000000000000000000001256022732700140515ustar00rootroot00000000000000globalid-0.3.6/lib/global_id.rb000066400000000000000000000003121256022732700163060ustar00rootroot00000000000000require 'global_id/global_id' autoload :SignedGlobalID, 'global_id/signed_global_id' class GlobalID autoload :Locator, 'global_id/locator' autoload :Identification, 'global_id/identification' end globalid-0.3.6/lib/global_id/000077500000000000000000000000001256022732700157655ustar00rootroot00000000000000globalid-0.3.6/lib/global_id/global_id.rb000066400000000000000000000037161256022732700202350ustar00rootroot00000000000000require 'active_support' require 'active_support/core_ext/string/inflections' # For #model_class constantize require 'active_support/core_ext/array/access' require 'active_support/core_ext/object/try' # For #find require 'active_support/core_ext/module/delegation' require 'global_id/uri/gid' class GlobalID class << self attr_reader :app def create(model, options = {}) if app = options.fetch(:app) { GlobalID.app } params = options.except(:app, :verifier, :for) new URI::GID.create(app, model, params), options else raise ArgumentError, 'An app is required to create a GlobalID. ' \ 'Pass the :app option or set the default GlobalID.app.' end end def find(gid, options = {}) parse(gid, options).try(:find, options) end def parse(gid, options = {}) gid.is_a?(self) ? gid : new(gid, options) rescue URI::Error parse_encoded_gid(gid, options) end def app=(app) @app = URI::GID.validate_app(app) end private def parse_encoded_gid(gid, options) new(Base64.urlsafe_decode64(repad_gid(gid)), options) rescue nil end # We removed the base64 padding character = during #to_param, now we're adding it back so decoding will work def repad_gid(gid) padding_chars = gid.length.modulo(4).zero? ? 0 : (4 - gid.length.modulo(4)) gid + ('=' * padding_chars) end end attr_reader :uri delegate :app, :model_name, :model_id, :params, :to_s, to: :uri def initialize(gid, options = {}) @uri = gid.is_a?(URI::GID) ? gid : URI::GID.parse(gid) end def find(options = {}) Locator.locate self, options end def model_class model_name.constantize end def ==(other) other.is_a?(GlobalID) && @uri == other.uri end def to_param # remove the = padding character for a prettier param -- it'll be added back in parse_encoded_gid Base64.urlsafe_encode64(to_s).sub(/=+$/, '') end end globalid-0.3.6/lib/global_id/identification.rb000066400000000000000000000010411256022732700212770ustar00rootroot00000000000000require 'active_support/concern' class GlobalID module Identification extend ActiveSupport::Concern def to_global_id(options = {}) @global_id ||= GlobalID.create(self, options) end alias to_gid to_global_id def to_gid_param(options = {}) to_global_id(options).to_param end def to_signed_global_id(options = {}) SignedGlobalID.create(self, options) end alias to_sgid to_signed_global_id def to_sgid_param(options = {}) to_signed_global_id(options).to_param end end end globalid-0.3.6/lib/global_id/locator.rb000066400000000000000000000160451256022732700177630ustar00rootroot00000000000000require 'active_support' require 'active_support/core_ext/enumerable' # For Enumerable#index_by class GlobalID module Locator class << self # Takes either a GlobalID or a string that can be turned into a GlobalID # # Options: # * :only - A class, module or Array of classes and/or modules that are # allowed to be located. Passing one or more classes limits instances of returned # classes to those classes or their subclasses. Passing one or more modules in limits # instances of returned classes to those including that module. If no classes or # modules match, +nil+ is returned. def locate(gid, options = {}) if gid = GlobalID.parse(gid) locator_for(gid).locate gid if find_allowed?(gid.model_class, options[:only]) end end # Takes an array of GlobalIDs or strings that can be turned into a GlobalIDs. # All GlobalIDs must belong to the same app, as they will be located using # the same locator using its locate_many method. # # By default the GlobalIDs will be located using Model.find(array_of_ids), so the # models must respond to that finder signature. # # This approach will efficiently call only one #find (or #where(id: id), when using ignore_missing) # per model class, but still interpolate the results to match the order in which the gids were passed. # # Options: # * :only - A class, module or Array of classes and/or modules that are # allowed to be located. Passing one or more classes limits instances of returned # classes to those classes or their subclasses. Passing one or more modules in limits # instances of returned classes to those including that module. If no classes or # modules match, +nil+ is returned. # * :ignore_missing - By default, locate_many will call #find on the model to locate the # ids extracted from the GIDs. In Active Record (and other data stores following the same pattern), # #find will raise an exception if a named ID can't be found. When you set this option to true, # we will use #where(id: ids) instead, which does not raise on missing records. def locate_many(gids, options = {}) if (allowed_gids = parse_allowed(gids, options[:only])).any? locator = locator_for(allowed_gids.first) locator.locate_many(allowed_gids, options) else [] end end # Takes either a SignedGlobalID or a string that can be turned into a SignedGlobalID # # Options: # * :only - A class, module or Array of classes and/or modules that are # allowed to be located. Passing one or more classes limits instances of returned # classes to those classes or their subclasses. Passing one or more modules in limits # instances of returned classes to those including that module. If no classes or # modules match, +nil+ is returned. def locate_signed(sgid, options = {}) SignedGlobalID.find sgid, options end # Takes an array of SignedGlobalIDs or strings that can be turned into a SignedGlobalIDs. # The SignedGlobalIDs are located using Model.find(array_of_ids), so the models must respond to # that finder signature. # # This approach will efficiently call only one #find per model class, but still interpolate # the results to match the order in which the gids were passed. # # Options: # * :only - A class, module or Array of classes and/or modules that are # allowed to be located. Passing one or more classes limits instances of returned # classes to those classes or their subclasses. Passing one or more modules in limits # instances of returned classes to those including that module. If no classes or # modules match, +nil+ is returned. def locate_many_signed(sgids, options = {}) locate_many sgids.collect { |sgid| SignedGlobalID.parse(sgid, options.slice(:for)) }.compact, options end # Tie a locator to an app. # Useful when different apps collaborate and reference each others' Global IDs. # # The locator can be either a block or a class. # # Using a block: # # GlobalID::Locator.use :foo do |gid| # FooRemote.const_get(gid.model_name).find(gid.model_id) # end # # Using a class: # # GlobalID::Locator.use :bar, BarLocator.new # # class BarLocator # def locate(gid) # @search_client.search name: gid.model_name, id: gid.model_id # end # end def use(app, locator = nil, &locator_block) raise ArgumentError, 'No locator provided. Pass a block or an object that responds to #locate.' unless locator || block_given? URI::GID.validate_app(app) @locators[normalize_app(app)] = locator || BlockLocator.new(locator_block) end private def locator_for(gid) @locators.fetch(normalize_app(gid.app)) do gid.model_class.respond_to?(:unscoped) ? UNSCOPED_LOCATOR : DEFAULT_LOCATOR end end def find_allowed?(model_class, only = nil) only ? Array(only).any? { |c| model_class <= c } : true end def parse_allowed(gids, only = nil) gids.collect { |gid| GlobalID.parse(gid) }.compact.select { |gid| find_allowed?(gid.model_class, only) } end def normalize_app(app) app.to_s.downcase end end private @locators = {} class DefaultLocator def locate(gid) gid.model_class.find gid.model_id end def locate_many(gids, options = {}) models_and_ids = gids.collect { |gid| [ gid.model_class, gid.model_id ] } ids_by_model = models_and_ids.group_by(&:first) loaded_by_model = Hash[ids_by_model.map { |model, ids| [ model, find_records(model, ids.map(&:last), ignore_missing: options[:ignore_missing]).index_by { |record| record.id.to_s } ] }] models_and_ids.collect { |(model, id)| loaded_by_model[model][id] }.compact end private def find_records(model_class, ids, options) if options[:ignore_missing] model_class.where(id: ids) else model_class.find(ids) end end end DEFAULT_LOCATOR = DefaultLocator.new class UnscopedLocator < DefaultLocator def locate(gid) gid.model_class.unscoped { super } end private def find_records(model_class, ids, options) model_class.unscoped { super } end end UNSCOPED_LOCATOR = UnscopedLocator.new class BlockLocator def initialize(block) @locator = block end def locate(gid) @locator.call(gid) end def locate_many(gids, options = {}) gids.map { |gid| locate(gid) } end end end end globalid-0.3.6/lib/global_id/railtie.rb000066400000000000000000000020751256022732700177470ustar00rootroot00000000000000begin require 'rails/railtie' rescue LoadError else require 'global_id' require 'active_support' require 'active_support/core_ext/string/inflections' class GlobalID # = GlobalID Railtie # Set up the signed GlobalID verifier and include Active Record support. class Railtie < Rails::Railtie # :nodoc: config.global_id = ActiveSupport::OrderedOptions.new initializer 'global_id' do |app| app.config.global_id.app ||= app.railtie_name.remove('_application').dasherize GlobalID.app = app.config.global_id.app app.config.global_id.expires_in ||= 1.month SignedGlobalID.expires_in = app.config.global_id.expires_in config.after_initialize do app.config.global_id.verifier ||= begin app.message_verifier(:signed_global_ids) rescue ArgumentError nil end SignedGlobalID.verifier = app.config.global_id.verifier end ActiveSupport.on_load(:active_record) do require 'global_id/identification' send :include, GlobalID::Identification end end end end end globalid-0.3.6/lib/global_id/signed_global_id.rb000066400000000000000000000044021256022732700215570ustar00rootroot00000000000000require 'global_id' require 'active_support/message_verifier' require 'time' class SignedGlobalID < GlobalID class ExpiredMessage < StandardError; end class << self attr_accessor :verifier def parse(sgid, options = {}) if sgid.is_a? self sgid else super verify(sgid, options), options end end # Grab the verifier from options and fall back to SignedGlobalID.verifier. # Raise ArgumentError if neither is available. def pick_verifier(options) options.fetch :verifier do verifier || raise(ArgumentError, 'Pass a `verifier:` option with an `ActiveSupport::MessageVerifier` instance, or set a default SignedGlobalID.verifier.') end end attr_accessor :expires_in DEFAULT_PURPOSE = "default" def pick_purpose(options) options.fetch :for, DEFAULT_PURPOSE end private def verify(sgid, options) metadata = pick_verifier(options).verify(sgid) raise_if_expired(metadata['expires_at']) metadata['gid'] if pick_purpose(options) == metadata['purpose'] rescue ActiveSupport::MessageVerifier::InvalidSignature, ExpiredMessage nil end def raise_if_expired(expires_at) if expires_at && Time.now.utc > Time.iso8601(expires_at) raise ExpiredMessage, 'This signed global id has expired.' end end end attr_reader :verifier, :purpose, :expires_at def initialize(gid, options = {}) super @verifier = self.class.pick_verifier(options) @purpose = self.class.pick_purpose(options) @expires_at = pick_expiration(options) end def to_s @sgid ||= @verifier.generate(to_h) end alias to_param to_s def to_h # Some serializers decodes symbol keys to symbols, others to strings. # Using string keys remedies that. { 'gid' => @uri.to_s, 'purpose' => purpose, 'expires_at' => encoded_expiration } end def ==(other) super && @purpose == other.purpose end private def encoded_expiration expires_at.utc.iso8601(3) if expires_at end def pick_expiration(options) return options[:expires_at] if options.key?(:expires_at) if expires_in = options.fetch(:expires_in) { self.class.expires_in } expires_in.from_now end end end globalid-0.3.6/lib/global_id/uri/000077500000000000000000000000001256022732700165645ustar00rootroot00000000000000globalid-0.3.6/lib/global_id/uri/gid.rb000066400000000000000000000137101256022732700176560ustar00rootroot00000000000000require 'uri/generic' require 'active_support/core_ext/module/aliasing' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/hash/indifferent_access' module URI class GID < Generic # URI::GID encodes an app unique reference to a specific model as an URI. # It has the components: app name, model class name, model id and params. # All components except params are required. # # The URI format looks like "gid://app/model_name/model_id". # # Simple metadata can be stored in params. Useful if your app has multiple databases, # for instance, and you need to find out which one to look up the model in. # # Params will be encoded as query parameters like so # "gid://app/model_name/model_id?key=value&another_key=another_value". # # Params won't be typecast, they're always strings. # For convenience params can be accessed using both strings and symbol keys. # # Multi value params aren't supported. Any params encoding multiple values under # the same key will return only the last value. For example, when decoding # params like "key=first_value&key=last_value" key will only be last_value. # # Read the documentation for +parse+, +create+ and +build+ for more. alias :app :host attr_reader :model_name, :model_id, :params # Raised when creating a Global ID for a model without an id class MissingModelIdError < URI::InvalidComponentError; end class << self # Validates +app+'s as URI hostnames containing only alphanumeric characters # and hyphens. An ArgumentError is raised if +app+ is invalid. # # URI::GID.validate_app('bcx') # => 'bcx' # URI::GID.validate_app('foo-bar') # => 'foo-bar' # # URI::GID.validate_app(nil) # => ArgumentError # URI::GID.validate_app('foo/bar') # => ArgumentError def validate_app(app) parse("gid://#{app}/Model/1").app rescue URI::Error raise ArgumentError, 'Invalid app name. ' \ 'App names must be valid URI hostnames: alphanumeric and hyphen characters only.' end # Create a new URI::GID by parsing a gid string with argument check. # # URI::GID.parse 'gid://bcx/Person/1?key=value' # # This differs from URI() and URI.parse which do not check arguments. # # URI('gid://bcx') # => URI::GID instance # URI.parse('gid://bcx') # => URI::GID instance # URI::GID.parse('gid://bcx/') # => raises URI::InvalidComponentError def parse(uri) generic_components = URI.split(uri) << nil << true # nil parser, true arg_check new(*generic_components) end # Shorthand to build a URI::GID from an app, a model and optional params. # # URI::GID.create('bcx', Person.find(5), database: 'superhumans') def create(app, model, params = nil) build app: app, model_name: model.class.name, model_id: model.id, params: params end # Create a new URI::GID from components with argument check. # # The allowed components are app, model_name, model_id and params, which can be # either a hash or an array. # # Using a hash: # # URI::GID.build(app: 'bcx', model_name: 'Person', model_id: '1', params: { key: 'value' }) # # Using an array, the arguments must be in order [app, model_name, model_id, params]: # # URI::GID.build(['bcx', 'Person', '1', key: 'value']) def build(args) parts = Util.make_components_hash(self, args) parts[:host] = parts[:app] parts[:path] = "/#{parts[:model_name]}/#{CGI.escape(parts[:model_id].to_s)}" if parts[:params] && !parts[:params].empty? parts[:query] = URI.encode_www_form(parts[:params]) end super parts end end def to_s # Implement #to_s to avoid no implicit conversion of nil into string when path is nil "gid://#{app}#{path}#{'?' + query if query}" end protected def set_path(path) set_model_components(path) unless defined?(@model_name) && @model_id super end # Ruby 2.2 uses #query= instead of #set_query def query=(query) set_params parse_query_params(query) super end # Ruby 2.1 or less uses #set_query to assign the query def set_query(query) set_params parse_query_params(query) super end def set_params(params) @params = params end private COMPONENT = [ :scheme, :app, :model_name, :model_id, :params ].freeze # Extracts model_name and model_id from the URI path. PATH_REGEXP = %r(\A/([^/]+)/?([^/]+)?\z) def check_host(host) validate_component(host) super end def check_path(path) validate_component(path) set_model_components(path, true) end def check_scheme(scheme) if scheme == 'gid' super else raise URI::BadURIError, "Not a gid:// URI scheme: #{inspect}" end end def set_model_components(path, validate = false) _, model_name, model_id = path.match(PATH_REGEXP).to_a model_id = CGI.unescape(model_id) if model_id validate_component(model_name) && validate_model_id(model_id, model_name) if validate @model_name = model_name @model_id = model_id end def validate_component(component) return component unless component.blank? raise URI::InvalidComponentError, "Expected a URI like gid://app/Person/1234: #{inspect}" end def validate_model_id(model_id, model_name) return model_id unless model_id.blank? raise MissingModelIdError, "Unable to create a Global ID for " \ "#{model_name} without a model id." end def parse_query_params(query) Hash[URI.decode_www_form(query)].with_indifferent_access if query end end @@schemes['GID'] = GID end globalid-0.3.6/lib/globalid.rb000066400000000000000000000000601256022732700161470ustar00rootroot00000000000000require 'global_id' require 'global_id/railtie' globalid-0.3.6/test/000077500000000000000000000000001256022732700142625ustar00rootroot00000000000000globalid-0.3.6/test/cases/000077500000000000000000000000001256022732700153605ustar00rootroot00000000000000globalid-0.3.6/test/cases/global_id_test.rb000066400000000000000000000170541256022732700206670ustar00rootroot00000000000000require 'helper' class GlobalIDTest < ActiveSupport::TestCase test 'value equality' do assert_equal GlobalID.new('gid://app/model/id'), GlobalID.new('gid://app/model/id') end test 'invalid app name' do assert_raises ArgumentError do GlobalID.app = '' end assert_raises ArgumentError do GlobalID.app = 'blog_app' end assert_raises ArgumentError do GlobalID.app = nil end end end class GlobalIDParamEncodedTest < ActiveSupport::TestCase setup do model = Person.new('id') @gid = GlobalID.create(model) end test 'parsing' do assert_equal GlobalID.parse(@gid.to_param), @gid end test 'finding' do found = GlobalID.find(@gid.to_param) assert_kind_of @gid.model_class, found assert_equal @gid.model_id, found.id end end class GlobalIDCreationTest < ActiveSupport::TestCase setup do @uuid = '7ef9b614-353c-43a1-a203-ab2307851990' @person_gid = GlobalID.create(Person.new(5)) @person_uuid_gid = GlobalID.create(Person.new(@uuid)) @person_namespaced_gid = GlobalID.create(Person::Child.new(4)) @person_model_gid = GlobalID.create(PersonModel.new(id: 1)) end test 'find' do assert_equal Person.find(@person_gid.model_id), @person_gid.find assert_equal Person.find(@person_uuid_gid.model_id), @person_uuid_gid.find assert_equal Person::Child.find(@person_namespaced_gid.model_id), @person_namespaced_gid.find assert_equal PersonModel.find(@person_model_gid.model_id), @person_model_gid.find end test 'find with class' do assert_equal Person.find(@person_gid.model_id), @person_gid.find(only: Person) assert_equal Person.find(@person_uuid_gid.model_id), @person_uuid_gid.find(only: Person) assert_equal PersonModel.find(@person_model_gid.model_id), @person_model_gid.find(only: PersonModel) end test 'find with class no match' do assert_nil @person_gid.find(only: Hash) assert_nil @person_uuid_gid.find(only: Array) assert_nil @person_namespaced_gid.find(only: String) assert_nil @person_model_gid.find(only: Float) end test 'find with subclass' do assert_equal Person::Child.find(@person_namespaced_gid.model_id), @person_namespaced_gid.find(only: Person) end test 'find with subclass no match' do assert_nil @person_namespaced_gid.find(only: String) end test 'find with module' do assert_equal Person.find(@person_gid.model_id), @person_gid.find(only: GlobalID::Identification) assert_equal Person.find(@person_uuid_gid.model_id), @person_uuid_gid.find(only: GlobalID::Identification) assert_equal PersonModel.find(@person_model_gid.model_id), @person_model_gid.find(only: ActiveModel::Model) assert_equal Person::Child.find(@person_namespaced_gid.model_id), @person_namespaced_gid.find(only: GlobalID::Identification) end test 'find with module no match' do assert_nil @person_gid.find(only: Enumerable) assert_nil @person_uuid_gid.find(only: Forwardable) assert_nil @person_namespaced_gid.find(only: Base64) assert_nil @person_model_gid.find(only: Enumerable) end test 'find with multiple class' do assert_equal Person.find(@person_gid.model_id), @person_gid.find(only: [Fixnum, Person]) assert_equal Person.find(@person_uuid_gid.model_id), @person_uuid_gid.find(only: [Fixnum, Person]) assert_equal PersonModel.find(@person_model_gid.model_id), @person_model_gid.find(only: [Float, PersonModel]) assert_equal Person::Child.find(@person_namespaced_gid.model_id), @person_namespaced_gid.find(only: [Person, Person::Child]) end test 'find with multiple class no match' do assert_nil @person_gid.find(only: [Fixnum, Numeric]) assert_nil @person_uuid_gid.find(only: [Fixnum, String]) assert_nil @person_model_gid.find(only: [Array, Hash]) assert_nil @person_namespaced_gid.find(only: [String, Set]) end test 'find with multiple module' do assert_equal Person.find(@person_gid.model_id), @person_gid.find(only: [Enumerable, GlobalID::Identification]) assert_equal Person.find(@person_uuid_gid.model_id), @person_uuid_gid.find(only: [Bignum, GlobalID::Identification]) assert_equal PersonModel.find(@person_model_gid.model_id), @person_model_gid.find(only: [String, ActiveModel::Model]) assert_equal Person::Child.find(@person_namespaced_gid.model_id), @person_namespaced_gid.find(only: [Integer, GlobalID::Identification]) end test 'find with multiple module no match' do assert_nil @person_gid.find(only: [Enumerable, Base64]) assert_nil @person_uuid_gid.find(only: [Enumerable, Forwardable]) assert_nil @person_model_gid.find(only: [Base64, Enumerable]) assert_nil @person_namespaced_gid.find(only: [Enumerable, Forwardable]) end test 'as string' do assert_equal 'gid://bcx/Person/5', @person_gid.to_s assert_equal "gid://bcx/Person/#{@uuid}", @person_uuid_gid.to_s assert_equal 'gid://bcx/Person::Child/4', @person_namespaced_gid.to_s assert_equal 'gid://bcx/PersonModel/1', @person_model_gid.to_s end test 'as param' do assert_equal 'Z2lkOi8vYmN4L1BlcnNvbi81', @person_gid.to_param assert_equal @person_gid, GlobalID.parse('Z2lkOi8vYmN4L1BlcnNvbi81') assert_equal 'Z2lkOi8vYmN4L1BlcnNvbi83ZWY5YjYxNC0zNTNjLTQzYTEtYTIwMy1hYjIzMDc4NTE5OTA', @person_uuid_gid.to_param assert_equal @person_uuid_gid, GlobalID.parse('Z2lkOi8vYmN4L1BlcnNvbi83ZWY5YjYxNC0zNTNjLTQzYTEtYTIwMy1hYjIzMDc4NTE5OTA') assert_equal 'Z2lkOi8vYmN4L1BlcnNvbjo6Q2hpbGQvNA', @person_namespaced_gid.to_param assert_equal @person_namespaced_gid, GlobalID.parse('Z2lkOi8vYmN4L1BlcnNvbjo6Q2hpbGQvNA') assert_equal 'Z2lkOi8vYmN4L1BlcnNvbk1vZGVsLzE', @person_model_gid.to_param assert_equal @person_model_gid, GlobalID.parse('Z2lkOi8vYmN4L1BlcnNvbk1vZGVsLzE') end test 'as URI' do assert_equal URI('gid://bcx/Person/5'), @person_gid.uri assert_equal URI("gid://bcx/Person/#{@uuid}"), @person_uuid_gid.uri assert_equal URI('gid://bcx/Person::Child/4'), @person_namespaced_gid.uri assert_equal URI('gid://bcx/PersonModel/1'), @person_model_gid.uri end test 'model id' do assert_equal '5', @person_gid.model_id assert_equal @uuid, @person_uuid_gid.model_id assert_equal '4', @person_namespaced_gid.model_id assert_equal '1', @person_model_gid.model_id end test 'model name' do assert_equal 'Person', @person_gid.model_name assert_equal 'Person', @person_uuid_gid.model_name assert_equal 'Person::Child', @person_namespaced_gid.model_name assert_equal 'PersonModel', @person_model_gid.model_name end test 'model class' do assert_equal Person, @person_gid.model_class assert_equal Person, @person_uuid_gid.model_class assert_equal Person::Child, @person_namespaced_gid.model_class assert_equal PersonModel, @person_model_gid.model_class end test ':app option' do person_gid = GlobalID.create(Person.new(5)) assert_equal 'gid://bcx/Person/5', person_gid.to_s person_gid = GlobalID.create(Person.new(5), app: "foo") assert_equal 'gid://foo/Person/5', person_gid.to_s assert_raise ArgumentError do person_gid = GlobalID.create(Person.new(5), app: nil) end end end class GlobalIDCustomParamsTest < ActiveSupport::TestCase test 'create custom params' do gid = GlobalID.create(Person.new(5), hello: 'world') assert_equal 'world', gid.params[:hello] end test 'parse custom params' do gid = GlobalID.parse 'gid://bcx/Person/5?hello=world' assert_equal 'world', gid.params[:hello] end end globalid-0.3.6/test/cases/global_identification_test.rb000066400000000000000000000023351256022732700232600ustar00rootroot00000000000000require 'helper' class GlobalIdentificationTest < ActiveSupport::TestCase setup do @model = PersonModel.new id: 1 end test 'creates a Global ID from self' do assert_equal GlobalID.create(@model), @model.to_global_id assert_equal GlobalID.create(@model), @model.to_gid end test 'creates a Global ID with custom params' do assert_equal GlobalID.create(@model, some: 'param'), @model.to_global_id(some: 'param') assert_equal GlobalID.create(@model, some: 'param'), @model.to_gid(some: 'param') end test 'creates a signed Global ID from self' do assert_equal SignedGlobalID.create(@model), @model.to_signed_global_id assert_equal SignedGlobalID.create(@model), @model.to_sgid end test 'creates a signed Global ID with purpose ' do assert_equal SignedGlobalID.create(@model, for: 'login'), @model.to_signed_global_id(for: 'login') assert_equal SignedGlobalID.create(@model, for: 'login'), @model.to_sgid(for: 'login') end test 'creates a signed Global ID with custom params' do assert_equal SignedGlobalID.create(@model, some: 'param'), @model.to_signed_global_id(some: 'param') assert_equal SignedGlobalID.create(@model, some: 'param'), @model.to_sgid(some: 'param') end end globalid-0.3.6/test/cases/global_locator_test.rb000066400000000000000000000214421256022732700217320ustar00rootroot00000000000000require 'helper' class GlobalLocatorTest < ActiveSupport::TestCase setup do model = Person.new('id') @gid = model.to_gid @sgid = model.to_sgid end test 'by GID' do found = GlobalID::Locator.locate(@gid) assert_kind_of @gid.model_class, found assert_equal @gid.model_id, found.id end test 'by GID with only: restriction with match' do found = GlobalID::Locator.locate(@gid, only: Person) assert_kind_of @gid.model_class, found assert_equal @gid.model_id, found.id end test 'by GID with only: restriction with match subclass' do instance = Person::Child.new gid = instance.to_gid found = GlobalID::Locator.locate(gid, only: Person) assert_kind_of gid.model_class, found assert_equal gid.model_id, found.id end test 'by GID with only: restriction with no match' do found = GlobalID::Locator.locate(@gid, only: String) assert_nil found end test 'by GID with only: restriction by multiple types' do found = GlobalID::Locator.locate(@gid, only: [String, Person]) assert_kind_of @gid.model_class, found assert_equal @gid.model_id, found.id end test 'by GID with only: restriction by module' do found = GlobalID::Locator.locate(@gid, only: GlobalID::Identification) assert_kind_of @gid.model_class, found assert_equal @gid.model_id, found.id end test 'by GID with only: restriction by module no match' do found = GlobalID::Locator.locate(@gid, only: Forwardable) assert_nil found end test 'by GID with only: restriction by multiple types w/module' do found = GlobalID::Locator.locate(@gid, only: [String, GlobalID::Identification]) assert_kind_of @gid.model_class, found assert_equal @gid.model_id, found.id end test 'by many GIDs of one class' do assert_equal [ Person.new('1'), Person.new('2') ], GlobalID::Locator.locate_many([ Person.new('1').to_gid, Person.new('2').to_gid ]) end test 'by many GIDs of mixed classes' do assert_equal [ Person.new('1'), Person::Child.new('1'), Person.new('2') ], GlobalID::Locator.locate_many([ Person.new('1').to_gid, Person::Child.new('1').to_gid, Person.new('2').to_gid ]) end test 'by many GIDs with only: restriction to match subclass' do assert_equal [ Person::Child.new('1') ], GlobalID::Locator.locate_many([ Person.new('1').to_gid, Person::Child.new('1').to_gid, Person.new('2').to_gid ], only: Person::Child) end test 'by SGID' do found = GlobalID::Locator.locate_signed(@sgid) assert_kind_of @sgid.model_class, found assert_equal @sgid.model_id, found.id end test 'by SGID with only: restriction with match' do found = GlobalID::Locator.locate_signed(@sgid, only: Person) assert_kind_of @sgid.model_class, found assert_equal @sgid.model_id, found.id end test 'by SGID with only: restriction with match subclass' do instance = Person::Child.new sgid = instance.to_sgid found = GlobalID::Locator.locate_signed(sgid, only: Person) assert_kind_of sgid.model_class, found assert_equal sgid.model_id, found.id end test 'by SGID with only: restriction with no match' do found = GlobalID::Locator.locate_signed(@sgid, only: String) assert_nil found end test 'by SGID with only: restriction by multiple types' do found = GlobalID::Locator.locate_signed(@sgid, only: [String, Person]) assert_kind_of @sgid.model_class, found assert_equal @sgid.model_id, found.id end test 'by SGID with only: restriction by module' do found = GlobalID::Locator.locate_signed(@sgid, only: GlobalID::Identification) assert_kind_of @sgid.model_class, found assert_equal @sgid.model_id, found.id end test 'by SGID with only: restriction by module no match' do found = GlobalID::Locator.locate_signed(@sgid, only: Enumerable) assert_nil found end test 'by SGID with only: restriction by multiple types w/module' do found = GlobalID::Locator.locate_signed(@sgid, only: [String, GlobalID::Identification]) assert_kind_of @sgid.model_class, found assert_equal @sgid.model_id, found.id end test 'by many SGIDs of one class' do assert_equal [ Person.new('1'), Person.new('2') ], GlobalID::Locator.locate_many_signed([ Person.new('1').to_sgid, Person.new('2').to_sgid ]) end test 'by many SGIDs of mixed classes' do assert_equal [ Person.new('1'), Person::Child.new('1'), Person.new('2') ], GlobalID::Locator.locate_many_signed([ Person.new('1').to_sgid, Person::Child.new('1').to_sgid, Person.new('2').to_sgid ]) end test 'by many SGIDs with only: restriction to match subclass' do assert_equal [ Person::Child.new('1') ], GlobalID::Locator.locate_many_signed([ Person.new('1').to_sgid, Person::Child.new('1').to_sgid, Person.new('2').to_sgid ], only: Person::Child) end test 'by GID string' do found = GlobalID::Locator.locate(@gid.to_s) assert_kind_of @gid.model_class, found assert_equal @gid.model_id, found.id end test 'by SGID string' do found = GlobalID::Locator.locate_signed(@sgid.to_s) assert_kind_of @sgid.model_class, found assert_equal @sgid.model_id, found.id end test 'by many SGID strings with for: restriction to match purpose' do assert_equal [ Person::Child.new('2') ], GlobalID::Locator.locate_many_signed([ Person.new('1').to_sgid(for: 'adoption').to_s, Person::Child.new('1').to_sgid.to_s, Person::Child.new('2').to_sgid(for: 'adoption').to_s ], for: 'adoption', only: Person::Child) end test 'by to_param encoding' do found = GlobalID::Locator.locate(@gid.to_param) assert_kind_of @gid.model_class, found assert_equal @gid.model_id, found.id end test 'by non-GID returns nil' do assert_nil GlobalID::Locator.locate 'This is not a GID' end test 'by non-SGID returns nil' do assert_nil GlobalID::Locator.locate_signed 'This is not a SGID' end test 'by invalid GID URI returns nil' do assert_nil GlobalID::Locator.locate 'http://app/Person/1' assert_nil GlobalID::Locator.locate 'gid://Person/1' assert_nil GlobalID::Locator.locate 'gid://app/Person' assert_nil GlobalID::Locator.locate 'gid://app/Person/1/2' end test 'use locator with block' do GlobalID::Locator.use :foo do |gid| :foo end with_app 'foo' do assert_equal :foo, GlobalID::Locator.locate('gid://foo/Person/1') end end test 'use locator with class' do class BarLocator def locate(gid); :bar; end def locate_many(gids, options = {}); gids.map(&:model_id); end end GlobalID::Locator.use :bar, BarLocator.new with_app 'bar' do assert_equal :bar, GlobalID::Locator.locate('gid://bar/Person/1') assert_equal ['1', '2'], GlobalID::Locator.locate_many(['gid://bar/Person/1', 'gid://bar/Person/2']) end end test 'app locator is case insensitive' do GlobalID::Locator.use :insensitive do |gid| :insensitive end with_app 'insensitive' do assert_equal :insensitive, GlobalID::Locator.locate('gid://InSeNsItIvE/Person/1') end end test 'locator name cannot have underscore' do assert_raises ArgumentError do GlobalID::Locator.use('under_score') { |gid| 'will never be found' } end end test "by valid purpose returns right model" do instance = Person.new login_sgid = instance.to_signed_global_id(for: 'login') found = GlobalID::Locator.locate_signed(login_sgid.to_s, for: 'login') assert_kind_of login_sgid.model_class, found assert_equal login_sgid.model_id, found.id end test "by invalid purpose returns nil" do instance = Person.new login_sgid = instance.to_signed_global_id(for: 'login') assert_nil GlobalID::Locator.locate_signed(login_sgid.to_s, for: 'like_button') end test "by many with one record missing leading to a raise" do assert_raises RuntimeError do GlobalID::Locator.locate_many([ Person.new('1').to_gid, Person.new(Person::HARDCODED_ID_FOR_MISSING_PERSON).to_gid ]) end end test "by many with one record missing not leading to a raise when ignoring missing" do assert_nothing_raised do GlobalID::Locator.locate_many([ Person.new('1').to_gid, Person.new(Person::HARDCODED_ID_FOR_MISSING_PERSON).to_gid ], ignore_missing: true) end end private def with_app(app) old_app, GlobalID.app = GlobalID.app, app yield ensure GlobalID.app = old_app end end class ScopedRecordLocatingTest < ActiveSupport::TestCase setup do @gid = Person::Scoped.new('1').to_gid end test "by GID with scoped record" do found = GlobalID::Locator.locate(@gid) assert_kind_of @gid.model_class, found assert_equal @gid.model_id, found.id end test "by many with scoped records" do assert_equal [ Person::Scoped.new('1'), Person::Scoped.new('2') ], GlobalID::Locator.locate_many([ Person::Scoped.new('1').to_gid, Person::Scoped.new('2').to_gid ]) end end globalid-0.3.6/test/cases/railtie_test.rb000066400000000000000000000031501256022732700203740ustar00rootroot00000000000000require 'rails' require 'global_id/railtie' require 'active_support/testing/isolation' module BlogApp class Application < Rails::Application; end end class RailtieTest < ActiveSupport::TestCase include ActiveSupport::Testing::Isolation def setup Rails.env = 'development' @app = BlogApp::Application.new @app.config.eager_load = false @app.config.logger = Logger.new(nil) end test 'GlobalID.app for Blog::Application defaults to blog' do @app.initialize! assert_equal 'blog-app', GlobalID.app end test 'GlobalID.app can be set with config.global_id.app =' do @app.config.global_id.app = 'foo' @app.initialize! assert_equal 'foo', GlobalID.app end test 'SignedGlobalID.verifier defaults to Blog::Application.message_verifier(:signed_global_ids) when secret_token is present' do @app.config.secret_token = ('x' * 30) @app.initialize! message = {id: 42} signed_message = SignedGlobalID.verifier.generate(message) assert_equal @app.message_verifier(:signed_global_ids).generate(message), signed_message end test 'SignedGlobalID.verifier defaults to nil when secret_token is not present' do @app.initialize! assert_nil SignedGlobalID.verifier end test 'SignedGlobalID.verifier can be set with config.global_id.verifier =' do custom_verifier = @app.config.global_id.verifier = ActiveSupport::MessageVerifier.new('muchSECRETsoHIDDEN', serializer: SERIALIZER) @app.initialize! message = {id: 42} signed_message = SignedGlobalID.verifier.generate(message) assert_equal custom_verifier.generate(message), signed_message end end globalid-0.3.6/test/cases/signed_global_id_test.rb000066400000000000000000000153361256022732700222210ustar00rootroot00000000000000require 'helper' require 'minitest/mock' # for stubbing Time.now as #travel doesn't have subsecond precision. class SignedGlobalIDTest < ActiveSupport::TestCase setup do @person_sgid = SignedGlobalID.create(Person.new(5)) end test 'as string' do assert_equal 'eyJnaWQiOiJnaWQ6Ly9iY3gvUGVyc29uLzUiLCJwdXJwb3NlIjoiZGVmYXVsdCIsImV4cGlyZXNfYXQiOm51bGx9--04a6f59140259756b22008c8c0f76ea5ed485579', @person_sgid.to_s end test 'model id' do assert_equal "5", @person_sgid.model_id end test 'model class' do assert_equal Person, @person_sgid.model_class end test 'value equality' do assert_equal SignedGlobalID.create(Person.new(5)), SignedGlobalID.create(Person.new(5)) end test 'value equality with an unsigned id' do assert_equal GlobalID.create(Person.new(5)), SignedGlobalID.create(Person.new(5)) end test 'to param' do assert_equal @person_sgid.to_s, @person_sgid.to_param end end class SignedGlobalIDVerifierTest < ActiveSupport::TestCase setup do @person_sgid = SignedGlobalID.create(Person.new(5)) end test 'parse raises when default verifier is nil' do gid = @person_sgid.to_s with_default_verifier nil do assert_raise ArgumentError do SignedGlobalID.parse(gid) end end end test 'create raises when default verifier is nil' do with_default_verifier nil do assert_raise ArgumentError do SignedGlobalID.create(Person.new(5)) end end end test 'create accepts a :verifier' do with_default_verifier nil do expected = SignedGlobalID.create(Person.new(5), verifier: VERIFIER) assert_equal @person_sgid, expected end end test 'new accepts a :verifier' do with_default_verifier nil do expected = SignedGlobalID.new(Person.new(5).to_gid.uri, verifier: VERIFIER) assert_equal @person_sgid, expected end end def with_default_verifier(verifier) original, SignedGlobalID.verifier = SignedGlobalID.verifier, verifier yield ensure SignedGlobalID.verifier = original end end class SignedGlobalIDPurposeTest < ActiveSupport::TestCase setup do @login_sgid = SignedGlobalID.create(Person.new(5), for: 'login') end test 'sign with purpose when :for is provided' do assert_equal "eyJnaWQiOiJnaWQ6Ly9iY3gvUGVyc29uLzUiLCJwdXJwb3NlIjoibG9naW4iLCJleHBpcmVzX2F0IjpudWxsfQ==--4b9630f3a1fb3d7d6584d95d4fac96433ec2deef", @login_sgid.to_s end test 'sign with default purpose when no :for is provided' do sgid = SignedGlobalID.create(Person.new(5)) default_sgid = SignedGlobalID.create(Person.new(5), for: "default") assert_equal "eyJnaWQiOiJnaWQ6Ly9iY3gvUGVyc29uLzUiLCJwdXJwb3NlIjoiZGVmYXVsdCIsImV4cGlyZXNfYXQiOm51bGx9--04a6f59140259756b22008c8c0f76ea5ed485579", sgid.to_s assert_equal sgid, default_sgid end test 'create accepts a :for' do expected = SignedGlobalID.create(Person.new(5), for: "login") assert_equal @login_sgid, expected end test 'new accepts a :for' do expected = SignedGlobalID.new(Person.new(5).to_gid.uri, for: 'login') assert_equal @login_sgid, expected end test 'parse returns nil when purpose mismatch' do sgid = @login_sgid.to_s assert_nil SignedGlobalID.parse sgid assert_nil SignedGlobalID.parse sgid, for: 'like_button' end test 'equal only with same purpose' do expected = SignedGlobalID.create(Person.new(5), for: 'login') like_sgid = SignedGlobalID.create(Person.new(5), for: 'like_button') no_purpose_sgid = SignedGlobalID.create(Person.new(5)) assert_equal @login_sgid, expected assert_not_equal @login_sgid, like_sgid assert_not_equal @login_sgid, no_purpose_sgid end end class SignedGlobalIDExpirationTest < ActiveSupport::TestCase setup do @uri = Person.new(5).to_gid.uri end test 'expires_in defaults to class level expiration' do with_expiration_in 1.hour do encoded_sgid = SignedGlobalID.new(@uri).to_s travel 59.minutes assert SignedGlobalID.parse(encoded_sgid) travel 2.minutes assert_not SignedGlobalID.parse(encoded_sgid) end end test 'passing in expires_in overrides class level expiration' do with_expiration_in 1.hour do encoded_sgid = SignedGlobalID.new(@uri, expires_in: 2.hours).to_s travel 1.hour assert SignedGlobalID.parse(encoded_sgid) travel 1.hour + 3.seconds assert_not SignedGlobalID.parse(encoded_sgid) end end test 'passing expires_in less than a second is not expired' do encoded_sgid = SignedGlobalID.new(@uri, expires_in: 1.second).to_s present = Time.now Time.stub :now, present + 0.5.second do assert SignedGlobalID.parse(encoded_sgid) end Time.stub :now, present + 2.seconds do assert_not SignedGlobalID.parse(encoded_sgid) end end test 'passing expires_in nil turns off expiration checking' do with_expiration_in 1.hour do encoded_sgid = SignedGlobalID.new(@uri, expires_in: nil).to_s travel 1.hour assert SignedGlobalID.parse(encoded_sgid) travel 1.hour assert SignedGlobalID.parse(encoded_sgid) end end test 'passing expires_at sets expiration date' do date = Date.today.end_of_day sgid = SignedGlobalID.new(@uri, expires_at: date) assert_equal date, sgid.expires_at travel 1.day assert_not SignedGlobalID.parse(sgid.to_s) end test 'passing nil expires_at turns off expiration checking' do with_expiration_in 1.hour do encoded_sgid = SignedGlobalID.new(@uri, expires_at: nil).to_s travel 4.hours assert SignedGlobalID.parse(encoded_sgid) end end test 'passing expires_at overrides class level expires_in' do with_expiration_in 1.hour do date = Date.tomorrow.end_of_day sgid = SignedGlobalID.new(@uri, expires_at: date) assert_equal date, sgid.expires_at travel 2.hours assert SignedGlobalID.parse(sgid.to_s) end end test 'favor expires_at over expires_in' do sgid = SignedGlobalID.new(@uri, expires_at: Date.tomorrow.end_of_day, expires_in: 1.hour) travel 1.hour assert SignedGlobalID.parse(sgid.to_s) end private def with_expiration_in(expires_in) old_expires, SignedGlobalID.expires_in = SignedGlobalID.expires_in, expires_in yield ensure SignedGlobalID.expires_in = old_expires end end class SignedGlobalIDCustomParamsTest < ActiveSupport::TestCase test 'create custom params' do sgid = SignedGlobalID.create(Person.new(5), hello: 'world') assert_equal 'world', sgid.params[:hello] end test 'parse custom params' do sgid = SignedGlobalID.parse('eyJnaWQiOiJnaWQ6Ly9iY3gvUGVyc29uLzU/aGVsbG89d29ybGQiLCJwdXJwb3NlIjoiZGVmYXVsdCIsImV4cGlyZXNfYXQiOm51bGx9--7c042f09483dec470fa1088b76d9fd946eb30ffa') assert_equal 'world', sgid.params[:hello] end end globalid-0.3.6/test/cases/uri_gid_test.rb000066400000000000000000000101401256022732700203620ustar00rootroot00000000000000require 'helper' class URI::GIDTest < ActiveSupport::TestCase setup do @gid_string = 'gid://bcx/Person/5' @gid = URI::GID.parse(@gid_string) end test 'parsed' do assert_equal @gid.app, 'bcx' assert_equal @gid.model_name, 'Person' assert_equal @gid.model_id, '5' end test 'new returns invalid gid when not checking' do assert URI::GID.new(*URI.split('gid:///')) end test 'create' do model = Person.new('5') assert_equal @gid_string, URI::GID.create('bcx', model).to_s end test 'build' do array = URI::GID.build(['bcx', 'Person', '5', nil]) assert array hash = URI::GID.build(app: 'bcx', model_name: 'Person', model_id: '5', params: nil) assert hash assert_equal array, hash end test 'build with wrong ordered array creates a wrong ordered gid' do assert_not_equal @gid_string, URI::GID.build(['Person', '5', 'bcx', nil]).to_s end test 'as String' do assert_equal @gid_string, @gid.to_s end test 'equal' do assert_equal @gid, URI::GID.parse(@gid_string) assert_not_equal @gid, URI::GID.parse('gid://bcxxx/Persona/1') end end class URI::GIDModelIDEncodingTest < ActiveSupport::TestCase test 'alphanumeric' do model = Person.new('John123') assert_equal 'gid://app/Person/John123', URI::GID.create('app', model).to_s end test 'non-alphanumeric' do model = Person.new('John Doe-Smith/Jones') assert_equal 'gid://app/Person/John+Doe-Smith%2FJones', URI::GID.create('app', model).to_s end end class URI::GIDModelIDDecodingTest < ActiveSupport::TestCase test 'alphanumeric' do assert_equal 'John123', URI::GID.parse('gid://app/Person/John123').model_id end test 'non-alphanumeric' do assert_equal 'John Doe-Smith/Jones', URI::GID.parse('gid://app/Person/John+Doe-Smith%2FJones').model_id end end class URI::GIDValidationTest < ActiveSupport::TestCase test 'missing app' do assert_invalid_component 'gid:///Person/1' end test 'missing path' do assert_invalid_component 'gid://bcx/' end test 'missing model id' do err = assert_raise(URI::GID::MissingModelIdError) { URI::GID.parse('gid://bcx/Person') } assert_match /Unable to create a Global ID for Person/, err.message end test 'too many model ids' do assert_invalid_component 'gid://bcx/Person/1/2' end test 'empty' do assert_invalid_component 'gid:///' end test 'invalid schemes' do assert_bad_uri 'http://bcx/Person/5' assert_bad_uri 'gyd://bcx/Person/5' assert_bad_uri '//bcx/Person/5' end private def assert_invalid_component(uri) assert_raise(URI::InvalidComponentError) { URI::GID.parse(uri) } end def assert_bad_uri(uri) assert_raise(URI::BadURIError) { URI::GID.parse(uri) } end end class URI::GIDAppValidationTest < ActiveSupport::TestCase test 'nil or blank apps are invalid' do assert_invalid_app nil assert_invalid_app '' end test 'apps containing non alphanumeric characters are invalid' do assert_invalid_app 'foo/bar' assert_invalid_app 'foo:bar' assert_invalid_app 'foo_bar' end test 'app with hyphen is allowed' do assert_equal 'foo-bar', URI::GID.validate_app('foo-bar') end private def assert_invalid_app(value) assert_raise(ArgumentError) { URI::GID.validate_app(value) } end end class URI::GIDParamsTest < ActiveSupport::TestCase setup do @gid = URI::GID.create('bcx', Person.find(5), hello: 'world') end test 'indifferent key access' do assert_equal 'world', @gid.params[:hello] assert_equal 'world', @gid.params['hello'] end test 'integer option' do gid = URI::GID.build(['bcx', 'Person', '5', integer: 20]) assert_equal '20', gid.params[:integer] end test 'multi value params returns last value' do gid = URI::GID.build(['bcx', 'Person', '5', multi: %w(one two)]) exp = { 'multi' => 'two' } assert_equal exp, gid.params end test 'as String' do assert_equal 'gid://bcx/Person/5?hello=world', @gid.to_s end test 'immutable params' do @gid.params[:param] = 'value' assert_not_equal 'gid://bcx/Person/5?hello=world¶m=value', @gid.to_s end end globalid-0.3.6/test/helper.rb000066400000000000000000000011741256022732700160710ustar00rootroot00000000000000require 'bundler/setup' require 'active_support' require 'active_support/testing/autorun' require 'global_id' require 'models/person' require 'models/person_model' require 'json' if ActiveSupport::TestCase.respond_to?(:test_order=) # TODO: remove check once ActiveSupport dependency is at least 4.2 ActiveSupport::TestCase.test_order = :random end GlobalID.app = 'bcx' # Default serializers is Marshal, whose format changed 1.9 -> 2.0, # so use a trivial serializer for our tests. SERIALIZER = JSON VERIFIER = ActiveSupport::MessageVerifier.new('muchSECRETsoHIDDEN', serializer: SERIALIZER) SignedGlobalID.verifier = VERIFIER globalid-0.3.6/test/models/000077500000000000000000000000001256022732700155455ustar00rootroot00000000000000globalid-0.3.6/test/models/person.rb000066400000000000000000000015541256022732700174050ustar00rootroot00000000000000class Person include GlobalID::Identification HARDCODED_ID_FOR_MISSING_PERSON = '1000' attr_reader :id def self.find(id_or_ids) if id_or_ids.is_a? Array ids = id_or_ids ids.collect { |id| find(id) } else id = id_or_ids if id == HARDCODED_ID_FOR_MISSING_PERSON raise 'Person missing' else new(id) end end end def self.where(conditions) (conditions[:id] - [HARDCODED_ID_FOR_MISSING_PERSON]).collect { |id| new(id) } end def initialize(id = 1) @id = id end def ==(other) other.is_a?(self.class) && id == other.try(:id) end end class Person::Scoped < Person def initialize(*) super @find_allowed = false end def self.unscoped @find_allowed = true yield end def self.find(*) super if @find_allowed end end class Person::Child < Person; end globalid-0.3.6/test/models/person_model.rb000066400000000000000000000003351256022732700205610ustar00rootroot00000000000000require 'active_model' class PersonModel include ActiveModel::Model include GlobalID::Identification attr_accessor :id def self.find(id) new id: id end def ==(other) id == other.try(:id) end end