diff --git a/.env b/.env
index da8ea0c..8a3d47d 100644
--- a/.env
+++ b/.env
@@ -2,3 +2,5 @@ HOST=lvh.me:3000
GMAIL_DOMAIN=gmail.com
GMAIL_USERNAME=dontreply1357@gmail.com
GMAIL_PASSWORD=13579rty
+AWS_SECRET_ACCESS_KEY=xbnXDiIvtqVAmoaFT5yYZWu4Wye/l+CvQt6Cp2bm
+AWS_ACCESS_KEY=AKIAIQ2MGPRTWCHJKK5A
diff --git a/Gemfile b/Gemfile
index d946c42..d0e36a4 100644
--- a/Gemfile
+++ b/Gemfile
@@ -16,11 +16,13 @@ gem 'coffee-rails', '~> 4.1.0'
# Use apartment for multi-tenant databases
gem 'apartment', '~> 1.0.1'
+gem 'apartment-sidekiq', '~> 0.2.0'
#bootstrap CSS
#gem 'bootstrap-sass'
# Devise is a flexible authentication solution for Rails based on Warden
gem 'devise', '~> 3.5.1'
+gem 'devise-async', '~>0.10.1'
# simple authorization solution for Rails which is decoupled from user roles
gem 'cancancan', '~> 1.12.0'
@@ -36,6 +38,11 @@ gem 'sdoc', '~> 0.4.0', group: :doc
gem 'slim-rails', '~> 3.0.1'
+# Simple, efficient background processing
+gem 'sidekiq', '~> 3.4.2'
+# For Sidekiq monitor
+gem 'sinatra', '~> 1.4.6'
+
# Use ActiveModel has_secure_password
# gem 'bcrypt', '~> 3.1.7'
@@ -48,6 +55,9 @@ gem 'unicorn', '~> 4.9.0'
# Shim to load environment variables from .env into ENV in development.
gem 'dotenv-rails', '~> 2.0.2', :groups => [:development, :test]
+# Official Amazon AWS SDK
+gem 'aws-sdk', '~> 2.1.7'
+
group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
gem 'byebug'
@@ -64,11 +74,13 @@ group :test do
gem 'capybara', '~> 2.4.4'
gem 'database_cleaner', '~> 1.4.1'
gem 'rspec-rails', '~> 3.2.3'
+ gem 'rspec-its', '~> 1.2.0'
gem 'factory_girl', '~> 4.5.0'
gem 'rspec-expectations', '~> 3.2.1'
gem 'factory_girl_rails', '~> 4.0'
gem 'shoulda-matchers', '~> 2.8.0'
gem 'faker', '~> 1.4.3'
+ gem 'rspec-activejob', '~> 0.4.1'
end
# Deployment
diff --git a/Gemfile.lock b/Gemfile.lock
index ebc053a..9559476 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -36,10 +36,19 @@ GEM
minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1)
- apartment (1.0.1)
+ apartment (1.0.2)
activerecord (>= 3.1.2, < 5.0)
rack (>= 1.3.6)
- arel (6.0.0)
+ apartment-sidekiq (0.2.0)
+ apartment (~> 1.0)
+ sidekiq (>= 2.11)
+ arel (6.0.2)
+ aws-sdk (2.1.8)
+ aws-sdk-resources (= 2.1.8)
+ aws-sdk-core (2.1.8)
+ jmespath (~> 1.0)
+ aws-sdk-resources (2.1.8)
+ aws-sdk-core (= 2.1.8)
bcrypt (3.1.10)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
@@ -83,7 +92,7 @@ GEM
colorize (0.7.7)
columnize (0.9.0)
connection_pool (2.2.0)
- cucumber (1.3.19)
+ cucumber (1.3.20)
builder (>= 2.1.2)
diff-lcs (>= 1.1.3)
gherkin (~> 2.12)
@@ -104,6 +113,8 @@ GEM
responders
thread_safe (~> 0.1)
warden (~> 1.2.3)
+ devise-async (0.10.1)
+ devise (~> 3.2)
diff-lcs (1.2.5)
dotenv (2.0.2)
dotenv-rails (2.0.2)
@@ -124,10 +135,12 @@ GEM
activesupport (>= 4.1.0)
hitimes (1.2.2)
i18n (0.7.0)
- jbuilder (2.3.0)
+ jbuilder (2.3.1)
activesupport (>= 3.0.0, < 5)
multi_json (~> 1.2)
- jquery-rails (4.0.3)
+ jmespath (1.0.2)
+ multi_json (~> 1.0)
+ jquery-rails (4.0.4)
rails-dom-testing (~> 1.0)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
@@ -140,7 +153,7 @@ GEM
mime-types (2.6.1)
mini_portile (0.6.2)
minitest (5.7.0)
- multi_json (1.11.1)
+ multi_json (1.11.2)
multi_test (0.1.2)
net-scp (1.2.1)
net-ssh (>= 2.6.5)
@@ -149,7 +162,9 @@ GEM
mini_portile (~> 0.6.0)
orm_adapter (0.5.0)
pg (0.18.2)
- rack (1.6.1)
+ rack (1.6.4)
+ rack-protection (1.5.3)
+ rack
rack-test (0.6.3)
rack (>= 1.0)
rails (4.2.1)
@@ -184,11 +199,17 @@ GEM
redis (~> 3.0, >= 3.0.4)
responders (2.1.0)
railties (>= 4.2.0, < 5)
+ rspec-activejob (0.4.1)
+ activejob (>= 4.2)
+ rspec-mocks
rspec-core (3.2.3)
rspec-support (~> 3.2.0)
rspec-expectations (3.2.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.2.0)
+ rspec-its (1.2.0)
+ rspec-core (>= 3.0.0)
+ rspec-expectations (>= 3.0.0)
rspec-mocks (3.2.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.2.0)
@@ -201,8 +222,8 @@ GEM
rspec-mocks (~> 3.2.0)
rspec-support (~> 3.2.0)
rspec-support (3.2.2)
- sass (3.4.14)
- sass-rails (5.0.2)
+ sass (3.4.16)
+ sass-rails (5.0.3)
railties (>= 4.0.0, < 5.0)
sass (~> 3.1)
sprockets (>= 2.8, < 4.0)
@@ -219,6 +240,10 @@ GEM
json (~> 1.0)
redis (~> 3.2, >= 3.2.1)
redis-namespace (~> 1.5, >= 1.5.2)
+ sinatra (1.4.6)
+ rack (~> 1.4)
+ rack-protection (~> 1.4)
+ tilt (>= 1.3, < 3)
slim (3.0.6)
temple (~> 0.7.3)
tilt (>= 1.3.3, < 2.1)
@@ -231,7 +256,7 @@ GEM
spring (1.3.6)
sprockets (3.2.0)
rack (~> 1.0)
- sprockets-rails (2.3.1)
+ sprockets-rails (2.3.2)
actionpack (>= 3.0)
activesupport (>= 3.0)
sprockets (>= 2.8, < 4.0)
@@ -258,7 +283,7 @@ GEM
raindrops (~> 0.7)
warden (1.2.3)
rack (>= 1.0)
- web-console (2.1.2)
+ web-console (2.2.1)
activemodel (>= 4.0)
binding_of_caller (>= 0.7.2)
railties (>= 4.0)
@@ -271,6 +296,8 @@ PLATFORMS
DEPENDENCIES
apartment (~> 1.0.1)
+ apartment-sidekiq (~> 0.2.0)
+ aws-sdk (~> 2.1.7)
byebug
cancancan (~> 1.12.0)
capistrano (~> 3.4.0)
@@ -284,6 +311,7 @@ DEPENDENCIES
cucumber-rails (~> 1.4.2)
database_cleaner (~> 1.4.1)
devise (~> 3.5.1)
+ devise-async (~> 0.10.1)
dotenv-rails (~> 2.0.2)
factory_girl (~> 4.5.0)
factory_girl_rails (~> 4.0)
@@ -292,14 +320,21 @@ DEPENDENCIES
jquery-rails
pg (~> 0.18.2)
rails (= 4.2.1)
+ rspec-activejob (~> 0.4.1)
rspec-expectations (~> 3.2.1)
+ rspec-its (~> 1.2.0)
rspec-rails (~> 3.2.3)
sass-rails (~> 5.0)
sdoc (~> 0.4.0)
shoulda-matchers (~> 2.8.0)
+ sidekiq (~> 3.4.2)
+ sinatra (~> 1.4.6)
slim-rails (~> 3.0.1)
spring
turbolinks
uglifier (>= 1.3.0)
unicorn (~> 4.9.0)
web-console (~> 2.0)
+
+BUNDLED WITH
+ 1.10.3
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 618ab77..06f83ef 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -2,8 +2,16 @@ class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
- rescue_from CanCan::AccessDenied do
- redirect_to root_path
+ rescue_from CanCan::AccessDenied do |exception|
+ redirect_to root_path, alert: exception.message
end
+ protected
+
+ # Helper method used in layout to render model errors, should be overriden in controllers
+ def errors
+ {}
+ end
+ helper_method :errors
+
end
diff --git a/app/controllers/themes_controller.rb b/app/controllers/themes_controller.rb
new file mode 100644
index 0000000..d9b9235
--- /dev/null
+++ b/app/controllers/themes_controller.rb
@@ -0,0 +1,30 @@
+class ThemesController < ApplicationController
+ before_action :authenticate_user!
+ load_and_authorize_resource
+
+ # GET#index
+ def index
+ @themes = Theme.all
+ end
+
+ # GET#create_completed redirect from AWS
+ def create_completed
+ key = params.require(:key)
+ theme_file_name = key.sub(/[^_]*_/, '') # theme zip file name (theme name) is located after the first _ (underscore)
+ theme_name = File.basename theme_file_name, File.extname(theme_file_name)
+ theme = Theme.new name: theme_name, zip_file_url: key, status: :processing
+ redirect_to action: (theme.save ? 'index' : 'new')
+ if theme.errors.blank?
+ flash.notice = "Theme '#{theme_name}' uploaded successfully"
+ else
+ flash.alert = theme.errors.full_messages
+ end
+ end
+
+ # DELETE#destroy
+ def destroy
+ @theme.destroy
+ flash.notice = "Theme '#{@theme.name}' successfully deleted"
+ redirect_to themes_path
+ end
+end
diff --git a/app/helpers/themes_helper.rb b/app/helpers/themes_helper.rb
new file mode 100644
index 0000000..20a1ac5
--- /dev/null
+++ b/app/helpers/themes_helper.rb
@@ -0,0 +1,32 @@
+module ThemesHelper
+ UPLOADS_FOLDER = 'uploads/'
+
+ def theme_upload_params
+ return @theme_upload_params if @theme_upload_params
+
+ bucket_name = Rails.application.config.aws_public_bucket_name
+ redirect_url = request.protocol + request.host_with_port + '/themes/create_completed'
+
+ @theme_upload_params = { form_action: "https://#{bucket_name}.s3.amazonaws.com/",
+ form_method: 'post',
+ form_enclosure_type: 'multipart/form-data',
+ acl: 'private',
+ redirect_url: redirect_url,
+ file_id: "#{UPLOADS_FOLDER}#{SecureRandom.uuid}_${filename}",
+ access_key: Rails.application.config.aws_access_key }
+
+ conditions = [[:eq, :$bucket, bucket_name],
+ [:eq, :$acl, :private],
+ [:"content-length-range", 1, Rails.application.config.aws_max_theme_zip_file_length],
+ [:"starts-with", :$key, UPLOADS_FOLDER],
+ [:eq, :$success_action_redirect, redirect_url]]
+ policy = { conditions: conditions, expiration: (Time.now + 10.hours).utc.iso8601 }
+ policy = Base64.strict_encode64(policy.to_json)
+
+ @theme_upload_params.merge! policy: policy
+ @theme_upload_params.merge! signature:
+ Base64.strict_encode64(OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha1'),
+ Rails.application.config.aws_secret_access_key,
+ policy))
+ end
+end
diff --git a/app/jobs/delete_theme_job.rb b/app/jobs/delete_theme_job.rb
new file mode 100644
index 0000000..1f372ba
--- /dev/null
+++ b/app/jobs/delete_theme_job.rb
@@ -0,0 +1,11 @@
+class DeleteThemeJob < ActiveJob::Base
+ queue_as :low_priority
+
+ rescue_from(Aws::S3::Errors::RequestTimeout) do
+ retry_job wait: 5.minutes, queue: :low_priority
+ end
+
+ def perform(zip_file_url, is_public_bucket)
+ AmazonAwsClient.try (is_public_bucket ? :delete_from_public_bucket : :delete_from_private_bucket), zip_file_url
+ end
+end
diff --git a/app/jobs/transfer_theme_job.rb b/app/jobs/transfer_theme_job.rb
new file mode 100644
index 0000000..29dd699
--- /dev/null
+++ b/app/jobs/transfer_theme_job.rb
@@ -0,0 +1,12 @@
+class TransferThemeJob < ActiveJob::Base
+ queue_as :default
+
+ rescue_from(Aws::S3::Errors::RequestTimeout) do
+ retry_job wait: 5.minutes, queue: :default
+ end
+
+ def perform(theme)
+ AmazonAwsClient.transfer_from_public_to_private_bucket theme.zip_file_url
+ theme.update! status: :uploaded
+ end
+end
diff --git a/app/libs/amazon_aws_client.rb b/app/libs/amazon_aws_client.rb
new file mode 100644
index 0000000..2537785
--- /dev/null
+++ b/app/libs/amazon_aws_client.rb
@@ -0,0 +1,28 @@
+module AmazonAwsClient
+ def self.client
+ credentials = Aws::Credentials.new Rails.application.config.aws_access_key, Rails.application.config.aws_secret_access_key
+ Aws::S3::Client.new region: Rails.application.config.aws_region, credentials: credentials
+ end
+
+ def self.transfer_from_public_to_private_bucket key
+ client = self.client
+ client.copy_object(bucket: Rails.application.config.aws_private_bucket_name,
+ key: key,
+ copy_source: Rails.application.config.aws_public_bucket_name + '/' + key)
+ client.delete_object bucket: Rails.application.config.aws_public_bucket_name, key: key
+ end
+
+ def self.delete_from_public_bucket key
+ self.delete_from_bucket Rails.application.config.aws_public_bucket_name, key
+ end
+
+ def self.delete_from_private_bucket key
+ self.delete_from_bucket Rails.application.config.aws_private_bucket_name, key
+ end
+
+ private
+
+ def self.delete_from_bucket bucket, key
+ self.client.delete_object bucket: bucket, key: key
+ end
+end
diff --git a/app/models/ability.rb b/app/models/ability.rb
index a755be1..de6e7b6 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -10,5 +10,9 @@ def initialize(user)
can :destroy, User if user.role.can_delete_users?
can :update_users_role, User if user.role.can_update_users_role?
can :update_users_password, User if user.role.can_update_users_password?
+ can :read, Theme if user.role.can_create_themes? || user.role.can_delete_themes?
+ can :create, Theme if user.role.can_create_themes?
+ can :create_completed, Theme if user.role.can_create_themes?
+ can :destroy, Theme if user.role.can_delete_themes?
end
end
\ No newline at end of file
diff --git a/app/models/theme.rb b/app/models/theme.rb
new file mode 100644
index 0000000..18fb3c2
--- /dev/null
+++ b/app/models/theme.rb
@@ -0,0 +1,24 @@
+class Theme < ActiveRecord::Base
+ after_validation :schedule_theme_delete, on: :create, if: "errors.present?"
+ before_destroy :schedule_theme_delete
+ after_create :schedule_theme_transfer
+
+ NAME_LIMIT = 100
+ ZIP_FILE_URL_LIMIT = 2000
+
+ enum status: { processing: 0, uploaded: 1 }
+
+ validates :name, presence: true, length: { maximum: NAME_LIMIT }, uniqueness: { case_sensitive: false }
+ validates :zip_file_url, presence: true, format: { with: /\.(zip)\z/i }, length: { maximum: ZIP_FILE_URL_LIMIT }, uniqueness: { case_sensitive: false }
+ validates :status, presence: true
+
+ private
+
+ def schedule_theme_transfer
+ TransferThemeJob.perform_later(self)
+ end
+
+ def schedule_theme_delete
+ DeleteThemeJob.perform_later(zip_file_url, processing?)
+ end
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index 96c5366..8cf1db7 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1,7 +1,7 @@
class User < ActiveRecord::Base
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable, :rememberable, :trackable
- devise :database_authenticatable, :registerable, :recoverable, :validatable
+ devise :database_authenticatable, :registerable, :recoverable, :validatable, :async
belongs_to :role
validates_presence_of :role
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
index 5595040..fdecce5 100644
--- a/app/views/layouts/application.html.erb
+++ b/app/views/layouts/application.html.erb
@@ -8,6 +8,32 @@
+
+ <% if flash.notice %>
+
+ <%= flash.notice %>
+
+ <% end %>
+
+
+
+ <% if flash.alert %>
+
+ <%= flash.alert %>
+
+ <% end %>
+ <% if errors.any? %>
+
+
<%= pluralize(errors.count, "error") %> occured:
+
+ <% errors.full_messages.each do |msg| %>
+ - <%= msg %>
+ <% end %>
+
+
+ <% end %>
+
+
<%= yield %>
diff --git a/app/views/themes/index.html.slim b/app/views/themes/index.html.slim
new file mode 100644
index 0000000..d033c23
--- /dev/null
+++ b/app/views/themes/index.html.slim
@@ -0,0 +1,17 @@
+- if can? :create, Theme
+ = link_to "Create New", new_theme_path
+
+table
+ thead
+ tr
+ th Name
+ th Status
+ th Actions
+ tbody
+ - @themes.each do |theme|
+ tr
+ td = theme.name
+ td = theme.status
+ td
+ - if can? :destroy, Theme
+ = link_to 'Delete', theme_path(theme), method: :delete
diff --git a/app/views/themes/new.html.slim b/app/views/themes/new.html.slim
new file mode 100644
index 0000000..47e40d1
--- /dev/null
+++ b/app/views/themes/new.html.slim
@@ -0,0 +1,11 @@
+= form_tag(theme_upload_params[:form_action], multipart: true, enforce_utf8: false, authenticity_token: false)
+
+ = hidden_field_tag('key', theme_upload_params[:file_id])
+ = hidden_field_tag('AWSAccessKeyId', theme_upload_params[:access_key])
+ = hidden_field_tag('acl', theme_upload_params[:acl])
+ = hidden_field_tag('success_action_redirect', theme_upload_params[:redirect_url])
+ = hidden_field_tag('policy', theme_upload_params[:policy])
+ = hidden_field_tag('signature', theme_upload_params[:signature])
+
+ = file_field_tag('file', accept: '.zip')
+ = submit_tag('Upload Theme')
diff --git a/config/application.rb b/config/application.rb
index 9c3e7ed..c62ca08 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -32,18 +32,28 @@ class Application < Rails::Application
# Do not swallow errors in after_commit/after_rollback callbacks.
config.active_record.raise_in_transactional_callbacks = true
- # ActionMailer config
- config.action_mailer.default_url_options = { :host => ENV["HOST"] }
- config.action_mailer.raise_delivery_errors = true
- config.action_mailer.delivery_method = :smtp
- config.action_mailer.perform_deliveries = true
- config.action_mailer.smtp_settings = {
- address: "smtp.gmail.com",
- port: 587,
- domain: ENV["GMAIL_DOMAIN"],
- authentication: "plain",
- enable_starttls_auto: true,
- user_name: ENV["GMAIL_USERNAME"],
- password: ENV["GMAIL_PASSWORD"]}
+ config.active_job.queue_adapter = :sidekiq
+
+ # ActionMailer config
+ config.action_mailer.default_url_options = { :host => ENV["HOST"] }
+ config.action_mailer.raise_delivery_errors = true
+ config.action_mailer.delivery_method = :smtp
+ config.action_mailer.perform_deliveries = true
+ config.action_mailer.smtp_settings = {
+ address: "smtp.gmail.com",
+ port: 587,
+ domain: ENV["GMAIL_DOMAIN"],
+ authentication: "plain",
+ enable_starttls_auto: true,
+ user_name: ENV["GMAIL_USERNAME"],
+ password: ENV["GMAIL_PASSWORD"]}
+
+ # AWS bucket config
+ config.aws_max_theme_zip_file_length = 1.gigabyte
+ config.aws_access_key = ENV["AWS_ACCESS_KEY"]
+ config.aws_secret_access_key = ENV["AWS_SECRET_ACCESS_KEY"]
+ config.aws_public_bucket_name = "flexcommerce-uploadedthemes"
+ config.aws_private_bucket_name = "flexcommerce-productionthemes"
+ config.aws_region = "us-east-1"
end
end
diff --git a/config/environments/test.rb b/config/environments/test.rb
index 1c19f08..f1b7e8e 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -39,4 +39,7 @@
# Raises error for missing translations
# config.action_view.raise_on_missing_translations = true
+
+ # For rspec-activejob gem
+ config.active_job.queue_adapter = :test
end
diff --git a/config/initializers/devise_async.rb b/config/initializers/devise_async.rb
new file mode 100644
index 0000000..0084a27
--- /dev/null
+++ b/config/initializers/devise_async.rb
@@ -0,0 +1,5 @@
+# Supported options: :resque, :sidekiq, :delayed_job, :queue_classic, :torquebox, :backburner, :que, :sucker_punch
+Devise::Async.setup do |config|
+ config.backend = :sidekiq
+ config.queue = :default
+end
diff --git a/config/routes.rb b/config/routes.rb
index 003a52b..a1c4cef 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -13,10 +13,17 @@
# Put here routes to tenant-resource only
constraints ->(request) { request.subdomain.present? } do
get 'accounts/login_with_token' => 'accounts#login_with_token'
+ resources :themes, only: [:index, :new, :destroy]
+ get 'themes/create_completed' => "themes#create_completed" #redirect from AWS
devise_for :users, skip: :registrations
resources :users, except: :show
end
+ require 'sidekiq/web'
+ authenticate :user do
+ mount Sidekiq::Web => '/sidekiq'
+ end
+
# Example of regular route:
# get 'products/:id' => 'catalog#view'
diff --git a/db/migrate/20150615091157_create_roles.rb b/db/migrate/20150615091157_create_roles.rb
index ba0ab2c..fdc768a 100644
--- a/db/migrate/20150615091157_create_roles.rb
+++ b/db/migrate/20150615091157_create_roles.rb
@@ -6,6 +6,8 @@ def change
t.boolean :can_update_users_password, null: false, default: false
t.boolean :can_update_users_role, null: false, default: false
t.boolean :can_delete_users, null: false, default: false
+ t.boolean :can_create_themes, null: false, default: false
+ t.boolean :can_delete_themes, null: false, default: false
t.timestamps null: false
end
diff --git a/db/migrate/20150624111841_create_themes.rb b/db/migrate/20150624111841_create_themes.rb
new file mode 100644
index 0000000..12a5b68
--- /dev/null
+++ b/db/migrate/20150624111841_create_themes.rb
@@ -0,0 +1,9 @@
+class CreateThemes < ActiveRecord::Migration
+ def change
+ create_table :themes do |t|
+ t.string :name, null:false, limit:100
+ t.string :zip_file_url, null:false, limit:2000
+ t.integer :status, null:false, default: 0
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index cbb6880..87a94b2 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20150616064739) do
+ActiveRecord::Schema.define(version: 20150624111841) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -32,12 +32,20 @@
t.boolean "can_update_users_password", default: false, null: false
t.boolean "can_update_users_role", default: false, null: false
t.boolean "can_delete_users", default: false, null: false
+ t.boolean "can_create_themes", default: false, null: false
+ t.boolean "can_delete_themes", default: false, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_index "roles", ["name"], name: "index_roles_on_name", unique: true, using: :btree
+ create_table "themes", force: :cascade do |t|
+ t.string "name", limit: 100, null: false
+ t.string "zip_file_url", limit: 2000, null: false
+ t.integer "status", default: 0, null: false
+ end
+
create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
diff --git a/spec/acceptance_tests/common_methods.rb b/spec/acceptance_tests/common_methods.rb
new file mode 100644
index 0000000..b692da3
--- /dev/null
+++ b/spec/acceptance_tests/common_methods.rb
@@ -0,0 +1,66 @@
+module Common_methods
+require './spec/acceptance_tests/strings.rb'
+
+ def strings(str)
+ Strings[str]
+ end
+
+ def visit_url(url)
+ Capybara.visit "http://#{url}"
+ end
+
+ def expect_page_url_to_be(url)
+ expect(current_url).to eq("http://#{url}")
+ end
+
+ def expect_error(message)
+ expect(page.find('#errors')).to have_content(message)
+ end
+
+ def expect_no_errors
+ expect(page.find('#errors').text).to eq ""
+ end
+
+ def expect_notification(message)
+ expect(page.find('#notifications')).to have_content(message)
+ end
+
+ def expect_no_notifications
+ expect(page.find('#notifications').text).to eq ""
+ end
+
+ def log_in_with(login, pass)
+ fill_in('E-mail', with: login)
+ fill_in('Password', with: pass)
+ click_button "Log in"
+ end
+
+ def cleanup_database
+ Account.destroy_all
+# Apartment::Tenant.drop 'MySubdomain1' rescue nil
+ Apartment::Tenant.reset
+ DatabaseCleaner.clean
+ end
+
+ def create_roles
+ FactoryGirl.create(:account_owner_role)
+ FactoryGirl.create(:user_role)
+ FactoryGirl.create(:create_users_role)
+ FactoryGirl.create(:update_users_role_role)
+ FactoryGirl.create(:update_users_password_role)
+ FactoryGirl.create(:update_users_role_and_password_role)
+ FactoryGirl.create(:delete_users_role)
+ end
+
+ def register_account(login, password, name, subdomain)
+ visit_url 'lvh.me:3000'
+ click_link (strings(:registration))
+ fill_in(strings(:login), with: login)
+ fill_in(strings(:password), with: password)
+ fill_in(strings(:pass_confirm), with: password)
+ fill_in(strings(:name), with: name)
+ fill_in(strings(:subdomain), with: subdomain)
+ click_button(strings(:create_account_btn))
+ end
+
+end
diff --git a/spec/acceptance_tests/strings.rb b/spec/acceptance_tests/strings.rb
new file mode 100644
index 0000000..95dc16c
--- /dev/null
+++ b/spec/acceptance_tests/strings.rb
@@ -0,0 +1,39 @@
+module Strings
+ def self.[](key)
+ {
+ #UI elements
+ #start page
+ registration: 'Registration',
+ #create account page
+ login: 'Email',
+ password: 'Password',
+ pass_confirm: 'Confirm password',
+ subdomain: 'Subdomain',
+ name: 'Name',
+ create_account_btn: 'Create Account',
+ #themes index page
+ create_theme_btn: 'Create New',
+ status: 'Status',
+ actions: 'Actions',
+ delete: 'Delete',
+ open_file_btn: 'file',
+ upload_file_btn: 'Upload Theme',
+
+ #Messages
+ # Common
+ authorization_error: 'You are not authorized to access this page.',
+ #Themes index page
+ theme_created: 'uploaded successfully',
+ theme_deleted: 'successfully deleted',
+ theme_blank_name_error: "Name can't be blank",
+ theme_file_ext_error: 'Zip file url is invalid',
+
+
+ #URLs
+ new_account_page: '/accounts/new',
+ themes_index_page: '/themes',
+ themes_new_page: '/themes/new',
+ themes_create_completed: '/themes/create_completed',
+ }[key]
+ end
+end
diff --git a/spec/acceptance_tests/themes_management/themes_index_page_spec.rb b/spec/acceptance_tests/themes_management/themes_index_page_spec.rb
new file mode 100644
index 0000000..320c8f7
--- /dev/null
+++ b/spec/acceptance_tests/themes_management/themes_index_page_spec.rb
@@ -0,0 +1,143 @@
+require "rails_helper"
+require './spec/acceptance_tests/common_methods.rb'
+
+RSpec.shared_context "visit page with permission" do
+ before(:each) do
+ User.last.update role: create(role)
+ visit_url url
+ end
+end
+
+RSpec.feature "theme_management.start_page", :type => :feature do
+ let(:subdomain) { "testsubdomain" }
+ let(:host_url) { subdomain + ".lvh.me:3000" }
+ let(:themes_url) { host_url + strings(:themes_index_page) }
+ let(:themes_new_url) { host_url + strings(:themes_new_page) }
+ let(:theme) { create(:theme) }
+
+ include Common_methods
+
+ background do
+ cleanup_database
+ register_account('test@mail.com', 'password', 'Test Tenant', subdomain)
+ User.last.update role: create(:manage_themes_role)
+ end
+
+ context "User without themes permissions" do
+ context "User without all themes permissions" do
+ include_context "visit page with permission" do
+ let(:role) { :role }
+ let(:url) { themes_url }
+ end
+ scenario "gets auth.error when visits themes index page" do
+ expect_page_url_to_be (host_url + "/")
+ expect_error(strings(:authorization_error))
+ end
+ end
+ context "User without create themes permissions" do
+ include_context "visit page with permission" do
+ let(:role) { :delete_themes_role }
+ let(:url) { themes_url }
+ end
+ scenario "cannot create new theme" do
+ expect_page_url_to_be themes_url
+ expect(page).to have_no_link(strings(:create_theme_btn), href: new_theme_path)
+ end
+ end
+ context "User without delete themes permissions" do
+ before(:each) do
+ theme
+ User.last.update role: create(:create_themes_role)
+ visit_url themes_url
+ end
+ scenario "cannot delete theme" do
+ expect_page_url_to_be themes_url
+ expect(all('tr td')[2]).to have_no_link(strings(:delete), href: theme_path(1))
+ end
+ end
+ end
+
+ context "User with themes permissions" do
+ let(:url) { host_url + strings(:themes_create_completed) + param }
+ before(:each) do
+ theme
+ visit_url themes_url
+ end
+ context "User with all themes permissions visits themes index page" do
+ scenario "and page contains all fields" do
+ expect_page_url_to_be themes_url
+
+ expect(page).to have_link(strings(:create_theme_btn), href: new_theme_path)
+
+ expect(page).to have_table ''
+ expect(page).to have_selector 'table tr', count: 2
+ expect(all('tr th')[0]).to have_text strings(:name)
+ expect(all('tr th')[1]).to have_text strings(:status)
+ expect(all('tr th')[2]).to have_text strings(:actions)
+
+ expect(all('tr td')[0]).to have_text theme.name
+ expect(all('tr td')[1]).to have_text theme.status
+ expect(all('tr td')[2]).to have_link(strings(:delete), href: theme_path(1))
+ end
+ end
+ context "User clicks Create New link and opens Create Theme page" do
+ scenario "and page contains all fields" do
+ click_link(strings(:create_theme_btn))
+ expect_page_url_to_be themes_new_url
+ expect(page).to have_field(strings(:open_file_btn))
+ expect(page).to have_button(strings(:upload_file_btn))
+ end
+ end
+ context "clicks Create New link and selects theme zip file" do
+ let(:param) { "?key=12345_test theme.zip" }
+ scenario "clicks Upload Theme and successfully uploads new Theme" do
+ click_link(strings(:create_theme_btn))
+
+ # click_button(strings(:upload_file_btn))
+ # Couldn't stub redirect from Amazon, just call action directly
+ visit_url url
+
+ expect_page_url_to_be themes_url
+ expect_no_errors
+ expect_notification strings(:theme_created)
+ expect(all('tr td')[3]).to have_text Theme.last.name
+ expect(all('tr td')[4]).to have_text Theme.last.status
+ expect(all('tr td')[5]).to have_link(strings(:delete), href: theme_path(2))
+ end
+ end
+ context "clicks Create New link and does not select theme file" do
+ let(:param) { "?key=12345_" }
+ scenario "clicks Upload Theme and gets an error" do
+ click_link(strings(:create_theme_btn))
+
+ visit_url url
+
+ expect_page_url_to_be themes_new_url
+ expect_no_notifications
+ expect_error strings(:theme_blank_name_error)
+ end
+ end
+ context "clicks Create New link and select theme file not in ZIP format" do
+ let(:param) { "?key=12345_theme.exe" }
+ scenario "clicks Upload Theme and gets an error" do
+ click_link(strings(:create_theme_btn))
+
+ visit_url url
+
+ expect_page_url_to_be themes_new_url
+ expect_no_notifications
+ expect_error strings(:theme_file_ext_error)
+ end
+ end
+ context "User with all themes permissions visits themes index page" do
+ scenario "clicks Delete link and successfully deletes theme" do
+ click_link(strings(:delete))
+
+ expect_page_url_to_be themes_url
+ expect(page).to have_selector 'table tr', count: 1
+ expect_no_errors
+ expect_notification strings(:theme_deleted)
+ end
+ end
+ end
+end
diff --git a/spec/config/aws_spec.rb b/spec/config/aws_spec.rb
new file mode 100644
index 0000000..2fb1ad2
--- /dev/null
+++ b/spec/config/aws_spec.rb
@@ -0,0 +1,12 @@
+require 'rails_helper'
+
+RSpec.describe Rails.application.config do
+ describe "has set Amazon S3 options" do
+ it {expect(subject.aws_max_theme_zip_file_length).to be_between(1, 5.gigabytes - 1).inclusive }
+ it { expect(subject.aws_access_key).to be_a String }
+ it { expect(subject.aws_secret_access_key).to be_a String }
+ it { expect(subject.aws_public_bucket_name).to be_a String }
+ it { expect(subject.aws_private_bucket_name).to be_a String }
+ it { expect(subject.aws_region).to be_a String }
+ end
+end
\ No newline at end of file
diff --git a/spec/controllers/themes_controller_spec.rb b/spec/controllers/themes_controller_spec.rb
new file mode 100644
index 0000000..a92dd27
--- /dev/null
+++ b/spec/controllers/themes_controller_spec.rb
@@ -0,0 +1,158 @@
+require 'rails_helper'
+
+RSpec.shared_examples "creating Theme in DB" do |key, theme_name|
+ before(:each) { get :create_completed, key: key }
+ subject(:theme) { Theme.find_by_name theme_name }
+ describe "Theme with name #{theme_name}" do
+ it { is_expected.to be }
+ its(:zip_file_url) { is_expected.to eq key }
+ it { is_expected.to be_processing }
+ it { expect(Theme.count).to eq(1) }
+ end
+end
+
+RSpec.shared_examples "uncussessfull create_completed" do
+ it { expect(Theme.count).to be_zero }
+ it { is_expected.to redirect_to action: :new }
+ it "should set flash" do
+ subject
+ expect(controller).to set_flash[:alert]
+ end
+end
+
+RSpec.describe ThemesController, type: :controller do
+ let(:account) { create :account }
+ let(:user_under_test) { create(:user) }
+ before(:each) do
+ Apartment::Tenant.switch! account.subdomain
+ user_under_test.update role: create(:manage_themes_role)
+ sign_in user_under_test
+ end
+
+ describe "when user not logged in" do
+ before(:each) { sign_out user_under_test }
+ subject { response }
+ let(:params) { {} }
+
+ { index: :get, new: :get, create_completed: :get, destroy: :delete }.each do |action, method|
+ describe "#{method}##{action}" do
+ let(:params) { {id: create(:theme).id} } if action == :destroy
+ before(:each) { send(method, action, params) }
+ it { is_expected.to have_http_status(:found) }
+ it { is_expected.to redirect_to(new_user_session_path) }
+ end
+ end
+ end
+
+ describe "when user logged in" do
+ subject { response }
+ %w{ index new }.each do |action|
+ describe "GET##{action}" do
+ context "when user does not have permission" do
+ before(:each) do
+ case action
+ when 'index' then user_under_test.update role: create(:role)
+ when 'new' then user_under_test.role.update can_create_themes: false
+ end
+ get action
+ end
+ it { is_expected.to redirect_to(root_path) }
+ end
+ context "when user has permission" do
+ before(:each) do
+ user_under_test.update role: create(:create_themes_role)
+ get action
+ end
+ it { is_expected.to have_http_status(:ok) }
+ it { is_expected.to render_template(action) }
+ end
+ end
+ end
+
+ describe "GET#create_completed (redirect from AWS)" do
+ let!(:theme_count){ Theme.count }
+ context "when user does not have permission" do
+ let(:key) { "1234_qwerty.zip" }
+ before(:each) do
+ user_under_test.role.update can_create_themes: false
+ get :create_completed, key: key
+ end
+ it { is_expected.to redirect_to(root_path) }
+ end
+ context "when user has permission" do
+ let(:theme_name) { "#{Faker::Internet.url}.zip" }
+ let(:key) {"#{SecureRandom.uuid}_#{theme_name}"}
+ subject { get :create_completed, key: key }
+ context "with valid parameters" do
+ {"_qwerty.zip" => "qwerty", "1234_qwerty.zip" => "qwerty", "12345_67890_qwerty.zip" => "67890_qwerty"}.each do |key, theme_name|
+ context "when parsing key: #{key}" do
+ it_behaves_like "creating Theme in DB", key, theme_name
+ it { is_expected.to redirect_to action: :index }
+ it "should set flash" do
+ subject
+ expect(controller).to set_flash[:notice]
+ end
+ end
+ end
+ context "when theme already exists" do
+ let(:existing_theme) { create(:theme) }
+ let(:key) {"#{SecureRandom.uuid}_#{existing_theme.name}.zip"}
+ it_behaves_like "uncussessfull create_completed"
+ end
+ end
+ context "when invalid parameters" do
+ context "when wrong file extension (not .zip)" do
+ let(:theme_name) { "#{Faker::Internet.url}.rar" }
+ subject { get :create_completed, key: key }
+ it_behaves_like "uncussessfull create_completed"
+ end
+ context "when theme name blank" do
+ let(:theme_name) { '' }
+ it_behaves_like "uncussessfull create_completed"
+ end
+ context "when params missing" do
+ it "raises exception" do
+ expect { get :create_completed }.to raise_error(ActionController::ParameterMissing)
+ end
+ end
+ end
+ end
+ end
+
+ describe "DELETE#destroy" do
+ let(:theme) { create(:theme) }
+ let(:id) { theme.id }
+ context "when user does not have permission" do
+ before(:each) do
+ user_under_test.role.update can_delete_themes: false
+ delete :destroy, id: id
+ end
+ it { is_expected.to redirect_to(root_path) }
+ end
+
+ context "when user has permission" do
+ context "when id wrong" do
+ let(:id) { theme.id+100 }
+ subject { delete :destroy, id: id }
+ it "raises exception" do
+ expect {subject}.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+ context "when successful" do
+ before(:each) do
+ id = theme.id
+ @count = Theme.count
+ delete :destroy, id: id
+ end
+ it "deletes theme" do
+ expect{theme.reload}.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ it "Theme.count decreased by 1" do
+ expect(Theme.count).to eq(@count-1)
+ end
+ it { is_expected.to redirect_to(themes_path) }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/factories/roles.rb b/spec/factories/roles.rb
index 5ad8874..d6b56f3 100644
--- a/spec/factories/roles.rb
+++ b/spec/factories/roles.rb
@@ -31,6 +31,16 @@
can_update_users_role true
can_delete_users true
end
+ factory :create_themes_role do
+ can_create_themes true
+ end
+ factory :delete_themes_role do
+ can_delete_themes true
+
+ factory :manage_themes_role do
+ can_create_themes true
+ end
+ end
end
factory :account_owner_role, parent: :manage_users_role do
diff --git a/spec/factories/themes.rb b/spec/factories/themes.rb
new file mode 100644
index 0000000..4a57f3d
--- /dev/null
+++ b/spec/factories/themes.rb
@@ -0,0 +1,7 @@
+FactoryGirl.define do
+ factory :theme do
+ name { Faker::Internet.slug }
+ zip_file_url "#{Faker::Internet.url}.zip"
+ status { :processing }
+ end
+end
\ No newline at end of file
diff --git a/spec/helpers/themes_helper_spec.rb b/spec/helpers/themes_helper_spec.rb
new file mode 100644
index 0000000..9ebf2e3
--- /dev/null
+++ b/spec/helpers/themes_helper_spec.rb
@@ -0,0 +1,53 @@
+require 'rails_helper'
+
+RSpec.describe ThemesHelper, type: :helper do
+ describe "#theme_upload_params" do
+ let(:redirect_url) {"http://test.host/themes/create_completed" }
+ let (:policy) {
+ timestamp = Time.now
+ allow(Time).to receive(:now).and_return(timestamp)
+ conditions = [[:eq, :$bucket, Rails.application.config.aws_public_bucket_name],
+ [:eq, :$acl, :private],
+ [:"content-length-range", 1, Rails.application.config.aws_max_theme_zip_file_length],
+ [:"starts-with", :$key, "#{ThemesHelper::UPLOADS_FOLDER}"],
+ [:eq, :$success_action_redirect, redirect_url]]
+ policy = { conditions: conditions, expiration: (Time.now + 10.hours).utc.iso8601 }
+ policy = Base64.strict_encode64(policy.to_json) }
+
+ subject {helper.theme_upload_params}
+
+ it {is_expected.to be_a Hash}
+
+ it "has form_action key with value" do
+ expect(subject[:form_action]).to eq "https://#{Rails.application.config.aws_public_bucket_name}.s3.amazonaws.com/"
+ end
+ it "has form_method key with value" do
+ expect(subject[:form_method]).to eq "post"
+ end
+ it "has form_enclosure_type key with value" do
+ expect(subject[:form_enclosure_type]).to eq "multipart/form-data"
+ end
+ it "has acl key with value" do
+ expect(subject[:acl]).to eq "private"
+ end
+ it "has access_key key with value" do
+ expect(subject[:access_key]).to eq Rails.application.config.aws_access_key
+ end
+ it "has redirect_url key with value" do
+ expect(subject[:redirect_url]).to eq redirect_url
+ end
+ it "has file_id key with value" do
+ uuid= SecureRandom.uuid
+ allow(SecureRandom).to receive(:uuid).and_return(uuid)
+ expect(subject[:file_id]).to eq "#{ThemesHelper::UPLOADS_FOLDER}#{SecureRandom.uuid}_${filename}"
+ end
+ it "has policy key with value" do
+ expect(subject[:policy]).to eq policy
+ end
+
+ it "has signature key with value" do
+ expect(subject[:signature]).to eq Base64.strict_encode64(OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha1'),
+ Rails.application.config.aws_secret_access_key, policy))
+ end
+ end
+end
\ No newline at end of file
diff --git a/spec/jobs/delete_theme_job_spec.rb b/spec/jobs/delete_theme_job_spec.rb
new file mode 100644
index 0000000..3ff0965
--- /dev/null
+++ b/spec/jobs/delete_theme_job_spec.rb
@@ -0,0 +1,38 @@
+require 'rails_helper'
+
+RSpec.describe DeleteThemeJob, type: :job do
+ include ActiveJob::TestHelper
+ let(:zip_file_url) { 'some_zip_file_url' }
+ let(:is_public_bucket) { true }
+ subject(:job) { described_class.perform_later(zip_file_url, is_public_bucket) }
+
+ describe "when queued" do
+ it "queues the job" do
+ expect { job }.to change(ActiveJob::Base.queue_adapter.enqueued_jobs, :size).by(1)
+ end
+ it "is in low_priority queue" do
+ expect(DeleteThemeJob.new.queue_name).to eq('low_priority')
+ end
+ end
+
+ describe "when executes perform" do
+ it "deletes theme from public bucket" do
+ expect(AmazonAwsClient).to receive(:delete_from_public_bucket).with(zip_file_url)
+ perform_enqueued_jobs { job }
+ end
+ describe "when theme in private bucket" do
+ let(:is_public_bucket) { false }
+ it "deletes theme from private bucket" do
+ expect(AmazonAwsClient).to receive(:delete_from_private_bucket).with(zip_file_url)
+ perform_enqueued_jobs { job }
+ end
+ end
+ it "handles request timeout error" do
+ allow(AmazonAwsClient).to receive(:delete_from_public_bucket).and_raise(Aws::S3::Errors::RequestTimeout.new('',''))
+ perform_enqueued_jobs do
+ expect_any_instance_of(DeleteThemeJob).to receive(:retry_job).with(wait: 5.minutes, queue: :low_priority)
+ job
+ end
+ end
+ end
+end
diff --git a/spec/jobs/transfer_theme_job_spec.rb b/spec/jobs/transfer_theme_job_spec.rb
new file mode 100644
index 0000000..edd50fa
--- /dev/null
+++ b/spec/jobs/transfer_theme_job_spec.rb
@@ -0,0 +1,37 @@
+require 'rails_helper'
+
+RSpec.describe TransferThemeJob, type: :job do
+ include ActiveJob::TestHelper
+
+ let(:theme) { create(:theme) }
+
+ describe "when queued" do
+ it "queues the job" do
+ expect { theme }.to change(ActiveJob::Base.queue_adapter.enqueued_jobs, :size).by(1)
+ end
+
+ it "is in default queue" do
+ expect(TransferThemeJob.new.queue_name).to eq('default')
+ end
+ end
+
+ describe "when executes perform" do
+ subject(:job) { described_class.perform_later(theme) }
+ it "transfer theme from public to private bucket" do
+ expect(AmazonAwsClient).to receive(:transfer_from_public_to_private_bucket).with(theme.zip_file_url)
+ perform_enqueued_jobs { job }
+ end
+ it "updates theme status" do
+ allow(AmazonAwsClient).to receive(:transfer_from_public_to_private_bucket)
+ perform_enqueued_jobs { job }
+ expect(theme.reload.uploaded?).to be_truthy
+ end
+ it "handles request timeout error" do
+ allow(AmazonAwsClient).to receive(:transfer_from_public_to_private_bucket).and_raise(Aws::S3::Errors::RequestTimeout.new('',''))
+ perform_enqueued_jobs do
+ expect_any_instance_of(TransferThemeJob).to receive(:retry_job).with(wait: 5.minutes, queue: :default)
+ theme
+ end
+ end
+ end
+end
diff --git a/spec/models/role_spec.rb b/spec/models/role_spec.rb
index a5a3d76..09bfe1c 100644
--- a/spec/models/role_spec.rb
+++ b/spec/models/role_spec.rb
@@ -8,7 +8,7 @@
it {expect(build(:role)).to validate_uniqueness_of(:name)}
it {is_expected.to have_db_column(:name).with_options null: false, limit: Role::NAME_LIMIT_MAX }
- %w{ can_create_users can_update_users_password can_update_users_role can_delete_users }.each do |column_name|
+ %w{ can_create_users can_update_users_password can_update_users_role can_delete_users can_create_themes can_delete_themes }.each do |column_name|
it {is_expected.to have_db_column(column_name).with_options null: false, default: false }
end
end
diff --git a/spec/models/theme_spec.rb b/spec/models/theme_spec.rb
new file mode 100644
index 0000000..0cff823
--- /dev/null
+++ b/spec/models/theme_spec.rb
@@ -0,0 +1,56 @@
+require 'rails_helper'
+
+RSpec.describe Theme, type: :model do
+ let(:theme) { create :theme}
+ subject { theme }
+
+ context "when validate" do
+ it {is_expected.to validate_presence_of :name}
+ it {expect(build(:theme)).to validate_uniqueness_of(:name).case_insensitive }
+ it {expect(build(:theme)).to validate_length_of(:name).is_at_most Theme::NAME_LIMIT }
+ it {is_expected.to have_db_column(:name).with_options limit: Theme::NAME_LIMIT, null: false }
+
+ it {is_expected.to validate_presence_of :zip_file_url}
+ it {expect(build(:theme)).to validate_uniqueness_of(:zip_file_url).case_insensitive}
+ it {is_expected.to validate_length_of(:zip_file_url).is_at_most Theme::ZIP_FILE_URL_LIMIT }
+ it {is_expected.to have_db_column(:zip_file_url).with_options limit: Theme::ZIP_FILE_URL_LIMIT, null: false }
+ %w{filename.zip filename.ZIP filename.ZiP}.each do |filename|
+ it {is_expected.to allow_value(filename).for(:zip_file_url) }
+ end
+ %w{filename filename. filename.z filename.zi filename.abc filename.ziip}.each do |filename|
+ it {is_expected.to_not allow_value(filename).for(:zip_file_url) }
+ end
+ it {is_expected.to validate_presence_of :status}
+ it {is_expected.to have_db_column(:status).with_options null: false, default: Theme.statuses[:processing]}
+ end
+
+ context "when create" do
+ context "when successfull" do
+ it "schedules transfer theme from public to private bucket" do
+ expect { subject }.to enqueue_a(TransferThemeJob).with(global_id(Theme))
+ end
+ it "does not schedules deletion theme from public bucket" do
+ expect { subject }.to_not enqueue_a(DeleteThemeJob).with(String, TrueClass)
+ end
+ end
+ context "when unsuccessfull" do
+ let(:theme) { build :theme, zip_file_url: "wrong_url" }
+ it "schedules deletion theme from public bucket" do
+ expect { theme.save }.to enqueue_a(DeleteThemeJob).with(String, TrueClass)
+ end
+ it "schedules transfer theme from public to private bucket" do
+ expect { theme.save }.to_not enqueue_a(TransferThemeJob).with(global_id(Theme))
+ end
+ end
+ end
+
+ context "when destroy" do
+ it "schedules deletion theme from public bucket" do
+ expect { theme.destroy }.to enqueue_a(DeleteThemeJob).with(String, TrueClass)
+ end
+ it "schedules deletion theme from private bucket" do
+ theme.update status: :uploaded
+ expect { theme.destroy }.to enqueue_a(DeleteThemeJob).with(String, FalseClass)
+ end
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 5009e7a..df8eaa6 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -1,4 +1,5 @@
require 'factory_girl'
+require 'rspec/active_job'
# This file was generated by the `rails generate rspec:install` command. Conventionally, all
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
@@ -19,6 +20,8 @@
#
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
RSpec.configure do |config|
+ config.include(RSpec::ActiveJob)
+
# rspec-expectations config goes here. You can use an alternate
# assertion/expectation library such as wrong or the stdlib/minitest
# assertions if you prefer.
@@ -109,6 +112,8 @@
config.before(:each) do
Account.destroy_all
Apartment::Tenant.reset
+ ActiveJob::Base.queue_adapter.enqueued_jobs = []
+ ActiveJob::Base.queue_adapter.performed_jobs = []
end
config.around(:each) do |example|
@@ -116,5 +121,4 @@
example.run
end
end
-
end