pax_global_header00006660000000000000000000000064141475362300014517gustar00rootroot0000000000000052 comment=beab3e4bc7d3fe1af27367de6c90b4d31618ed44 globalid-0.6.0/000077500000000000000000000000001414753623000132775ustar00rootroot00000000000000globalid-0.6.0/.devcontainer/000077500000000000000000000000001414753623000160365ustar00rootroot00000000000000globalid-0.6.0/.devcontainer/Dockerfile000066400000000000000000000017631414753623000200370ustar00rootroot00000000000000# [Choice] Ruby version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.0, 2, 2.7, 2.6, 3-bullseye, 3.0-bullseye, 2-bullseye, 2.7-bullseye, 2.6-bullseye, 3-buster, 3.0-buster, 2-buster, 2.7-buster, 2.6-buster ARG VARIANT=2-bullseye FROM mcr.microsoft.com/vscode/devcontainers/ruby:0-${VARIANT} # [Choice] Node.js version: none, lts/*, 16, 14, 12, 10 ARG NODE_VERSION="none" RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi # [Optional] Uncomment this section to install additional OS packages. # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ # && apt-get -y install --no-install-recommends # [Optional] Uncomment this line to install additional gems. # RUN gem install # [Optional] Uncomment this line to install global node packages. # RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1globalid-0.6.0/.devcontainer/base.Dockerfile000066400000000000000000000041471414753623000207470ustar00rootroot00000000000000# [Choice] Ruby version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.0, 2, 2.7, 2.6, 3-bullseye, 3.0-bullseye, 2-bullseye, 2.7-bullseye, 2.6-bullseye, 3-buster, 3.0-buster, 2-buster, 2.7-buster, 2.6-buster ARG VARIANT=2-bullseye FROM ruby:${VARIANT} # Copy library scripts to execute COPY library-scripts/*.sh library-scripts/*.env /tmp/library-scripts/ # [Option] Install zsh ARG INSTALL_ZSH="true" # [Option] Upgrade OS packages to their latest versions ARG UPGRADE_PACKAGES="true" # Install needed packages and setup non-root user. Use a separate RUN statement to add your own dependencies. ARG USERNAME=vscode ARG USER_UID=1000 ARG USER_GID=$USER_UID RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ # Remove imagemagick due to https://security-tracker.debian.org/tracker/CVE-2019-10131 && apt-get purge -y imagemagick imagemagick-6-common \ # Install common packages, non-root user, rvm, core build tools && bash /tmp/library-scripts/common-debian.sh "${INSTALL_ZSH}" "${USERNAME}" "${USER_UID}" "${USER_GID}" "${UPGRADE_PACKAGES}" "true" "true" \ && bash /tmp/library-scripts/ruby-debian.sh "none" "${USERNAME}" "true" "true" \ && apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/* # [Choice] Node.js version: none, lts/*, 16, 14, 12, 10 ARG NODE_VERSION="none" ENV NVM_DIR=/usr/local/share/nvm ENV NVM_SYMLINK_CURRENT=true \ PATH=${NVM_DIR}/current/bin:${PATH} RUN bash /tmp/library-scripts/node-debian.sh "${NVM_DIR}" "${NODE_VERSION}" "${USERNAME}" \ && apt-get clean -y && rm -rf /var/lib/apt/lists/* # Remove library scripts for final image RUN rm -rf /tmp/library-scripts # [Optional] Uncomment this section to install additional OS packages. # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ # && apt-get -y install --no-install-recommends # [Optional] Uncomment this line to install additional gems. # RUN gem install # [Optional] Uncomment this line to install global node packages. # RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1globalid-0.6.0/.devcontainer/devcontainer.json000066400000000000000000000021721414753623000214140ustar00rootroot00000000000000// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: // https://github.com/microsoft/vscode-dev-containers/tree/v0.205.2/containers/ruby { "name": "Ruby", "build": { "dockerfile": "Dockerfile", "args": { // Update 'VARIANT' to pick a Ruby version: 3, 3.0, 2, 2.7, 2.6 // Append -bullseye or -buster to pin to an OS version. // Use -bullseye variants on local on arm64/Apple Silicon. "VARIANT": "3-bullseye", // Options "NODE_VERSION": "lts/*" } }, // Set *default* container specific settings.json values on container create. "settings": {}, // Add the IDs of extensions you want installed when the container is created. "extensions": [ "rebornix.Ruby" ], // Use 'forwardPorts' to make a list of ports inside the container available locally. // "forwardPorts": [], // Use 'postCreateCommand' to run commands after the container is created. // "postCreateCommand": "ruby --version", // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. "remoteUser": "vscode", "features": { "github-cli": "latest" } }globalid-0.6.0/.github/000077500000000000000000000000001414753623000146375ustar00rootroot00000000000000globalid-0.6.0/.github/workflows/000077500000000000000000000000001414753623000166745ustar00rootroot00000000000000globalid-0.6.0/.github/workflows/ci.yml000066400000000000000000000017501414753623000200150ustar00rootroot00000000000000name: CI on: [push, pull_request] jobs: tests: runs-on: ubuntu-latest strategy: fail-fast: false matrix: ruby: [ 2.5, 2.6, 2.7 ] rails: [ '5.0', '5.1', '5.2', '6.0', '6.1' ] include: - ruby: '3.0' rails: '6.0' - ruby: '3.0' rails: '6.1' - ruby: head rails: '6.0' - ruby: head rails: '6.1' - ruby: truffleruby rails: '6.0' - ruby: truffleruby rails: '6.1' env: BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/rails_${{ matrix.rails }}.gemfile steps: - uses: actions/checkout@v2 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} bundler-cache: true - name: Run tests run: bundle exec rake test continue-on-error: ${{ matrix.ruby == 'head' || matrix.ruby == 'truffleruby' }} timeout-minutes: 3 globalid-0.6.0/.gitignore000066400000000000000000000000711414753623000152650ustar00rootroot00000000000000/.ruby-version /.ruby-gemset gemfiles/*.gemfile.lock pkg globalid-0.6.0/CHANGELOG.md000066400000000000000000000001351414753623000151070ustar00rootroot00000000000000Changelog is maintained under [Github Releases](https://github.com/rails/globalid/releases). globalid-0.6.0/CONTRIBUTING.md000066400000000000000000000052441414753623000155350ustar00rootroot00000000000000Contributing to GlobalID ===================== [![Build Status](https://secure.travis-ci.org/rails/globalid.png)](https://travis-ci.org/rails/globalid) GlobalID is work of [many contributors](https://github.com/rails/globalid/graphs/contributors). You're encouraged to submit [pull requests](https://github.com/rails/globalid/pulls), [propose features and discuss issues](https://github.com/rails/globalid/issues). #### Fork the Project Fork the [project on Github](https://github.com/rails/globalid) and check out your copy. ``` git clone https://github.com/contributor/globalid.git cd globalid git remote add upstream https://github.com/rails/globalid.git ``` #### Create a Topic Branch Make sure your fork is up-to-date and create a topic branch for your feature or bug fix. ``` git checkout master git pull upstream master git checkout -b my-feature-branch ``` #### Bundle Install and Test Ensure that you can build the project and run tests. ``` bundle install bundle exec rake test ``` #### Write Tests Try to write a test that reproduces the problem you're trying to fix or describes a feature that you want to build. Add to [test](test). We definitely appreciate pull requests that highlight or reproduce a problem, even without a fix. #### Write Code Implement your feature or bug fix. Make sure that `bundle exec rake test` completes without errors. #### Write Documentation Document any external behavior in the [README](README.md). #### Commit Changes Make sure git knows your name and email address: ``` git config --global user.name "Your Name" git config --global user.email "contributor@example.com" ``` Writing good commit logs is important. A commit log should describe what changed and why. ``` git add ... git commit ``` #### Push ``` git push origin my-feature-branch ``` #### Make a Pull Request Go to https://github.com/contributor/globalid and select your feature branch. Click the 'Pull Request' button and fill out the form. Pull requests are usually reviewed within a few days. #### Rebase If you've been working on a change for a while, rebase with upstream/master. ``` git fetch upstream git rebase upstream/master git push origin my-feature-branch -f ``` #### Check on Your Pull Request Go back to your pull request after a few minutes and see whether it passed muster with Travis-CI. Everything should look green, otherwise fix issues and amend your commit as described above. #### Be Patient It's likely that your change will not be merged and that the nitpicky maintainers will ask you to do more, or fix seemingly benign problems. Hang on there! #### Thank You Please do know that we really appreciate and value your time and work. We love you, really. globalid-0.6.0/Gemfile000066400000000000000000000001111414753623000145630ustar00rootroot00000000000000source 'https://rubygems.org' gemspec gem 'activemodel' gem 'railties' globalid-0.6.0/Gemfile.lock000066400000000000000000000032041414753623000155200ustar00rootroot00000000000000PATH remote: . specs: globalid (0.6.0) activesupport (>= 5.0) GEM remote: https://rubygems.org/ specs: actionpack (6.1.4.1) actionview (= 6.1.4.1) activesupport (= 6.1.4.1) rack (~> 2.0, >= 2.0.9) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) actionview (6.1.4.1) activesupport (= 6.1.4.1) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) activemodel (6.1.4.1) activesupport (= 6.1.4.1) activesupport (6.1.4.1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) zeitwerk (~> 2.3) builder (3.2.4) concurrent-ruby (1.1.9) crass (1.0.6) erubi (1.10.0) i18n (1.8.11) concurrent-ruby (~> 1.0) loofah (2.12.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) method_source (1.0.0) mini_portile2 (2.6.1) minitest (5.14.4) nokogiri (1.12.5) mini_portile2 (~> 2.6.1) racc (~> 1.4) racc (1.6.0) rack (2.2.3) rack-test (1.1.0) rack (>= 1.0, < 3) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) rails-html-sanitizer (1.4.2) loofah (~> 2.3) railties (6.1.4.1) actionpack (= 6.1.4.1) activesupport (= 6.1.4.1) method_source rake (>= 0.13) thor (~> 1.0) rake (13.0.6) thor (1.1.0) tzinfo (2.0.4) concurrent-ruby (~> 1.0) zeitwerk (2.5.1) PLATFORMS ruby DEPENDENCIES activemodel globalid! railties rake BUNDLED WITH 2.2.22 globalid-0.6.0/MIT-LICENSE000066400000000000000000000020621414753623000147330ustar00rootroot00000000000000Copyright (c) 2014-2016 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.6.0/README.md000066400000000000000000000151321414753623000145600ustar00rootroot00000000000000# Global ID - Reference models by URI 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 # => # # "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_sgid = 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 # => # ``` **Expiration** Signed Global IDs can 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') # => # # Within 2 hours... GlobalID::Locator.locate_signed(expiring_sgid.to_s, for: 'sharing') # => # # More than 2 hours later... GlobalID::Locator.locate_signed(expiring_sgid.to_s, for: 'sharing') # => nil ``` **In Rails, an auto-expiry of 1 month is set by default.** You can alter that deal in an initializer with: ```ruby # config/initializers/global_id.rb Rails.application.config.global_id.expires_in = 3.months ``` You can assign a default SGID lifetime like so: ```ruby SignedGlobalID.expires_in = 1.month ``` This way any generated SGID will use that relative expiry. It's worth noting that _expiring SGIDs are not idempotent_ because they encode the current timestamp; repeated calls to `to_sgid` will produce different results. For example, in Rails ```ruby Document.find(5).to_sgid.to_s == Document.find(5).to_sgid.to_s # => false ``` You need to explicitly pass `expires_in: nil` to generate a permanent SGID that will not expire, ```ruby # Passing a false value to either expiry option turns off expiration entirely. never_expiring_sgid = Document.find(5).to_sgid(expires_in: nil) # => # # Any time later... GlobalID::Locator.locate_signed never_expiring_sgid # => # ``` It's also possible to pass a specific expiry time ```ruby explicit_expiring_sgid = SecretAgentMessage.find(5).to_sgid(expires_at: Time.now.advance(hours: 1)) # => # # 1 hour later... GlobalID::Locator.locate_signed explicit_expiring_sgid.to_s # => nil ``` Note that an explicit `:expires_at` takes precedence over a relative `:expires_in`. **Purpose** 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') # => # # ``` ### Locating many Global IDs When needing to locate many Global IDs use `GlobalID::Locator.locate_many` or `GlobalID::Locator.locate_many_signed` for Signed Global IDs to allow loading Global IDs more efficiently. For instance, the default locator passes every `model_id` per `model_name` thus using `model_name.where(id: model_ids)` versus `GlobalID::Locator.locate`'s `model_name.find(id)`. In the case of looking up Global IDs from a database, it's only necessary to query once per `model_name` as shown here: ```ruby gids = users.concat(people).sort_by(&:id).map(&:to_global_id) # => [#>, #>, #>, #>, #>, #>] GlobalID::Locator.locate_many gids # SELECT "users".* FROM "users" WHERE "users"."id" IN ($1, $2, $3) [["id", 1], ["id", 2], ["id", 3]] # SELECT "students".* FROM "students" WHERE "students"."id" IN ($1, $2, $3) [["id", 1], ["id", 2], ["id", 3]] # => [#, #, #, #, #, #] ``` Note the order is maintained in the returned results. ### 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. ## Contributing to GlobalID GlobalID is work of many contributors. You're encouraged to submit pull requests, propose features and discuss issues. See [CONTRIBUTING](CONTRIBUTING.md). ## License GlobalID is released under the [MIT License](http://www.opensource.org/licenses/MIT). globalid-0.6.0/Rakefile000066400000000000000000000003311414753623000147410ustar00rootroot00000000000000require '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.6.0/gemfiles/000077500000000000000000000000001414753623000150725ustar00rootroot00000000000000globalid-0.6.0/gemfiles/rails_5.0.gemfile000066400000000000000000000002111414753623000201120ustar00rootroot00000000000000source "https://rubygems.org" gem "activemodel", "~> 5.0.0" gem "railties", "~> 5.0.0" gem "minitest", "~> 5.10.0" gemspec path: "../" globalid-0.6.0/gemfiles/rails_5.1.gemfile000066400000000000000000000001551414753623000201220ustar00rootroot00000000000000source "https://rubygems.org" gem "activemodel", "~> 5.1.0" gem "railties", "~> 5.1.0" gemspec path: "../" globalid-0.6.0/gemfiles/rails_5.2.gemfile000066400000000000000000000002361414753623000201230ustar00rootroot00000000000000source "https://rubygems.org" gem "activemodel", "~> 5.2.0" gem "railties", git: "https://github.com/rails/rails", branch: "5-2-stable" gemspec path: "../" globalid-0.6.0/gemfiles/rails_6.0.gemfile000066400000000000000000000001511414753623000201160ustar00rootroot00000000000000source "https://rubygems.org" gem "activemodel", "~> 6.0" gem "railties", "~> 6.0" gemspec path: "../" globalid-0.6.0/gemfiles/rails_6.1.gemfile000066400000000000000000000001511414753623000201170ustar00rootroot00000000000000source "https://rubygems.org" gem "activemodel", "~> 6.1" gem "railties", "~> 6.1" gemspec path: "../" globalid-0.6.0/globalid.gemspec000066400000000000000000000012111414753623000164140ustar00rootroot00000000000000Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY s.name = 'globalid' s.version = '0.6.0' 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 = '>= 2.5.0' 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.md', 'lib/**/*'] s.require_path = 'lib' s.add_runtime_dependency 'activesupport', '>= 5.0' s.add_development_dependency 'rake' end globalid-0.6.0/lib/000077500000000000000000000000001414753623000140455ustar00rootroot00000000000000globalid-0.6.0/lib/global_id.rb000066400000000000000000000005321414753623000163060ustar00rootroot00000000000000require 'global_id/global_id' require 'active_support' autoload :SignedGlobalID, 'global_id/signed_global_id' class GlobalID extend ActiveSupport::Autoload eager_autoload do autoload :Locator autoload :Identification autoload :Verifier end def self.eager_load! super require 'global_id/signed_global_id' end end globalid-0.6.0/lib/global_id/000077500000000000000000000000001414753623000157615ustar00rootroot00000000000000globalid-0.6.0/lib/global_id/fixture_set.rb000066400000000000000000000006111414753623000206450ustar00rootroot00000000000000# frozen_string_literal: true class GlobalID module FixtureSet def signed_global_id(fixture_set_name, label, column_type: :integer, **options) identifier = identify(label, column_type) model_name = default_fixture_model_name(fixture_set_name) uri = URI::GID.build([GlobalID.app, model_name, identifier, {}]) SignedGlobalID.new(uri, **options) end end end globalid-0.6.0/lib/global_id/global_id.rb000066400000000000000000000040321414753623000202210ustar00rootroot00000000000000require '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 alias_method :eql?, :== def hash self.class.hash | @uri.hash 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.6.0/lib/global_id/identification.rb000066400000000000000000000007151414753623000213020ustar00rootroot00000000000000class GlobalID module Identification def to_global_id(options = {}) 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.6.0/lib/global_id/locator.rb000066400000000000000000000161471414753623000177620ustar00rootroot00000000000000require '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)) { DEFAULT_LOCATOR } 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 BaseLocator 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 class UnscopedLocator < BaseLocator def locate(gid) unscoped(gid.model_class) { super } end private def find_records(model_class, ids, options) unscoped(model_class) { super } end def unscoped(model_class) if model_class.respond_to?(:unscoped) model_class.unscoped { yield } else yield end end end DEFAULT_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.6.0/lib/global_id/railtie.rb000066400000000000000000000030221414753623000177340ustar00rootroot00000000000000begin require 'rails/railtie' rescue LoadError else require 'global_id' require 'active_support' require 'active_support/core_ext/string/inflections' require 'active_support/core_ext/integer/time' 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 config.eager_load_namespaces << GlobalID initializer 'global_id' do |app| default_expires_in = 1.month default_app_name = app.railtie_name.remove('_application').dasherize GlobalID.app = app.config.global_id.app ||= default_app_name SignedGlobalID.expires_in = app.config.global_id.fetch(:expires_in, default_expires_in) config.after_initialize do GlobalID.app = app.config.global_id.app ||= default_app_name SignedGlobalID.expires_in = app.config.global_id.fetch(:expires_in, default_expires_in) app.config.global_id.verifier ||= begin GlobalID::Verifier.new(app.key_generator.generate_key('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 ActiveSupport.on_load(:active_record_fixture_set) do require 'global_id/fixture_set' send :extend, GlobalID::FixtureSet end end end end end globalid-0.6.0/lib/global_id/signed_global_id.rb000066400000000000000000000043121414753623000215530ustar00rootroot00000000000000require '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 = {}) super verify(sgid.to_s, options), options 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.6.0/lib/global_id/uri/000077500000000000000000000000001414753623000165605ustar00rootroot00000000000000globalid-0.6.0/lib/global_id/uri/gid.rb000066400000000000000000000140321414753623000176500ustar00rootroot00000000000000require '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 if respond_to?(:register_scheme) register_scheme('GID', GID) else @@schemes['GID'] = GID end end globalid-0.6.0/lib/global_id/verifier.rb000066400000000000000000000004561414753623000201260ustar00rootroot00000000000000require 'active_support' require 'active_support/message_verifier' class GlobalID class Verifier < ActiveSupport::MessageVerifier private def encode(data) ::Base64.urlsafe_encode64(data) end def decode(data) ::Base64.urlsafe_decode64(data) end end end globalid-0.6.0/lib/globalid.rb000066400000000000000000000000601414753623000161430ustar00rootroot00000000000000require 'global_id' require 'global_id/railtie' globalid-0.6.0/test/000077500000000000000000000000001414753623000142565ustar00rootroot00000000000000globalid-0.6.0/test/cases/000077500000000000000000000000001414753623000153545ustar00rootroot00000000000000globalid-0.6.0/test/cases/global_id_test.rb000066400000000000000000000206211414753623000206550ustar00rootroot00000000000000require '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: [0.class, Person]) assert_equal Person.find(@person_uuid_gid.model_id), @person_uuid_gid.find(only: [0.class, 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: [0.class, Numeric]) assert_nil @person_uuid_gid.find(only: [0.class, 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]) bignum_class = RUBY_VERSION >= '2.4' ? Integer : Bignum assert_equal Person.find(@person_uuid_gid.model_id), @person_uuid_gid.find(only: [bignum_class, 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 test 'equality' do p1 = Person.new(5) p2 = Person.new(5) p3 = Person.new(10) assert_equal p1, p2 assert_not_equal p2, p3 gid1 = GlobalID.create(p1) gid2 = GlobalID.create(p2) gid3 = GlobalID.create(p3) assert_equal gid1, gid2 assert_not_equal gid2, gid3 # hash and eql? to match for two GlobalID's pointing to the same object assert_equal [gid1], [gid1, gid2].uniq assert_equal [gid1, gid3], [gid1, gid2, gid3].uniq # verify that the GlobalID's hash is different to the underlaying URI assert_not_equal gid1.hash, gid1.uri.hash # verify that URI and GlobalID do not pass the uniq test assert_equal [gid1, gid1.uri], [gid1, gid1.uri].uniq 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.6.0/test/cases/global_identification_test.rb000066400000000000000000000027201414753623000232520ustar00rootroot00000000000000require '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 test 'dup should clear memoized to_global_id' do global_id = @model.to_global_id dup_model = @model.dup dup_model.id = @model.id + 1 dup_global_id = dup_model.to_global_id assert_not_equal global_id, dup_global_id end end globalid-0.6.0/test/cases/global_locator_test.rb000066400000000000000000000230361414753623000217270ustar00rootroot00000000000000require '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 valid purpose with SGID returns right model" do instance = Person.new login_sgid = instance.to_signed_global_id(for: 'login') found = GlobalID::Locator.locate_signed(login_sgid, 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 invalid purpose with SGID returns nil" do instance = Person.new login_sgid = instance.to_signed_global_id(for: 'login') assert_nil GlobalID::Locator.locate_signed(login_sgid, 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 test "by many with scoped and unscoped records" do assert_equal [ Person::Scoped.new('1'), Person.new('2') ], GlobalID::Locator.locate_many([ Person::Scoped.new('1').to_gid, Person.new('2').to_gid ]) end end globalid-0.6.0/test/cases/railtie_test.rb000066400000000000000000000052501414753623000203730ustar00rootroot00000000000000require '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) @app.config.secret_key_base = ('x' * 30) 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.expires_in can be explicitly set to nil with config.global_id.expires_in' do @app.config.global_id.expires_in = nil @app.initialize! assert_nil SignedGlobalID.expires_in end test 'config.global_id can be used to set configurations after the railtie has been loaded' do @app.config.eager_load = true @app.config.before_eager_load do @app.config.global_id.app = 'foobar' @app.config.global_id.expires_in = 1.year end @app.initialize! assert_equal 'foobar', GlobalID.app assert_equal 1.year, SignedGlobalID.expires_in end test 'config.global_id can be used to explicitly set SignedGlobalID.expires_in to nil after the railtie has been loaded' do @app.config.eager_load = true @app.config.before_eager_load do @app.config.global_id.expires_in = nil end @app.initialize! assert_nil SignedGlobalID.expires_in end test 'SignedGlobalID.verifier defaults to Blog::Application.message_verifier(:signed_global_ids) when secret_key_base is present' do @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_key_base is not present' do original_env, Rails.env = Rails.env, 'production' begin @app.config.secret_key_base = nil @app.initialize! assert_nil SignedGlobalID.verifier ensure Rails.env = original_env end 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.6.0/test/cases/signed_global_id_test.rb000066400000000000000000000153361414753623000222150ustar00rootroot00000000000000require '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.6.0/test/cases/uri_gid_test.rb000066400000000000000000000101411414753623000203570ustar00rootroot00000000000000require '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.6.0/test/cases/verifier_test.rb000066400000000000000000000027751414753623000205660ustar00rootroot00000000000000require 'helper' class VerifierTest < ActiveSupport::TestCase setup do @verifier = GlobalID::Verifier.new('muchSECRETsoHIDDEN') end # Marshal.dump serializes the hash used in this test to a different string in older versions of Ruby. if RUBY_VERSION > "1.9.3" test "generates URL-safe messages" do assert_equal "BAh7B0kiCGdpZAY6BkVUSSInZ2lkOi8vYmN4L1BlcnNvbi8xMTUxODY_ZXhwaXJlc19pbgY7AFRJIg9leHBpcmVzX2F0BjsAVDA=--fa4b8c7a28d213288fdd9b6764a5dd119a18a6fe", @verifier.generate({ "gid" => "gid://bcx/Person/115186?expires_in", "expires_at" => nil }) end else test "generates URL-safe messages" do assert_equal "BAh7B0kiCGdpZAY6BkVGSSInZ2lkOi8vYmN4L1BlcnNvbi8xMTUxODY_ZXhwaXJlc19pbgY7AEZJIg9leHBpcmVzX2F0BjsARjA=--b52bf45c68710c5c80e04e44fb122be11f9f2c49", @verifier.generate({ "gid" => "gid://bcx/Person/115186?expires_in", "expires_at" => nil }) end end test "verifies URL-safe messages" do assert_equal({ "gid" => "gid://bcx/Person/115186?expires_in", "expires_at" => nil }, @verifier.verify("BAh7B0kiCGdpZAY6BkVUSSInZ2lkOi8vYmN4L1BlcnNvbi8xMTUxODY_ZXhwaXJlc19pbgY7AFRJIg9leHBpcmVzX2F0BjsAVDA=--fa4b8c7a28d213288fdd9b6764a5dd119a18a6fe")) end test "verifies non-URL-safe messages" do assert_equal({ "gid" => "gid://bcx/Person/115186?expires_in", "expires_at" => nil }, @verifier.verify("BAh7B0kiCGdpZAY6BkVUSSInZ2lkOi8vYmN4L1BlcnNvbi8xMTUxODY/ZXhwaXJlc19pbgY7AFRJIg9leHBpcmVzX2F0BjsAVDA=--ae5e44055262447fdbf5d5d39d5120cfa7d5fbe6")) end end globalid-0.6.0/test/helper.rb000066400000000000000000000012221414753623000160570ustar00rootroot00000000000000require 'bundler/setup' require 'forwardable' 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.6.0/test/models/000077500000000000000000000000001414753623000155415ustar00rootroot00000000000000globalid-0.6.0/test/models/person.rb000066400000000000000000000015541414753623000174010ustar00rootroot00000000000000class 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.6.0/test/models/person_model.rb000066400000000000000000000003351414753623000205550ustar00rootroot00000000000000require '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