Build Status Code Climate Test Coverage

Witch Doctor

Rails engine that provides simple API so that external antivirus script can pull down files that need to be scanned and update their results.

Engine was designed to work alongside virus_scan_service gem, to which it provides a list of VirusScans. These are created upon file resource create / update events.

API is trying to comply with JSON API standard

CarrierWave

gem is by default assuming you use Carriewave gem however it's not a dependancy as long as :mount_point responds to url call (look at spec/dummy/app/models/document.rb) for more details

working along ActiveModel::Serializer

please read more in docs/action_model_serializer.md

Setup

In your application:

# Gemfile

# ...
gem 'witch_doctor'

# config/routes.rb
MyCoolApplication::Application.routes.draw do
  mount WitchDoctor::Engine => "/wd", :as => "witch_doctor"
  # ...
end
# /config/initializers/witch_doctor.rb

WitchDoctor.token = Rails
  .application
  .secrets
  .fetch('antivirus_scan')
  .fetch('token')
bundle install
rake db:migrate

Optional

Add helper

# app/helpers/application_helper.rb
include WitchDoctor::ApplicationHelper

after this you can use the antivirus helper

= antivirus(@document, :attachment) do
  - link_to @document.attachment_name, @document.attachment.url

This will show the link when VirusScan for @document is Clean

Overiding WitchDoctor Examples

extending controller

module WitchDoctor
  module MyAppControllerExtension
    def self.included(base)
      base.force_ssl unless: :development?
      base.skip_before_filter :do_stuff
    end

    def development?
      Rails.env.in? ['test', 'development']
    end
  end
end
WitchDoctor::VirusScansController.send(:include,WitchDoctor::MyAppControllerExtension)

extending Antivirus helper

include WitchDoctor::ApplicationHelper
module ApplicationHelper
  alias_method :antivirus_without_view_requirement_respect, :antivirus
  alias_method :antivirus, :antivirus_with_view_requirement_respect

  def antivirus_with_view_requirement_respect(decorated_resource, mount_point)
    if decorated_resource.send("view_requires_#{mount_point}_virus_check?")
      antivirus_without_view_requirement_respect(decorated_resource, mount_point)
    else
      yield
    end
  end
end

Testing

Make sure you turn of virus_scan_scheduling_on option so that gem wont create extra records when your tests are running

# config/initializers/witch_doctor.rb
WitchDoctor.skip_virus_scan_scheduling = true

turn it on only when needed

# spec/request/virus_scan.rb

# ...
before do
  WitchDoctor.skip_virus_scan_scheduling = false
end

after do
  WitchDoctor.skip_virus_scan_scheduling = true
end

# ...

The gem/engine is pretty well tested but I recomend to write interation test for every application it is introduced to.

Example with RSpec request test:

require 'spec_helper'

RSpec.describe 'VirusScans', :type => :request do

  before(:all) { WitchDoctor.time_stamper = -> { Time.now.midnight } }
  after(:all)  { WitchDoctor.time_stamper = (reset_stamper_to_default = nil) }

  let(:token) { '1234' }
  let!(:virus_scan) { FactoryGirl.create(:document).virus_scans.last }

  describe 'GET index' do
    before do
      get "/wd/virus_scans", token: token, format: 'json'
    end

    it 'responds with success' do
      expect(response.status).to be 200
    end

    it 'expect the JSON response to be JSON API hash' do
      expect(JSON.parse response.body).to eq({
        "data" => [
          {
            "id" => virus_scan.id,
            "scan_result" => nil,
            "scanned_at" => nil,
            "file_url" => "/uploads/documents/#{virus_scan.id}/passport.jpg" # don't care about file storage (tests)
                                                                             # as virus scans are needed only on s3
          }
        ]
      })
    end
  end

  describe 'PUT update' do
    let(:virus_scan_params) { { scan_result: 'Clean' } }

    before do

      put "/wd/virus_scans/#{virus_scan.id}",
        { format: 'json', virus_scan: virus_scan_params },
        { 'Authorization' => "Token 1234" }
    end

    it 'responds with success' do
      expect(response.status).to be 200
    end

    it 'expect to update existing virus_scan' do
      expect(JSON.parse response.body).to eq({
        "data" => {
          "id" => virus_scan.id,
          "scan_result" => 'Clean',
          "scanned_at" => Time.now.midnight.utc.iso8601,
          "file_url" => "/uploads/documents/#{virus_scan.id}/passport.jpg"
        }
      })
    end
  end
end