Skip to content

Instantly share code, notes, and snippets.

@nicooga
Last active January 3, 2019 16:11
Show Gist options
  • Save nicooga/59679402789bd1f023187c5305d24c8d to your computer and use it in GitHub Desktop.
Save nicooga/59679402789bd1f023187c5305d24c8d to your computer and use it in GitHub Desktop.
Factory for creating talents for testing in screening wizard based on Eugene Mironov's code

Talent factory meant to speed up manual testing process, based on Eugene Mironov's code.

Just throw files in /lib and use in console.

Factory methods can be chained and used both from class or instances.

TalentFactory.new(vertical: :desginer).create_user.pass_application_submission.claim_english # ...
TalentFactory.create_developer_on_english_step

I regularly update this as I make modifications.

Background

The wizard is a flow/process/UI across which the applicant muyst go through to be screened and possibly accepted into the platform.

The process is composed of steps that should be completed in order, but this is not a hard rule. Screeners or other admin users can claim and approve steps in any order at any time.

I've created a list of steps that can be checked at TalentFactory::STEP_ORDER, and it's used by the factory to automate the process of creating a talent in an specific step. This is how it looks ATM:

  STEP_ORDER = -> do
    [
      :application_submission,
      :english,
      case vertical
      when :designer then :portfolio
      when :developer then :online_test
      end,
      :tech_one,
      :tech_two,
      :project,
      :payment,
      :training,
      :work_hours,
      :profile_creation,
      :legal,
      :profile_editing,
      :toptal_email
    ].compact
  end

API

::new(attributes)

Assigns some attributes that serve as config for the talent being created, like vertical, passsword, etc.

Builder methods

All the methods mentioned here can be used both from class or instance

::claim(step)

Claims an step

::approve(step)

Approves an step

::complete(step)

Given some action by the talent is need, this should automate that. This is used for example, to simulate the talent completed the online test for developers.

::on_step(step)

Passes all the steps of the wizard before step and claims step.

::just_after_completing(step)

Passes all the steps before step, and also passesstep itself.

