Team --
I have a ruby library helper in which I have defined multiple functions/methods. How do I reference those in Chef Inspec tests?
def interface_name
# some code with logic
end
In my specific use case, I am checking to see if a custom networking device was specified as a JSON parameter, and if it was, I am validating it make sure its real (incase they misspelled it) and also gathering its IP and state and other data as reported by Ohai
This is what I have so far, but I'm not sure if this is correct
describe file('/path/to/custom_attributes.json') do
it { should exist }
unless json('/path/to/custom_attributes.json').empty? do
its(['networking']['interface_name']) { should_not be_empty }
interface_name = file(json('/path/to/custom_attributes.json').params['networking']['interface_name'])
end
end
describe file('/etc/NetworkManager/system-connections/wired_connection') do
unless interface_name.empty?
its('content') { should_not match(/^interface-name=wl.*/mx) }
end
its('content') { should match(%r{ca-cert=/etc/ssl/certs/ca-certificates\.crt}mx) }
its('content') { should match(/id=\.corp.*type=ethernet.*autoconnect-priority=100.*dns-search=corp\.domain.com.*/mx) }
end
end
The problem / question is that if I gather the parameter directly from the JSON file, then I am bypassing all the validation logic that I'm doing in the library, which defeats the purpose. So, how do I get access to that library function/method in the Inspec test?
For reference, here is the function:
def interface_name
file = '/path/to/custom_attributes.json'
if File.exist?(file) && !File.stat(file).zero?
attributes = JSON.parse(File.read(file))
device_name = attributes['networking']['interface_name']
if device_name && !device_name.empty? && networking_devices.include?(device_name)
interface = device_name
Chef::Log.info("Valid custom interface provided, using \"#{device_name}\".")
else
Chef::Log.debug("Invalid interface (\"#{device_name}\") provided. Valid options are: \"#{networking_devices.keys}\"")
interface = nil
end
else
Chef::Log.debug('No custom interface provided.')
end
interface
rescue JSON::ParserError
nil
end
Related
I have recently started using Rubocop to "standardise" my code, and it has helped me optimise a lot of my code, as well as help me learn a lot of Ruby "tricks". I understand that I should use my own judgement and disable Cops where necessary, but I have found myself quite stuck with the below code:
def index
if params[:filters].present?
if params[:filters][:deleted].blank? || params[:filters][:deleted] == "false"
# if owned is true, then we don't need to filter by admin
params[:filters][:admin] = nil if params[:filters][:admin].present? && params[:filters][:owned] == "true"
# if admin is true, then must not filter by owned if false
params[:filters][:owned] = nil if params[:filters][:owned].present? && params[:filters][:admin] == "false"
companies_list =
case params[:filters][:admin]&.to_b
when true
current_user.admin_companies
when false
current_user.non_admin_companies
end
if params[:filters][:owned].present?
companies_list ||= current_user.companies
if params[:filters][:owned].to_b
companies_list = companies_list.where(owner: current_user)
else
companies_list = companies_list.where.not(owner: current_user)
end
end
else
# Filters for deleted companies
companies_list = {}
end
end
companies_list ||= current_user.companies
response = { data: companies_list.alphabetical.as_json(current_user: current_user) }
json_response(response)
end
Among others, the error that I'm getting is the following:
C: Metrics/AbcSize: Assignment Branch Condition size for index is too high. [<13, 57, 16> 60.61/15]
I understand the maths behind it, but I don't know how to simplify this code to achieve the same result.
Could someone please give me some guidance on this?
Thanks in advance.
Well first and foremost, is this code fully tested, including all the myriad conditions? It's so complex that refactoring will surely be disastrous unless the test suite is rigorous. So, write a comprehensive test suite if you don't already have one. If there's already a test suite, make sure it tests all the conditions.
Second, apply the "fat model skinny controller" paradigm. So move all the complexity into a model, let's call it CompanyFilter
def index
companies_list = CompanyFilter.new(current_user, params).list
response = { data: companies_list.alphabetical.as_json(current_user: current_user) }
json_response(response)
end
and move all those if/then/else statements into the CompanyFilter#list method
tests still pass? great, you'll still get the Rubocop warnings, but related to the CompanyFilter class.
Now you need to untangle all the conditions. It's a bit hard for me to understand what's going on, but it looks as if it should be reducible to a single case statement, with 5 possible outcomes. So the CompanyFilter class might look something like this:
class CompanyFilter
attr_accessors :current_user, :params
def initialize(current_user, params)
#current_user = current_user
#params = params
end
def list
case
when no_filter_specified
{}
when user_is_admin
#current_user.admin_companies
when user_is_owned
# etc
when # other condition
# etc
end
end
private
def no_filter_specified
#params[:filter].blank?
end
def user_is_admin
# returns boolean based on params hash
end
def user_is_owned
# returns boolean based on params hash
end
end
tests still passing? perfect! [Edit] Now you can move most of your controller tests into a model test for the CompanyFilter class.
Finally I would define all the different companies_list queries as scopes on the Company model, e.g.
class Company < ApplicationRecord
# some examples, I don't know what's appropriate in this app
scope :for_user, ->(user){ where("...") }
scope :administered_by, ->(user){ where("...") }
end
When composing database scopes ActiveRecord::SpawnMethods#merge is your friend.
Post.where(title: 'How to use .merge')
.merge(Post.where(published: true))
While it doesn't look like much it lets you programatically compose scopes without overelying on mutating assignment and if/else trees. You can for example compose an array of conditions and merge them together into a single ActiveRecord::Relation object with Array#reduce:
[Post.where(title: 'foo'), Post.where(author: 'bar')].reduce(&:merge)
# => SELECT "posts".* FROM "posts" WHERE "posts"."title" = $1 AND "posts"."author" = $2 LIMIT $3
So lets combine that with a skinny controllers approach where you handle filtering in a seperate object:
class ApplicationFilter
include ActiveModel::Attributes
include ActiveModel::AttributeAssignment
attr_accessor :user
def initialize(**attributes)
super()
assign_attributes(attributes)
end
# A convenience method to both instanciate and apply the filters
def self.call(user, params, scope: model_class.all)
return scope unless params[:filters].present?
scope.merge(
new(
permit_params(params).merge(user: user)
).to_scope
)
end
def to_scope
filters.map { |filter| apply_filter(filter) }
.compact
.select {|f| f.respond_to?(:merge) }
.reduce(&:merge)
end
private
# calls a filter_by_foo method if present or
# defaults to where(key => value)
def apply_filter(attribute)
if respond_to? "filter_by_#{attribute}"
send("filter_by_#{attribute}")
else
self.class.model_class.where(
attribute => send(attribute)
)
end
end
# Convention over Configuration is sexy.
def self.model_class
name.chomp("Filter").constantize
end
# filters the incoming params hash based on the attributes of this filter class
def self.permit_params
params.permit(filters).reject{ |k,v| v.blank? }
end
# provided for modularity
def self.filters
attribute_names
end
end
This uses some of the goodness provided by Rails to setup objects with attributes that will dynamically handle filtering attributes. It looks at the list of attributes you have declared and then slices those off the params and applies a method for that filter if present.
We can then write a concrete implementation:
class CompanyFilter < ApplicationFilter
attribute :admin, :boolean, default: false
attribute :owned, :boolean
private
def filter_by_admin
if admin
user.admin_companies
else
user.non_admin_companies
end
end
# this should be refactored to use an assocation on User
def filter_by_owned
case owned
when nil
nil
when true
Company.where(owner: user)
when false
Company.where.not(owner: user)
end
end
end
And you can call it with:
# scope is optional
#companies = CompanyFilter.call(current_user, params), scope: current_user.companies)
here is the class for which i'm writing a rspecs.
# frozen_string_literal: true. Thiis is the method **SiteLicenseService.get_package_for_site_license site_license_id, package_id**
module SiteLicenseGrants
# Raise an error if no site license is found for a given package
class Package
# Fail if the given packages are not found or if they are not associated
# with the site license
def self.valid_package?(opts)
package_id = opts[:package_id]
site_license_id = opts[:site_license_id]
return true if SiteLicenseService.get_package_for_site_license site_license_id, package_id
rescue Errors::EntityNotFound
raise Errors::NoSiteLicenseForPackage.new package_id # rubocop:disable Style/RaiseArgs
end
end
end
i want to stub a class method to return true.
require "rails_helper"
RSpec.describe SiteLicenseGrants::Package do
describe "check valid package" do
context "valid package" do
let(:package_id) { 1 }
let(:site_license_id) { 1 }
let(:opts) { { package_id: package_id, site_license_id: site_license_id } }
before do
site_license_service = class_double("SiteLicenseService")
allow(site_license_service).to receive(:get_package_for_site_license).with(site_license_id, package_id).and_return(true)
end
it "returns true" do
expect do
described_class.valid_package?(opts)
end.to be_truthy
end
end
context "invalid package" do
let(:opts) { { package_id: nil, site_license_id: nil } }
it "throws error" do
expect do
described_class.valid_package?(opts)
end.to raise_error(Errors::NoSiteLicenseForPackage)
end
end
end
end
here is the error im getting
Failures:
1) SiteLicenseGrants::Package check valid package valid package returns true
Failure/Error: raise Errors::NoSiteLicenseForPackage.new package_id # rubocop:disable Style/RaiseArgs
Errors::NoSiteLicenseForPackage:
Package 1 doesn't have a site license associated with it
I just want to mock my class method to return true. I dont want to test the class methods of SiteLicenseService. I dont want to create site license with package literally.
can anyone explain me what is the mistake i'm doing.
Thanks
Ensure the syntaxing of mocking the return value is correct:
allow(SiteLicenseService).to
receive(:get_package_for_site_license).and_return(true)
In Ruby, classes are written in TitleCase and methods are written in snake_case. The method to be received should be a :symbol.
I chnaged the stubbing like this and it started working
before do
allow(SiteLicenseService).to receive(:get_package_for_site_license).and_return(true)
end
now it does not look for a relation. but i appreciate if anyone tells me what changed internally.
I'm trying write a test to assert that all defined operations are called on a successful run. I have the operations for a given process defined in a list and resolve them from a container, like so:
class ProcessController
def call(input)
operations.each { |o| container[o].(input) }
end
def operations
['operation1', 'operation2']
end
def container
My::Container # This is a Dry::Web::Container
end
end
Then I test is as follows:
RSpec.describe ProcessController do
let(:container) { My::Container }
it 'executes all operations' do
subject.operations.each do |op|
expect(container[op]).to receive(:call).and_call_original
end
expect(subject.(input)).to be_success
end
end
This fails because calling container[operation_name] from inside ProcessController and from inside the test yield different instances of the operations. I can verify it by comparing the object ids. Other than that, I know the code is working correctly and all operations are being called.
The container is configured to auto register these operations and has been finalized before the test begins to run.
How do I make resolving the same key return the same item?
TL;DR - https://dry-rb.org/gems/dry-system/test-mode/
Hi, to get the behaviour you're asking for, you'd need to use the memoize option when registering items with your container.
Note that Dry::Web::Container inherits Dry::System::Container, which includes Dry::Container::Mixin, so while the following example is using dry-container, it's still applicable:
require 'bundler/inline'
gemfile(true) do
source 'https://rubygems.org'
gem 'dry-container'
end
class MyItem; end
class MyContainer
extend Dry::Container::Mixin
register(:item) { MyItem.new }
register(:memoized_item, memoize: true) { MyItem.new }
end
MyContainer[:item].object_id
# => 47171345299860
MyContainer[:item].object_id
# => 47171345290240
MyContainer[:memoized_item].object_id
# => 47171345277260
MyContainer[:memoized_item].object_id
# => 47171345277260
However, to do this from dry-web, you'd need to either memoize all objects auto-registered under the same path, or add the # auto_register: false magic comment to the top of the files that define the dependencies and boot them manually.
Memoizing could cause concurrency issues depending on which app server you're using and whether or not your objects are mutated during the request lifecycle, hence the design of dry-container to not memoize by default.
Another, arguably better option, is to use stubs:
# Extending above code
require 'dry/container/stub'
MyContainer.enable_stubs!
MyContainer.stub(:item, 'Some string')
MyContainer[:item]
# => "Some string"
Side note:
dry-system provides an injector so that you don't need to call the container manually in your objects, so your process controller would become something like:
class ProcessController
include My::Importer['operation1', 'operation2']
def call(input)
[operation1, operation2].each do |operation|
operation.(input)
end
end
end
I thought I understood how implicit subjects work in RSpec, but I don't.
Why is it that in the following example, the first spec with an explicit subject passes, but the second spec using an implicit subject fails with "undefined method `matches' for #":
class Example
def matches(str) ; true ; end
end
describe Example do
subject { Example.new }
specify { subject.matches('bar').should be_true }
it { matches('bar').should be_true }
end
(I'm using rspec 1.3, but I verified the same behavior with 2.10.1.)
Step back to some basic ruby: You're basically calling self.matches, and self in this case is an RSpec example.
You can call things like "should" on this example, with parameters, so you might try something like:
it { should matches('bar') }
but this will fail; there's no method matches on self still!
In this case, though, the subject really is the matches method, not the Example instance. So, if you want to continue using the implicit subject, your tests might be something like:
class Example
def matches(str) ; str == "bar" ; end
end
describe Example do
describe "#matches" do
let(:method) { Example.new.method(:matches) }
context "when passed a valid value" do
subject { method.call("bar") }
it { should be_true }
end
context "when passed an invalid value" do
subject { method.call("foo") }
it { should be_false }
end
end
end
I don't think you can call any methods of implicit subject. Implicit subject meanings you don't need to specify the subject, but if you want call any method you need to specify the subject.
Although Chris provided very nice answer, I recommend you to take a look at this blog post: http://blog.davidchelimsky.net/2012/05/13/spec-smell-explicit-use-of-subject/
I've got a spec for an object that's in a number of levels of modules. Something like this:
describe Foo::Bar::Baz::Quux::Widget do
it "should == another Widget for the same Doohickey" do
doohickey = stub
Foo::Bar::Baz::Quux::Widget.new(doohickey).should == Foo::Bar::Baz::Quux::Widget.new(doohickey)
end
it "should != another Widget for a different Doohickey" do
one_doohickey = stub
another_doohickey = stub
Foo::Bar::Baz::Quux::Widget.new(one_doohickey).should == Foo::Bar::Baz::Quux::Widget.new(another_doohickey)
end
end
That's a lot of repetition, and it makes it look like I'm using an object
from some other namespace. I'd like to set the context of the spec to
Foo::Bar::Baz::Quux. The following works surprisingly well:
module Foo::Bar::Baz::Quux
describe Widget do
it "should == another Widget for the same Doohickey" do
doohickey = stub
Widget.new(doohickey).should == Widget.new(doohickey)
end
it "should != another Widget for a different Doohickey" do
one_doohickey = stub
another_doohickey = stub
Widget.new(one_doohickey).should == Widget.new(another_doohickey)
end
end
end
There's only one problem. Since I'm in Rails, I'm depending on
ActiveSupport's dependency management to autoload the Foo::Bar::Baz::Quux
module. Before, that happened when I mentioned Foo::Bar::Baz::Quux::Widget.
Now, I'm defining the module myself, so the real definition of the module in
foo/bar/baz/quux.rb is never loaded.
How can I change the constant-lookup context for my spec without defining
the module myself?
You can use the described_class helper...
describe Foo::Bar::Baz::Quux::Widget do
it "has described_class helper" do
described_class.should == Foo::Bar::Baz::Quux::Widget
end
end
Or, for the lol:
describe Foo::Bar::Baz::Quux::Widget do
def Widget
described_class
end
it "has described_class helper" do
Widget.should == Foo::Bar::Baz::Quux::Widget
end
end
Can you assign that to a variable ?
widget_class = Foo::Bar::Baz::Quux::Widget
this should DRY out the code a little bit. Just a thought.