Extract 37id and QB controllers, models, and tests

This commit is contained in:
Mike Dalessio
2025-09-12 13:55:06 -04:00
parent f4e2fe31b4
commit 2630e4bd74
57 changed files with 540 additions and 539 deletions
+2 -1
View File
@@ -61,7 +61,8 @@ group :test do
gem "mocha"
end
if ENV.fetch("SAAS_EXTENSION", "") != ""
require_relative "lib/bootstrap"
unless Bootstrap.local_authentication?
eval_gemfile "gems/fizzy-saas/Gemfile"
gem "fizzy-saas", path: "gems/fizzy-saas"
end
-6
View File
@@ -1,5 +1,4 @@
class SessionsController < ApplicationController
before_action :require_local_auth, only: %i[ new create ]
require_unauthenticated_access only: %i[ new create ]
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_path, alert: "Try again later." }
@@ -19,9 +18,4 @@ class SessionsController < ApplicationController
terminate_session
redirect_to_logout_url
end
private
def require_local_auth
head :forbidden unless Rails.application.config.x.local_authentication
end
end
+2 -14
View File
@@ -1,22 +1,10 @@
module LoginHelper
def login_url
if Rails.application.config.x.local_authentication
new_session_path
else
if ApplicationRecord.current_tenant
Launchpad.login_url(product: true, account: Account.sole)
else
Launchpad.login_url(product: true)
end
end
new_session_path
end
def logout_url
if Rails.application.config.x.local_authentication
new_session_path
else
Launchpad.logout_url
end
new_session_path
end
def redirect_to_login_url
-3
View File
@@ -21,9 +21,6 @@ class User < ApplicationRecord
def deactivate
sessions.delete_all
accesses.destroy_all
unless Rails.application.config.x.local_authentication
SignalId::Database.on_master { signal_user&.destroy }
end
update! active: false, email_address: deactived_email_address
end
@@ -1,3 +0,0 @@
<%= auto_submit_form_with(method: :put) do |form| %>
<%= form.hidden_field(:sig, value: @sig) %>
<% end %>
@@ -1 +0,0 @@
<%= auto_submit_form_with url: signup_completions_path, data: { turbo: false } %>
+1 -1
View File
@@ -4,7 +4,7 @@ echo "Login with david@37signals.com / secret123456 to:"
bin/rails runner - <<EOF
ApplicationRecord.with_each_tenant do |tenant|
next unless tenant =~ /\A\d+\z/
if Account.sole.signal_account || Rails.application.config.x.local_authentication
if Account.sole.queenbee_id
puts " - #{Account.sole.name}: http://fizzy.localhost:3006/#{Account.sole.queenbee_id}"
end
end
+6
View File
@@ -1,4 +1,10 @@
#!/usr/bin/env ruby
require_relative "../lib/bootstrap"
if !Bootstrap.local_authentication?
# default from rails/test_unit/runner.rb but adding the saas gem test files
ENV["DEFAULT_TEST"] = "{gems/fizzy-saas/,}test/**/*_test.rb"
ENV["DEFAULT_TEST_EXCLUDE"] = "{gems/fizzy-saas/,}test/{system,dummy,fixtures}/**/*_test.rb"
end
APP_PATH = File.expand_path('../config/application', __dir__)
require_relative '../config/boot'
require 'rails/commands'
+2 -3
View File
@@ -7,9 +7,8 @@ system("gem install bundler --conservative")
system("bundle config set --local auto_install true")
system("bundle check") || system!("bundle install")
if ENV.fetch("SAAS_EXTENSION", "") != ""
load File.expand_path("../gems/fizzy-saas/bin/setup", __dir__)
end
require_relative "../lib/bootstrap"
load File.expand_path("../gems/fizzy-saas/bin/setup", __dir__) unless Bootstrap.local_authentication?
puts "\n== Preparing database =="
if ARGV.include?("--reset")
+2 -1
View File
@@ -1 +1,2 @@
Rails.application.config.x.local_authentication = ENV["LOCAL_AUTHENTICATION"].present? || ENV["SAAS_EXTENSION"].blank?
require "bootstrap"
Rails.application.config.x.local_authentication = Bootstrap.local_authentication?
+4 -8
View File
@@ -96,17 +96,9 @@ Rails.application.routes.draw do
resource :session do
scope module: "sessions" do
resources :transfers, only: %i[ show update ]
resource :launchpad, only: %i[ show update ], controller: "launchpad"
end
end
namespace :signup do
get "/" => "accounts#new"
resources :accounts, only: %i[ new create ]
get "/session" => "sessions#create" # redirect from Launchpad after mid-signup authentication
resources :completions, only: %i[ new create ]
end
resources :users do
scope module: :users do
resource :avatar
@@ -190,6 +182,10 @@ Rails.application.routes.draw do
root "events#index"
unless Rails.application.config.x.local_authentication
mount Fizzy::Saas::Engine, at: "/", as: "saas"
end
namespace :admin do
mount MissionControl::Jobs::Engine, at: "/jobs"
end
+34 -18
View File
@@ -13,6 +13,8 @@ end
def create_tenant(signal_account_name, bare: false)
if bare
queenbee_id = Digest::SHA256.hexdigest(signal_account_name)[0..8].to_i(16)
elsif Rails.application.config.x.local_authentication
queenbee_id = ActiveRecord::FixtureSet.identify signal_account_name
else
signal_account = SignalId::Account.find_by_product_and_name!("fizzy", signal_account_name)
queenbee_id = signal_account.queenbee_id
@@ -20,7 +22,7 @@ def create_tenant(signal_account_name, bare: false)
ApplicationRecord.destroy_tenant queenbee_id
ApplicationRecord.create_tenant(queenbee_id) do
account = if bare
account = if bare || Rails.application.config.x.local_authentication
Account.create(name: signal_account_name, queenbee_id: queenbee_id).tap do
User.create!(
name: "David Heinemeier Hansson",
@@ -38,31 +40,45 @@ def create_tenant(signal_account_name, bare: false)
end
def find_or_create_user(full_name, email_address)
SignalId::Database.on_master do
unless signal_identity = SignalId::Identity.find_by_email_address(email_address)
signal_identity = SignalId::Identity.create!(
name: full_name,
email_address: email_address,
username: email_address,
password: "secret123456"
)
end
signal_account = Account.sole.signal_account
signal_user = SignalId::User.find_or_create_by!(identity: signal_identity, account: signal_account)
if user = User.find_by(signal_user_id: signal_user.id)
if Rails.application.config.x.local_authentication
if user = User.find_by(email_address: email_address)
user.password = "secret123456"
user.save!
user
else
User.create!(
signal_user_id: signal_user.id,
name: signal_identity.name,
email_address: signal_identity.email_address,
name: full_name,
email_address: email_address,
password: "secret123456"
)
end
else
SignalId::Database.on_master do
unless signal_identity = SignalId::Identity.find_by_email_address(email_address)
signal_identity = SignalId::Identity.create!(
name: full_name,
email_address: email_address,
username: email_address,
password: "secret123456"
)
end
signal_account = Account.sole.signal_account
signal_user = SignalId::User.find_or_create_by!(identity: signal_identity, account: signal_account)
if user = User.find_by(signal_user_id: signal_user.id)
user.password = "secret123456"
user.save!
user
else
User.create!(
signal_user_id: signal_user.id,
name: signal_identity.name,
email_address: signal_identity.email_address,
password: "secret123456"
)
end
end
end
end
+1 -2
View File
@@ -2,8 +2,7 @@ source "https://rubygems.org"
git_source(:bc) { |repo| "https://github.com/basecamp/#{repo}" }
# 37id and Queenbee integration
need_signal_id = ENV.fetch("LOCAL_AUTHENTICATION", "") == ""
gem "signal_id", bc: "signal_id", branch: "rails4", require: need_signal_id, ref: "d6c2a5ceed7bb57bf7dcdebdb439e7cd526fca09"
gem "signal_id", bc: "signal_id", branch: "rails4", ref: "d6c2a5ceed7bb57bf7dcdebdb439e7cd526fca09"
gem "mysql2", github: "jeremy/mysql2", branch: "force_latin1_to_utf8", ref: "b5766f5d296745d6aed014c8c0b7d82ef149ade1" # needed by signal_id
gem "queuety", bc: "queuety", branch: "rails4", ref: "bf7e2a552fb674533d3e092f06dbf9b5d1ebc753" # needed by signal_id
gem "service_concurrency_prevention", bc: "service_concurrency_prevention", ref: "320da99431718615ed86f8723e8b9d3259c602a3" # needed by queuety
@@ -0,0 +1,14 @@
module Sessions
module SignalSessions
extend ActiveSupport::Concern
included do
before_action :require_local_auth, only: %i[ new create ]
end
private
def require_local_auth
head :forbidden
end
end
end
@@ -1,6 +0,0 @@
module Fizzy
module Saas
class ApplicationController < ActionController::Base
end
end
end
@@ -1,6 +1,6 @@
class Signup::AccountsController < Signup::BaseController
before_action :reset_signup_storage
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_signup_account_path, alert: "Try again later." }
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to saas.new_signup_account_path, alert: "Try again later." }
def new
@signup = Signup.new
@@ -11,7 +11,7 @@ class Signup::AccountsController < Signup::BaseController
if @signup.recognized?
store_signup
redirect_to Launchpad.authentication_url(purpose: "signup", login_hint: @signup.email_address, redirect_uri: signup_session_url), allow_other_host: true
redirect_to Launchpad.authentication_url(purpose: "signup", login_hint: @signup.email_address, redirect_uri: saas.signup_session_url), allow_other_host: true
elsif @signup.process
redirect_to_account(@signup.account)
else
@@ -1,6 +1,5 @@
class Signup::BaseController < ApplicationController
require_untenanted_access
before_action :redirect_if_local_auth
# TODO: Remove this auth before launch.
http_basic_authenticate_with(
@@ -34,8 +33,4 @@ class Signup::BaseController < ApplicationController
redirect_to account.signal_account.owner.remote_login_url(proceed_to: root_path),
allow_other_host: true
end
def redirect_if_local_auth
render plain: "Unauthorized", status: :unauthorized if Rails.application.config.x.local_authentication
end
end
@@ -1,7 +1,7 @@
class Signup::SessionsController < Signup::BaseController
def create
if self.authenticated_identity = SignalId::Identity.authenticate(params.permit(:sig))
redirect_to new_signup_completion_url
redirect_to saas.new_signup_completion_url
else
render plain: "Authentication failed. This is probably a bug.", status: :unauthorized
end
@@ -1,6 +0,0 @@
module Fizzy
module Saas
module ApplicationHelper
end
end
end
@@ -0,0 +1,15 @@
module LoginHelper
module SignalLogin
def login_url
if ApplicationRecord.current_tenant
Launchpad.login_url(product: true, account: Account.sole)
else
Launchpad.login_url(product: true)
end
end
def logout_url
Launchpad.logout_url
end
end
end
@@ -1,6 +0,0 @@
module Fizzy
module Saas
class ApplicationJob < ActiveJob::Base
end
end
end
@@ -1,8 +0,0 @@
module Fizzy
module Saas
class ApplicationMailer < ActionMailer::Base
default from: "from@example.com"
layout "mailer"
end
end
end
@@ -1,10 +1,8 @@
module Account::SignalAccount
extend ActiveSupport::Concern
included do
unless Rails.application.config.x.local_authentication
belongs_to :signal_account, class_name: "SignalId::Account", primary_key: :queenbee_id, foreign_key: :queenbee_id, optional: true
end
prepended do
belongs_to :signal_account, class_name: "SignalId::Account", primary_key: :queenbee_id, foreign_key: :queenbee_id, optional: true
end
class_methods do
@@ -1,7 +0,0 @@
module Fizzy
module Saas
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end
end
end
@@ -1,9 +1,12 @@
module User::SignalUser
extend ActiveSupport::Concern
included do
unless Rails.application.config.x.local_authentication
belongs_to :signal_user, dependent: :destroy, class_name: "SignalId::User", optional: true
end
prepended do
belongs_to :signal_user, dependent: :destroy, class_name: "SignalId::User", optional: true
end
def deactivate
super
SignalId::Database.on_master { signal_user&.destroy }
end
end
@@ -0,0 +1,3 @@
<%= auto_submit_form_with(method: :put, url: saas.session_launchpad_path) do |form| %>
<%= form.hidden_field(:sig, value: @sig) %>
<% end %>
@@ -4,7 +4,7 @@
<h1 class="txt-xx-large margin-block-end-double">Fizzy</h1>
<h2 class="txt-large margin-block-end-double">Create your account</h2>
<%= form_with model: @signup, url: signup_accounts_path, scope: "signup", class: "flex flex-column gap txt-large", data: { turbo: false } do |form| %>
<%= form_with model: @signup, url: saas.signup_accounts_path, scope: "signup", class: "flex flex-column gap txt-large", data: { turbo: false } do |form| %>
<div class="flex align-center gap">
<label class="flex align-center gap input input--actor txt-large">
<%= form.text_field :full_name, class: "input", autocomplete: "name", placeholder: "Your name", autofocus: true, required: true %>
@@ -0,0 +1 @@
<%= auto_submit_form_with url: saas.signup_completions_path, data: { turbo: false } %>
+14 -1
View File
@@ -1,3 +1,16 @@
Fizzy::Saas::Engine.routes.draw do
::Queenbee.routes(self)
resource :session do
scope module: "sessions" do
resource :launchpad, only: %i[ show update ], controller: "launchpad"
end
end
namespace :signup do
get "/" => "accounts#new"
resources :accounts, only: %i[ new create ]
get "/session" => "sessions#create" # redirect from Launchpad after mid-signup authentication
resources :completions, only: %i[ new create ]
end
Queenbee.routes(self)
end
+4 -4
View File
@@ -3,12 +3,12 @@ require "signal_id"
module Fizzy
module Saas
class Engine < ::Rails::Engine
isolate_namespace Fizzy::Saas
# extend application models
config.to_prepare do
User.include User::SignalUser
Account.include Account::SignalAccount
User.prepend User::SignalUser
Account.prepend Account::SignalAccount
LoginHelper.prepend LoginHelper::SignalLogin
SessionsController.include Sessions::SignalSessions
end
# moved from config/initializers/queenbee.rb
@@ -0,0 +1,17 @@
require "test_helper"
class ControllerAuthenticationTest < ActionDispatch::IntegrationTest
test "access without an account slug redirects to launchpad" do
integration_session.default_url_options[:script_name] = "" # no tenant
get cards_path
assert_redirected_to Launchpad.login_url(product: true)
end
test "access with an account slug but no session redirects to launchpad" do
get cards_path
assert_redirected_to Launchpad.login_url(product: true, account: Account.sole)
end
end
@@ -0,0 +1,38 @@
require "test_helper"
class Sessions::LaunchpadControllerTest < ActionDispatch::IntegrationTest
test "show renders when not signed in" do
get saas.session_launchpad_path(params: { sig: "test-sig" })
assert_response :success
assert_select "form input#sig" do |node|
assert_equal node.length, 1
assert_equal node.first["value"], "test-sig"
end
end
test "create establishes a session when the sig is valid" do
user = users(:david)
put saas.session_launchpad_path(params: { sig: user.signal_user.perishable_signature })
assert_redirected_to root_url
assert parsed_cookies.signed[:session_token]
end
test "create checks user.active?" do
user = users(:david)
user.update! active: false
put saas.session_launchpad_path(params: { sig: user.signal_user.perishable_signature })
assert_response :unauthorized
end
test "returns 401 when the sig is invalid" do
put saas.session_launchpad_path(params: { sig: "invalid" })
assert_response :unauthorized
end
end
@@ -0,0 +1,24 @@
require "test_helper"
class SessionsControllerTest < ActionDispatch::IntegrationTest
test "destroy" do
sign_in_as :kevin
delete session_path
assert_redirected_to Launchpad.logout_url
assert_not cookies[:session_token].present?
end
test "new" do
get new_session_path
assert_response :forbidden
end
test "create" do
post session_path, params: { email_address: "david@37signals.com", password: "secret123456" }
assert_response :forbidden
end
end
@@ -0,0 +1,103 @@
require "test_helper"
class Signup::AccountsControllerTest < ActionDispatch::IntegrationTest
test "new under a tenanted URL redirects to the root" do
get saas.new_signup_account_url
assert_redirected_to root_url
end
test "new under an untenanted URL is OK" do
integration_session.default_url_options[:script_name] = "" # no tenant
get saas.new_signup_account_url, headers: auth_headers
assert_response :success
end
test "create with invalid params" do
integration_session.default_url_options[:script_name] = "" # no tenant
post saas.signup_accounts_url,
headers: auth_headers,
params: { signup: { full_name: "Jim", email_address: "jim@example.com", password: "", company_name: "" } }
assert_response :unprocessable_entity
assert_select "div.alert--error", text: /you need to choose a password/i
end
test "create for a new " do
integration_session.default_url_options[:script_name] = "" # no tenant
assert_difference -> { SignalId::Identity.count }, +1 do
assert_difference -> { SignalId::Account.count }, +1 do
post saas.signup_accounts_url, headers: auth_headers,
params: {
signup: {
full_name: "Jim",
email_address: "jim@example.com",
password: SecureRandom.hex(12),
company_name: "signup-accounts-controller-test-1"
}
}
end
end
signal_account = SignalId::Account.last
assert_redirected_to(/#{signal_account.login_url}/)
end
test "create for an existing identity" do
integration_session.default_url_options[:script_name] = "" # no tenant
identity = signal_identities(:david)
post saas.signup_accounts_url, headers: auth_headers,
params: { signup: { email_address: identity.email_address, company_name: "signup-accounts-controller-test-2" } }
assert_authentication_requested_for identity
assert_no_difference -> { SignalId::Identity.count } do
assert_difference -> { SignalId::Account.count } do
authenticate_via_launchpad_as(identity)
assert_redirected_to_account
assert_nil session[:signup]
end
end
signal_account = SignalId::Account.last
ApplicationRecord.with_tenant(signal_account.queenbee_id) do
assert_equal Account.last, signal_account.peer
end
end
test "actions require HTTP basic authentication while we're in internal-only mode" do
integration_session.default_url_options[:script_name] = "" # no tenant
get saas.new_signup_account_url
assert_response :unauthorized
end
private
def auth_headers
{ "HTTP_AUTHORIZATION" => ActionController::HttpAuthentication::Basic.encode_credentials("testname", "testpassword") }
end
def assert_authentication_requested_for(identity)
assert_response :redirect
assert_match(/#{Regexp.escape(Launchpad.url("/authenticate", login_hint: identity.email_address))}.*&purpose=signup/, redirect_to_url)
end
def authenticate_via_launchpad_as(identity)
get saas.signup_session_url, headers: auth_headers, params: { sig: identity.perishable_signature }
assert_equal identity.id, session[:signup]["identity_id"]
assert_redirected_to saas.new_signup_completion_url
post saas.signup_completions_url, headers: auth_headers
end
def assert_redirected_to_account(signal_account = SignalId::Account.last)
assert_response :redirect
assert_match(/#{Regexp.escape(signal_account.url("/session/launchpad"))}.*&sig=/, redirect_to_url)
end
end
@@ -0,0 +1,39 @@
require "test_helper"
class Account::SignalAccountTest < ActiveSupport::TestCase
# # TODO(MIKE): Queenbee client API compliance tests
# include Queenbee::Testing::Client
setup do
@account = accounts("37s")
end
test "belongs to a signal_account via a shared queenbee_id" do
assert_not_nil @account.queenbee_id
assert_equal @account.queenbee_id, Account.new(signal_account: @account.signal_account).queenbee_id
assert_equal @account.signal_account, Account.new(queenbee_id: @account.queenbee_id).signal_account
end
test ".create_with_admin_user creates a new local account and user peers" do
ApplicationRecord.create_tenant("account-create-with-dependents") do
signal_account = signal_accounts(:honcho_fizzy)
account = Account.create_with_admin_user(queenbee_id: signal_account.queenbee_id)
assert_not_nil account
assert account.persisted?
assert_equal 1, Account.count
assert_equal signal_account.queenbee_id, account.queenbee_id
assert_equal signal_account.name, account.name
assert_equal account, signal_account.peer
assert_equal 1, User.count
User.first.tap do |user|
assert signal_account.owner.name, user.name
assert signal_account.owner.email_address, user.email_address
assert signal_account.owner.id, user.signal_user_id
assert_equal "admin", user.role
assert_equal user, signal_account.owner.peer
end
end
end
end
+151
View File
@@ -0,0 +1,151 @@
require "test_helper"
class SignupTest < ActiveSupport::TestCase
setup do
@signup = Signup.new(
email_address: "brian@example.com",
full_name: "Brian Wilson",
company_name: "Beach Boys",
password: SecureRandom.hex(16)
)
end
test "#to_h allows persistence of the signup data" do
actual = @signup.to_h
expected = {
email_address: @signup.email_address,
full_name: @signup.full_name,
company_name: @signup.company_name
}
assert_equal expected, actual
end
test "#process creates all the necessary objects for a new identity" do
Account.any_instance.expects(:setup_basic_template).once
assert @signup.process, @signup.errors.full_messages.to_sentence(words_connector: ". ")
assert_empty @signup.errors
assert @signup.signal_identity
assert @signup.signal_identity.persisted?
assert @signup.queenbee_account
assert @signup.queenbee_account.id
assert @signup.signal_account
assert @signup.signal_account.persisted?
assert_equal @signup.signal_identity, @signup.signal_account.owner.identity
assert_equal @signup.company_name, @signup.signal_account.name
assert @signup.account
assert @signup.account.persisted?
assert @signup.user
assert @signup.user.persisted?
assert_equal @signup.user.signal_user, @signup.signal_account.owner
assert_equal @signup.queenbee_account.id.to_s, @signup.tenant_name
assert_includes ApplicationRecord.tenants, @signup.tenant_name
end
test "the new account is named with company name if present" do
assert_equal @signup.company_name, @signup.send(:queenbee_account_name)
end
test "in beta, the new account is named distinctly" do
Rails.stubs(:env).returns(stub(beta?: true))
assert_equal "#{@signup.company_name} (Beta)", @signup.send(:queenbee_account_name)
end
test "#process creates all the necessary objects for an existing identity" do
signal_identity = SignalId::Identity.find_by_email_address("david@37signals.com")
@signup = Signup.new(
signal_identity: signal_identity,
company_name: "Team LeMans",
)
Account.any_instance.expects(:setup_basic_template).once
assert @signup.process, @signup.errors.full_messages.to_sentence(words_connector: ". ")
assert_empty @signup.errors
assert @signup.signal_identity
assert_equal signal_identity, @signup.signal_identity
assert @signup.queenbee_account
assert @signup.queenbee_account.id
assert @signup.signal_account
assert @signup.signal_account.persisted?
assert_equal @signup.signal_identity, @signup.signal_account.owner.identity
assert @signup.account
assert @signup.account.persisted?
assert @signup.user
assert @signup.user.persisted?
assert_equal @signup.user.signal_user, @signup.signal_account.owner
assert_equal @signup.queenbee_account.id.to_s, @signup.tenant_name
assert_includes ApplicationRecord.tenants, @signup.tenant_name
end
test "#process does nothing if a validation error occurs creating identity" do
@signup.password = ""
assert_not @signup.process
assert_not_empty @signup.errors[:password]
assert_nil @signup.signal_identity
assert_nil @signup.queenbee_account
assert_nil @signup.account
assert_nil @signup.user
end
test "#process does nothing if a validation error occurs creating the queenbee account" do
Queenbee::Remote::Account.stubs(:create!).raises(RuntimeError, "Invalid account data")
SignalId::Identity.any_instance.expects(:destroy).once
assert_not @signup.process
assert_not_empty @signup.errors[:base]
assert_nil @signup.signal_identity
assert_nil @signup.queenbee_account
assert_nil @signup.account
assert_nil @signup.user
end
test "#process does nothing if a validation error occurs creating the tenant" do
ApplicationRecord.stubs(:create_tenant).raises(RuntimeError, "Tenant already exists")
Queenbee::Remote::Account.any_instance.expects(:cancel).once
SignalId::Identity.any_instance.expects(:destroy).once
assert_not @signup.process
assert_not_empty @signup.errors[:base]
assert_nil @signup.signal_identity
assert_nil @signup.queenbee_account
assert_nil @signup.account
assert_nil @signup.user
end
test "#process does nothing if a validation error occurs creating the account" do
Account.stubs(:create_with_admin_user).raises(RuntimeError, "Account creation failed")
ApplicationRecord.expects(:destroy_tenant).once
Queenbee::Remote::Account.any_instance.expects(:cancel).once
SignalId::Identity.any_instance.expects(:destroy).once
assert_not @signup.process
assert_not_empty @signup.errors[:base]
assert_nil @signup.signal_identity
assert_nil @signup.queenbee_account
assert_nil @signup.account
assert_nil @signup.user
end
end
@@ -0,0 +1,12 @@
require "test_helper"
class User::SignalUserTest < ActiveSupport::TestCase
setup do
@user = users(:david)
end
test "belongs to a Signal::User" do
assert_not_nil @user.signal_user_id
assert_equal signal_users("37s_fizzy_david"), @user.signal_user
end
end
+9
View File
@@ -0,0 +1,9 @@
require "test_helper"
class UserTest < ActiveSupport::TestCase
test "deactivate clears signal user" do
users(:jz).deactivate
assert_nil users(:jz).reload.signal_user
end
end
+17 -12
View File
@@ -1,15 +1,20 @@
# Configure Rails Environment
ENV["RAILS_ENV"] = "test"
require "signal_id/testing"
require "queenbee/testing/mocks"
require_relative "../test/dummy/config/environment"
ActiveRecord::Migrator.migrations_paths = [ File.expand_path("../test/dummy/db/migrate", __dir__) ]
ActiveRecord::Migrator.migrations_paths << File.expand_path("../db/migrate", __dir__)
require "rails/test_help"
module ActiveSupport
class TestCase
include SignalId::Testing
# Load fixtures from the engine
if ActiveSupport::TestCase.respond_to?(:fixture_paths=)
ActiveSupport::TestCase.fixture_paths = [ File.expand_path("fixtures", __dir__) ]
ActionDispatch::IntegrationTest.fixture_paths = ActiveSupport::TestCase.fixture_paths
ActiveSupport::TestCase.file_fixture_path = File.expand_path("fixtures", __dir__) + "/files"
ActiveSupport::TestCase.fixtures :all
def saas_extension_sign_in_as(user)
put saas.session_launchpad_path, params: { sig: user.signal_user.perishable_signature }
end
end
end
Queenbee::Remote::Account.class_eval do
# because we use the account ID as the tenant name, we need it to be unique in each test to avoid
# parallelized tests clobbering each other.
def next_id
super + Random.rand(1000000)
end
end
+5
View File
@@ -0,0 +1,5 @@
module Bootstrap
def self.local_authentication?
ENV.fetch("LOCAL_AUTHENTICATION", "") != "" || !File.directory?(File.expand_path("../gems/fizzy-saas", __dir__))
end
end
@@ -15,20 +15,6 @@ class ControllerAuthenticationTest < ActionDispatch::IntegrationTest
assert_redirected_to new_session_path
end
else
test "access without an account slug redirects to launchpad" do
integration_session.default_url_options[:script_name] = "" # no tenant
get cards_path
assert_redirected_to Launchpad.login_url(product: true)
end
test "access with an account slug but no session redirects to launchpad" do
get cards_path
assert_redirected_to Launchpad.login_url(product: true, account: Account.sole)
end
end
test "access with an account slug and a session allows functional access" do
@@ -1,40 +0,0 @@
require "test_helper"
class Sessions::LaunchpadControllerTest < ActionDispatch::IntegrationTest
unless Rails.application.config.x.local_authentication
test "show renders when not signed in" do
get session_launchpad_path(params: { sig: "test-sig" })
assert_response :success
assert_select "form input#sig" do |node|
assert_equal node.length, 1
assert_equal node.first["value"], "test-sig"
end
end
test "create establishes a session when the sig is valid" do
user = users(:david)
put session_launchpad_path(params: { sig: user.signal_user.perishable_signature })
assert_redirected_to root_url
assert parsed_cookies.signed[:session_token]
end
test "create checks user.active?" do
user = users(:david)
user.update! active: false
put session_launchpad_path(params: { sig: user.signal_user.perishable_signature })
assert_response :unauthorized
end
test "returns 401 when the sig is invalid" do
put session_launchpad_path(params: { sig: "invalid" })
assert_response :unauthorized
end
end
end
+1 -22
View File
@@ -1,28 +1,7 @@
require "test_helper"
class SessionsControllerTest < ActionDispatch::IntegrationTest
unless Rails.application.config.x.local_authentication
test "destroy" do
sign_in_as :kevin
delete session_path
assert_redirected_to Launchpad.logout_url
assert_not cookies[:session_token].present?
end
test "new" do
get new_session_path
assert_response :forbidden
end
test "create" do
post session_path, params: { email_address: "david@37signals.com", password: "secret123456" }
assert_response :forbidden
end
else
if Rails.application.config.x.local_authentication
test "destroy" do
sign_in_as :kevin
@@ -1,105 +0,0 @@
require "test_helper"
class Signup::AccountsControllerTest < ActionDispatch::IntegrationTest
unless Rails.application.config.x.local_authentication
test "new under a tenanted URL redirects to the root" do
get new_signup_account_url
assert_redirected_to root_url
end
test "new under an untenanted URL is OK" do
integration_session.default_url_options[:script_name] = "" # no tenant
get new_signup_account_url, headers: auth_headers
assert_response :success
end
test "create with invalid params" do
integration_session.default_url_options[:script_name] = "" # no tenant
post signup_accounts_url,
headers: auth_headers,
params: { signup: { full_name: "Jim", email_address: "jim@example.com", password: "", company_name: "" } }
assert_response :unprocessable_entity
assert_select "div.alert--error", text: /you need to choose a password/i
end
test "create for a new " do
integration_session.default_url_options[:script_name] = "" # no tenant
assert_difference -> { SignalId::Identity.count }, +1 do
assert_difference -> { SignalId::Account.count }, +1 do
post signup_accounts_url, headers: auth_headers,
params: {
signup: {
full_name: "Jim",
email_address: "jim@example.com",
password: SecureRandom.hex(12),
company_name: "signup-accounts-controller-test-1"
}
}
end
end
signal_account = SignalId::Account.last
assert_redirected_to(/#{signal_account.login_url}/)
end
test "create for an existing identity" do
integration_session.default_url_options[:script_name] = "" # no tenant
identity = signal_identities(:david)
post signup_accounts_url, headers: auth_headers,
params: { signup: { email_address: identity.email_address, company_name: "signup-accounts-controller-test-2" } }
assert_authentication_requested_for identity
assert_no_difference -> { SignalId::Identity.count } do
assert_difference -> { SignalId::Account.count } do
authenticate_via_launchpad_as(identity)
assert_redirected_to_account
assert_nil session[:signup]
end
end
signal_account = SignalId::Account.last
ApplicationRecord.with_tenant(signal_account.queenbee_id) do
assert_equal Account.last, signal_account.peer
end
end
test "actions require HTTP basic authentication while we're in internal-only mode" do
integration_session.default_url_options[:script_name] = "" # no tenant
get new_signup_account_url
assert_response :unauthorized
end
private
def auth_headers
{ "HTTP_AUTHORIZATION" => ActionController::HttpAuthentication::Basic.encode_credentials("testname", "testpassword") }
end
def assert_authentication_requested_for(identity)
assert_response :redirect
assert_match(/#{Regexp.escape(Launchpad.url("/authenticate", login_hint: identity.email_address))}.*&purpose=signup/, redirect_to_url)
end
def authenticate_via_launchpad_as(identity)
get signup_session_url, headers: auth_headers, params: { sig: identity.perishable_signature }
assert_equal identity.id, session[:signup]["identity_id"]
assert_redirected_to new_signup_completion_url
post signup_completions_url, headers: auth_headers
end
def assert_redirected_to_account(signal_account = SignalId::Account.last)
assert_response :redirect
assert_match(/#{Regexp.escape(signal_account.url("/session/launchpad"))}.*&sig=/, redirect_to_url)
end
end
end
+1 -1
View File
@@ -1,6 +1,6 @@
37s:
name: 37signals
join_code: "ejpP-THlQ-Cc2f"
<% unless Rails.application.config.x.local_authentication %>
<% if Account.reflect_on_association :signal_account %>
queenbee_id: <%= ActiveRecord::FixtureSet.identify :'37s_fizzy' %>
<% end %>
+3 -3
View File
@@ -5,7 +5,7 @@ david:
email_address: david@37signals.com
password_digest: <%= digest %>
role: member
<% unless Rails.application.config.x.local_authentication %>
<% if User.reflect_on_association :signal_user %>
signal_user: 37s_fizzy_david
<% end %>
@@ -14,7 +14,7 @@ jz:
email_address: jz@37signals.com
password_digest: <%= digest %>
role: member
<% unless Rails.application.config.x.local_authentication %>
<% if User.reflect_on_association :signal_user %>
signal_user: 37s_fizzy_jzimdars
<% end %>
@@ -23,7 +23,7 @@ kevin:
email_address: kevin@37signals.com
password_digest: <%= digest %>
role: admin
<% unless Rails.application.config.x.local_authentication %>
<% if User.reflect_on_association :signal_user %>
signal_user: 37s_fizzy_kevin
<% end %>
@@ -1,41 +0,0 @@
require "test_helper"
class Account::SignalAccountTest < ActiveSupport::TestCase
unless Rails.application.config.x.local_authentication
# # TODO(MIKE): Queenbee client API compliance tests
# include Queenbee::Testing::Client
setup do
@account = accounts("37s")
end
test "belongs to a signal_account via a shared queenbee_id" do
assert_not_nil @account.queenbee_id
assert_equal @account.queenbee_id, Account.new(signal_account: @account.signal_account).queenbee_id
assert_equal @account.signal_account, Account.new(queenbee_id: @account.queenbee_id).signal_account
end
test ".create_with_admin_user creates a new local account and user peers" do
ApplicationRecord.create_tenant("account-create-with-dependents") do
signal_account = signal_accounts(:honcho_fizzy)
account = Account.create_with_admin_user(queenbee_id: signal_account.queenbee_id)
assert_not_nil account
assert account.persisted?
assert_equal 1, Account.count
assert_equal signal_account.queenbee_id, account.queenbee_id
assert_equal signal_account.name, account.name
assert_equal account, signal_account.peer
assert_equal 1, User.count
User.first.tap do |user|
assert signal_account.owner.name, user.name
assert signal_account.owner.email_address, user.email_address
assert signal_account.owner.id, user.signal_user_id
assert_equal "admin", user.role
assert_equal user, signal_account.owner.peer
end
end
end
end
end
-153
View File
@@ -1,153 +0,0 @@
require "test_helper"
class SignupTest < ActiveSupport::TestCase
unless Rails.application.config.x.local_authentication
setup do
@signup = Signup.new(
email_address: "brian@example.com",
full_name: "Brian Wilson",
company_name: "Beach Boys",
password: SecureRandom.hex(16)
)
end
test "#to_h allows persistence of the signup data" do
actual = @signup.to_h
expected = {
email_address: @signup.email_address,
full_name: @signup.full_name,
company_name: @signup.company_name
}
assert_equal expected, actual
end
test "#process creates all the necessary objects for a new identity" do
Account.any_instance.expects(:setup_basic_template).once
assert @signup.process, @signup.errors.full_messages.to_sentence(words_connector: ". ")
assert_empty @signup.errors
assert @signup.signal_identity
assert @signup.signal_identity.persisted?
assert @signup.queenbee_account
assert @signup.queenbee_account.id
assert @signup.signal_account
assert @signup.signal_account.persisted?
assert_equal @signup.signal_identity, @signup.signal_account.owner.identity
assert_equal @signup.company_name, @signup.signal_account.name
assert @signup.account
assert @signup.account.persisted?
assert @signup.user
assert @signup.user.persisted?
assert_equal @signup.user.signal_user, @signup.signal_account.owner
assert_equal @signup.queenbee_account.id.to_s, @signup.tenant_name
assert_includes ApplicationRecord.tenants, @signup.tenant_name
end
test "the new account is named with company name if present" do
assert_equal @signup.company_name, @signup.send(:queenbee_account_name)
end
test "in beta, the new account is named distinctly" do
Rails.stubs(:env).returns(stub(beta?: true))
assert_equal "#{@signup.company_name} (Beta)", @signup.send(:queenbee_account_name)
end
test "#process creates all the necessary objects for an existing identity" do
signal_identity = SignalId::Identity.find_by_email_address("david@37signals.com")
@signup = Signup.new(
signal_identity: signal_identity,
company_name: "Team LeMans",
)
Account.any_instance.expects(:setup_basic_template).once
assert @signup.process, @signup.errors.full_messages.to_sentence(words_connector: ". ")
assert_empty @signup.errors
assert @signup.signal_identity
assert_equal signal_identity, @signup.signal_identity
assert @signup.queenbee_account
assert @signup.queenbee_account.id
assert @signup.signal_account
assert @signup.signal_account.persisted?
assert_equal @signup.signal_identity, @signup.signal_account.owner.identity
assert @signup.account
assert @signup.account.persisted?
assert @signup.user
assert @signup.user.persisted?
assert_equal @signup.user.signal_user, @signup.signal_account.owner
assert_equal @signup.queenbee_account.id.to_s, @signup.tenant_name
assert_includes ApplicationRecord.tenants, @signup.tenant_name
end
test "#process does nothing if a validation error occurs creating identity" do
@signup.password = ""
assert_not @signup.process
assert_not_empty @signup.errors[:password]
assert_nil @signup.signal_identity
assert_nil @signup.queenbee_account
assert_nil @signup.account
assert_nil @signup.user
end
test "#process does nothing if a validation error occurs creating the queenbee account" do
Queenbee::Remote::Account.stubs(:create!).raises(RuntimeError, "Invalid account data")
SignalId::Identity.any_instance.expects(:destroy).once
assert_not @signup.process
assert_not_empty @signup.errors[:base]
assert_nil @signup.signal_identity
assert_nil @signup.queenbee_account
assert_nil @signup.account
assert_nil @signup.user
end
test "#process does nothing if a validation error occurs creating the tenant" do
ApplicationRecord.stubs(:create_tenant).raises(RuntimeError, "Tenant already exists")
Queenbee::Remote::Account.any_instance.expects(:cancel).once
SignalId::Identity.any_instance.expects(:destroy).once
assert_not @signup.process
assert_not_empty @signup.errors[:base]
assert_nil @signup.signal_identity
assert_nil @signup.queenbee_account
assert_nil @signup.account
assert_nil @signup.user
end
test "#process does nothing if a validation error occurs creating the account" do
Account.stubs(:create_with_admin_user).raises(RuntimeError, "Account creation failed")
ApplicationRecord.expects(:destroy_tenant).once
Queenbee::Remote::Account.any_instance.expects(:cancel).once
SignalId::Identity.any_instance.expects(:destroy).once
assert_not @signup.process
assert_not_empty @signup.errors[:base]
assert_nil @signup.signal_identity
assert_nil @signup.queenbee_account
assert_nil @signup.account
assert_nil @signup.user
end
end
end
-14
View File
@@ -1,14 +0,0 @@
require "test_helper"
class User::SignalUserTest < ActiveSupport::TestCase
unless Rails.application.config.x.local_authentication
setup do
@user = users(:david)
end
test "belongs to a Signal::User" do
assert_not_nil @user.signal_user_id
assert_equal signal_users("37s_fizzy_david"), @user.signal_user
end
end
end
-3
View File
@@ -31,9 +31,6 @@ class UserTest < ActiveSupport::TestCase
end
end
end
unless Rails.application.config.x.local_authentication
assert_nil users(:jz).reload.signal_user
end
end
test "initials" do
+1 -14
View File
@@ -6,10 +6,6 @@ Rails.application.config.active_record_tenanted.default_tenant = ActiveRecord::F
require "rails/test_help"
require "webmock/minitest"
require "vcr"
unless Rails.application.config.x.local_authentication
require "signal_id/testing"
require "queenbee/testing/mocks"
end
require "mocha/minitest"
WebMock.allow_net_connect!
@@ -49,9 +45,6 @@ module ActiveSupport
fixtures :all
include ActiveJob::TestHelper
unless Rails.application.config.x.local_authentication
include SignalId::Testing
end
include ActionTextTestHelper, CardTestHelper, ChangeTestHelper, SessionTestHelper
end
end
@@ -73,11 +66,5 @@ RubyLLM.configure do |config|
end
unless Rails.application.config.x.local_authentication
Queenbee::Remote::Account.class_eval do
# because we use the account ID as the tenant name, we need it to be unique in each test to avoid
# parallelized tests clobbering each other.
def next_id
super + Random.rand(1000000)
end
end
load File.expand_path("../gems/fizzy-saas/test/test_helper.rb", __dir__)
end
+1 -1
View File
@@ -10,7 +10,7 @@ module SessionTestHelper
if Rails.application.config.x.local_authentication
post session_path, params: { email_address: user.email_address, password: "secret123456" }
else
put session_launchpad_path, params: { sig: user.signal_user.perishable_signature }
saas_extension_sign_in_as(user)
end
cookie = cookies.get_cookie "session_token"