require 'active_data'
module BaseFactory
extend ActiveSupport::Concern
include ActiveData::Model
included do
delegate :pipe, to: :new
class_attribute :config
attr_reader :history
end
class_methods do
# Defined a chainable operation that returns `self` and can
# be called both from an instance or as a class method
def defop(name, &block)
define_method(name) do |*args|
ActiveRecord::Base.transaction do
instance_exec(*args, &block)
self.talent = talent.reload
self
end
end
define_singleton_method(name) do |*args|
new.send(name, *args)
end
end
end
# You can always access the latest factory you created via `$talent_factory`,
# so you don't have to remember to assign it to a variable, or how that variable was named.
def initialize(*args)
res = super(*args)
$talent_factory = self
res
end
private
def step_action_defined?(action, step)
self.config.key?(step) && self.config.fetch(step).key?(action)
end
# Performs an step related action like "claim" or "approve" given the configuration for that step action is set.
# This helper saved me of writing lot of redundant code, at the cost of a rather hard to understand API.
# The options are pretty much self-explanatory (I hope):
#
# self.step_config = {
# english: {
# claim: BA::Step::ClaimEnglish,
# before_claim: -> {
# # do_something
# }
# approve: BA::Step::ApproveEnglish,
# approve_extra_opts: {
# new_applicant_skill_names: [],
# specialization_id: '1',
# applicant_skill_ids: %w[7889 684]
# },
# before_approve: -> {
# # do something
# }
# },
#
def perform_step_action(action, step, default_action_opts = { talent: talent, comment: 'Meh' })
ActiveRecord::Base.logger = nil
ActiveJob::Base.logger = nil
Rails.logger.info(%{[Talent Factory ##{talent.id}] Performing action "#{action} #{step}" })
step_config = self.config.fetch(step)
raw_action =
begin
step_config.fetch(action)
rescue KeyError
raise %{No action config found for action "#{action}" on step "#{step}"}
end
action_instance =
if raw_action.is_a?(Class) && raw_action < ::Granite::Action
raw_extra_opts = step_config[:"#{action}_extra_opts"]
extra_opts =
case raw_extra_opts
when Hash then raw_extra_opts
when Proc then instance_eval(&raw_extra_opts)
when nil then {}
end
opts = default_action_opts.merge(extra_opts)
raw_action.as(claimer).new(talent.next_step, opts)
elsif raw_action.is_a?(Proc)
instance_eval(&raw_action)
else
raise "WHAT HAPPENED??"
end
before_action = step_config[:"before_#{action}"]
ActiveRecord::Base.transaction do
instance_eval(&before_action) if before_action
perform_action(action_instance)
end
@history ||= []
@history.push({ action: action, step: step })
rescue Exception => e
binding.pry
raise e
end
# Performs an action while keeping track of it.
# This way, when it fails you can access `$last_performed_action.errors` to undestand what happened.
def perform_action(action)
$last_performed_action = action
action.perform!
end
end
# Creates a talent for testing in the screening wizzard
#
# Usage:
#
# TalentFactory.new
# .create_user
#
# Uncomment this to known exactly when and who is adding some shady "Something went wrong" error to the model.
#original_add_method = ActiveModel::Errors.instance_method(:add)
#ActiveModel::Errors.define_method(:add) do |*args|
#binding.pry
#original_add_method.bind(self).call(*args)
#end
require 'active_data'
class TalentFactory
include BaseFactory
attribute :id, Integer
attribute :email, String, default: 'auto.created.talent@toptal.io'
attribute :password, String, default: '123123'
attribute :priority, Symbol, default: :high
attribute :vertical, Symbol, default: :developer
attribute :claimer, Role, default: -> { Admin.active.first }
attribute :online_test_result, Integer, default: 150
attribute :skill_ids, Array, default: %w[7889 684]
attribute :enable_wizard, Boolean, default: true
attribute :enable_ntof, Boolean, default: true
attribute :country, String, default: 'Serbia'
STEP_ORDER = proc do
[
:application_submission,
:english,
case vertical
when :designer then :portfolio
else :online_test
end,
:tech_one,
:tech_two,
:project,
:payment,
:training,
:work_hours,
:profile_creation,
:legal,
:profile_editing,
:toptal_email
].compact
end
self.config = {
application_submission: {
complete: proc do
country_id = Country.find_by!(name: self.country).id
BA::Talent::Onboarding::SubmitApplicationDetails.as(talent).new(
talent,
applicant_skill_ids: skill_ids,
legal_name: talent.full_name,
citizenship_id: country_id,
country_id: country_id,
city: 'Belgrade, Serbia',
place_id: 'ChIJvT-116N6WkcR5H4X8lxkuB0',
city_name: 'Belgrade',
resume: open_file('resume.pdf'),
photo: open_file('2048x2048_photo_cropped.jpg')
)
end,
},
english: {
claim: BA::Step::ClaimEnglish,
approve: BA::Step::ApproveEnglish,
approve_extra_opts: proc do
{
new_applicant_skill_names: [],
specialization_id: '1',
applicant_skill_ids: skill_ids
}
end
},
online_test: {
claim: BA::Step::ClaimOnlineTest,
claim_extra_opts: { test_option: :codility, online_test_id: 2 },
complete: proc do
online_test = talent.online_test_results.first
raise 'you must send the test instructions manually as an admin' unless online_test.present?
report = {
url: "/#{online_test.external_id}",
report_url: "/#{online_test.external_id}",
id: online_test.external_id,
candidate: talent.id,
evaluation: {
result: online_test_result,
max_result: 300
},
score: online_test_result,
max_score: 300
}
if online_test.is_a?(CodilityResult)
BA::CodilityResult::ProcessReport.as_system.new(report: report)
else
BA::HackerRankResult::ProcessReport.as_system.new(online_test, report: report)
end
end
},
portfolio: { claim: BA::Step::ClaimPortfolio, approve: BA::Step::ApprovePortfolio },
tech_one: { claim: BA::Step::ClaimTechnicalOne, approve: BA::Step::ApproveTechnicalOne },
tech_two: { claim: BA::Step::ClaimTechnicalTwo, approve: BA::Step::ApproveTechnicalTwo },
project: { claim: BA::Step::ClaimProject, approve: BA::Step::ApproveProject },
payment: { claim: BA::Step::ClaimPayment, approve: BA::Step::ApprovePayment },
training: { claim: BA::Step::ClaimTraining, approve: BA::Step::ApproveTraining },
work_hours: {
claim: BA::Step::ClaimWorkHours,
approve: BA::Step::ApproveWorkHours,
approve_extra_opts: { allocated_hours: 40 }
},
legal: {
claim: BA::Step::ClaimLegal,
approve: BA::Step::ApproveLegal,
before_approve: -> do
%i[tax_form talent_agreement].each do |kind|
talent.contracts.create!(
subject: talent,
kind: kind,
status: :signed,
sent_at: Time.now,
title: kind,
guid: 22.times.map { rand(0..9) }.join,
signed_at: Time.now,
signature_received_at: Time.now
)
end
end
},
profile_creation: { claim: BA::Step::ClaimProfileCreation, approve: BA::Step::ApproveProfileCreation },
profile_editing: { claim: BA::Step::ClaimProfileEditing, approve: BA::Step::ApproveProfileEditing },
toptal_email: {
claim: BA::Step::ClaimToptalEmail,
approve: BA::Step::ApproveToptalEmail,
before_approve: -> { talent.update!(toptal_email: "auto.created.talent+#{talent.id}+@toptal.io") }
}
}
defop(:create_approved_developer) { create_almost_approved_talent.approve(:toptal_email) }
defop(:create_almost_approved_talent) { create_user.on_step(:toptal_email) }
defop(:create_finance_expert_on_tech_one_step) { create_finance_expert.on_step(:tech_one) }
defop(:create_designer_on_portfolio_upload_step) { create_designer.on_step(:portfolio) }
defop(:create_developer_on_english_step) { create_user.on_step(:english) }
defop :create_designer do
self.vertical = :designer
create_user
end
defop :create_finance_expert do
self.vertical = :finance_expert
create_user
end
defop :create_user do
local_priority = self.priority
ScreeningPriorityService.define_method(:call) { local_priority }
predicted_id = Talent.last.id + 1
if User.where(email: email).exists?
email_array = email.split('@')
self.email = "#{email_array.first}+#{predicted_id}+@#{email_array.second}"
end
create_ba = BA::Talent::CreateApplicant.as_system.new(
talent_kind: vertical,
email: email,
password: password,
password_confirmation: password,
full_name: "TALENT #{vertical} #{predicted_id}",
acknowledge_confidentiality: true,
screening_wizard_enabled: enable_wizard,
new_talent_onboarding_flow_enabled: enable_ntof
)
create_ba.perform!
new_talent = create_ba.subject
BA::User::ConfirmNewTalent.as_system.new(new_talent.user).perform!
self.talent = new_talent
end
defop :upload_resume do
perform_action(
BA::Talent::SaveApplication
.as(talent)
.new(talent, resume: open_file('resume.pdf'))
)
end
defop(:pass) do |step|
claim(step) if step_action_defined?(:claim, step)
complete(step) if step_action_defined?(:complete, step)
approve(step) if step_action_defined?(:approve, step)
end
defop(:claim) { |step| perform_step_action(:claim, step) }
defop(:approve) { |step| perform_step_action(:approve, step) }
defop(:complete) { |step| perform_step_action(:complete, step) }
# Passes all steps before the one you to be in, and claims the latter so you are considered to be in this step
defop(:on_step) do |step|
step_order[0...step_order.index(step)].each(&method(:pass))
claim(step) if step_action_defined?(:claim, step)
end
# Passes all required steps in order, including the one you specify
defop(:just_after_completing) do |step|
step_order[0..step_order.index(step)].each(&method(:pass))
end
def talent
@talent ||=
if id then Talent.find(id)
elsif email then Talent.find_by(email: email)
end
end
def talent=(t)
@talent = t
end
def email=(email)
t = Talent.joins(:user).find_by(users: { email: email })
self.talent = t if t
super(email)
end
private
def step_order
instance_eval(&STEP_ORDER)
end
def open_file(filename)
File.open Rails.root.join('testing', 'support', 'files', filename).to_s
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment