From a9cf7c2c7ccc0f8fc835aae76410fcb24705f417 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Tue, 7 Oct 2025 13:23:16 -0400 Subject: [PATCH] Re-introduce a Signup PORO model --- gems/fizzy-saas/app/models/signup.rb | 121 +++++++++++++++++++++ gems/fizzy-saas/test/models/signup_test.rb | 94 ++++++++++++++++ 2 files changed, 215 insertions(+) create mode 100644 gems/fizzy-saas/app/models/signup.rb create mode 100644 gems/fizzy-saas/test/models/signup_test.rb diff --git a/gems/fizzy-saas/app/models/signup.rb b/gems/fizzy-saas/app/models/signup.rb new file mode 100644 index 000000000..c0114436a --- /dev/null +++ b/gems/fizzy-saas/app/models/signup.rb @@ -0,0 +1,121 @@ +class Signup + include ActiveModel::Model + include ActiveModel::Attributes + include ActiveModel::Validations + + PERMITTED_KEYS = %i[ full_name email_address password company_name ] + + # Input attributes + attr_accessor :company_name, :full_name, :email_address, :password + validates_presence_of :company_name, :full_name, :email_address, :password + + # Output attributes + attr_reader :tenant, :account, :user, :queenbee_account + + def initialize(...) + @company_name = nil + @full_name = nil + @email_address = nil + @password = nil + @tenant = nil + @account = nil + @user = nil + @queenbee_account = nil + + super + end + + def process + return false unless valid? + + create_queenbee_account + create_tenant + + true + rescue => error + destroy_tenant + destroy_queenbee_account + + errors.add(:base, "An error occurred during signup: #{error.message}") + + false + end + + private + def create_queenbee_account + @queenbee_account = Queenbee::Remote::Account.create!(queenbee_account_attributes) + end + + def destroy_queenbee_account + @queenbee_account&.cancel + @queenbee_account = nil + end + + def create_tenant + @tenant = queenbee_account.id.to_s + ApplicationRecord.create_tenant(tenant) do + @account = Account.create_with_admin_user( + account: { + external_account_id: tenant, + name: company_name + }, + owner: { + name: full_name, + email_address: email_address, + password: password + } + ) + @user = User.first + @account.setup_basic_template + end + end + + def destroy_tenant + if tenant.present? && ApplicationRecord.tenant_exist?(tenant) + ApplicationRecord.destroy_tenant(tenant) + end + @user = nil + @account = nil + @tenant = nil + end + + def queenbee_account_attributes + {}.tap do |attributes| + # Tell Queenbee to skip the request to create a local account. We've created it ourselves. + attributes[:skip_remote] = true + + # # TODO: once we are doing our own email validation, consider setting this + # # Queenbee should not do spam checks on this account, we've done our own. + # attributes[:auto_allow] = true + + # # TODO: Terms of Service + # attributes[:terms_of_service] = true + + attributes[:product_name] = "fizzy" + attributes[:name] = company_name + attributes[:owner_name] = full_name + attributes[:owner_email] = email_address + + attributes[:trial] = true + attributes[:subscription] = subscription_attributes + attributes[:remote_request] = request_attributes + end + end + + def subscription_attributes + subscription = FreeV1Subscription + + {}.tap do |attributes| + attributes[:name] = subscription.to_param + attributes[:price] = subscription.price + end + end + + def request_attributes + {}.tap do |attributes| + attributes[:remote_address] = Current.ip_address + attributes[:user_agent] = Current.user_agent + attributes[:referrer] = Current.referrer + end + end +end diff --git a/gems/fizzy-saas/test/models/signup_test.rb b/gems/fizzy-saas/test/models/signup_test.rb new file mode 100644 index 000000000..9566fc0b4 --- /dev/null +++ b/gems/fizzy-saas/test/models/signup_test.rb @@ -0,0 +1,94 @@ +require "test_helper" + +class SignupTest < ActiveSupport::TestCase + setup do + @starting_tenants = ApplicationRecord.tenants + @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 for a new Fizzy account" 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.tenant + assert_includes ApplicationRecord.tenants, @signup.tenant + + assert @signup.account + assert @signup.account.persisted? + assert @signup.account.external_account_id + assert_equal @signup.company_name, @signup.account.name + assert_equal @signup.tenant, @signup.account.external_account_id.to_s + assert_equal @signup.tenant, @signup.account.tenant + + assert @signup.user + assert @signup.user.persisted? + assert_equal @signup.full_name, @signup.user.name + assert_equal @signup.email_address, @signup.user.email_address + + auth_params = { email_address: @signup.email_address, password: @signup.password } + user = ApplicationRecord.with_tenant(@signup.tenant) { User.authenticate_by(**auth_params) } + + assert user, "User should be able to authenticate with #{auth_params.inspect}" + assert_equal @signup.user, user + assert_equal @signup.tenant, @signup.user.tenant + end + + test "#process does nothing if a basic validation error occurs" do + @signup.password = "" + + assert_not @signup.process + assert_not_empty @signup.errors[:password] + + assert_nil @signup.tenant + assert_nil @signup.account + assert_nil @signup.user + assert_equal @starting_tenants, ApplicationRecord.tenants + end + + test "#process does nothing if an error occurs creating the queenbee record" do + Queenbee::Remote::Account.stubs(:create!).raises(RuntimeError, "Invalid account data") + + assert_not @signup.process + assert_not_empty @signup.errors[:base] + + assert_nil @signup.tenant + assert_nil @signup.account + assert_nil @signup.user + assert_equal @starting_tenants, ApplicationRecord.tenants + end + + test "#process does nothing if an error occurs creating the tenant" do + ApplicationRecord.stubs(:create_tenant).raises(RuntimeError, "Tenant already exists") + + Queenbee::Remote::Account.any_instance.expects(:cancel).once + + assert_not @signup.process + assert_not_empty @signup.errors[:base] + + assert_nil @signup.tenant + assert_nil @signup.account + assert_nil @signup.user + assert_equal @starting_tenants, ApplicationRecord.tenants + end + + test "#process does nothing if an error occurs creating the tenanted records" do + Account.stubs(:create_with_admin_user).raises(ActiveRecord::RecordInvalid, "Validation failed: Name can't be blank") + + Queenbee::Remote::Account.any_instance.expects(:cancel).once + + assert_not @signup.process + assert_not_empty @signup.errors[:base] + + assert_nil @signup.tenant + assert_nil @signup.account + assert_nil @signup.user + assert_equal @starting_tenants, ApplicationRecord.tenants + end +end