Introduce a Signup model and integrate with QB/37id

- Creates a Queenbee::Account
- SignalId peer classes work properly
- Set up a basic template for the account (closure reasons, workflow, collection)
- Load signal_id fixtures during `bin/setup --reset`
- Update signal_id to pull in the Fizzy product
This commit is contained in:
Mike Dalessio
2025-06-13 11:38:11 -04:00
parent dacb53b8b1
commit 73ba0c074f
9 changed files with 309 additions and 1 deletions
+3
View File
@@ -39,6 +39,8 @@ gem "queuety", bc: "queuety", branch: "rails4" # needed by signal_id
gem "service_concurrency_prevention", bc: "service_concurrency_prevention" # needed by queuety
gem "portfolio", ">= 4.6", bc: "portfolio" # needed by signal_id
gem "file_repository", "~> 1.4.5", bc: "file_repository" # needed by portfolio
gem "queenbee", bc: "queenbee-plugin"
gem "activeresource", require: "active_resource" # needed by queenbee
# Telemetry, logging, and operations
gem "mission_control-jobs"
@@ -63,4 +65,5 @@ group :test do
gem "selenium-webdriver"
gem "webmock"
gem "vcr"
gem "mocha"
end
+21 -1
View File
@@ -46,6 +46,15 @@ GIT
activesupport
file_repository (>= 1.4.4)
GIT
remote: https://github.com/basecamp/queenbee-plugin
revision: bf887b5c128570d1b19a0140146ae9aa4ae2915f
specs:
queenbee (3.2.0)
activeresource
builder
rexml
GIT
remote: https://github.com/basecamp/queuety
revision: bf7e2a552fb674533d3e092f06dbf9b5d1ebc753
@@ -72,7 +81,7 @@ GIT
GIT
remote: https://github.com/basecamp/signal_id
revision: 7090b0d518b6656d8c8792da8301146da40f3eae
revision: 129483ccc1d9452fa7713080fcbf0c012b2f4107
branch: rails4
specs:
signal_id (4.3.2)
@@ -205,6 +214,14 @@ GEM
specs:
action_text-trix (2.1.15)
railties
activemodel-serializers-xml (1.0.3)
activemodel (>= 5.0.0.a)
activesupport (>= 5.0.0.a)
builder (~> 3.1)
activeresource (6.1.4)
activemodel (>= 6.0)
activemodel-serializers-xml (~> 1.0)
activesupport (>= 6.0)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
ast (2.4.3)
@@ -568,6 +585,7 @@ PLATFORMS
DEPENDENCIES
actiontext-lexical!
active_record-tenanted!
activeresource
aws-sdk-s3
bcrypt (~> 3.1.7)
bootsnap
@@ -582,11 +600,13 @@ DEPENDENCIES
jbuilder
kamal!
mission_control-jobs
mocha
mysql2
platform_agent
portfolio (>= 4.6)!
propshaft
puma (>= 5.0)
queenbee!
queuety!
rails!
rails_structured_logging!
+9
View File
@@ -2,4 +2,13 @@ class Account < ApplicationRecord
include Entropic, Joinable, SignalAccount
has_many_attached :uploads
def setup_basic_template
user = User.first
Closure::Reason.create_defaults
Collection.create!(name: "Cards", creator: user, all_access: true)
workflow = Workflow.create!(name: "Basic")
Collection.first.update!(workflow: workflow)
end
end
+19
View File
@@ -5,4 +5,23 @@ module Account::SignalAccount
# TODO: remove the "optional: true" once we've populated the accounts properly
belongs_to :signal_account, class_name: "SignalId::Account", primary_key: :queenbee_id, foreign_key: :queenbee_id, optional: true
end
class_methods do
def create_with_admin_user(queenbee_id:)
new(queenbee_id: queenbee_id).tap do |account|
SignalId::Database.on_master do
account.name = account.signal_account.name
account.save!
User.create!(
name: account.signal_account.owner.name,
email_address: account.signal_account.owner.email_address,
signal_user_id: account.signal_account.owner.id,
role: "admin",
password: SecureRandom.hex(36) # TODO: remove password column?
)
end
end
end
end
end
+124
View File
@@ -0,0 +1,124 @@
class Signup
include ActiveModel::Model
include ActiveModel::Attributes
include ActiveModel::Validations
attribute :full_name, :string
attribute :email_address, :string
attribute :password, :string
attribute :company_name, :string
attr_reader :signal_identity, :queenbee_account, :signal_account, :account, :user
validate :validate_new_identity
def initialize(...)
super
@signal_identity = nil
@queenbee_account = nil
@account = nil
@user = nil
end
def process
return false unless valid?
create_signal_identity
create_queenbee_account
create_tenant
true
rescue => error
destroy_tenant
destroy_queenbee_account
destroy_signal_identity
errors.add(:base, "An error occurred during signup: #{error.message}")
false
end
private
def create_signal_identity
SignalId::Database.on_master do
@signal_identity = build_new_identity.tap(&:save!)
end
end
def create_queenbee_account
@queenbee_account = Queenbee::Remote::Account.create!(queenbee_account_attributes)
SignalId::Database.on_master do
@signal_account = SignalId::Account.find_by(queenbee_id: @queenbee_account.id)
end
end
def create_tenant(&block)
ApplicationRecord.create_tenant(tenant_name) do
@account = Account.create_with_admin_user(queenbee_id: queenbee_account.id)
@account.setup_basic_template
@user = User.first
end
end
def destroy_tenant
if queenbee_account && ApplicationRecord.tenant_exist?(tenant_name)
ApplicationRecord.destroy_tenant(tenant_name)
@account = nil
@user = nil
end
end
def destroy_queenbee_account
queenbee_account&.cancel
@queenbee_account = nil
end
def destroy_signal_identity
SignalId::Database.on_master do
signal_identity&.destroy
end
@signal_identity = nil
end
def validate_new_identity
build_new_identity.tap do |identity|
unless identity.valid?
identity.errors.each { |error| errors.add(error.attribute, error.message) }
end
end
end
def build_new_identity
SignalId::Identity.new do |identity|
identity.name = full_name || email_address
identity.email_address = email_address
identity.username = email_address
identity.password = password
end
end
def queenbee_account_attributes
{
skip_remote: true, # Fizzy creates its own local account
product_name: "fizzy",
name: company_name.presence || signal_identity.name,
owner_identity_id: signal_identity.id,
trial: false,
subscription: subscription_attributes,
remote_request: request_attributes
}
end
def subscription_attributes
{ name: "FreeV1", price: 0 }
end
def request_attributes
{ remote_address: Current.ip_address, user_agent: Current.user_agent, referrer: Current.referrer }
end
def tenant_name
@tenant_name ||= signal_account.subdomain
end
end
+7
View File
@@ -1,8 +1,15 @@
require "signal_id"
Rails.application.config.to_prepare do
SignalId.product = "fizzy"
SignalId::Database.load_configuration SignalId::Database.default_configuration
SignalId::Database.enable_rw_splitting!
silence_warnings do
SignalId::Account::Peer = Account
SignalId::User::Peer = User
end
end
Rails.application.config.after_initialize do
@@ -10,4 +10,27 @@ class Account::SignalAccountTest < ActiveSupport::TestCase
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
+93
View File
@@ -0,0 +1,93 @@
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 "#process creates all the necessary objects" 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 @signup.account
assert @signup.account.persisted?
assert @signup.user
assert @signup.user.persisted?
assert_includes ApplicationRecord.tenants, @signup.signal_account.subdomain
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
+10
View File
@@ -4,6 +4,8 @@ require "rails/test_help"
require "webmock/minitest"
require "vcr"
require "signal_id/testing"
require "queenbee/testing/mocks"
require "mocha/minitest"
WebMock.allow_net_connect!
@@ -34,3 +36,11 @@ end
RubyLLM.configure do |config|
config.openai_api_key ||= "DUMMY-TEST-KEY" # Run tests with VCR without having to configure OpenAI API key locally.
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