RSpec spies work differently on two different classes - ruby

I have a class Uploader which takes a file and uploads it to S3. I'm trying to test that #s3 is actually receiving a file body when upload_file is called. When I test that File is getting messages sent, the test passes. However, trying to spy on Aws::S3::Client does not work.
class Uploader
def initialize(tmp_dir_name, bucket)
#base_tmp_dir = tmp_dir_name
#s3 = Aws::S3::Client.new(region: 'us-east-1')
#bucket = bucket
#uploaded_assets = []
end
def upload_file(key, file_path)
file = File.new(file_path)
#s3.put_object(bucket: #bucket, key: key.to_s, body: file.read)
end
end
RSpec.describe Uploader do
let(:bucket) { 'test_bucket' }
let(:base_temp_dir) { 'test_temp_dir' }
let(:uploader) { Uploader.new(base_temp_dir, bucket) }
describe "#upload_file" do
let(:file) { double('file') }
before { allow(File).to receive(:new) { file } }
before { allow(file).to receive(:read).and_return('text') }
before { allow(Aws::S3::Client).to receive(:put_object) }
it "uses one file" do
uploader.upload_file('test_key', 'file_path')
expect(File).to have_received(:new).with('file_path')
end
it "sends data to s3" do
uploader.upload_file('test_key', 'file_path')
expect(Aws::S3::Client).to have_received(:put_object)
end
end
end

I ended up mocking out s3 for this particular test.
it "sends data to s3" do
test_key = 'test_key'
bucket = 'test_bucket'
fake_s3 = instance_double(Aws::S3::Client)
allow(Aws::S3::Client).to receive(:new).and_return(fake_s3)
allow(fake_s3).to receive(:put_object)
uploader.upload_file(test_key, 'file_path', record=true)
expect(fake_s3).to have_received(:put_object).with(
{bucket: bucket, key: test_key, body: 'text'})
end

Related

Copy text in html.slim using clipboard.js

