From afa87002854cc0fd9eb7b5fc1f9f69584f16f811 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A8r=20Kessels?= Date: Wed, 20 Jan 2021 12:55:03 +0100 Subject: [PATCH 1/8] Make Handle aware of local or remote handles --- .../controllers/web/profiles_controller.rb | 4 ++++ config/environment.rb | 3 ++- lib/handle.rb | 21 +++++++++++-------- test/lib/handle_test.rb | 14 +++++++++++-- 4 files changed, 30 insertions(+), 12 deletions(-) diff --git a/app/web/controllers/web/profiles_controller.rb b/app/web/controllers/web/profiles_controller.rb index 2015025..fc2f78b 100644 --- a/app/web/controllers/web/profiles_controller.rb +++ b/app/web/controllers/web/profiles_controller.rb @@ -10,6 +10,10 @@ class ProfilesController < WebController # TODO: /@handle should redirect to /@handle@example.org when we are # on example.org # TODO: /handle should redirect to /@handle@example.org as well + # TODO: redirect to remote host if handle is note remote. E.g. + # if we are on example.com and get a @handle@example.org we should redirect + # to example.org/m/handle@example.org. But for that we need remote + # canonical location determination (e.g. webfinger) first. get '/m/:handle' do raise NotFound, 'Member with that handle not found' if profile.null? diff --git a/config/environment.rb b/config/environment.rb index f275050..e823a98 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -20,7 +20,7 @@ class Roost ## # Holds the configuration for Roost. Mainly event-sourcery config. class Config - attr_accessor :database_url, :secret_base, :web_url + attr_accessor :database_url, :secret_base, :web_url, :domain end def self.config @@ -104,6 +104,7 @@ def self.base_path Roost.configure do |config| config.web_url = 'https://example.com' + config.domain = 'example.com' config.secret_base = ENV['SECRET_BASE'] config.database_url = ENV['DATABASE_URL'] diff --git a/lib/handle.rb b/lib/handle.rb index 357c0a2..d32f087 100644 --- a/lib/handle.rb +++ b/lib/handle.rb @@ -5,27 +5,30 @@ # Parses and formats consistently @harry@example.org alike handles over domains # and into usenames and domains class Handle - attr_reader :username, :uri + attr_reader :username, :domain - def initialize(username, uri = Roost.config.web_url) - @uri = uri + def initialize(username, + handle_domain = Roost.config.domain, + local_domain = Roost.config.domain) + @domain = handle_domain + @local_domain = local_domain @username = username end def self.parse(handle) uri = URI.parse("http://#{handle.gsub(/^@/, '')}") - new(uri.user, URI::HTTP.build(host: uri.host).to_s) - end - - def domain - URI.parse(uri).host + new(uri.user, uri.host) end def to_s "@#{username}@#{domain}" end + def local? + domain == @local_domain + end + def ==(other) - username == other.username && uri == other.uri + username == other.username && domain == other.domain end end diff --git a/test/lib/handle_test.rb b/test/lib/handle_test.rb index 7cb7653..4fee310 100644 --- a/test/lib/handle_test.rb +++ b/test/lib/handle_test.rb @@ -5,16 +5,26 @@ ## # Test the Handle library class HandleTest < Minitest::Spec + describe '#local?' do + it 'is false when the domain is not our domain' do + refute Handle.new('ron', 'example.org', 'example.com').local? + end + + it 'is true when the domain is our domain' do + assert Handle.new('ron', 'example.org', 'example.org').local? + end + end + it 'parses @ron@example.org handles and strips the @' do assert_equal(Handle.parse('@ron@example.org').username, 'ron') end it 'parses ron@example.org handles' do - assert_equal(Handle.parse('@ron@example.org').username, 'ron') + assert_equal(Handle.parse('ron@example.org').username, 'ron') end it 'parses ron@any.example.org handles' do - assert_equal(Handle.parse('@ron@any.example.org').domain, 'any.example.org') + assert_equal(Handle.parse('ron@any.example.org').domain, 'any.example.org') end it 'builds a handle as string from a username using local web_url' do From 454f20d7ee0fe1633ac9173dd6e4b621415230b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A8r=20Kessels?= Date: Wed, 20 Jan 2021 21:24:49 +0100 Subject: [PATCH 2/8] Change username form field to use a handle instead --- app/web/controllers/application_controller.rb | 6 ++++++ app/web/views/_handle_field.erb | 16 ++++++++++++++++ app/web/views/login.erb | 7 +------ app/web/views/register.erb | 7 +------ test/integration/web/member_logs_in_test.rb | 4 ++-- test/support/workflows/member_logs_in.rb | 2 +- test/support/workflows/member_registers.rb | 2 +- 7 files changed, 28 insertions(+), 16 deletions(-) create mode 100644 app/web/views/_handle_field.erb diff --git a/app/web/controllers/application_controller.rb b/app/web/controllers/application_controller.rb index 11d1537..ee09b23 100644 --- a/app/web/controllers/application_controller.rb +++ b/app/web/controllers/application_controller.rb @@ -19,6 +19,12 @@ class ApplicationController < Sinatra::Base # :nocov: end + helpers do + def domain + Roost.config.domain + end + end + protected def requires_authorization diff --git a/app/web/views/_handle_field.erb b/app/web/views/_handle_field.erb new file mode 100644 index 0000000..fec8a26 --- /dev/null +++ b/app/web/views/_handle_field.erb @@ -0,0 +1,16 @@ + +
+

+ + @ + +

+
+ +
+

+ + @<%= domain %> + +

+
diff --git a/app/web/views/login.erb b/app/web/views/login.erb index 6f52cc9..72158a2 100644 --- a/app/web/views/login.erb +++ b/app/web/views/login.erb @@ -1,10 +1,5 @@
-
- -
- -
-
+ <%= erb :_handle_field %>
diff --git a/app/web/views/register.erb b/app/web/views/register.erb index 2452ee3..d29dc32 100644 --- a/app/web/views/register.erb +++ b/app/web/views/register.erb @@ -1,10 +1,5 @@ -
- -
- -
-
+ <%= erb :_handle_field %>
diff --git a/test/integration/web/member_logs_in_test.rb b/test/integration/web/member_logs_in_test.rb index 8a185d6..64bc546 100644 --- a/test/integration/web/member_logs_in_test.rb +++ b/test/integration/web/member_logs_in_test.rb @@ -19,7 +19,7 @@ class MemberLogsInTest < Minitest::WebSpec end it 'logs in using credentials set at test' do - fill_in('Username', with: @workflow.form_attributes[:username]) + fill_in('Handle', with: @workflow.form_attributes[:username]) fill_in('Password', with: @workflow.form_attributes[:password]) click_button('Login') @@ -30,7 +30,7 @@ class MemberLogsInTest < Minitest::WebSpec end it 'attempts to login using wrong password' do - fill_in('Username', with: @workflow.form_attributes[:username]) + fill_in('Handle', with: @workflow.form_attributes[:username]) fill_in('Password', with: 'pure-blood') click_button('Login') diff --git a/test/support/workflows/member_logs_in.rb b/test/support/workflows/member_logs_in.rb index 2310e5b..53914df 100644 --- a/test/support/workflows/member_logs_in.rb +++ b/test/support/workflows/member_logs_in.rb @@ -7,7 +7,7 @@ class MemberLogsIn < Base def logged_in visit_login - fill_in('Username', with: form_attributes[:username]) + fill_in('Handle', with: form_attributes[:username]) fill_in('Password', with: form_attributes[:password]) click_button('Login') diff --git a/test/support/workflows/member_registers.rb b/test/support/workflows/member_registers.rb index b5a9226..b421da3 100644 --- a/test/support/workflows/member_registers.rb +++ b/test/support/workflows/member_registers.rb @@ -8,7 +8,7 @@ def registered visit '/' click_link 'Register' - fill_in('Username', with: form_attributes[:username]) + fill_in('Handle', with: form_attributes[:username]) fill_in('Password', with: form_attributes[:password]) fill_in('Email', with: form_attributes[:email]) click_button('Register') From 025846c5bb402a526183d972d722df64ae9f36c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A8r=20Kessels?= Date: Wed, 20 Jan 2021 22:32:19 +0100 Subject: [PATCH 3/8] Refactor Handle to not return @@example.com handles for empty usernames --- lib/handle.rb | 2 ++ test/integration/api/member_authenticates_test.rb | 2 +- test/lib/handle_test.rb | 5 +++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/handle.rb b/lib/handle.rb index d32f087..fa9b8b6 100644 --- a/lib/handle.rb +++ b/lib/handle.rb @@ -21,6 +21,8 @@ def self.parse(handle) end def to_s + return '' if username.to_s.empty? + "@#{username}@#{domain}" end diff --git a/test/integration/api/member_authenticates_test.rb b/test/integration/api/member_authenticates_test.rb index e13a5db..87a3e9d 100644 --- a/test/integration/api/member_authenticates_test.rb +++ b/test/integration/api/member_authenticates_test.rb @@ -33,7 +33,7 @@ def secret aggregate_id: workflow.aggregate_id, name: workflow.member_name, email: workflow.member_email, - handle: '@@example.com' + handle: '' }, parsed_response ) diff --git a/test/lib/handle_test.rb b/test/lib/handle_test.rb index 4fee310..87d06f9 100644 --- a/test/lib/handle_test.rb +++ b/test/lib/handle_test.rb @@ -31,6 +31,11 @@ class HandleTest < Minitest::Spec assert_equal(Handle.new('harry').to_s, '@harry@example.com') end + it 'returns empty string when username is empty' do + assert_empty(Handle.new(nil).to_s) + assert_empty(Handle.new('').to_s) + end + it 'is equal when both url and username are equal' do assert_equal(Handle.new('harry'), Handle.new('harry')) assert_equal( From 994e5b55feee9bce0f256a6a1cdee023b6a0f75f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A8r=20Kessels?= Date: Wed, 20 Jan 2021 22:33:01 +0100 Subject: [PATCH 4/8] Refactor username to be converted in login controller to handle --- app/commands/session/start.rb | 6 +++--- app/projections/members/query.rb | 4 ++-- app/web/controllers/web/login_controller.rb | 4 +++- test/commands/session/start_test.rb | 17 ++++++++++++----- 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/app/commands/session/start.rb b/app/commands/session/start.rb index 3d6714b..6b8d7a8 100644 --- a/app/commands/session/start.rb +++ b/app/commands/session/start.rb @@ -16,7 +16,7 @@ class Command < ApplicationCommand UUID_USERNAME_NAMESPACE = UUIDTools::UUID.parse( 'fb0f6f73-a16d-4032-b508-16519fb4a73a' ) - DEFAULT_PARAMS = { 'username' => '', 'password' => '' }.freeze + DEFAULT_PARAMS = { 'handle' => '', 'password' => '' }.freeze def initialize(params, projection: Projections::Members::Query) @payload = DEFAULT_PARAMS.merge(params).slice(*DEFAULT_PARAMS.keys) @@ -47,7 +47,7 @@ def payload attr_reader :projection def aggregate_id_name - @payload['username'] + @payload['handle'] end def aggregate_id_namespace @@ -55,7 +55,7 @@ def aggregate_id_namespace end def member - @member ||= projection.find_by(username: @payload['username']) || {} + @member ||= projection.find_by(handle: @payload['handle']) || {} end end diff --git a/app/projections/members/query.rb b/app/projections/members/query.rb index 6a2edc7..8124504 100644 --- a/app/projections/members/query.rb +++ b/app/projections/members/query.rb @@ -5,8 +5,8 @@ module Members # Query the Members projection with helpers that return # Sequel collection objects. class Query - def self.find_by(username:) - collection.first(username: username) + def self.find_by(handle:) + collection.first(handle: handle) end def self.find(id) diff --git a/app/web/controllers/web/login_controller.rb b/app/web/controllers/web/login_controller.rb index 171f866..791faf1 100644 --- a/app/web/controllers/web/login_controller.rb +++ b/app/web/controllers/web/login_controller.rb @@ -16,7 +16,9 @@ class LoginController < WebController private def post_params - params.slice('username', 'password') + post_params = params.slice(:password) + post_params[:handle] = Handle.new(params[:username]).to_s + post_params end end end diff --git a/test/commands/session/start_test.rb b/test/commands/session/start_test.rb index 27aa546..42cf902 100644 --- a/test/commands/session/start_test.rb +++ b/test/commands/session/start_test.rb @@ -1,20 +1,27 @@ # frozen_string_literal: true require 'test_helper' +require 'sinatra' class SessionStartCommandTest < Minitest::Spec let(:projection) { Minitest::Mock.new } - let(:params) { { 'username' => 'hpotter', 'password' => 'caput draconis' } } + let(:params) do + Sinatra::IndifferentHash.new.merge( + { handle: Handle.new('hpotter').to_s, password: 'caput draconis' } + ) + end let(:member) { nil } subject do Commands::Session::Start::Command.new(params, projection: projection) end - before { projection.expect(:find_by, member, [{ username: 'hpotter' }]) } + before do + projection.expect(:find_by, member, [{ handle: '@hpotter@example.com' }]) + end describe 'with username' do - let(:uuid_v5_for_username) { '404fd9f9-c5fc-5b21-b2a9-9ad650520aff' } + let(:uuid_v5_for_username) { 'f0f63857-aa2b-5260-ab3d-886116f0f369' } it 'generates a UUIDv5 for this username' do assert_equal(uuid_v5_for_username, subject.aggregate_id) @@ -22,7 +29,7 @@ class SessionStartCommandTest < Minitest::Spec end describe 'without username' do - before { params['username'] = '' } + before { params[:handle] = '' } it 'handles empty username' do assert_equal(subject.aggregate_id, '') @@ -44,7 +51,7 @@ class SessionStartCommandTest < Minitest::Spec end it "fails if passwords don't match" do - params['password'] = 'pure-blood' + params[:password] = 'pure-blood' subject = Commands::Session::Start::Command.new( params, projection: projection From efaceee609f3e936ddd0855b551d94875bf22695 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A8r=20Kessels?= Date: Fri, 22 Jan 2021 16:54:15 +0100 Subject: [PATCH 5/8] Refactor username to be converted in registration controller to handle --- app/aggregates/member.rb | 3 +- app/aggregates/registration.rb | 6 ++-- app/commands/registration/new_registration.rb | 6 ++-- app/projections/members/projector.rb | 6 +--- .../controllers/web/contacts_controller.rb | 19 ++++++---- .../web/registrations_controller.rb | 4 ++- app/web/policies/contact_policy.rb | 2 +- test/aggregates/member_test.rb | 6 ++-- test/aggregates/registration_test.rb | 4 +-- .../registration/new_registration_test.rb | 35 ++++++++++++------- .../api/member_authenticates_test.rb | 2 +- .../integration/web/visitor_registers_test.rb | 2 +- test/support/workflows/add_member.rb | 7 +++- 13 files changed, 60 insertions(+), 42 deletions(-) diff --git a/app/aggregates/member.rb b/app/aggregates/member.rb index e2bcacd..48c1bef 100644 --- a/app/aggregates/member.rb +++ b/app/aggregates/member.rb @@ -22,10 +22,9 @@ def initialize(id, events) end apply MemberAdded do |event| - username = event.body['username'] write_attributes( added: true, - handle: Handle.new(username), + handle: event.body['handle'], email: event.body['email'], name: event.body['name'] ) diff --git a/app/aggregates/registration.rb b/app/aggregates/registration.rb index 1e08383..e82f603 100644 --- a/app/aggregates/registration.rb +++ b/app/aggregates/registration.rb @@ -28,7 +28,7 @@ def initialize(*arguments) end apply RegistrationRequested do |event| - write_attributes(event.body.slice('username', 'password', 'email')) + write_attributes(event.body.slice('handle', 'password', 'email')) end apply RegistrationConfirmed do @@ -69,8 +69,8 @@ def password attributes[:password] end - def username - attributes[:username] + def handle + attributes[:handle] end end end diff --git a/app/commands/registration/new_registration.rb b/app/commands/registration/new_registration.rb index cd053f0..6597b38 100644 --- a/app/commands/registration/new_registration.rb +++ b/app/commands/registration/new_registration.rb @@ -15,7 +15,7 @@ class Command < ApplicationCommand '2282b78c-85d6-419f-b240-0263d67ee6e6' ) - REQUIRED_PARAMS = %w[email username password].freeze + REQUIRED_PARAMS = %w[email handle password].freeze ALLOWED_PARAMS = REQUIRED_PARAMS # NewRegistration builds a UUIDv5 based on the mailaddress. @@ -23,7 +23,9 @@ def initialize(params) @payload = params.slice(*ALLOWED_PARAMS) # overwrite the password - @payload['password'] = Password.create(@payload.delete('password')) + unless (@payload['password'] || '').empty? + @payload['password'] = Password.create(@payload.delete('password')) + end @aggregate_id = aggregate_id end diff --git a/app/projections/members/projector.rb b/app/projections/members/projector.rb index 5f0e7e3..b04ba64 100644 --- a/app/projections/members/projector.rb +++ b/app/projections/members/projector.rb @@ -12,17 +12,13 @@ class Projector table :members do column :member_id, 'UUID NOT NULL' column :handle, :text, null: false - column :username, :text column :password, :text end project MemberAdded do |event| - username = event.body['username'] - table.insert( member_id: event.aggregate_id, - handle: Handle.new(username).to_s, - username: username, + handle: event.body['handle'], password: event.body['password'] ) end diff --git a/app/web/controllers/web/contacts_controller.rb b/app/web/controllers/web/contacts_controller.rb index 24ccc38..0160296 100644 --- a/app/web/controllers/web/contacts_controller.rb +++ b/app/web/controllers/web/contacts_controller.rb @@ -4,6 +4,7 @@ module Web ## # Handles contacts index and addition class ContactsController < WebController + include LoadHelpers include PolicyHelpers # Index @@ -21,22 +22,28 @@ class ContactsController < WebController Commands.handle( 'Contact', 'Add', - { 'handle' => contact.handle, 'owner_id' => current_member.member_id } + { 'handle' => handle, 'owner_id' => current_member.member_id } ) - flash[:success] = "#{contact.handle} was added to your contacts" - redirect "/m/#{contact.handle}" + flash[:success] = "#{handle} was added to your contacts" + redirect "/m/#{handle}" end private def contact - aggregate_id = Projections::Members::Query.aggregate_id_for(handle) - Roost.repository.load(Aggregates::Member, aggregate_id) + decorate( + load( + Aggregates::Member, + Projections::Members::Query.aggregate_id_for(handle.to_s) + ), + ViewModels::Profile, + ViewModels::Profile::NullProfile + ) end def handle - Handle.parse(params['handle']) + Handle.parse(params[:handle]) end end end diff --git a/app/web/controllers/web/registrations_controller.rb b/app/web/controllers/web/registrations_controller.rb index 270b74f..9b558bb 100644 --- a/app/web/controllers/web/registrations_controller.rb +++ b/app/web/controllers/web/registrations_controller.rb @@ -20,7 +20,9 @@ class RegistrationsController < WebController private def post_params - params.slice('username', 'password', 'email') + post_params = params.slice(:password, :email) + post_params[:handle] = Handle.new(params[:username]).to_s + post_params end end end diff --git a/app/web/policies/contact_policy.rb b/app/web/policies/contact_policy.rb index ca22493..07b3b3c 100644 --- a/app/web/policies/contact_policy.rb +++ b/app/web/policies/contact_policy.rb @@ -10,7 +10,7 @@ def add? private def anon? - actor.null? + actor.null? || aggregate.nil? || aggregate.null? end def self? diff --git a/test/aggregates/member_test.rb b/test/aggregates/member_test.rb index 715ef9c..1fdafb8 100644 --- a/test/aggregates/member_test.rb +++ b/test/aggregates/member_test.rb @@ -21,10 +21,7 @@ class MemberTest < Minitest::Spec it_sets_attribute(:add_member, 'email') it_sets_attribute(:add_member, 'name') - it 'add_member sets handle from username' do - subject.add_member('username' => 'harry') - assert_equal(subject.handle, Handle.new('harry')) - end + it_sets_attribute(:add_member, 'handle') it '#add_tag adds a tag' do author_id = fake_uuid(Aggregates::Member, 2) @@ -50,6 +47,7 @@ class MemberTest < Minitest::Spec ) end + # TODO: still needed? it 'MemberAdded sets added attribute to true' do assert(Aggregates::Member.new(id, [MemberAdded.new]).attributes[:added]) end diff --git a/test/aggregates/registration_test.rb b/test/aggregates/registration_test.rb index 858d122..dd6c385 100644 --- a/test/aggregates/registration_test.rb +++ b/test/aggregates/registration_test.rb @@ -14,7 +14,7 @@ class RegistrationTest < Minitest::Spec let(:payload) do { - username: 'hpotter', + handle: '@hpotter@example.com', password: 'caput draconis', email: 'hpotter@hogwards.edu.wiz' } @@ -28,7 +28,7 @@ class RegistrationTest < Minitest::Spec end it 'sets username' do - assert_equal(subject.username, 'hpotter') + assert_equal(subject.handle, '@hpotter@example.com') end it 'sets email' do diff --git a/test/commands/registration/new_registration_test.rb b/test/commands/registration/new_registration_test.rb index e9a62ec..d5d6d17 100644 --- a/test/commands/registration/new_registration_test.rb +++ b/test/commands/registration/new_registration_test.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'test_helper' +require 'sinatra' require 'bcrypt' class NewRegistrationCommandTest < Minitest::Spec @@ -8,7 +9,10 @@ class NewRegistrationCommandTest < Minitest::Spec subject { subject_class.new(params) } describe 'with email' do - let(:params) { { 'email' => 'harry@example.com' } } + let(:params) do + Sinatra::IndifferentHash.new.merge(email: 'harry@example.com') + end + let(:uuid_v5_for_email) { '377fc540-ff6b-5ddc-9ad8-1d9e9917f626' } it 'generates a UUIDv5 for this email' do @@ -17,7 +21,9 @@ class NewRegistrationCommandTest < Minitest::Spec end describe 'without email' do - let(:params) { { 'email' => '' } } + let(:params) do + Sinatra::IndifferentHash.new.merge(email: '') + end it 'has no aggregate_id' do assert_equal(subject.aggregate_id, '') @@ -26,7 +32,10 @@ class NewRegistrationCommandTest < Minitest::Spec describe 'password' do let(:password) { 'caput draconis' } - subject { subject_class.new('password' => password) } + subject do + params = Sinatra::IndifferentHash.new.merge(password: password) + subject_class.new(params) + end it 'generates a secure hash of the password' do assert_equal( @@ -38,34 +47,34 @@ class NewRegistrationCommandTest < Minitest::Spec describe 'validate' do let(:valid_params) do - { - 'email' => 'harry@example.com', - 'usernane' => 'hpotter', - 'password' => 'caput draconis' - } + Sinatra::IndifferentHash.new.merge( + email: 'harry@example.com', + handle: Handle.new('hpotter').to_s, + password: 'caput draconis' + ) end it 'raises BadRequest when email is empty' do assert_raises(BadRequest, 'email is blank') do - subject_class.new(valid_params.merge('email' => '')).validate + subject_class.new(valid_params.merge(email: '')).validate end end it 'raises BadRequest when email is nil' do assert_raises(BadRequest, 'email is blank') do - subject_class.new(valid_params.merge('email' => nil)).validate + subject_class.new(valid_params.merge(email: nil)).validate end end it 'raises BadRequest when username is empty' do - assert_raises(BadRequest, 'username is blank') do - subject_class.new(valid_params.merge('username' => '')).validate + assert_raises(BadRequest, 'handle is blank') do + subject_class.new(valid_params.merge(handle: '')).validate end end it 'raises BadRequest when password is empty' do assert_raises(BadRequest, 'password is blank') do - subject_class.new(valid_params.merge('password' => '')).validate + subject_class.new(valid_params.merge(password: '')).validate end end end diff --git a/test/integration/api/member_authenticates_test.rb b/test/integration/api/member_authenticates_test.rb index 87a3e9d..c8d8a4e 100644 --- a/test/integration/api/member_authenticates_test.rb +++ b/test/integration/api/member_authenticates_test.rb @@ -33,7 +33,7 @@ def secret aggregate_id: workflow.aggregate_id, name: workflow.member_name, email: workflow.member_email, - handle: '' + handle: workflow.member_handle }, parsed_response ) diff --git a/test/integration/web/visitor_registers_test.rb b/test/integration/web/visitor_registers_test.rb index 574a8c5..4558db3 100644 --- a/test/integration/web/visitor_registers_test.rb +++ b/test/integration/web/visitor_registers_test.rb @@ -70,7 +70,7 @@ class VisitorRegistersTest < Minitest::WebSpec assert_content( find('.notification.is-error'), - 'username is blank'\ + 'handle is blank'\ ) end end diff --git a/test/support/workflows/add_member.rb b/test/support/workflows/add_member.rb index 2542b9b..6a3340d 100644 --- a/test/support/workflows/add_member.rb +++ b/test/support/workflows/add_member.rb @@ -21,7 +21,11 @@ def member_name end def member_email - 'harry@example.com' + 'harry@example.email' + end + + def member_handle + '@harry@example.com' end def aggregate_id @@ -34,6 +38,7 @@ def member_attributes { aggregate_id: aggregate_id, name: member_name, + handle: member_handle, email: member_email } end From e570189895a63352c681b6c19e7a04573db0eacc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A8r=20Kessels?= Date: Fri, 5 Feb 2021 15:58:07 +0100 Subject: [PATCH 6/8] Move all events into one file for simplicity. --- app/events/confirmation_email_sent.rb | 5 --- app/events/contact_added.rb | 5 --- app/events/events.rb | 49 +++++++++++++++++++++++++++ app/events/follower_added.rb | 5 --- app/events/member_added.rb | 5 --- app/events/member_bio_updated.rb | 5 --- app/events/member_invited.rb | 6 ---- app/events/member_name_updated.rb | 5 --- app/events/member_tag_added.rb | 5 --- app/events/registration_confirmed.rb | 5 --- app/events/registration_requested.rb | 5 --- app/events/session_started.rb | 5 --- 12 files changed, 49 insertions(+), 56 deletions(-) delete mode 100644 app/events/confirmation_email_sent.rb delete mode 100644 app/events/contact_added.rb create mode 100644 app/events/events.rb delete mode 100644 app/events/follower_added.rb delete mode 100644 app/events/member_added.rb delete mode 100644 app/events/member_bio_updated.rb delete mode 100644 app/events/member_invited.rb delete mode 100644 app/events/member_name_updated.rb delete mode 100644 app/events/member_tag_added.rb delete mode 100644 app/events/registration_confirmed.rb delete mode 100644 app/events/registration_requested.rb delete mode 100644 app/events/session_started.rb diff --git a/app/events/confirmation_email_sent.rb b/app/events/confirmation_email_sent.rb deleted file mode 100644 index caa0586..0000000 --- a/app/events/confirmation_email_sent.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -## -# A confirmation email has been sent -ConfirmationEmailSent = Class.new(EventSourcery::Event) diff --git a/app/events/contact_added.rb b/app/events/contact_added.rb deleted file mode 100644 index 1ed6d31..0000000 --- a/app/events/contact_added.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -## -# A Contact was added to a Member -ContactAdded = Class.new(EventSourcery::Event) diff --git a/app/events/events.rb b/app/events/events.rb new file mode 100644 index 0000000..9ddc7bd --- /dev/null +++ b/app/events/events.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +## +# A confirmation email has been sent +ConfirmationEmailSent = Class.new(EventSourcery::Event) + +## +# A Contact was added to a Member +# +aggregate_id+, UUID of the contact aggregate +# +body.owner_id+, UUID of the member owning the contact: the contact shows +# up in this member's contacts +# +body.handle+, Handle, the handle of the contact to be added. +ContactAdded = Class.new(EventSourcery::Event) + +## +# A follower was added to a member +FollowerAdded = Class.new(EventSourcery::Event) + +## +# A Member was added +MemberAdded = Class.new(EventSourcery::Event) + +## +# A profile bio was updated +MemberBioUpdated = Class.new(EventSourcery::Event) + +## +# A Member was invited +MemberInvited = Class.new(EventSourcery::Event) + +## +# A profile bio was updated +MemberNameUpdated = Class.new(EventSourcery::Event) + +## +# A Member was Tagged +MemberTagAdded = Class.new(EventSourcery::Event) + +## +# A Registration is confirmed +RegistrationConfirmed = Class.new(EventSourcery::Event) + +## +# A visitor has registered: reguested a new registration +RegistrationRequested = Class.new(EventSourcery::Event) + +## +# A Logged in session was started +SessionStarted = Class.new(EventSourcery::Event) diff --git a/app/events/follower_added.rb b/app/events/follower_added.rb deleted file mode 100644 index 0839c76..0000000 --- a/app/events/follower_added.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -## -# A follower was added to a member -FollowerAdded = Class.new(EventSourcery::Event) diff --git a/app/events/member_added.rb b/app/events/member_added.rb deleted file mode 100644 index 23b693e..0000000 --- a/app/events/member_added.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -## -# A Member was added -MemberAdded = Class.new(EventSourcery::Event) diff --git a/app/events/member_bio_updated.rb b/app/events/member_bio_updated.rb deleted file mode 100644 index 879146c..0000000 --- a/app/events/member_bio_updated.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -## -# A profile bio was updated -MemberBioUpdated = Class.new(EventSourcery::Event) diff --git a/app/events/member_invited.rb b/app/events/member_invited.rb deleted file mode 100644 index 9945826..0000000 --- a/app/events/member_invited.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -## -# A Member was invited -class MemberInvited < EventSourcery::Event -end diff --git a/app/events/member_name_updated.rb b/app/events/member_name_updated.rb deleted file mode 100644 index d19a27c..0000000 --- a/app/events/member_name_updated.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -## -# A profile bio was updated -MemberNameUpdated = Class.new(EventSourcery::Event) diff --git a/app/events/member_tag_added.rb b/app/events/member_tag_added.rb deleted file mode 100644 index 928d424..0000000 --- a/app/events/member_tag_added.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -## -# A Member was Tagged -MemberTagAdded = Class.new(EventSourcery::Event) diff --git a/app/events/registration_confirmed.rb b/app/events/registration_confirmed.rb deleted file mode 100644 index 52c640f..0000000 --- a/app/events/registration_confirmed.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -## -# A Registration is confirmed -RegistrationConfirmed = Class.new(EventSourcery::Event) diff --git a/app/events/registration_requested.rb b/app/events/registration_requested.rb deleted file mode 100644 index 1fe8c82..0000000 --- a/app/events/registration_requested.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -## -# A visitor has registered: reguested a new registration -RegistrationRequested = Class.new(EventSourcery::Event) diff --git a/app/events/session_started.rb b/app/events/session_started.rb deleted file mode 100644 index e394b52..0000000 --- a/app/events/session_started.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -## -# A Logged in session was started -SessionStarted = Class.new(EventSourcery::Event) From 791328bff218466d09d6d184fbf9af2e67f3a7b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A8r=20Kessels?= Date: Fri, 5 Feb 2021 19:31:35 +0100 Subject: [PATCH 7/8] Move uuid generation into lib helper --- app/commands/application_command.rb | 5 +---- app/commands/registration/new_registration.rb | 6 +----- app/commands/session/start.rb | 5 +---- lib/uuid_gen.rb | 16 ++++++++++++++++ 4 files changed, 19 insertions(+), 13 deletions(-) create mode 100644 lib/uuid_gen.rb diff --git a/app/commands/application_command.rb b/app/commands/application_command.rb index 7228b74..217183d 100644 --- a/app/commands/application_command.rb +++ b/app/commands/application_command.rb @@ -23,10 +23,7 @@ def uuid_v5 if aggregate_id_name.empty? '' else - UUIDTools::UUID.sha1_create( - aggregate_id_namespace, - aggregate_id_name - ).to_s + UUIDGen.uuid(aggregate_id_namespace, aggregate_id_name).to_s end end diff --git a/app/commands/registration/new_registration.rb b/app/commands/registration/new_registration.rb index 6597b38..2ca32db 100644 --- a/app/commands/registration/new_registration.rb +++ b/app/commands/registration/new_registration.rb @@ -11,10 +11,6 @@ module NewRegistration class Command < ApplicationCommand include BCrypt - UUID_EMAIL_NAMESPACE = UUIDTools::UUID.parse( - '2282b78c-85d6-419f-b240-0263d67ee6e6' - ) - REQUIRED_PARAMS = %w[email handle password].freeze ALLOWED_PARAMS = REQUIRED_PARAMS @@ -49,7 +45,7 @@ def aggregate_id_name end def aggregate_id_namespace - UUID_EMAIL_NAMESPACE + UUIDGen::NS_EMAIL end end diff --git a/app/commands/session/start.rb b/app/commands/session/start.rb index 6b8d7a8..7018d9a 100644 --- a/app/commands/session/start.rb +++ b/app/commands/session/start.rb @@ -13,9 +13,6 @@ module Start # with e.g. a rate limiter, or notification mail or so. class Command < ApplicationCommand include BCrypt - UUID_USERNAME_NAMESPACE = UUIDTools::UUID.parse( - 'fb0f6f73-a16d-4032-b508-16519fb4a73a' - ) DEFAULT_PARAMS = { 'handle' => '', 'password' => '' }.freeze def initialize(params, projection: Projections::Members::Query) @@ -51,7 +48,7 @@ def aggregate_id_name end def aggregate_id_namespace - UUID_USERNAME_NAMESPACE + UUIDGen::NS_USERNAME end def member diff --git a/lib/uuid_gen.rb b/lib/uuid_gen.rb new file mode 100644 index 0000000..0dbd068 --- /dev/null +++ b/lib/uuid_gen.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +## +# Generates a UUID for a Namespace and an readable identifier +class UUIDGen + include UUIDTools + + # TODO: replace with HANDLE namespace + NS_USERNAME = UUID.parse('fb0f6f73-a16d-4032-b508-16519fb4a73a').freeze + NS_EMAIL = UUID.parse('2282b78c-85d6-419f-b240-0263d67ee6e6').freeze + NS_HANDLE = UUID.parse('7712db07-df77-419a-8c93-2071e8fd2c50').freeze + + def self.uuid(namespace, identifier) + UUID.sha1_create(namespace, identifier) + end +end From a3541012f0b20330059cbf5e9c0eef7cc733cfff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A8r=20Kessels?= Date: Fri, 12 Mar 2021 13:38:18 +0100 Subject: [PATCH 8/8] Implement naive and mostly hardcoded flow for following a remote user --- app/aggregates/peer.rb | 22 +++++++ app/events/events.rb | 8 +++ app/reactors/contact_fetcher.rb | 23 ++++++++ app/web/controllers/application_controller.rb | 4 ++ .../controllers/web/contacts_controller.rb | 3 +- .../web/remote_confirmations_controller.rb | 45 ++++++++++++++ app/web/controllers/web/remote_controller.rb | 58 +++++++++++++++++++ app/web/views/remote.erb | 20 +++++++ app/web/views/remote_confirmation.erb | 7 +++ config.ru | 3 + lib/handle.rb | 2 +- test/aggregates/peer_test.rb | 22 +++++++ .../web/federated_contacts_test.rb | 30 ++++++++++ test/integration/web/remote_action_test.rb | 54 +++++++++++++++++ test/support/data_helpers.rb | 14 +++++ test/support/remote_helpers.rb | 37 ++++++++++++ test/test_helper.rb | 2 + 17 files changed, 352 insertions(+), 2 deletions(-) create mode 100644 app/aggregates/peer.rb create mode 100644 app/reactors/contact_fetcher.rb create mode 100644 app/web/controllers/web/remote_confirmations_controller.rb create mode 100644 app/web/controllers/web/remote_controller.rb create mode 100644 app/web/views/remote.erb create mode 100644 app/web/views/remote_confirmation.erb create mode 100644 test/aggregates/peer_test.rb create mode 100644 test/integration/web/federated_contacts_test.rb create mode 100644 test/integration/web/remote_action_test.rb create mode 100644 test/support/remote_helpers.rb diff --git a/app/aggregates/peer.rb b/app/aggregates/peer.rb new file mode 100644 index 0000000..3b8f493 --- /dev/null +++ b/app/aggregates/peer.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Aggregates + ## + # A +Peer+ is a remote actor. + class Peer + include EventSourcery::AggregateRoot + + AWAIT_SYNCING_VALUE = 'Synching...' + + attr_reader :name, :bio + + def initialize(id, events) + @name = AWAIT_SYNCING_VALUE + @bio = AWAIT_SYNCING_VALUE + super(id, events) + end + + apply PeerSynched do + end + end +end diff --git a/app/events/events.rb b/app/events/events.rb index 9ddc7bd..bc88a04 100644 --- a/app/events/events.rb +++ b/app/events/events.rb @@ -36,6 +36,14 @@ # A Member was Tagged MemberTagAdded = Class.new(EventSourcery::Event) +## +# Fetching of Details for peer from remote server requested. +PeerFetchRequested = Class.new(EventSourcery::Event) + +## +# Fetching of Details for peer finished +PeerSynched = Class.new(EventSourcery::Event) + ## # A Registration is confirmed RegistrationConfirmed = Class.new(EventSourcery::Event) diff --git a/app/reactors/contact_fetcher.rb b/app/reactors/contact_fetcher.rb new file mode 100644 index 0000000..1cbfd02 --- /dev/null +++ b/app/reactors/contact_fetcher.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Reactors + ## + # Adds an actor to a members' followers on various Events + class ContactFetcher + include EventSourcery::Postgres::Reactor + + processor_name :contact_fetcher + + emits_events PeerSynched + + process PeerFetchRequested do |event| + emit_event( + PeerSynched.new( + aggregate_id: event.aggregate_id, + body: event.body, + causation_id: event.uuid + ) + ) + end + end +end diff --git a/app/web/controllers/application_controller.rb b/app/web/controllers/application_controller.rb index ee09b23..f4d908f 100644 --- a/app/web/controllers/application_controller.rb +++ b/app/web/controllers/application_controller.rb @@ -35,6 +35,10 @@ def authorize(&block) raise Unauthorized unless block.call end + def authorized? + current_member.active? + end + def current_member return OpenStruct.new(active?: false) unless member_id diff --git a/app/web/controllers/web/contacts_controller.rb b/app/web/controllers/web/contacts_controller.rb index 0160296..deb61a0 100644 --- a/app/web/controllers/web/contacts_controller.rb +++ b/app/web/controllers/web/contacts_controller.rb @@ -17,7 +17,8 @@ class ContactsController < WebController # Add post '/contacts' do - requires_authorization + redirect '/remote' unless authorized? + authorize { may_add_contact? } Commands.handle( diff --git a/app/web/controllers/web/remote_confirmations_controller.rb b/app/web/controllers/web/remote_confirmations_controller.rb new file mode 100644 index 0000000..623333c --- /dev/null +++ b/app/web/controllers/web/remote_confirmations_controller.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Web + ## + # Handles incoming redirects from remotes to confirm an action + class RemoteConfirmationsController < WebController + # TODO: authenticate and authorize + get '/remote_confirmation' do + erb( + :remote_confirmation, + layout: :layout_member, + locals: { + message: message, + form_action: form_action, + target: target, + taget_type: target_type + } + ) + end + + private + + # TODO: Once we have more actions, fetch this from signed attributes and + # pull through an allowlist + def form_action + 'contacts' + end + + # TODO: Unhardcode this message + def message + "As @harry@example.com you want to follow #{target}" + end + + # TODO: Fetch the target from attributes + def target + '@luna@ravenclaw.example.org' + end + + # TODO: Once we have more target types, fetch this from signed attributes + # and pull through an allowlist + def target_type + 'account' + end + end +end diff --git a/app/web/controllers/web/remote_controller.rb b/app/web/controllers/web/remote_controller.rb new file mode 100644 index 0000000..ba254cf --- /dev/null +++ b/app/web/controllers/web/remote_controller.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +module Web + ## + # Handles Remote redirects + # TODO: sanitize and whitelist actions + # TODO: handle misparsed handles + # TODO: exchange server-server signed secrets with remote instance so that + # remote instance knows this request is coming and can validate it. + class RemoteController < WebController + get '/remote' do + erb( + :remote, + layout: :layout_anonymous, + locals: { + message: message, + action: action, + target: target, + taget_type: target_type + } + ) + end + + post '/remote' do + redirect remote_confirm_uri + end + + private + + def remote_confirm_uri + URI::HTTPS.build( + host: Handle.parse(params[:handle]).domain, + path: '/remote_confirmation', + query: URI.encode_www_form( + action: action, + target: target, + target_type: target_type + ) + ) + end + + def action + 'follow' + end + + def message + 'Provide your handle to follow @luna@ravenclaw.example.org' + end + + def target + '@luna.ravenclaw.example.org' + end + + def target_type + 'account' + end + end +end diff --git a/app/web/views/remote.erb b/app/web/views/remote.erb new file mode 100644 index 0000000..e0dd4b9 --- /dev/null +++ b/app/web/views/remote.erb @@ -0,0 +1,20 @@ + + + + + + +
+ +
+ +
+
+
+ +
+ diff --git a/app/web/views/remote_confirmation.erb b/app/web/views/remote_confirmation.erb new file mode 100644 index 0000000..857175d --- /dev/null +++ b/app/web/views/remote_confirmation.erb @@ -0,0 +1,7 @@ +

<%= message %>

+
+ +
+ +
+
diff --git a/config.ru b/config.ru index f902766..b21093c 100644 --- a/config.ru +++ b/config.ru @@ -12,6 +12,9 @@ use Web::HomeController use Web::ProfilesController use Web::TagsController +use Web::RemoteController +use Web::RemoteConfirmationsController + use Web::LoginController # TODO: change from RPC alike "register" to "registration" use Web::RegistrationsController diff --git a/lib/handle.rb b/lib/handle.rb index fa9b8b6..b42d186 100644 --- a/lib/handle.rb +++ b/lib/handle.rb @@ -9,7 +9,7 @@ class Handle def initialize(username, handle_domain = Roost.config.domain, - local_domain = Roost.config.domain) + local_domain = handle_domain) @domain = handle_domain @local_domain = local_domain @username = username diff --git a/test/aggregates/peer_test.rb b/test/aggregates/peer_test.rb new file mode 100644 index 0000000..9b6154a --- /dev/null +++ b/test/aggregates/peer_test.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'test_helper' + +module Aggregates + ## + # Unit test for the more complex logic in Peer Aggregate + class PeerTest < Minitest::Spec + let(:applied_events) { [] } + let(:aggregate_id) { fake_uuid(Aggregates::Peer, 1) } + + let(:subject) { Aggregates::Peer.new(aggregate_id, applied_events) } + + it '.name defaults to "Synching..."' do + assert_equal('Synching...', subject.name) + end + + it '.bio defaults to "Synching..."' do + assert_equal('Synching...', subject.bio) + end + end +end diff --git a/test/integration/web/federated_contacts_test.rb b/test/integration/web/federated_contacts_test.rb new file mode 100644 index 0000000..63ccdc0 --- /dev/null +++ b/test/integration/web/federated_contacts_test.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'test_helper' + +## +# As a member using the web-app +# When I visit another members' profile +# And I click the "add to contacts" button +# Then the member is added to my contacts +class FederatedContactsTest < Minitest::WebSpec + before do + skip 'implement remote flow first' + harry + as(harry) + + # INK: remote_action. + adds_contact.upto(:contact_added) + end + + it 'adds another member to contacts' do + # NOTE that the handle uses .com and rons email .org + assert_content( + flash(:success), + 'ron@example.com was added to your contacts' + ) + + visit '/contacts' + assert_content('ron@example.com') + end +end diff --git a/test/integration/web/remote_action_test.rb b/test/integration/web/remote_action_test.rb new file mode 100644 index 0000000..01578d8 --- /dev/null +++ b/test/integration/web/remote_action_test.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'test_helper' + +## +# As a member of this.example.com instance +# When I visit other.example.com instance +# And I request an action there +# Then I am am presented with a form for my handle +# And when I fill that form and click "{action}" button +# Then I am redirected to my own instance with the proper payload +# So that I can finalize the action on my own instance +# +# NOTE: {action} is any of (but not limited to), add contact, tag, annotate, +# etc. +class RemoteActionTest < Minitest::WebSpec + let(:handle) { luna[:handle] } + + it 'adds remote member as contact' do + landing_path = at(ravenclaw) do + visit "/m/#{handle}" + click_icon('account-plus') + + fill_in( + "Provide your handle to follow #{handle}", + with: '@harry@example.com' + ) + click_button('Follow') + page.current_path + end + + # Revisit the page to open it with harry as session + as(harry) + visit landing_path + + assert_content(page, "As @harry@example.com you want to follow #{handle}") + click_button('Confirm') + assert_content(flash(:success), "#{handle} was added to your contacts") + + process_events(%w[contact_fetch_requested contact_added]) + + visit '/contacts' + assert_content(luna[:handle]) + end + + # TODO: handle non-logged in on local. + # TODO: handle authentication properly with oauth secrets exchange. + + private + + def ravenclaw + @ravenclaw ||= RemoteInstance.new('ravenclaw.example.org', self) + end +end diff --git a/test/support/data_helpers.rb b/test/support/data_helpers.rb index 9289fab..e49498f 100644 --- a/test/support/data_helpers.rb +++ b/test/support/data_helpers.rb @@ -10,6 +10,20 @@ module DataHelpers 'Aggregates::Contact' => 4 }.freeze + def luna + return @_luna if @_luna + + @_luna = { + handle: '@luna@ravenclaw.example.org', + username: 'luna', + email: 'luna@ravenclaw.example.org', + password: 'secret' + } + member_registers(@_luna).upto(:confirmed).html + + @_luna + end + protected ## diff --git a/test/support/remote_helpers.rb b/test/support/remote_helpers.rb new file mode 100644 index 0000000..e8917b0 --- /dev/null +++ b/test/support/remote_helpers.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +## +# Helpers for testing events. +module RemoteHelpers + def at(instance, &block) + web_url_was = config.web_url + domain_was = config.domain + fqdn = instance.fqdn + + move_to_instance(URI::HTTP.build(host: fqdn, path: '').to_s, fqdn) + Capybara.using_session("on #{fqdn}") { instance.instance_eval(&block) } + ensure + move_to_instance(web_url_was, domain_was) + end + + private + + def move_to_instance(web_url, domain) + config.web_url = web_url + config.domain = domain + end + + def config + Roost.config + end + + # Test standin for a server, instance, that is external. + class RemoteInstance < SimpleDelegator + attr_reader :fqdn + + def initialize(fqdn, test_context) + @fqdn = fqdn + super(test_context) + end + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 8cf0387..564123c 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -12,6 +12,7 @@ require_relative 'support/file_helpers' require_relative 'support/mail_helpers' require_relative 'support/request_helpers' +require_relative 'support/remote_helpers' require_relative 'support/time_helpers' require_relative 'support/web_test_helpers' require_relative 'support/workflows' @@ -76,6 +77,7 @@ def app class WebSpec < Spec include Capybara::DSL include Capybara::Minitest::Assertions + include RemoteHelpers include WebTestHelpers def setup