ENV["RAILS_ENV"] = "test"
# Startup the simple coverage gem so that our test results are captured
require 'simplecov'
SimpleCov.start 'rails'
require File.expand_path('../../config/environment', __FILE__)
require 'rails/test_help'
require 'webmock/minitest'
require 'active_support/inflector' # For pluralization utility
class ActiveSupport::TestCase
include GlobalHelpers
# Suppress noisy ActiveRecord logs because fixtures load for each test
ActiveRecord::Base.logger.level = Logger::INFO
# Setup all fixtures in test/fixtures/*.(yml|csv) for all tests in alphabetical order.
#
# Note: You'll currently still have to declare fixtures explicitly in integration tests
# -- they do not yet inherit this setting
#fixtures :all
# Use the seeds.rb file to seed the test database
require_relative '../db/seeds.rb'
# Sometimes TravisCI fails when accessing the LANGUAGES array, so reload it here if necessary
LANGUAGES = Language.all if LANGUAGES.empty?
# Default attributes for model initialization
def org_seed
{ name: 'Test Institution',
abbreviation: 'TST',
org_type: Org.org_type_values_for(:institution).min,
target_url: 'http://test-funder.org',
language: LANGUAGES.first,
contact_email: 'help.desk@test-funder.org',
contact_name: 'Help Desk',
links: {"org":[{"link":"http://dmproadmap.org","text":"DMPRoadmap"}]},
}
end
def user_seed
{
email: 'test-user@testing-roadmap.org',
firstname: 'Test',
surname: 'User',
language: Language.find_by(abbreviation: FastGettext.locale),
password: "password123",
password_confirmation: "password123",
accept_terms: true,
confirmed_at: Time.zone.now,
}
end
def template_seed
{
title: 'Test template',
description: 'this is a test template',
org: Org.first,
}
end
def phase_seed
{
title: 'Test phase',
description: 'This is a phase used for testing',
number: 1,
modifiable: true,
}
end
def section_seed
{
title: 'Test section',
description: 'This is a section used for testing',
number: 1,
modifiable: true,
}
end
def question_format_seed
{
title: 'Text area',
option_based: false,
formattype: QuestionFormat.formattypes[:text_area]
}
end
def question_seed
{
text: 'how is our test coverage?',
default_value: 'Not as good as it could be.',
number: 1,
option_comment_display: true,
modifiable: true,
}
end
def annotation_seed
{
text: 'This is some test guidance for a customization',
type: Annotation.types[:guidance]
}
end
def question_option_seed
{
text: 'Option A',
number: 1,
is_default: true,
}
end
def plan_seed
{
title: 'Test plan',
funder_name: 'Organisation with a lot of funds',
grant_number: 'Grant123',
identifier: '123456789',
description: 'This is the project abstract.',
visibility: Plan.visibilities[:privately_visible],
principal_investigator: 'Jane Doe',
principal_investigator_identifier: 'ORCID123',
principal_investigator_email: 'jane.doe@pi.roadmap.org',
principal_investigator_phone: '1234',
data_contact: 'John Doe',
data_contact_email: 'john.doe@pi.roadmap.org',
data_contact_phone: '5678',
}
end
def theme_seed
{
title: 'Test theme',
description: 'This theme is used for testing',
locale: Language.find_by(abbreviation: FastGettext.locale),
}
end
def guidance_group_seed
{
name: 'Test guidance group',
optional_subset: false,
published: true,
}
end
def guidance_seed
{
text: 'This is thematic test guidance.',
published: true,
}
end
def validate_and_create_obj(obj)
obj.validate
if obj.errors.present?
# Unable to save the object, so output an error rather than burying it
puts "Unable to save #{obj.class.name} because: #{obj.errors.collect{ |e,m| "#{e}: #{m}" }.join(', ')}"
else
obj.save!
end
assert obj.valid?
obj
end
# Org initializers
def init_institution(**props)
validate_and_create_obj(Org.new(org_seed.merge(props)))
end
def init_funder(**props)
hash = { name: 'Test Funder', abbreviation: 'TSTFNDR', org_type: Org.org_type_values_for(:funder).min }
validate_and_create_obj(Org.new(org_seed.merge(hash.merge(props))))
end
def init_organisation(**props)
hash = { name: 'Test Organisation', abbreviation: 'TSTORG', org_type: Org.org_type_values_for(:organisation).min }
validate_and_create_obj(Org.new(org_seed.merge(hash.merge(props))))
end
def init_funder_organisation(**props)
hash = { name: 'Test Funder/Organisation', abbreviation: 'TSTFNDRORG', org_type: Org.org_type_values_for(:funder, :organisation).min }
validate_and_create_obj(Org.new(org_seed.merge(hash.merge(props))))
end
# User initializers
def init_researcher(org, **props)
validate_and_create_obj(User.new(user_seed.merge({
org: org,
surname: 'Researcher',
email: 'researcher@testing-roadmap.org',
}.merge(props))))
end
def init_org_admin(org, **props)
perms = Perm.where.not(name: ['admin', 'add_organisations',
'change_org_affiliation', 'grant_api_to_orgs',
'change_org_details'])
validate_and_create_obj(User.new(user_seed.merge({
org: org,
surname: 'OrgAdmin',
email: 'org.admin@testing-roadmap.org',
perms: perms,
}.merge(props))))
end
def init_super_admin(org, **props)
perms = Perm.all
validate_and_create_obj(User.new(user_seed.merge({
org: org,
surname: 'SuperAdmin',
email: 'super.admin@testing-roadmap.org',
perms: perms
}.merge(props))))
end
# Template initializers
def init_template(org, **props)
if org.is_a? Org
validate_and_create_obj(Template.new(template_seed.merge({ org: org }.merge(props))))
else
puts "You must supply an Org when creating a template! Got the following instead: #{org.inspect}"
nil
end
end
def init_phase(template, **props)
if template.is_a? Template
validate_and_create_obj(Phase.new(phase_seed.merge({ template: template }.merge(props))))
else
puts "You must supply a Template when creating a phase! Got the following instead: #{template.inspect}"
nil
end
end
def init_section(phase, **props)
if phase.is_a? Phase
validate_and_create_obj(Section.new(section_seed.merge({ phase: phase }.merge(props))))
else
puts "You must supply a Phase when creating a section! Got the following instead: #{phase.inspect}"
nil
end
end
def init_question_format(**props)
validate_and_create_obj(QuestionFormat.new(question_format_seed.merge(props)))
end
def init_question(section, **props)
if section.is_a? Section
# TODO call init_question_format instead once the seeds.rb has been removed
props[:question_format] = QuestionFormat.first unless props[:question_format].present?
validate_and_create_obj(Question.new(question_seed.merge({ section: section }.merge(props))))
else
puts "You must supply a Section when creating a question! Got the following instead: #{section.inspect}"
nil
end
end
def init_annotation(org, question, **props)
if org.is_a?(Org) && question.is_a?(Question)
validate_and_create_obj(Annotation.new(annotation_seed.merge({ org: org, question: question }.merge(props))))
else
puts "You must supply an Org and Question when creating an annotation! Got the following instead: ORG - #{org.inspect}, QUESTION - #{question.inspect}"
nil
end
end
def init_question_option(question, **props)
if question.is_a?(Question)
validate_and_create_obj(QuestionOption.new(question_option_seed.merge({ question: question }.merge(props))))
else
puts "You must supply a Question when creating a question option! Got the following instead: QUESTION - #{question.inspect}"
nil
end
end
def init_theme(**props)
validate_and_create_obj(Theme.new(theme_seed.merge(props)))
end
def init_guidance_group(org, **props)
if org.is_a? Org
validate_and_create_obj(GuidanceGroup.new(guidance_group_seed.merge({ org: org }.merge(props))))
else
puts "You must supply an Org when creating a GuidanceGroup! Got the following instead: ORG: #{org.inspect}"
end
end
def init_guidance(guidance_group, **props)
if guidance_group.is_a?(GuidanceGroup)
validate_and_create_obj(Guidance.new(guidance_seed.merge({ guidance_group: guidance_group }.merge(props))))
else
puts "You must supply a GuidanceGroup when creating a Guidance! Got the following instead: GUIDANCE_GROUP: #{guidance_group.inspect}"
end
end
def init_plan(template, **props)
if template.is_a? Template
validate_and_create_obj(Plan.new(plan_seed.merge({ template: template }.merge(props))))
else
puts "You must supply a Template when creating a plan! Got the following instead: #{template.inspect}"
nil
end
end
# equality helpers for complex objects
def assert_annotations_equal(annotation1, annotation2)
assert_equal annotation1.text, annotation2.text, 'expected the annotations to have the same text'
assert_equal annotation1.type, annotation2.type, 'expected the annotations to be of the same type'
end
def assert_question_options_equal(option1, option2)
assert_equal option1.text, option2.text, 'expecetd the question options to have the same text'
assert_equal option1.number, option2.number, 'expecetd the question options to have the same number'
assert_equal option1.is_default, option2.is_default, 'expecetd the question options to have the same default flag value'
end
def assert_questions_equal(question1, question2)
assert_equal question1.number, question2.number, 'expected the question numbers to match'
assert_equal question1.text, question2.text, 'expected the question text to match'
assert_equal question1.question_format, question2.question_format, 'expected the question formats to match'
assert_equal question1.option_comment_display, question2.option_comment_display, 'expected the question optional comment display flags to match'
assert_equal question1.annotations.length, question2.annotations.length, 'expected the questions to have the same number of annotations'
assert_equal question1.question_options.length, question2.question_options.length, 'expected the questions to have the same number of options'
question1.annotations.each_with_index do |annotation, idx|
assert_annotations_equal(annotation, question2.annotations[idx])
end
question1.question_options.each_with_index do |option, idx|
assert_question_options_equal(option, question2.question_options[idx])
end
end
def assert_sections_equal(section1, section2)
assert_equal section1.number, section2.number, 'expected the section numbers to match'
assert_equal section1.title, section2.title, 'expected the section titles to match'
assert_equal section1.description, section2.description, 'expected the section descriptions to match'
assert_equal section1.questions.length, section2.questions.length, 'expected the sections to have the same number of questions'
section1.questions.each_with_index do |question, idx|
assert_questions_equal(question, section2.questions[idx])
end
end
def assert_phases_equal(phase1, phase2)
assert_equal phase1.number, phase2.number, 'expected the phase numbers to match'
assert_equal phase1.title, phase2.title, 'expected the phase titles to match'
assert_equal phase1.description, phase2.description, 'expected the phase descriptions to match'
assert_equal phase1.sections.length, phase2.sections.length, 'expected the phase to have the same number of sections'
phase1.sections.each_with_index do |section, idx|
assert_sections_equal(section, phase2.sections[idx])
end
end
# Get the organisational admin for the Org specified or create one
# ----------------------------------------------------------------------
def scaffold_org_admin(org)
@user = User.create!(email: "admin-#{org.abbreviation.downcase}@example.com", firstname: "Org", surname: "Admin",
language: Language.find_by(abbreviation: FastGettext.locale),
password: "password123", password_confirmation: "password123",
org: org, accept_terms: true, confirmed_at: Time.zone.now,
perms: Perm.where.not(name: ['admin', 'add_organisations', 'change_org_affiliation', 'grant_api_to_orgs', 'change_org_details']))
#perms: [Perm::GRANT_PERMISSIONS, Perm::MODIFY_TEMPLATES, Perm::MODIFY_GUIDANCE, Perm::CHANGE_ORG_DETAILS])
end
# Convert Ruby Class Names into attribute names (e.g. MyClass --> my_class)
# ----------------------------------------------------------------------
def class_name_to_attribute_name(name)
name.gsub(/([a-z]+)([A-Z])/, '\1_\2').gsub('-', '_').downcase
end
# Scaffold a new Template with one Phase, one Section, and a Question for
# each of the possible Question Formats.
# ----------------------------------------------------------------------
def scaffold_template
template = Template.new(title: 'Test template',
description: 'My test template',
links: {"funder":[],"sample_plan":[]},
org: Org.first, archived: false, family_id: "0000009999")
template.phases << Phase.new(title: 'Test phase',
description: 'My test phase',
number: 1, template: template)
template.phases.first.sections << Section.new(title: 'Test section',
description: 'My test section',
number: 99, phase: template.phases.first)
section = template.phases.first.sections.first
i = 1
# Add each type of Question to the new section
QuestionFormat.all.each do |frmt|
question = Question.new(text: "Test question - #{frmt.title}", number: i,
question_format: frmt, section: section)
if frmt.option_based?
3.times do |j|
question.question_options << QuestionOption.new(text: "Option #{j}", number: j, question: question)
end
end
section.questions << question
i += 1
end
template.save!
assert template.valid?, "unable to create new Template: #{template.errors.map{|f, m| f.to_s + ' ' + m}.join(', ')}"
@template = template.reload
end
# Version the template
# ----------------------------------------------------------------------
def version_the_template
@template = @template.generate_version!
end
# Scaffold a new Plan based on the scaffolded Template
# ----------------------------------------------------------------------
def scaffold_plan
scaffold_template if @template.nil?
@plan = Plan.new(template: @template, title: 'Test Plan', grant_number: 'Grant-123',
principal_investigator: 'me', principal_investigator_identifier: 'me-1234',
description: "this is my plan's informative description",
identifier: '1234567890', data_contact: 'me@example.com', visibility: :privately_visible,
roles: [Role.new(user: User.last, creator: true)])
assert @plan.valid?, "unable to create new Plan: #{@plan.errors.map{|f, m| f.to_s + ' ' + m}.join(', ')}"
@plan.save!
end
# FUNCTIONAL/INTEGRATION TEST HELPERS
# ----------------------------------------------------------------------
def assert_unauthorized_redirect_to_root_path
assert_response :redirect
assert_match "#{root_url}", @response.redirect_url
follow_redirects
assert_response :success
assert_select 'main h1', _("Welcome to #{Rails.configuration.branding[:application][:name]}.")
end
# ----------------------------------------------------------------------
def assert_authorized_redirect_to_plans_page
assert_response :redirect
assert_match "#{root_url}", @response.redirect_url
# Sometimes Devise has an intermediary step prior to sending the user to the final destination
follow_redirects
assert_response :success
assert_select 'main h1', _('My Dashboard')
end
# ----------------------------------------------------------------------
def follow_redirects
while @response.status >= 300 && @response.status < 400
follow_redirect!
end
end
# UNIT TEST HELPERS
# ----------------------------------------------------------------------
def verify_deep_copy(object, exclusions)
clazz = Object.const_get(object.class.name)
assert clazz.respond_to?(:deep_copy), "#{object.class.name} does not have a deep_copy method!"
copy = clazz.deep_copy(object)
object.attributes.each do |name, val|
if exclusions.include?(name)
assert_not_equal object.send(name), copy.send(name), "expected the deep_copy of #{object.class.name}.#{name} to be unique in the copy"
else
unless object.send(name).nil? || copy.send(name)
assert_equal object.send(name), copy.send(name), "expected the deep_copy of #{object.class.name}.#{name} to match"
end
end
end
end
def assert_deep_copy(original, copy, **options)
if original.class == copy.class
relations = options.fetch(:relations, []).map(&:to_sym)
assert(original.object_id != copy.object_id)
assert_nil(copy.id, "id should be nil for #{copy.class}") if copy.respond_to?(:id)
assert_nil(copy.created_at, "created_at should be nil for #{copy.class}") if copy.respond_to?(:created_at)
assert_nil(copy.updated_at, "updated_at should be nil for #{copy.class}") if copy.respond_to?(:updated_at)
relations.each do |relation|
if copy.respond_to?(relation)
relation_obj = copy.send(relation)
if relation_obj.respond_to?(:each)
relation_obj.each do |obj|
assert_nil(obj.id, "id should be nil for the relation object from #{obj.class}") if copy.respond_to?(:id)
end
end
end
end
end
end
# ----------------------------------------------------------------------
def verify_has_many_relationship(object, new_association, initial_expected_count)
# Assumes that the association name matches the pluralized name of the class
rel = "#{class_name_to_attribute_name(new_association.class.name).pluralize}"
assert_equal initial_expected_count, object.send(rel).count, "was expecting #{object.class.name} to initially have #{initial_expected_count} #{rel}"
# Add another association for the object
object.send(rel) << new_association
object.save!
assert_equal (initial_expected_count + 1), object.send(rel).count, "was expecting #{object.class.name} to have #{initial_expected_count + 1} #{rel} after adding a new one - #{new_association.errors.map{|f, m| f.to_s + ' ' + m}.join(', ')}"
# Remove the newly added association
object.send(rel).delete(new_association)
object.save!
assert_equal initial_expected_count, object.send(rel).count, "was expecting #{object.class.name} to have #{initial_expected_count} #{rel} after removing the new one we added"
end
# ----------------------------------------------------------------------
def verify_belongs_to_relationship(child, parent)
# Assumes that the association name matches the lower case name of the class
prnt = "#{class_name_to_attribute_name(parent.class.name)}"
chld = "#{class_name_to_attribute_name(child.class.name)}"
child.send("#{prnt}=", parent)
child.save!
assert_equal parent, child.send(prnt), "was expecting #{chld} to have a #{prnt}.id == #{parent.id}"
# Search the parent for the child
parent.reload
assert_includes parent.send("#{chld.pluralize}"), child, "was expecting the #{prnt}.#{chld.pluralize} to contain the #{chld}"
end
# STUBS FOR CALLS To EXTERNAL SITES
# ----------------------------------------------------------------------
def stub_blog_calls
blog_feed = "<?xml version=\"1.0\" encoding=\"utf-8\" ?><rss version=\"2.0\" " +
"xml:base=\"http://www.example.com/stubbed/blog\" " +
"xmlns:dc=\"http://purl.org/dc/elements/1.1\">" +
"<channel>" +
"<title>Testing</title>" +
"<link>http://www.example.com/stubbed/blog/feed</link>" +
"<item>" +
"<title>Stub blog post</title>" +
"<link>http://www.example.com/stubbed/blog/articles/1</link>" +
"<description>This is a stuubed blog post</description>" +
"<category domain=\"http://www.example.com/stubbed/blog\">Test</category>" +
"<pubDate>Thu, 03 Nov 2016 12:38:17 +0000</pubDate>" +
"<dc:creator />" +
"<guid isPermaLink=\"false\">1 at http://www.example.com/stubbed/blog</guid>" +
"</item>" +
"</channel>"
stub_request(:get, "http://www.dcc.ac.uk/news/dmponline-0/feed").
with(:headers => {'Accept'=>'*/*', 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'User-Agent'=>'Faraday v0.9.2'}).
to_return(:status => 200, :body => blog_feed, :headers => {})
end
end