Newer
Older
dmpopidor / test / test_helper.rb
@briley briley on 23 May 2018 21 KB Template Versioning
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.')
  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