I have a two-factor verification page, a secret key(Ciphertext) is displayed on it and I already have clipboard.js installed in my application.
I wonder how it is possible to create a button to copy that secret key?
= simple_form_for #google_auth, as: 'google_auth', url: verify_google_auth_path do |f|
h4 = t('.step-1')
p
span = t('.download-app')
span == t('.guide-link')
h4 = t('.step-2')
p: span = t('.scan-qr-code')
= f.input :uri do
= qr_tag(#google_auth.uri)
= f.input :otp_secret do
.input-group
= f.input_field :otp_secret, class: 'upcase', readonly: true
span.input-group-btn
a.btn.btn-default href='#{verify_google_auth_path(:app, refresh: true)}'
i.fa.fa-refresh
h4 = t('.step-3')
p: span = t('.enter-passcode')
= f.input :otp
hr.split
= f.button :wrapped, t('.submit'), cancel: settings_path
= content_for :guide do
ul.list-unstyled
li: a target='_blank' href='https://apps.apple.com/br/app/authy/id494168017'
i.fa.fa-apple
span = t('.ios')
li: a target='_blank' href='https://play.google.com/store/apps/details?id=com.authy.authy'
i.fa.fa-android
span = t('.android')
I tried to do like this, but it didn't work:
a.btn.btn-default data-clipboard-action='copy' data-clipboard-target=':otp_secret'
i.fa.fa-clipboard
In the example above it is copying only the pure otp_secret text.
spec\models\two_factor\app_spec.rb:
require 'spec_helper'
describe TwoFactor::App do
let(:member) { create :member }
let(:app) { member.app_two_factor }
describe "generate code" do
subject { app }
its(:otp_secret) { should_not be_blank }
end
describe '#refresh' do
context 'inactivated' do
it {
orig_otp_secret = app.otp_secret.dup
app.refresh!
expect(app.otp_secret).not_to eq(orig_otp_secret)
}
end
context 'activated' do
subject { create :two_factor_app, activated: true }
it {
orig_otp_secret = subject.otp_secret.dup
subject.refresh!
expect(subject.otp_secret).to eq(orig_otp_secret)
}
end
end
describe 'uniq validate' do
let(:member) { create :member }
it "reject duplicate creation" do
duplicate = TwoFactor.new app.attributes
expect(duplicate).not_to be_valid
end
end
describe 'self.fetch_by_type' do
it "return nil for wrong type" do
expect(TwoFactor.by_type(:foobar)).to be_nil
end
it "create new one by type" do
expect {
expect(app).not_to be_nil
}.to change(TwoFactor::App, :count).by(1)
end
it "retrieve exist one instead of creating" do
two_factor = member.app_two_factor
expect(member.app_two_factor).to eq(two_factor)
end
end
describe '#active!' do
subject { member.app_two_factor }
before { subject.active! }
its(:activated?) { should be_true }
end
describe '#deactive!' do
subject { create :two_factor_app, activated: true }
before { subject.deactive! }
its(:activated?) { should_not be_true }
end
describe '.activated' do
before { create :member, :app_two_factor_activated }
it "should has activated" do
expect(TwoFactor.activated?).to be_true
end
end
describe 'send_notification_mail' do
let(:mail) { ActionMailer::Base.deliveries.last }
describe "activated" do
before { app.active! }
it { expect(mail.subject).to match('Google authenticator activated') }
end
describe "deactived" do
let(:member) { create :member, :app_two_factor_activated }
before { app.deactive! }
it { expect(mail.subject).to match('Google authenticator deactivated') }
end
end
end
app.rb:
class TwoFactor::App < ::TwoFactor
def verify?
return false if otp_secret.blank?
rotp = ROTP::TOTP.new(otp_secret)
if rotp.verify(otp)
touch(:last_verify_at)
true
else
errors.add :otp, :invalid
false
end
end
def uri
totp = ROTP::TOTP.new(otp_secret)
totp.provisioning_uri(member.email) + "&issuer=#{ENV['URL_HOST']}"
end
def now
ROTP::TOTP.new(otp_secret).now
end
def refresh!
return if activated?
super
end
private
def gen_code
self.otp_secret = ROTP::Base32.random_base32
self.refreshed_at = Time.new
end
def send_notification
return if not self.activated_changed?
if self.activated
MemberMailer.google_auth_activated(member.id).deliver
else
MemberMailer.google_auth_deactivated(member.id).deliver
end
end
end
EDIT:
app\models\two_factor.rb:
class TwoFactor < ActiveRecord::Base
belongs_to :member
before_validation :gen_code, on: :create
after_update :send_notification
validates_presence_of :member, :otp_secret, :refreshed_at
attr_accessor :otp
SUBCLASS = ['app', 'sms', 'email', 'wechat']
validates_uniqueness_of :type, scope: :member_id
scope :activated, -> { where(activated: true) }
scope :require_signin, -> { where(require_signin: 1) }
class << self
def by_type(type)
return if not SUBCLASS.include?(type.to_s)
klass = "two_factor/#{type}".camelize.constantize
klass.find_or_create_by(type: klass.name)
end
def activated?
activated.any?
end
def require_signin?
require_signin.any?
end
end
def verify?
msg = "#{self.class.name}#verify? is not implemented."
raise NotImplementedError.new(msg)
end
def expired?
Time.now >= 30.minutes.since(refreshed_at)
end
def refresh!
gen_code
save
end
def active!
update activated: true, last_verify_at: Time.now
end
def set_require_signin
update require_signin: 1
end
def reset_require_signin
update require_signin: nil
end
def deactive!
update activated: false, require_signin: nil
end
private
def gen_code
msg = "#{self.class.name}#gen_code is not implemented."
raise NotImplementedError.new(msg)
end
def send_notification
msg = "#{self.class.name}#send_notification is not implemented."
raise NotImplementedError.new(msg)
end
end
What it seems you're trying to do is just to copy the value of an input field(which has been populated by other code you have) to the system clipboard. You need to use javascript to do this, if you have jquery this should work.
For your slim you need an id to target it
a.btn.btn-default id= "copy"
i.fa.fa-clipboard
Try to add an id to the input element you want to copy from
= f.input_field :otp_secret, class: 'upcase', id: "secret", readonly: true
Now try to change this and see if works.
a.btn.btn-default data-clipboard-action='copy' data-clipboard-target='secret'
i.fa.fa-clipboard
Also somewhere in your javascript you'll need to target the clip event with something like this:
new ClipboardJS('#secret');
See example here https://jsfiddle.net/ec3ywrzd/
Then you'll need this javascript to load in your html. But you'll need to be able to target the cipher field, in this example I'm using id="secret". I'm not sure if the OTP code you have generates it's own ID or now, so you may need to inspect your dom to figure out how to target it to add an ID. You may try adding an ID here:
= f.input_field :otp_secret, class: 'upcase', id: "secret", readonly: true
Otherwise you'll have to use other query selectors to target it.
But you may not need clipboardjs at all.
Here's a basic example on jsfiddle to test it you can just add any string to the input field. You'll need to add this to a JS file which will be loaded by your view layout, i.e. application.js
$(document).ready(function() {
$('#copy').click(function(){
$('#secret').select();
document.execCommand('copy');
alert("copied!");
})
})
You may also see answers to this question
I managed to solve based on suggestions from our friend #lacostenycoder.
There was only a need to change even in the show.html.slim file, looking like this:
= simple_form_for #google_auth, as: 'google_auth', url: verify_google_auth_path do |f|
h4 = t('.step-1')
p
span = t('.download-app')
span == t('.guide-link')
h4 = t('.step-2')
p: span = t('.scan-qr-code')
= f.input :uri do
= qr_tag(#google_auth.uri)
= f.input :otp_secret do
.input-group
.form-control.form-control-static = #google_auth.otp_secret
.input-group
a.btn.btn-default href="javascript:void(0)" data-clipboard-text = #google_auth.otp_secret
i.fa.fa-clipboard
a.btn.btn-default href='#{verify_google_auth_path(:app, refresh: true)}'
i.fa.fa-refresh
h4 = t('.step-3')
p: span = t('.enter-passcode')
= f.input :otp
hr.split
= f.button :wrapped, t('.submit'), cancel: settings_path
= content_for :guide do
ul.list-unstyled
li: a target='_blank' href='https://apps.apple.com/br/app/authy/id494168017'
i.fa.fa-apple
span = t('.ios')
li: a target='_blank' href='https://play.google.com/store/apps/details?id=com.authy.authy'
i.fa.fa-android
span = t('.android')

How to inspect CSV file columns inside RSpec test?

I really don't know how to inspect CSV file I created in my ROR App.
require "rails_helper"
require "shared_contexts/vcr/s3"
require "csv"
RSpec.describe ReportRuns::RunService do
describe "CSV columns" do
include_context "vcr s3 put csv"
let(:report_run) { create :report_run, report_template: report_template, created_by: user.id, mime_type: "csv" }
#let(:report_template) { create :report_template, template_structure: { module: "trial_members", filters: { trial_members: [trial_members.id] } } }
let(:report_template) { create :report_template, trial: trial }
let(:trial) { create :trial }
let(:user) { create :user }
let(:user_role) { create :user_role }
subject { described_class.new(report_run) }
before do
end
it do
get :index, format: :csv
p "response jee: #{response.body}"
p response.headers
p "report run: #{report_run.inspect}"
p "templejt: #{report_template.inspect}"
p "mime type: #{report_run[:mime_type]}"
#p "trila je: #{trial.inspect}"
p "users are: #{user.inspect}"
p "user roles su: #{user_role.inspect}"
is_expected.to be_truthy
expect(5).to match(5)
end
end
end
Use the CSV library to parse the body of the response. Then work with the CSV object.
csv = CSV.new(response.body)
You can also check the Content-type headers are correct, text/csv.

Configure expect in rspec

I want to implement rspec with expect. I tried this:
RSpec:
describe WechatRequestBuilder do
let(:request_builder) { described_class.new(env: 'test_env') }
let(:trx_types) { ['wechat'] }
let(:trx_type) { 'wechat' }
let(:gateway) { 'wechat' }
let(:currency) { 'CNY' }
let(:base_params) { request_builder.send(:base_params) }
it_behaves_like 'request builder', true
context '#submit!' do
it "sends test transactions" do
allow(request_builder).to receive(:process_trx).with(trx_types, gateway)
binding.pry
request_builder.submit!
expect(request_builder.submit!).to receive(:process_trx).with(trx_types, gateway)
end
end
end
Request modifier:
class RequestModifier
def get_trx_type(request_body)
doc = Nokogiri::XML(request_body)
doc.search("transaction_type").first.text
end
end
I tried to find some object with binding.pry but without a luck:
[1] pry(#<RSpec::ExampleGroups::WechatRequestBuilder::Submit>)> request_builder
=> #<WechatRequestBuilder:0x007ffc1af4fd80 #env="test_env", #request_modifier=#<RequestModifier:0x007ffc1af4fd30>>
Can you give e some example based on the above code what should I configure as 'expect'? Currently I get:
(nil).process_trx(["wechat"], "wechat")
expected: 1 time with arguments: (["wechat"], "wechat")
received: 0 times

How to properly mock objects in RSpec?

I have a simple class, which generates a download URL to a file stored on S3 and I need to write a unit test to test this class. So far I've had no luck.
class S3DownloadUrlGenerator
def initialize(filename)
#filename = filename
end
def presigned_url
signer = Aws::S3::Presigner.new(client: s3)
signer.presigned_url(
:get_object,
bucket: "my-bucket",
key: filename,
response_content_disposition: "attachment",
)
end
private
def s3
#s3 ||= Aws::S3::Client.new(
region: "my-region,
http_open_timeout: 5,
http_read_timeout: 25,
)
end
attr_reader :filename
end
I want to test if calling #presigned_url on an instance of S3DownloadUrlGenerator returns a URL.
This is my test:
describe S3DownloadUrlGenerator do
before do
allow(Aws::S3::Client).to receive(:new) { s3_client }
end
let(:s3_client) { spy("s3 client") }
let(:presigner) { spy("s3 presigner") }
it "generates download URL for a file" do
expect(Aws::S3::Presigner).to receive(:new).with(client: s3_client).and_return(presigner)
expect(presigner).to receive(:presigned_url).with(
:get_object,
bucket: "my-test-bucket",
key: "test_file.txt",
response_content_disposition: "attachment",
).and_return("https://www.example.com")
expect(described_class.new("Test_file.txt").presigned_url).to eq("https://www.example.com")
end
end
but I get an error:
Failure/Error: expect(described_class.new("Test_file.txt").presigned_url).to eq("https://www.example.com")
expected: "https://www.example.com"
got: #<Double "s3 presigner">
(compared using ==)
I am bit new to this and I would like to learn how to properly test such cases. Thank you very much for the help.
bucket and key parameters differ in actual calling and mocking.
Use below code it works:
describe S3DownloadUrlGenerator do
before do
allow(Aws::S3::Client).to receive(:new) { s3_client }
end
let(:s3_client) { spy("s3 client") }
let(:presigner) { spy("s3 presigner") }
it "generates download URL for a file" do
expect(Aws::S3::Presigner).to receive(:new).with(client: s3_client).and_return(presigner)
expect(presigner).to receive(:presigned_url).with(
:get_object,
bucket: "my-bucket",
key: "Test_file.txt",
response_content_disposition: "attachment",
).and_return("https://www.example.com")
expect(described_class.new("Test_file.txt").presigned_url).to eq("https://www.example.com")
end
end

Why is my rspec test doubling my objects?

This is one of those cases where my code is working but my test is failing and I need to know what I am doing wrong?
I have a Project class with an all method that just spits out instances of this class:
class Project
##all_projects = []
def initialize(options)
##all_projects << self
end
def self.all
##all_projects
end
end
Now Project.all works just fine but the spec I am writing doesn't.
context "manipulating projects" do
before do
options1 = {
name: 'Building house'
}
options2 = {
name: 'Getting a loan from the Bank'
}
#project1 = Project.new(options1)
#project2 = Project.new(options2)
end
it "can print all projects" do
Project.all.should eq([#project1, #project2])
end
The failure message I get is:
Project manipulating projects can print all projects
Failure/Error: Project.all.should eq([#project1, #project2])
expected: [Building house, Getting a loan from the Bank]
got: [Building house, Building house, Building house, Getting a loan from the Bank, Building house, Getting a loan from the Bank]
Here is the full spec in a gist: https://gist.github.com/4535863
What am I doing wrong? How can I fix it?
It is doubling the results because it runs the before block for each test, where the class attribute is modified (when two new projects are initialized), and (according to the gist) the test you're referring to is the second one.
To avoid the problem you'll need to reset ##all_projects in an after block:
after do
Project.class_variable_set :##all_projects, []
end
See also: How can I clear class variables between rspec tests in ruby
(Thanks to #iain for the suggestion to move the reset code to an after block rather than a before block.)
This doesn't use before blocks to set stinky instance variables.
describe Project do
let(:options1){
{
name: 'Building house',
priority: 2,
tasks: []
}
}
let(:options2) {
{
name: 'Getting a loan from the Bank',
priority: 3,
tasks: []
}
}
let(:project1) { Project.new(options1) }
let(:project2) { Project.new(options2) }
context "while starting up" do
subject { Project.new options1 }
its(:name) { should include('Building house') }
its(:tasks) { should be_empty }
end
context "manipulating projects" do
before :all do
Project.all.clear
end
subject { Project.all }
its(:count) { should be > 0 }
it { should eq [project1, project2] }
end
end

Resources