diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..d5c3d96 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,27 @@ +name: Tests + +on: + push: + branches: + - main + + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + name: Ruby ${{ matrix.ruby }} + strategy: + matrix: + ruby: + - '3.2.2' + + steps: + - uses: actions/checkout@v3 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + - name: Run the default task + run: bundle exec rake diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9106b2a --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/.bundle/ +/.yardoc +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/spec/reports/ +/tmp/ diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..6930ac3 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,25 @@ +AllCops: + TargetRubyVersion: 3.2 + +Style/StringLiterals: + Enabled: true + EnforcedStyle: double_quotes + +Style/StringLiteralsInInterpolation: + Enabled: true + EnforcedStyle: double_quotes + +Style/FrozenStringLiteralComment: + Enabled: false + +Style/Documentation: + Enabled: false + +Style/NumericPredicate: + Enabled: false + +Layout/LineLength: + Max: 120 + +Naming/PredicateName: + Enabled: false diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..11bee45 --- /dev/null +++ b/Gemfile @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +# Specify your gem's dependencies in based_uuid.gemspec +gemspec + +gem "rake", "~> 13.0" + +gem "minitest", "~> 5.0" + +gem "rubocop", "~> 1.21" diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..6041884 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,80 @@ +PATH + remote: . + specs: + based_uuid (0.5.0) + activerecord (>= 7.0) + activesupport (>= 7.0) + +GEM + remote: https://rubygems.org/ + specs: + activemodel (7.1.2) + activesupport (= 7.1.2) + activerecord (7.1.2) + activemodel (= 7.1.2) + activesupport (= 7.1.2) + timeout (>= 0.4.0) + activesupport (7.1.2) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.0.2) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + minitest (>= 5.1) + mutex_m + tzinfo (~> 2.0) + ast (2.4.2) + base64 (0.2.0) + bigdecimal (3.1.4) + concurrent-ruby (1.2.2) + connection_pool (2.4.1) + drb (2.2.0) + ruby2_keywords + i18n (1.14.1) + concurrent-ruby (~> 1.0) + json (2.7.0) + language_server-protocol (3.17.0.3) + minitest (5.20.0) + mutex_m (0.2.0) + parallel (1.23.0) + parser (3.2.2.4) + ast (~> 2.4.1) + racc + racc (1.7.3) + rainbow (3.1.1) + rake (13.1.0) + regexp_parser (2.8.2) + rexml (3.2.6) + rubocop (1.58.0) + json (~> 2.3) + language_server-protocol (>= 3.17.0) + parallel (~> 1.10) + parser (>= 3.2.2.4) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.30.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.30.0) + parser (>= 3.2.1.0) + ruby-progressbar (1.13.0) + ruby2_keywords (0.0.5) + timeout (0.4.1) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unicode-display_width (2.5.0) + +PLATFORMS + arm64-darwin-22 + x86_64-linux + +DEPENDENCIES + based_uuid! + minitest (~> 5.0) + rake (~> 13.0) + rubocop (~> 1.21) + +BUNDLED WITH + 2.4.19 diff --git a/MIT-LICENSE b/MIT-LICENSE new file mode 100644 index 0000000..3f4a674 --- /dev/null +++ b/MIT-LICENSE @@ -0,0 +1,19 @@ +Copyright (c) Piotr Chmolowski + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f25506e --- /dev/null +++ b/README.md @@ -0,0 +1,92 @@ +# BasedUUID: double-clickable, URL-friendly UUIDs for your Rails models + +[![Build Status](https://github.com/pch/based_uuid/workflows/Tests/badge.svg)](https://github.com/pch/based_uuid/actions) [![Gem Version](https://badge.fury.io/rb/based_uuid.svg)](https://badge.fury.io/rb/based_uuid) + + +Generate “double-clickable”, URL-friendly UUIDs with (optional) prefixes: + +``` +user_4yoiald3wfbvpeu2sbsk7cssdi +acc_ejwqg7b3gvaphiylb25xq545tm +``` + +This gem works by encoding UUIDs into 26-character lowercase strings using [Crockford’s base32](https://www.crockford.com/base32.html) encoding. The optional prefix is prepended to help you identify the model it represents. + +BasedUUID doesn’t affect how your ActiveRecord primary key UUIDs are stored in the database. Prefix and base32 encoding are only used for presentation. + +## Installation + +Add this line to your `Gemfile`: + +```ruby +gem "based_uuid" +``` + +## Usage + +BasedUUID assumes that you have a [UUID primary key](https://guides.rubyonrails.org/v5.0/active_record_postgresql.html#uuid) in your ActiveRecord model. + +```ruby +class BlogPost < ApplicationRecord + has_based_uuid prefix: :bpo +end + +post = BlogPost.last +post.based_uuid #=> bpo_3ah4veflijgy7de6bflk7k4ld4 +post.based_uuid(prefix: false) #=> 3ah4veflijgy7de6bflk7k4ld4 +``` + +### Lookup + +BasedUUID includes a `find_by_based_uuid` model method to look up records: + +```ruby +BlogPost.find_by_based_uuid("bpo_3ah4veflijgy7de6bflk7k4ld4") + +# or without the prefix: +BlogPost.find_by_based_uuid("3ah4veflijgy7de6bflk7k4ld4") + +# there’s also the bang version: +BlogPost.find_by_based_uuid!("3ah4veflijgy7de6bflk7k4ld4") +``` + +### Generic lookup + +The gem provides a generic lookup method to help you find the correct model for the UUID, based on prefix: + +```ruby +BasedUUID.find("bpo_3ah4veflijgy7de6bflk7k4ld4") +#=> # +BasedUUID.find("acc_ejwqg7b3gvaphiylb25xq545tm") +#=> # +``` + +**⚠️ NOTE:** Rails lazy-loads models in the development environment, so this method won’t know about your models until you’ve referenced them at least once. If you’re using this method in a Rails console, you’ll need to run `BlogPost` (or any other model) before you can use it. + +### BasedUUID as default URL identifiers + +BasedUUID aims to be unintrusive and it doesn’t affect how Rails URLs are generated, so if you want to use it as default URL param, add this to your model: + +```ruby +def to_param + based_uuid +end +``` + +* * * + +## Development + +After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. + +To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org). + +## Contributing + +Bug reports and pull requests are welcome on GitHub at https://github.com/pch/based_uuid. + +### Credits + +This gem is heavily inspired by [Stripe IDs](https://stripe.com/docs/api) and the [prefixed_ids](https://github.com/excid3/prefixed_ids/tree/master) gem by Chris Oliver. + +Parts of the base32 encoding code are borrowed from the [ulid gem](https://github.com/rafaelsales/ulid) by Rafael Sales. diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..21ef734 --- /dev/null +++ b/Rakefile @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require "bundler/gem_tasks" +require "rake/testtask" + +Rake::TestTask.new(:test) do |t| + t.libs << "test" + t.libs << "lib" + t.test_files = FileList["test/**/test_*.rb"] +end + +require "rubocop/rake_task" + +RuboCop::RakeTask.new + +task default: %i[test rubocop] diff --git a/based_uuid.gemspec b/based_uuid.gemspec new file mode 100644 index 0000000..21f659d --- /dev/null +++ b/based_uuid.gemspec @@ -0,0 +1,24 @@ +require_relative "lib/based_uuid/version" + +Gem::Specification.new do |spec| + spec.name = "based_uuid" + spec.version = BasedUUID::VERSION + spec.authors = ["Piotr Chmolowski"] + spec.email = ["piotr@chmolowski.pl"] + + spec.summary = "URL-friendly, Base32-encoded UUIDs for Rails models" + spec.homepage = "https://github.com/pchm/based_uuid" + spec.required_ruby_version = ">= 3.2.0" + spec.license = "MIT" + + spec.files = Dir.chdir(__dir__) do + `git ls-files -z`.split("\x0").reject do |f| + (File.expand_path(f) == __FILE__) || + f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor Gemfile]) + end + end + spec.require_paths = ["lib"] + + spec.add_dependency "activerecord", ">= 7.0" + spec.add_dependency "activesupport", ">= 7.0" +end diff --git a/bin/console b/bin/console new file mode 100755 index 0000000..a8c36e1 --- /dev/null +++ b/bin/console @@ -0,0 +1,11 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "bundler/setup" +require "based_uuid" + +# You can add fixtures and/or initialization code here to make experimenting +# with your gem easier. You can also use a different console, if you like. + +require "irb" +IRB.start(__FILE__) diff --git a/bin/setup b/bin/setup new file mode 100755 index 0000000..dce67d8 --- /dev/null +++ b/bin/setup @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' +set -vx + +bundle install + +# Do any other automated setup that you need to do here diff --git a/lib/based_uuid.rb b/lib/based_uuid.rb new file mode 100644 index 0000000..6378bd6 --- /dev/null +++ b/lib/based_uuid.rb @@ -0,0 +1,40 @@ +require "active_support/core_ext/module/attribute_accessors" + +require_relative "based_uuid/version" +require_relative "based_uuid/base32_uuid" +require_relative "based_uuid/has_based_uuid" + +module BasedUUID + class Error < StandardError; end + + mattr_accessor :delimiter, default: "_" + + class << self + def find(token) + prefix, = split(token) + registered_models.fetch(prefix.to_sym).find_by_based_uuid(token) + rescue KeyError + raise Error, "Unable to find model for `#{prefix}`. Registered prefixes: #{registered_models.keys.join(", ")}" + end + + def register_model_prefix(prefix, model) + registered_models[prefix] = model + end + + def registered_models + @registered_models ||= {} + end + + def split(token) + prefix, _, uuid_base32 = token.to_s.rpartition(delimiter) + [prefix.presence, uuid_base32] + end + + def based_uuid(uuid:, prefix:) + uuid_base32 = Base32UUID.encode(uuid) + return uuid_base32 unless prefix + + "#{prefix}#{delimiter}#{uuid_base32}" + end + end +end diff --git a/lib/based_uuid/base32_uuid.rb b/lib/based_uuid/base32_uuid.rb new file mode 100644 index 0000000..53d2ae9 --- /dev/null +++ b/lib/based_uuid/base32_uuid.rb @@ -0,0 +1,48 @@ +module BasedUUID + module Base32UUID + CROCKFORDS_ALPHABET = "0123456789abcdefghjkmnpqrstvwxyz".freeze + CHARACTER_MAP = CROCKFORDS_ALPHABET.bytes.freeze + + ENCODED_LENGTH = 26 + BITS_PER_CHAR = 5 + ZERO = "0".ord + MASK = 0x1f + UUID_LENGTH = 32 + + UUID_VALIDATION_REGEXP = /\A[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\z/ + UUID_REGEXP = /\A(.{8})(.{4})(.{4})(.{4})(.{12})\z/ + BASE32_VALIDATION_REGEXP = /\A[#{CROCKFORDS_ALPHABET}]{26}\z/ + + class << self + def encode(uuid) + raise ArgumentError, "Invalid UUID" if uuid !~ UUID_VALIDATION_REGEXP + + input = uuid.delete("-").to_i(16) + encoded = Array.new(ENCODED_LENGTH, ZERO) + i = ENCODED_LENGTH - 1 + + while input > 0 + encoded[i] = CHARACTER_MAP[input & MASK] + input >>= BITS_PER_CHAR + i -= 1 + end + + encoded.pack("c*") + end + + def decode(str) + raise ArgumentError, "Invalid base32 string" if str !~ BASE32_VALIDATION_REGEXP + + decoded = 0 + + str.each_byte do |byte| + value = CHARACTER_MAP.index(byte) + decoded = (decoded << BITS_PER_CHAR) | value + end + + uuid = decoded.to_s(16) + uuid.rjust(UUID_LENGTH, "0").scan(UUID_REGEXP).join("-") + end + end + end +end diff --git a/lib/based_uuid/has_based_uuid.rb b/lib/based_uuid/has_based_uuid.rb new file mode 100644 index 0000000..cf39955 --- /dev/null +++ b/lib/based_uuid/has_based_uuid.rb @@ -0,0 +1,57 @@ +require "active_support/lazy_load_hooks" +require "active_support/concern" +require "active_support/core_ext/object/blank" +require "active_support/core_ext/class/attribute" +require "active_record" + +module BasedUUID + module HasBasedUUID + extend ActiveSupport::Concern + + included do + class_attribute :_based_uuid_prefix + end + + class_methods do + def has_based_uuid(prefix: nil) + include ModelExtensions + + self._based_uuid_prefix = prefix + BasedUUID.register_model_prefix(prefix, self) if prefix + end + end + end + + module ModelExtensions + extend ActiveSupport::Concern + + class_methods do + def find_by_based_uuid(token) + prefix, uuid_base32 = BasedUUID.split(token) + raise ArgumentError, "Invalid prefix" if prefix && prefix.to_sym != _based_uuid_prefix + + find_by(primary_key => Base32UUID.decode(uuid_base32)) + end + + def find_by_based_uuid!(token) + find_by_based_uuid(token) or raise ActiveRecord::RecordNotFound + end + end + + def based_uuid(prefix: true) + raise ArgumentError, "UUID is empty" if _primary_key_value.blank? + + BasedUUID.based_uuid(uuid: _primary_key_value, prefix: prefix ? self.class._based_uuid_prefix : nil) + end + + private + + def _primary_key_value + self[self.class.primary_key] + end + end +end + +ActiveSupport.on_load :active_record do + include BasedUUID::HasBasedUUID +end diff --git a/lib/based_uuid/version.rb b/lib/based_uuid/version.rb new file mode 100644 index 0000000..e5c3ce1 --- /dev/null +++ b/lib/based_uuid/version.rb @@ -0,0 +1,3 @@ +module BasedUUID + VERSION = "0.5.0".freeze +end diff --git a/test/test_base32_uuid.rb b/test/test_base32_uuid.rb new file mode 100644 index 0000000..ca526c8 --- /dev/null +++ b/test/test_base32_uuid.rb @@ -0,0 +1,44 @@ +require "test_helper" + +class TestBase32UUID < Minitest::Test + UUIDS_V7 = { + "018c3b49-560c-719f-9daf-bae405f6ffca" => "01hgxmjngce6fsvbxtwg2zdzya", + "018c3b49-f34a-7e22-90af-13b6f0a69d47" => "01hgxmkwtafrh91brkpvrad7a7", + "018c3b4a-093a-7072-b1ae-0170b310000e" => "01hgxmm29te1sb3bg1e2sh000e", + "018c3b4a-22fd-7cbe-8223-68f65e9168d0" => "01hgxmm8qxfjz848v8ysf92t6g", + "018c3b4a-5826-7d9f-8b35-f19a3f054cfa" => "01hgxmmp16fpfrpdfhk8zgak7t", + "018c3b4a-6ef7-77cf-a5f6-21e488347729" => "01hgxmmvqqez7tbxh1wj438xs9", + "018c3b4a-869e-7a10-a397-2fb54a0055d1" => "01hgxmn1myf88a75sfpn500neh", + "018c3b4a-9a9f-7de7-a77f-9da02aa6da1e" => "01hgxmn6mzfqktezwxm0nadpgy", + "018c3b4a-ae68-7421-a992-9026a8fc60cb" => "01hgxmnbk8eggtk4mg4tmfrr6b", + "018c3b4e-44f4-77c9-b226-8e4b2d99166a" => "01hgxmwh7mez4v49me9cpsj5ka" + }.freeze + + def test_that_it_encodes_uuids_to_base32 + UUIDS_V7.each do |uuid, base32| + assert_equal base32, BasedUUID::Base32UUID.encode(uuid) + end + end + + def test_that_it_decodes_base32_to_uuids + UUIDS_V7.each do |uuid, base32| + assert_equal uuid, BasedUUID::Base32UUID.decode(base32) + end + end + + def test_that_it_encodes_and_decodes_random_uuids + 100.times do + uuid = SecureRandom.uuid + assert_equal uuid, BasedUUID::Base32UUID.decode(BasedUUID::Base32UUID.encode(uuid)) + end + end + + def test_that_it_handles_invalid_uuids + assert_raises(ArgumentError) { BasedUUID::Base32UUID.encode("i am not a valid uuid") } + end + + def test_that_it_handles_invalid_base32 + assert_raises(ArgumentError) { BasedUUID::Base32UUID.decode("i am not a valid base32 string") } + assert_raises(ArgumentError) { BasedUUID::Base32UUID.decode("ASDFASDFASDFASDFASDF") } + end +end diff --git a/test/test_based_uuid.rb b/test/test_based_uuid.rb new file mode 100644 index 0000000..2546e99 --- /dev/null +++ b/test/test_based_uuid.rb @@ -0,0 +1,60 @@ +require "test_helper" + +class TestBasedUUID < Minitest::Test + class DummyModel1; end + class DummyModel2; end + + def teardown + BasedUUID.delimiter = "_" + end + + def test_that_it_has_a_version_number + refute_nil ::BasedUUID::VERSION + end + + def test_based_uuid + uuid = "018c3b49-560c-719f-9daf-bae405f6ffca" + base32 = "01hgxmjngce6fsvbxtwg2zdzya" + + assert_equal "example_#{base32}", BasedUUID.based_uuid(uuid:, prefix: "example") + assert_equal base32, BasedUUID.based_uuid(uuid:, prefix: nil) + + BasedUUID.delimiter = "-" + + assert_equal "example-#{base32}", BasedUUID.based_uuid(uuid:, prefix: "example") + end + + def test_splitting_tokens_with_prefixes + BasedUUID.split("example_01hgxpfwgfexf87n5rxge96jkm").tap do |prefix, uuid_base32| + assert_equal "example", prefix + assert_equal "01hgxpfwgfexf87n5rxge96jkm", uuid_base32 + end + + BasedUUID.delimiter = "-" + BasedUUID.split("example-01hgxpfwgfexf87n5rxge96jkm").tap do |prefix, uuid_base32| + assert_equal "example", prefix + assert_equal "01hgxpfwgfexf87n5rxge96jkm", uuid_base32 + end + end + + def test_splitting_tokens_without_prefixes + BasedUUID.split("01hgxpfwgfexf87n5rxge96jkm").tap do |prefix, uuid_base32| + assert_nil prefix + assert_equal "01hgxpfwgfexf87n5rxge96jkm", uuid_base32 + end + + BasedUUID.split("01hgxmmp16fpfrpdfhk8zgak7t").tap do |prefix, uuid_base32| + assert_nil prefix + assert_equal "01hgxmmp16fpfrpdfhk8zgak7t", uuid_base32 + end + end + + def test_that_it_registeres_models + BasedUUID.register_model_prefix("dummy1", DummyModel1) + BasedUUID.register_model_prefix("dummy2", DummyModel2) + + assert_equal DummyModel1, BasedUUID.registered_models["dummy1"] + assert_equal DummyModel2, BasedUUID.registered_models["dummy2"] + assert_nil BasedUUID.registered_models["dummy3"] + end +end diff --git a/test/test_has_based_uuid.rb b/test/test_has_based_uuid.rb new file mode 100644 index 0000000..699f632 --- /dev/null +++ b/test/test_has_based_uuid.rb @@ -0,0 +1,85 @@ +require "test_helper" +require "ostruct" + +class FakeActiveRecordBase < OpenStruct + include BasedUUID::HasBasedUUID + + class_attribute :primary_key + self.primary_key = "id" + + class_attribute :fake_datastore + self.fake_datastore = {} + + def self.create(attrs) + fake_datastore[attrs[primary_key.to_sym]] = new(attrs) + end + + def self.find_by(attrs) + fake_datastore[attrs[primary_key]] + end + + def [](key) + send(key) + end +end + +class User < FakeActiveRecordBase + has_based_uuid prefix: :user +end + +class Article < FakeActiveRecordBase + has_based_uuid prefix: :article +end + +class Transaction < FakeActiveRecordBase + self.primary_key = "txid" + + has_based_uuid prefix: :tx +end + +class TestHasBasedUUID < Minitest::Test + def setup + @user = User.create(id: "59a9608b-bbbf-4d1a-9adc-2dc34875e423") + end + + def test_generic_lookup + assert_equal @user, BasedUUID.find(@user.based_uuid) + + article = Article.create(id: SecureRandom.uuid) + assert_equal article, BasedUUID.find(article.based_uuid) + end + + def test_based_uuid + assert_equal "user_2sn5g8qexz9md9nq1drd47bs13", @user.based_uuid + assert_equal "2sn5g8qexz9md9nq1drd47bs13", @user.based_uuid(prefix: false) + + assert_raises(ArgumentError) { User.new.based_uuid } + end + + def test_find_by_based_uuid + assert_nil User.find_by_based_uuid(random_user.based_uuid) + + assert_raises(ActiveRecord::RecordNotFound) do + User.find_by_based_uuid!(random_user.based_uuid) + end + assert_raises(ArgumentError) { User.find_by_based_uuid!("wrong_#{@user.based_uuid(prefix: false)}") } + + User.find_by_based_uuid(@user.based_uuid).tap do |found_user| + assert_equal @user, found_user + end + end + + def test_find_by_based_uuid_for_custom_primary_keys + tx = Transaction.create(txid: SecureRandom.uuid) + + Transaction.find_by_based_uuid(tx.based_uuid).tap do |found_tx| + assert_equal tx, found_tx + end + end + + private + + def random_user + User.new(id: SecureRandom.uuid) + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 0000000..757b5ac --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,4 @@ +$LOAD_PATH.unshift File.expand_path("../lib", __dir__) +require "based_uuid" + +require "minitest/autorun"