New implementation for attachments creation

This commit is contained in:
Urban Rotnik 2019-06-27 13:10:28 +02:00
parent 1fd0ca6795
commit 9cb4454537
18 changed files with 190 additions and 61 deletions

View file

@ -18,8 +18,8 @@ var ExternalProtocols = (function() {
var msg; var msg;
msg = key.charAt(0).toUpperCase() + key.slice(1) + ': ' + errors.protocol[key].join(', '); msg = key.charAt(0).toUpperCase() + key.slice(1) + ': ' + errors.protocol[key].join(', ');
if ((input.length > 0) && (errors.protocol[key].length > 0)) { if ((input.length > 0) && (errors.protocol[key].length > 0)) {
input.next('span.help-block').html(msg); input.parent().next('span.help-block').html(msg);
input.parent().addClass('has-error'); input.parent().parent().addClass('has-error');
} else if (errors.protocol[key].length > 0) { } else if (errors.protocol[key].length > 0) {
modal.find('.general-error > span').append(msg + '<br/>'); modal.find('.general-error > span').append(msg + '<br/>');
} }
@ -72,7 +72,7 @@ var ExternalProtocols = (function() {
} }
function initLoadProtocolModalPreview() { function initLoadProtocolModalPreview() {
var externalProtocols = $('.exteral-proocol-result'); var externalProtocols = $('.external-protocol-result');
externalProtocols.on('click', 'a[data-action="external-import"]', function(e) { externalProtocols.on('click', 'a[data-action="external-import"]', function(e) {
var link = $(this); var link = $(this);
animateSpinner(null, true); animateSpinner(null, true);
@ -120,7 +120,6 @@ var ExternalProtocols = (function() {
function initFormSubmits() { function initFormSubmits() {
var modal = $('#protocol-preview-modal'); var modal = $('#protocol-preview-modal');
modal.on('click', 'button[data-action=import_protocol]', function() { modal.on('click', 'button[data-action=import_protocol]', function() {
// console.log('click');
var form = modal.find('form'); var form = modal.find('form');
var hiddenField = form.find('#protocol_protocol_type'); var hiddenField = form.find('#protocol_protocol_type');
hiddenField.attr('value', $(this).data('import_type')); hiddenField.attr('value', $(this).data('import_type'));

View file

@ -15,7 +15,7 @@
} }
} }
.general-eror { .general-error {
text-align: center; text-align: center;
} }
} }

View file

@ -47,23 +47,26 @@ class ExternalProtocolsController < ApplicationController
protocol_source: new_params[:protocol_source], protocol_source: new_params[:protocol_source],
protocol_client_id: new_params[:protocol_client_id], protocol_client_id: new_params[:protocol_client_id],
user_id: current_user.id, user_id: current_user.id,
team_id: @team.id team_id: @team.id,
build_with_assets: false
) )
p = service_call.built_protocol
p.valid? # Get validations errors here
@protocol = p
if service_call.succeed? if service_call.succeed?
@protocol = service_call.built_protocol
@protocol&.valid? # Get validations errors here
render json: { render json: {
html: render_to_string( html: render_to_string(
partial: 'protocol_importers/import_form.html.erb', partial: 'protocol_importers/import_form.html.erb',
locals: { protocol: p, steps_json: service_call.serialized_steps } locals: { protocol: @protocol,
steps_json: service_call.serialized_steps,
steps_assets: service_call.steps_assets }
), ),
title: "Import protocol - #{p.name}", title: t('protocol_importers.new.modal_title', protocol_name: @protocol.name),
footer: render_to_string( footer: render_to_string(
partial: 'protocol_importers/preview_modal_footer.html.erb' partial: 'protocol_importers/preview_modal_footer.html.erb'
), ),
validation_errors: { protocol: p.errors.messages } validation_errors: { protocol: @protocol.errors.messages }
} }
else else
render json: { errors: service_call.errors }, status: 400 render json: { errors: service_call.errors }, status: 400
@ -74,7 +77,7 @@ class ExternalProtocolsController < ApplicationController
def create def create
service_call = ProtocolImporters::ImportProtocolService.call( service_call = ProtocolImporters::ImportProtocolService.call(
protocol_params: create_protocol_params, protocol_params: create_protocol_params,
steps_params: JSON.parse(create_steps_params[:steps]), steps_params_json: create_steps_params[:steps],
user_id: current_user.id, user_id: current_user.id,
team_id: @team.id team_id: @team.id
) )

View file

@ -5,13 +5,15 @@ module ProtocolImporters
extend Service extend Service
require 'protocol_importers/protocols_io/v3/errors' require 'protocol_importers/protocols_io/v3/errors'
attr_reader :errors, :built_protocol attr_reader :errors, :built_protocol, :steps_assets
def initialize(protocol_client_id:, protocol_source:, user_id:, team_id:) def initialize(protocol_client_id:, protocol_source:, user_id:, team_id:, build_with_assets: true)
@id = protocol_client_id @id = protocol_client_id
@protocol_source = protocol_source @protocol_source = protocol_source
@user = User.find user_id @user = User.find_by_id user_id
@team = Team.find team_id @team = Team.find_by_id team_id
@build_with_assets = build_with_assets
@steps_assets = {}
@errors = {} @errors = {}
end end
@ -26,11 +28,12 @@ module ProtocolImporters
pio = ProtocolImporters::ProtocolIntermediateObject.new(normalized_json: normalized_hash, pio = ProtocolImporters::ProtocolIntermediateObject.new(normalized_json: normalized_hash,
user: @user, user: @user,
team: @team) team: @team,
build_with_assets: @build_with_assets)
@built_protocol = pio.build @built_protocol = pio.build
# Protocol with AR errors should not handled here as someting whats wrong. @steps_assets = pio.steps_assets unless @build_with_assets
# @errors[:protocol] = pio.protocol.errors unless @built_protocol.valid?
self self
rescue api_errors => e rescue api_errors => e
@errors[e.error_type] = e.message @errors[e.error_type] = e.message
@ -48,14 +51,18 @@ module ProtocolImporters
end end
def serialized_steps def serialized_steps
# Serialize steps with nested attributes for Tables and NOT nasted attributes for Assets
# We want to avoid creating (downloading) Assets instances on building first time and again on importing/creating,
# when both actions are not in a row.
# Also serialization does not work properly with paperclip attrs
return nil unless built_protocol return nil unless built_protocol
built_protocol.steps.map do |step| built_protocol.steps.map do |step|
step_hash = step.attributes.symbolize_keys.slice(:name, :description, :position) step_hash = step.attributes.symbolize_keys.slice(:name, :description, :position)
# if step.assets.any? if !@build_with_assets && @steps_assets[step.position].any?
# step_hash[:assets_attributes] = step.assets.map { |a| a.attributes.symbolize_keys.except(:id) } step_hash[:attachments] = @steps_assets[step.position]
# end end
if step.tables.any? if step.tables.any?
step_hash[:tables_attributes] = step.tables.map { |t| t.attributes.symbolize_keys.slice(:contents) } step_hash[:tables_attributes] = step.tables.map { |t| t.attributes.symbolize_keys.slice(:contents) }

View file

@ -6,11 +6,11 @@ module ProtocolImporters
attr_reader :errors, :protocol attr_reader :errors, :protocol
def initialize(protocol_params:, steps_params:, team_id:, user_id:) def initialize(protocol_params:, steps_params_json:, team_id:, user_id:)
@user = User.find user_id @user = User.find_by_id user_id
@team = Team.find team_id @team = Team.find_by_id team_id
@protocol_params = protocol_params @protocol_params = protocol_params
@steps_params = steps_params @steps_params = JSON.parse(steps_params_json) # catch error here
@errors = {} @errors = {}
end end
@ -18,10 +18,17 @@ module ProtocolImporters
return self unless valid? return self unless valid?
@protocol = Protocol.new(@protocol_params.merge!(added_by: @user, team: @team)) @protocol = Protocol.new(@protocol_params.merge!(added_by: @user, team: @team))
@protocol.steps << @steps_params.collect do |step_params|
Step.new(step_params.merge(user: @user, completed: false))
# TODO: Manually create file here. "Accept nasted attributes won't work here" @protocol.steps << @steps_params.map do |step_params|
# Create step with nested attributes for tables
s = Step.new(step_params
.symbolize_keys
.slice(:name, :position, :description, :tables_attributes)
.merge(user: @user, completed: false))
# 'Manually' create assets here. "Accept nasted attributes won't work assets"
s.assets << AttachmentsBuilder.generate(step_params.deep_symbolize_keys, user: @user, team: @team)
s
end end
@errors[:protocol] = @protocol.errors.messages unless @protocol.save @errors[:protocol] = @protocol.errors.messages unless @protocol.save

View file

@ -2,16 +2,27 @@
module ProtocolImporters module ProtocolImporters
module AttachmentsBuilder module AttachmentsBuilder
def self.generate(step_json) def self.generate(step_json, user: nil, team: nil)
return [] unless step_json[:attachments]&.any? return [] unless step_json[:attachments]&.any?
step_json[:attachments].map do |f| step_json[:attachments].map do |f|
Asset.new(file: URI.parse(f[:url]), Asset.new(file: URI.parse(f[:url]),
created_by: @user, created_by: user,
last_modified_by: @user, last_modified_by: user,
team: @team, team: team,
file_file_name: f[:name]) file_file_name: f[:name])
end end
end end
def self.generate_json(step_json)
return [] unless step_json[:attachments]&.any?
step_json[:attachments].map do |f|
{
name: f[:name],
url: f[:url]
}
end
end
end end
end end

View file

@ -2,12 +2,14 @@
module ProtocolImporters module ProtocolImporters
class ProtocolIntermediateObject class ProtocolIntermediateObject
attr_accessor :normalized_protocol_data, :user, :team, :protocol attr_reader :normalized_protocol_data, :user, :team, :protocol, :steps_assets, :build_with_assets
def initialize(normalized_json: {}, user:, team:) def initialize(normalized_json: {}, user:, team:, build_with_assets: true)
@normalized_protocol_data = normalized_json.with_indifferent_access[:protocol] if normalized_json @normalized_protocol_data = normalized_json.with_indifferent_access[:protocol] if normalized_json
@user = user @user = user
@team = team @team = team
@steps_assets = {}
@build_with_assets = build_with_assets
end end
def import def import
@ -28,7 +30,11 @@ module ProtocolImporters
def build_steps def build_steps
@normalized_protocol_data[:steps].map do |s| @normalized_protocol_data[:steps].map do |s|
step = Step.new(step_attributes(s)) step = Step.new(step_attributes(s))
step.assets << AttachmentsBuilder.generate(s) if @build_with_assets
step.assets << AttachmentsBuilder.generate(s, user: user, team: team)
else
@steps_assets[step.position] = AttachmentsBuilder.generate_json(s)
end
step.tables << TablesBuilder.extract_tables_from_html_string(s[:description][:body], true) step.tables << TablesBuilder.extract_tables_from_html_string(s[:description][:body], true)
step.description = StepDescriptionBuilder.generate(s) step.description = StepDescriptionBuilder.generate(s)
step step

View file

@ -43,7 +43,7 @@
</div> </div>
<div id="steps"> <div id="steps">
<% protocol.steps.sort_by{ |s| s.position }.each do |step| %> <% protocol.steps.sort_by{ |s| s.position }.each do |step| %>
<%= render partial: "steps/step.html.erb", locals: { step: step, preview: true } %> <%= render partial: "steps/step.html.erb", locals: { step: step, steps_assets: steps_assets, preview: true, import: true } %>
<% end %> <% end %>
</div> </div>
</div> </div>

View file

@ -51,6 +51,9 @@
<div class='row empty-text'> <div class='row empty-text'>
<%= t('protocols.index.external_protocols.list_panel.empty_text') %> <%= t('protocols.index.external_protocols.list_panel.empty_text') %>
</div> </div>
<div class="external-protocol-result">
<a href="#" data-source="protocolsio/v3" data-id="11176" data-action="external-import" data-url="<%= team_build_external_protocol_path(current_team.id,) %>">Protocols IO, 11176</a>
</div>
<div class='protocol-card' <div class='protocol-card'
data-protocol-source='protocolsio/v3' data-protocol-source='protocolsio/v3'
@ -72,11 +75,6 @@
data-show-protocol-id='errorr'> data-show-protocol-id='errorr'>
Error protocol (click me, should default to default screen) Error protocol (click me, should default to default screen)
</div> </div>
<div class="exteral-proocol-result">
<a href="#" data-source="protocolsio/v3" data-id="11176" data-action="external-import" data-url="<%= team_build_external_protocol_path(current_team.id,) %>">Protocols IO, 11176</a>
</div>
</div> </div>
<div class='col-md-7 protocol-preview-panel'> <div class='col-md-7 protocol-preview-panel'>

View file

@ -1,4 +1,5 @@
<% preview = (defined?(preview) ? preview : false) %> <% preview = (defined?(preview) ? preview : false) %>
<% import = (defined?(import) ? import : false) %>
<div class ="step <%= step.completed? ? "completed" : "not-completed" %>"> <div class ="step <%= step.completed? ? "completed" : "not-completed" %>">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
@ -113,7 +114,12 @@
</div> </div>
<% end %> <% end %>
<% if import %>
<%= render partial: 'steps/attachments/preview_list.html.erb',
locals: { attachments: steps_assets[step.position]} %>
<% else %>
<%= render partial: 'steps/attachments/list.html.erb', locals: { step: step, preview: preview } %> <%= render partial: 'steps/attachments/list.html.erb', locals: { step: step, preview: preview } %>
<% end %>
<% unless step.checklists.blank? then %> <% unless step.checklists.blank? then %>
<div class="col-xs-12"> <div class="col-xs-12">
@ -147,10 +153,12 @@
</div> </div>
<% end %> <% end %>
</div> </div>
<% unless import %>
<hr> <hr>
<%= render partial: 'steps/comments.html.erb', locals: { comments: step.last_comments, <%= render partial: 'steps/comments.html.erb', locals: { comments: step.last_comments,
comments_count: step.step_comments.count, comments_count: step.step_comments.count,
step: step } %> step: step } %>
<% end %>
</div> </div>
</div> </div>
</div> </div>

View file

@ -0,0 +1,9 @@
<div class="attachment-placeholder pull-left new">
<div class="attachment-thumbnail no-shadow">
<i class="fas fa-image"></i>
</div>
<div class="attachment-label"><%= truncate(file_name || file_url, length: Constants::FILENAME_TRUNCATION_LENGTH) %>
</div>
<div class="spencer-bonnet-modif">
</div>
</div>

View file

@ -0,0 +1,23 @@
<div class="col-xs-12">
<hr>
</div>
<div class="col-xs-12 attachments-actions">
<div class="title">
<h4>
<%= t('protocols.steps.files', count: attachments.size) %>
</h4>
</div>
<div>
<div class="attachemnts-header pull-right">
</div>
</div>
</div>
<div class="col-xs-12 attachments">
<% attachments.each do |a| %>
<%= render partial: 'steps/attachments/new_attachment.html.erb',
locals: { file_url: a[:url], file_name: a[:name] } %>
<% end %>
</div>
<hr>

View file

@ -1,5 +1,7 @@
en: en:
protocol_importers: protocol_importers:
new:
modal_title: 'Import protocol - %{protocol_name}'
templates: templates:
amount: amount:
title: 'Amount' title: 'Amount'

View file

@ -110,6 +110,8 @@ describe ExternalProtocolsController, type: :controller do
service = double('success_service') service = double('success_service')
allow(service).to(receive(:succeed?)).and_return(true) allow(service).to(receive(:succeed?)).and_return(true)
allow(service).to(receive(:built_protocol)).and_return(protocol) allow(service).to(receive(:built_protocol)).and_return(protocol)
allow(service).to(receive(:serialized_steps)).and_return({}.to_s)
allow(service).to(receive(:steps_assets)).and_return([])
allow_any_instance_of(ProtocolImporters::BuildProtocolFromClientService) allow_any_instance_of(ProtocolImporters::BuildProtocolFromClientService)
.to(receive(:call)).and_return(service) .to(receive(:call)).and_return(service)
@ -148,8 +150,10 @@ describe ExternalProtocolsController, type: :controller do
let(:params) do let(:params) do
{ {
team_id: team.id, team_id: team.id,
protocol_params: {}, protocol: {
steps_params: {} name: 'name',
steps: {}.to_s
}
} }
end end
@ -159,7 +163,7 @@ describe ExternalProtocolsController, type: :controller do
# Setup double # Setup double
service = double('success_service') service = double('success_service')
allow(service).to(receive(:succeed?)).and_return(true) allow(service).to(receive(:succeed?)).and_return(true)
allow(service).to(receive(:protocol)).and_return({}) allow(service).to(receive(:protocol)).and_return(create(:protocol))
allow_any_instance_of(ProtocolImporters::ImportProtocolService).to(receive(:call)).and_return(service) allow_any_instance_of(ProtocolImporters::ImportProtocolService).to(receive(:call)).and_return(service)

View file

@ -9,6 +9,14 @@ describe ProtocolImporters::BuildProtocolFromClientService do
ProtocolImporters::BuildProtocolFromClientService ProtocolImporters::BuildProtocolFromClientService
.call(protocol_client_id: 'id', protocol_source: 'protocolsio/v3', user_id: user.id, team_id: team.id) .call(protocol_client_id: 'id', protocol_source: 'protocolsio/v3', user_id: user.id, team_id: team.id)
end end
let(:service_call_without_assets) do
ProtocolImporters::BuildProtocolFromClientService
.call(protocol_client_id: 'id',
protocol_source: 'protocolsio/v3',
user_id: user.id,
team_id: team.id,
build_with_assets: false)
end
let(:normalized_response) do let(:normalized_response) do
JSON.parse(file_fixture('protocol_importers/normalized_single_protocol.json').read) JSON.parse(file_fixture('protocol_importers/normalized_single_protocol.json').read)
.to_h.with_indifferent_access .to_h.with_indifferent_access
@ -16,7 +24,7 @@ describe ProtocolImporters::BuildProtocolFromClientService do
context 'when have invalid arguments' do context 'when have invalid arguments' do
it 'returns an error when can\'t find user' do it 'returns an error when can\'t find user' do
allow(User).to receive(:find).and_return(nil) allow(User).to receive(:find_by_id).and_return(nil)
expect(service_call.errors).to have_key(:invalid_arguments) expect(service_call.errors).to have_key(:invalid_arguments)
end end
@ -69,5 +77,13 @@ describe ProtocolImporters::BuildProtocolFromClientService do
expect(service_call.built_protocol).to be_instance_of(Protocol) expect(service_call.built_protocol).to be_instance_of(Protocol)
end end
# more tests will be implemented when add error handling to service # more tests will be implemented when add error handling to service
describe 'serialized_steps' do
context 'when build without assets' do
it 'returns JSON with attachments' do
expect(JSON.parse(service_call_without_assets.serialized_steps).first).to have_key('attachments')
end
end
end
end end
end end

View file

@ -8,19 +8,19 @@ describe ProtocolImporters::ImportProtocolService do
let(:protocol_params) { attributes_for :protocol, :in_public_repository } let(:protocol_params) { attributes_for :protocol, :in_public_repository }
let(:steps_params) do let(:steps_params) do
[ [
attributes_for(:step).merge!(assets_attributes: [attributes_for(:asset)]), attributes_for(:step).merge!(attachments: [{ name: 'random.jpg', url: 'http://www.example.com/random.jpg' }]),
attributes_for(:step).merge!(tables_attributes: [attributes_for(:table), attributes_for(:table)]) attributes_for(:step).merge!(tables_attributes: [attributes_for(:table), attributes_for(:table)])
] ].to_json
end end
let(:service_call) do let(:service_call) do
ProtocolImporters::ImportProtocolService ProtocolImporters::ImportProtocolService
.call(protocol_params: protocol_params, steps_params: steps_params, user_id: user.id, team_id: team.id) .call(protocol_params: protocol_params, steps_params_json: steps_params, user_id: user.id, team_id: team.id)
end end
context 'when have invalid arguments' do context 'when have invalid arguments' do
it 'returns an error when can\'t find user' do it 'returns an error when can\'t find user' do
allow(User).to receive(:find).and_return(nil) allow(User).to receive(:find_by_id).and_return(nil)
expect(service_call.errors).to have_key(:invalid_arguments) expect(service_call.errors).to have_key(:invalid_arguments)
end end
@ -28,19 +28,20 @@ describe ProtocolImporters::ImportProtocolService do
it 'returns invalid protocol when can\'t save it' do it 'returns invalid protocol when can\'t save it' do
# step with file without name # step with file without name
steps_invalid_params = [ steps_invalid_params = [
attributes_for(:step).merge!(assets_attributes: [attributes_for(:asset).except(:file_file_name)]) attributes_for(:step).except(:name).merge!(tables_attributes: [attributes_for(:table)])
] ].to_json
s = ProtocolImporters::ImportProtocolService.call(protocol_params: protocol_params, s = ProtocolImporters::ImportProtocolService.call(protocol_params: protocol_params,
steps_params: steps_invalid_params, steps_params_json: steps_invalid_params,
user_id: user.id, team_id: team.id) user_id: user.id, team_id: team.id)
expect(s.protocol).to be_invalid expect(s.protocol).to be_invalid
end end
end end
context 'when have valid arguments' do context 'when have valid arguments' do
before do before do
stub_request(:get, 'http://www.example.com/random.jpg').to_return(status: 200, body: '', headers: {})
@protocol = Protocol.new @protocol = Protocol.new
end end

View file

@ -8,6 +8,7 @@ RSpec.describe ProtocolImporters::AttachmentsBuilder do
end end
let(:generate_files_from_step) { described_class.generate(step) } let(:generate_files_from_step) { described_class.generate(step) }
let(:first_file_in_result) { generate_files_from_step.first } let(:first_file_in_result) { generate_files_from_step.first }
let(:generate_json_files_from_step) { described_class.generate_json(step) }
before do before do
stub_request(:get, 'https://pbs.twimg.com/media/Cwu3zrZWQAA7axs.jpg').to_return(status: 200, body: '', headers: {}) stub_request(:get, 'https://pbs.twimg.com/media/Cwu3zrZWQAA7axs.jpg').to_return(status: 200, body: '', headers: {})
@ -15,7 +16,7 @@ RSpec.describe ProtocolImporters::AttachmentsBuilder do
.to_return(status: 200, body: '', headers: {}) .to_return(status: 200, body: '', headers: {})
end end
describe 'self.build_assets_for_step' do describe 'self.generate' do
it 'returns array of Asset instances' do it 'returns array of Asset instances' do
expect(first_file_in_result).to be_instance_of(Asset) expect(first_file_in_result).to be_instance_of(Asset)
end end
@ -24,4 +25,14 @@ RSpec.describe ProtocolImporters::AttachmentsBuilder do
expect(first_file_in_result).to be_valid expect(first_file_in_result).to be_valid
end end
end end
describe 'self.generate_json' do
it 'returns JSON with 2 items (files)' do
expect(generate_json_files_from_step.size).to be == 2
end
it 'returns JSON item with name and url' do
expect(generate_json_files_from_step.first.keys).to contain_exactly(:name, :url)
end
end
end end

View file

@ -5,6 +5,9 @@ require 'rails_helper'
describe ProtocolImporters::ProtocolIntermediateObject do describe ProtocolImporters::ProtocolIntermediateObject do
subject(:pio) { described_class.new(normalized_json: normalized_result, user: user, team: team) } subject(:pio) { described_class.new(normalized_json: normalized_result, user: user, team: team) }
let(:invalid_pio) { described_class.new(normalized_json: normalized_result, user: nil, team: team) } let(:invalid_pio) { described_class.new(normalized_json: normalized_result, user: nil, team: team) }
let(:pio_without_assets) do
described_class.new(normalized_json: normalized_result, user: user, team: team, build_with_assets: false)
end
let(:user) { create :user } let(:user) { create :user }
let(:team) { create :team } let(:team) { create :team }
let(:normalized_result) do let(:normalized_result) do
@ -34,5 +37,26 @@ describe ProtocolImporters::ProtocolIntermediateObject do
it { expect(invalid_pio.import).to be_invalid } it { expect(invalid_pio.import).to be_invalid }
it { expect { invalid_pio.import }.not_to(change { Protocol.all.count }) } it { expect { invalid_pio.import }.not_to(change { Protocol.all.count }) }
end end
context 'when build wihout assets' do
it { expect { pio_without_assets.import }.to change { Protocol.all.count }.by(1) }
it { expect { invalid_pio.import }.not_to(change { Asset.all.count }) }
end
end
describe '.steps_assets' do
context 'when have default pio' do
it 'retuns empty hash for steps_assets' do
pio.build
expect(pio.steps_assets).to be == {}
end
end
context 'when have pio built without assets' do
it 'returns hash with two assets on steps by position' do
pio_without_assets.build
expect(pio_without_assets.steps_assets.size).to be == 2
end
end
end end
end end