+ <% if @build.branch_name.nil? or @build.username.nil? %>
+ <%= image_tag("vizzy-text.svg", class: "img-responsive pull-left vizzy-build-bottom-spacing") %>
+ <% end %>
+
+ <% if @build.dev_build? %>
+ Dev Build - No Action Can Be Taken. New tests will not be shown.
+ <% elsif @build.branch_name.nil? or @build.username.nil? %>
+ <% else %>
+ <%= @build.branch_name %>
+
+ (<%= @build.username %>)
+
+ <% end %>
+
This build has not been committed. Uploads may still be in progress.
+<% elsif !@build.failure_message.blank? %>
+
Build Failed: <%= @build.failure_message %>.
+ <%= link_to 'Please look at the CI build to diagnose the issue.', @build.url, :class => 'alert-link' %>
+
+<% elsif @build.num_of_images_in_build == 0 %>
+
No images were uploaded to this build, there was an issue generating or uploading
+ the images in the build. <%= link_to 'Please look at the CI build to diagnose the issue.', @build.url, :class => 'alert-link' %>
+
+<% elsif @unapproved_diffs.size > 0 %>
+ <% if @build.dev_build? %>
+ <% link_text = 'Click here to look through them.' %>
+ <% else %>
+ <% link_text = 'Please look through them to approve them.' %>
+ <% end %>
+
This build has <%= @unapproved_diffs.size %> unapproved visual
+ difference(s). <%= link_to link_text, diff_path(@unapproved_diffs.first), :class => 'alert-link' %>
+<% elsif @approved_diffs.size > 0 %>
+
All visual differences in this build have been approved. You can review what was
+ changed below.
+
+<% else %>
+
No visual differences were found between this build and the base images.
\ No newline at end of file
diff --git a/app/views/layouts/_diffs_panel.erb b/app/views/layouts/_diffs_panel.erb
new file mode 100644
index 0000000..fb47c87
--- /dev/null
+++ b/app/views/layouts/_diffs_panel.erb
@@ -0,0 +1,58 @@
+
+
+
+ <% if show_approver %>
+ Approved By: <%= link_to diff.approved_by_username, user_path(User.find_by_username(diff.approved_by_username)) %>
+ <% end %>
+ <% if diff.old_image.test.jira? %>
+ Jira: <%= link_to diff.old_image.test.jira, diff.old_image.test.jira %>
+ <% end %>
+
+ <% if diff.old_image.test.pull_request_link? %>
+ Pull Request: <%= link_to diff.old_image.test.pull_request_link, diff.old_image.test.pull_request_link %>
+ <% end %>
+ <% if diff.old_image.test.comment? %>
+ Comment: <%= diff.old_image.test.comment %>
+ <% end %>
+
+ <% end %>
+
+ <% end %>
+ <% end %>
+
+
+
+
+
diff --git a/app/views/layouts/_error_messages.erb b/app/views/layouts/_error_messages.erb
new file mode 100644
index 0000000..59f158b
--- /dev/null
+++ b/app/views/layouts/_error_messages.erb
@@ -0,0 +1,13 @@
+<% if object.errors.any? %>
+
+
+
+
This form contains <%= pluralize(object.errors.count, 'error') %>.
+
+ <% object.errors.full_messages.each do |msg| %>
+
<%= msg %>
+ <% end %>
+
+
+
+<% end %>
\ No newline at end of file
diff --git a/app/views/layouts/_test_images_panel.erb b/app/views/layouts/_test_images_panel.erb
new file mode 100644
index 0000000..d42397e
--- /dev/null
+++ b/app/views/layouts/_test_images_panel.erb
@@ -0,0 +1,48 @@
+
+
+
+ <%= form.text_field :test_name %>
+ <%= form.label :test_name, 'Name of the test' %>
+
+
+ <%= form.text_field :ancestry %>
+ <%= form.label :ancestry, 'Ancestry of the test' %>
+
+
+ <%= form.file_field :image %>
+
+
+ <%= form.submit %>
+
+<% end %>
\ No newline at end of file
diff --git a/app/views/test_images/show.html.erb b/app/views/test_images/show.html.erb
new file mode 100644
index 0000000..c36b162
--- /dev/null
+++ b/app/views/test_images/show.html.erb
@@ -0,0 +1,58 @@
+<% breadcrumb :test_image, @test_image %>
+
diff --git a/app/views/users/show.json.jbuilder b/app/views/users/show.json.jbuilder
new file mode 100644
index 0000000..4e1f3d0
--- /dev/null
+++ b/app/views/users/show.json.jbuilder
@@ -0,0 +1 @@
+json.extract! @user, :id, :username, :email, :role
diff --git a/bin/bundle b/bin/bundle
new file mode 100755
index 0000000..66e9889
--- /dev/null
+++ b/bin/bundle
@@ -0,0 +1,3 @@
+#!/usr/bin/env ruby
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
+load Gem.bin_path('bundler', 'bundle')
diff --git a/bin/rails b/bin/rails
new file mode 100755
index 0000000..0739660
--- /dev/null
+++ b/bin/rails
@@ -0,0 +1,4 @@
+#!/usr/bin/env ruby
+APP_PATH = File.expand_path('../config/application', __dir__)
+require_relative '../config/boot'
+require 'rails/commands'
diff --git a/bin/rake b/bin/rake
new file mode 100755
index 0000000..1724048
--- /dev/null
+++ b/bin/rake
@@ -0,0 +1,4 @@
+#!/usr/bin/env ruby
+require_relative '../config/boot'
+require 'rake'
+Rake.application.run
diff --git a/bin/setup b/bin/setup
new file mode 100755
index 0000000..e620b4d
--- /dev/null
+++ b/bin/setup
@@ -0,0 +1,34 @@
+#!/usr/bin/env ruby
+require 'pathname'
+require 'fileutils'
+include FileUtils
+
+# path to your application root.
+APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
+
+def system!(*args)
+ system(*args) || abort("\n== Command #{args} failed ==")
+end
+
+chdir APP_ROOT do
+ # This script is a starting point to setup your application.
+ # Add necessary setup steps to this file.
+
+ puts '== Installing dependencies =='
+ system! 'gem install bundler --conservative'
+ system('bundle check') || system!('bundle install')
+
+ # puts "\n== Copying sample files =="
+ # unless File.exist?('config/database.yml')
+ # cp 'config/database.yml.sample', 'config/database.yml'
+ # end
+
+ puts "\n== Preparing database =="
+ system! 'bin/rails db:setup'
+
+ puts "\n== Removing old logs and tempfiles =="
+ system! 'bin/rails log:clear tmp:clear'
+
+ puts "\n== Restarting application server =="
+ system! 'bin/rails restart'
+end
diff --git a/bin/spring b/bin/spring
new file mode 100755
index 0000000..9bc076b
--- /dev/null
+++ b/bin/spring
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+
+# This file loads spring without using Bundler, in order to be fast.
+# It gets overwritten when you run the `spring binstub` command.
+
+unless defined?(Spring)
+ require 'rubygems'
+ require 'bundler'
+
+ lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read)
+ if spring = lockfile.specs.detect { |spec| spec.name == "spring" }
+ Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path
+ gem 'spring', spring.version
+ require 'spring/binstub'
+ end
+end
diff --git a/bin/update b/bin/update
new file mode 100755
index 0000000..a8e4462
--- /dev/null
+++ b/bin/update
@@ -0,0 +1,29 @@
+#!/usr/bin/env ruby
+require 'pathname'
+require 'fileutils'
+include FileUtils
+
+# path to your application root.
+APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
+
+def system!(*args)
+ system(*args) || abort("\n== Command #{args} failed ==")
+end
+
+chdir APP_ROOT do
+ # This script is a way to update your development environment automatically.
+ # Add necessary update steps to this file.
+
+ puts '== Installing dependencies =='
+ system! 'gem install bundler --conservative'
+ system('bundle check') || system!('bundle install')
+
+ puts "\n== Updating database =="
+ system! 'bin/rails db:migrate'
+
+ puts "\n== Removing old logs and tempfiles =="
+ system! 'bin/rails log:clear tmp:clear'
+
+ puts "\n== Restarting application server =="
+ system! 'bin/rails restart'
+end
diff --git a/config.ru b/config.ru
new file mode 100644
index 0000000..f7ba0b5
--- /dev/null
+++ b/config.ru
@@ -0,0 +1,5 @@
+# This file is used by Rack-based servers to start the application.
+
+require_relative 'config/environment'
+
+run Rails.application
diff --git a/config/application.rb b/config/application.rb
new file mode 100644
index 0000000..c234651
--- /dev/null
+++ b/config/application.rb
@@ -0,0 +1,23 @@
+require_relative 'boot'
+
+require 'rails/all'
+
+# Require the gems listed in Gemfile, including any gems
+# you've limited to :test, :development, or :production.
+Bundler.require(*Rails.groups)
+
+module VisualAutomation
+ class Application < Rails::Application
+ # Settings in config/environments/* take precedence over those specified here.
+ # Application configuration should go into files in config/initializers
+ # -- all .rb files in that directory are automatically loaded.
+ config.time_zone = 'Pacific Time (US & Canada)'
+
+ # load vizzy server config
+ vizzy_config_path = Rails.root.join('config', 'vizzy.yaml')
+ unless File.exist?(vizzy_config_path)
+ raise "Missing vizzy.yaml configuration file"
+ end
+ config.vizzy = config_for(vizzy_config_path)
+ end
+end
diff --git a/config/boot.rb b/config/boot.rb
new file mode 100644
index 0000000..30f5120
--- /dev/null
+++ b/config/boot.rb
@@ -0,0 +1,3 @@
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
+
+require 'bundler/setup' # Set up gems listed in the Gemfile.
diff --git a/config/breadcrumbs.rb b/config/breadcrumbs.rb
new file mode 100644
index 0000000..b029c6f
--- /dev/null
+++ b/config/breadcrumbs.rb
@@ -0,0 +1,82 @@
+crumb :projects do
+ link 'Projects', projects_path
+end
+
+crumb :project do |project|
+ link project.name, project_path(project)
+ parent :projects
+end
+
+crumb :builds do
+ link 'Builds', builds_path
+end
+
+crumb :build do |build|
+ link build.title, build_path(build)
+ parent build.project
+end
+
+crumb :diffs do
+ link 'Diffs', diffs_path
+end
+
+crumb :diff do |diff|
+ link "Diff: #{diff.new_image.test_key}", diff_path(diff)
+ parent diff.build
+end
+
+crumb :jiras do
+ link 'Jiras', jiras_path
+end
+
+crumb :jira do |jira|
+ link 'Create Jira', jira_path(jira)
+ parent jira.diff
+end
+
+crumb :test_images do
+ link 'Test Images', test_images_path
+end
+
+crumb :test_image do |test_image|
+ link test_image.image_file_name, test_image_path(test_image)
+ parent test_image.build
+end
+
+crumb :test do |test|
+ link test.name, test_path(test)
+ if test.parent.nil?
+ parent Project.find(test.project_id)
+ else
+ parent test.parent
+ end
+end
+
+crumb :base_images do |project|
+ link 'Base Images', project_path(project)
+ parent project
+end
+
+crumb :base_images_test_images do |project|
+ link 'Base Images Test Images', project_path(project)
+ parent project
+end
+
+crumb :users do
+ link 'Users', users_path
+end
+
+crumb :user do |user|
+ link user.display_name, user_path(user)
+ parent :users
+end
+
+crumb :missing_tests do |build|
+ link 'Missing Tests', builds_path(build)
+ parent build
+end
+
+crumb :successful_tests do |build|
+ link 'Successful Tests', builds_path(build)
+ parent build
+end
\ No newline at end of file
diff --git a/config/cable.yml b/config/cable.yml
new file mode 100644
index 0000000..88f201c
--- /dev/null
+++ b/config/cable.yml
@@ -0,0 +1,12 @@
+development:
+ adapter: async
+
+ci_test:
+ adapter: async
+
+test:
+ adapter: async
+
+production:
+ adapter: redis
+ url: redis://localhost:6379/1
diff --git a/config/database.yml b/config/database.yml
new file mode 100644
index 0000000..63a7e4e
--- /dev/null
+++ b/config/database.yml
@@ -0,0 +1,30 @@
+default: &default
+ adapter: postgresql
+ encoding: utf8
+ database: visualautomation_dev
+ pool: 5
+ timeout: 5000
+
+development:
+ <<: *default
+ database: visualautomation_development
+
+production: &production
+ adapter: postgresql
+ pool: <%= ENV["DB_POOL"] || ENV['RAILS_MAX_THREADS'] || 20 %>
+ timeout: 5000
+ host: postgres
+ port: 5432
+ username: <%= ENV['POSTGRES_USER'] %>
+ password: <%= ENV['POSTGRES_PASSWORD'] %>
+ encoding: utf8
+
+# Warning: The database defined as "test" will be erased and
+# re-generated from your development database when you run "rake".
+# Do not set this db to the same as development or production.
+
+test:
+ <<: *default
+
+ci_test:
+ <<: *production
\ No newline at end of file
diff --git a/config/environment.rb b/config/environment.rb
new file mode 100644
index 0000000..426333b
--- /dev/null
+++ b/config/environment.rb
@@ -0,0 +1,5 @@
+# Load the Rails application.
+require_relative 'application'
+
+# Initialize the Rails application.
+Rails.application.initialize!
diff --git a/config/environments/ci_test.rb b/config/environments/ci_test.rb
new file mode 100644
index 0000000..8e480e9
--- /dev/null
+++ b/config/environments/ci_test.rb
@@ -0,0 +1,98 @@
+VisualAutomation::Application.configure do
+ # Settings specified here will take precedence over those in config/application.rb.
+
+ # Code is not reloaded between requests.
+ config.cache_classes = true
+
+ # Eager load code on boot. This eager loads most of Rails and
+ # your application in memory, allowing both threaded web servers
+ # and those relying on copy on write to perform better.
+ # Rake tasks automatically ignore this option for performance.
+ config.eager_load = true
+
+ # Full error reports are disabled and caching is turned on.
+ config.consider_all_requests_local = false
+ config.action_controller.perform_caching = true
+
+ # Disable serving static files from the `/public` folder by default since
+ # Apache or NGINX already handles this.
+ config.public_file_server.enabled = true
+
+ # Compress JavaScripts and CSS.
+ config.assets.js_compressor = :uglifier
+ # config.assets.css_compressor = :sass
+
+ # Do not fallback to assets pipeline if a precompiled asset is missed.
+ config.assets.compile = false
+
+ # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb
+
+ # Enable serving of images, stylesheets, and JavaScripts from an asset server.
+ # config.action_controller.asset_host = 'http://assets.example.com'
+
+ # Specifies the header that your server uses for sending files.
+ # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
+ # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
+
+ # Mount Action Cable outside main process or domain
+ # config.action_cable.mount_path = nil
+ # config.action_cable.url = 'wss://example.com/cable'
+ # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ]
+
+ # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
+ # config.force_ssl = true
+
+ # Use the lowest log level to ensure availability of diagnostic information
+ # when problems arise.
+ config.log_level = :debug
+
+ # Log rotating: The second arg is a number: the count of logs to retain at rotation -- in this case 5 files.
+ # The third arg is the number of bytes to put in the logs before rotating -- in this case 1000.megabytes, which means I will have at most 5GB of log files
+ # http://stackoverflow.com/questions/4883891/ruby-on-rails-production-log-rotation
+ num_logs_to_retain = 5
+ config.logger = Logger.new(config.paths['log'].first, num_logs_to_retain, 1000.megabytes)
+
+ # Prepend all log lines with the following tags.
+ config.log_tags = [ :request_id ]
+
+ # Use a different cache store in CI.
+ # config.cache_store = :mem_cache_store
+
+ # Use a real queuing backend for Active Job (and separate queues per environment)
+ # config.active_job.queue_adapter = :resque
+ # config.active_job.queue_name_prefix = "visual_automation_#{Rails.env}"
+ config.action_mailer.perform_caching = false
+
+ # Ignore bad email addresses and do not raise email delivery errors.
+ # Set this to true and configure the email server for immediate delivery to raise delivery errors.
+ # config.action_mailer.raise_delivery_errors = false
+
+ # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
+ # the I18n.default_locale when a translation cannot be found).
+ config.i18n.fallbacks = true
+
+ # Send deprecation notices to registered listeners.
+ config.active_support.deprecation = :notify
+
+ # Use default logging formatter so that PID and timestamp are not suppressed.
+ config.log_formatter = ::Logger::Formatter.new
+
+ # Use a different logger for distributed setups.
+ # require 'syslog/logger'
+ # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name')
+
+ if ENV["RAILS_LOG_TO_STDOUT"].present?
+ logger = ActiveSupport::Logger.new(STDOUT)
+ logger.formatter = config.log_formatter
+ config.logger = ActiveSupport::TaggedLogging.new(logger)
+ end
+
+ # Do not dump schema after migrations.
+ config.active_record.dump_schema_after_migration = false
+
+ Paperclip.options[:command_path] = '/usr/local/bin/'
+
+ config.read_encrypted_secrets = true
+
+ # ----- Custom Configurations -----
+end
diff --git a/config/environments/development.rb b/config/environments/development.rb
new file mode 100644
index 0000000..24a628f
--- /dev/null
+++ b/config/environments/development.rb
@@ -0,0 +1,59 @@
+VisualAutomation::Application.configure do
+ # Settings specified here will take precedence over those in config/application.rb.
+
+ # In the development environment your application's code is reloaded on
+ # every request. This slows down response time but is perfect for development
+ # since you don't have to restart the web server when you make code changes.
+ config.cache_classes = false
+
+ # Do not eager load code on boot.
+ config.eager_load = false
+
+ # Show full error reports.
+ config.consider_all_requests_local = true
+
+ # Enable/disable caching. By default caching is disabled.
+ if Rails.root.join('tmp/caching-dev.txt').exist?
+ config.action_controller.perform_caching = true
+
+ config.cache_store = :memory_store
+ config.public_file_server.headers = {
+ 'Cache-Control' => 'public, max-age=172800'
+ }
+ else
+ config.action_controller.perform_caching = false
+
+ config.cache_store = :null_store
+ end
+
+ # Don't care if the mailer can't send.
+ config.action_mailer.raise_delivery_errors = false
+ config.action_mailer.perform_caching = false
+
+ # Print deprecation notices to the Rails logger.
+ config.active_support.deprecation = :log
+
+ # Raise an error on page load if there are pending migrations.
+ config.active_record.migration_error = :page_load
+
+ # Debug mode disables concatenation and preprocessing of assets.
+ # This option may cause significant delays in view rendering with a large
+ # number of complex assets.
+ config.assets.debug = true
+
+ # Suppress logger output for asset requests.
+ config.assets.quiet = true
+
+ # Raises error for missing translations
+ # config.action_view.raise_on_missing_translations = true
+
+ # Use an evented file watcher to asynchronously detect changes in source code,
+ # routes, locales, etc. This feature depends on the listen gem.
+ config.file_watcher = ActiveSupport::EventedFileUpdateChecker
+
+ Paperclip.options[:command_path] = '/usr/local/bin/'
+
+ config.read_encrypted_secrets = true
+
+ # ----- Custom Configurations -----
+end
diff --git a/config/environments/production.rb b/config/environments/production.rb
new file mode 100644
index 0000000..80ca4c2
--- /dev/null
+++ b/config/environments/production.rb
@@ -0,0 +1,98 @@
+VisualAutomation::Application.configure do
+ # Settings specified here will take precedence over those in config/application.rb.
+
+ # Code is not reloaded between requests.
+ config.cache_classes = true
+
+ # Eager load code on boot. This eager loads most of Rails and
+ # your application in memory, allowing both threaded web servers
+ # and those relying on copy on write to perform better.
+ # Rake tasks automatically ignore this option for performance.
+ config.eager_load = true
+
+ # Full error reports are disabled and caching is turned on.
+ config.consider_all_requests_local = false
+ config.action_controller.perform_caching = true
+
+ # Disable serving static files from the `/public` folder by default since
+ # Apache or NGINX already handles this.
+ config.public_file_server.enabled = true
+
+ # Compress JavaScripts and CSS.
+ config.assets.js_compressor = :uglifier
+ # config.assets.css_compressor = :sass
+
+ # Do not fallback to assets pipeline if a precompiled asset is missed.
+ config.assets.compile = false
+
+ # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb
+
+ # Enable serving of images, stylesheets, and JavaScripts from an asset server.
+ # config.action_controller.asset_host = 'http://assets.example.com'
+
+ # Specifies the header that your server uses for sending files.
+ # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
+ # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
+
+ # Mount Action Cable outside main process or domain
+ # config.action_cable.mount_path = nil
+ # config.action_cable.url = 'wss://example.com/cable'
+ # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ]
+
+ # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
+ # config.force_ssl = true
+
+ # Use the lowest log level to ensure availability of diagnostic information
+ # when problems arise.
+ config.log_level = :debug
+
+ # Log rotating: The second arg is a number: the count of logs to retain at rotation -- in this case 5 files.
+ # The third arg is the number of bytes to put in the logs before rotating -- in this case 1000.megabytes, which means I will have at most 5GB of log files
+ # http://stackoverflow.com/questions/4883891/ruby-on-rails-production-log-rotation
+ num_logs_to_retain = 5
+ config.logger = Logger.new(config.paths['log'].first, num_logs_to_retain, 1000.megabytes)
+
+ # Prepend all log lines with the following tags.
+ config.log_tags = [ :request_id ]
+
+ # Use a different cache store in production.
+ # config.cache_store = :mem_cache_store
+
+ # Use a real queuing backend for Active Job (and separate queues per environment)
+ # config.active_job.queue_adapter = :resque
+ # config.active_job.queue_name_prefix = "visual_automation_#{Rails.env}"
+ config.action_mailer.perform_caching = false
+
+ # Ignore bad email addresses and do not raise email delivery errors.
+ # Set this to true and configure the email server for immediate delivery to raise delivery errors.
+ # config.action_mailer.raise_delivery_errors = false
+
+ # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
+ # the I18n.default_locale when a translation cannot be found).
+ config.i18n.fallbacks = true
+
+ # Send deprecation notices to registered listeners.
+ config.active_support.deprecation = :notify
+
+ # Use default logging formatter so that PID and timestamp are not suppressed.
+ config.log_formatter = ::Logger::Formatter.new
+
+ # Use a different logger for distributed setups.
+ # require 'syslog/logger'
+ # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name')
+
+ if ENV["RAILS_LOG_TO_STDOUT"].present?
+ logger = ActiveSupport::Logger.new(STDOUT)
+ logger.formatter = config.log_formatter
+ config.logger = ActiveSupport::TaggedLogging.new(logger)
+ end
+
+ # Do not dump schema after migrations.
+ config.active_record.dump_schema_after_migration = false
+
+ Paperclip.options[:command_path] = '/usr/local/bin/'
+
+ config.read_encrypted_secrets = true
+
+ # ----- Custom Configurations -----
+end
diff --git a/config/environments/test.rb b/config/environments/test.rb
new file mode 100644
index 0000000..6d309d1
--- /dev/null
+++ b/config/environments/test.rb
@@ -0,0 +1,46 @@
+VisualAutomation::Application.configure do
+ # Settings specified here will take precedence over those in config/application.rb.
+
+ # The test environment is used exclusively to run your application's
+ # test suite. You never need to work with it otherwise. Remember that
+ # your test database is "scratch space" for the test suite and is wiped
+ # and recreated between test runs. Don't rely on the data there!
+ config.cache_classes = true
+
+ # Do not eager load code on boot. This avoids loading your whole application
+ # just for the purpose of running a single test. If you are using a tool that
+ # preloads Rails for running tests, you may have to set it to true.
+ config.eager_load = false
+
+ # Configure public file server for tests with Cache-Control for performance.
+ config.public_file_server.enabled = true
+ config.public_file_server.headers = {
+ 'Cache-Control' => 'public, max-age=3600'
+ }
+
+ # Show full error reports and disable caching.
+ config.consider_all_requests_local = true
+ config.action_controller.perform_caching = false
+
+ # Raise exceptions instead of rendering exception templates.
+ config.action_dispatch.show_exceptions = false
+
+ # Disable request forgery protection in test environment.
+ config.action_controller.allow_forgery_protection = false
+ config.action_mailer.perform_caching = false
+
+ # Tell Action Mailer not to deliver emails to the real world.
+ # The :test delivery method accumulates sent emails in the
+ # ActionMailer::Base.deliveries array.
+ config.action_mailer.delivery_method = :test
+
+ # Print deprecation notices to the stderr.
+ config.active_support.deprecation = :stderr
+
+ # Raises error for missing translations
+ # config.action_view.raise_on_missing_translations = true
+
+ config.read_encrypted_secrets = true
+
+ # ----- Custom Configurations -----
+end
diff --git a/config/favicon.json b/config/favicon.json
new file mode 100644
index 0000000..564e3c5
--- /dev/null
+++ b/config/favicon.json
@@ -0,0 +1,53 @@
+{
+ "master_picture": "app/assets/images/vizzy-logo.svg",
+ "favicon_design": {
+ "ios": {
+ "picture_aspect": "no_change",
+ "assets": {
+ "ios6_and_prior_icons": false,
+ "ios7_and_later_icons": false,
+ "precomposed_icons": false,
+ "declare_only_default_icon": true
+ }
+ },
+ "desktop_browser": [
+
+ ],
+ "windows": {
+ "picture_aspect": "no_change",
+ "background_color": "#da532c",
+ "on_conflict": "override",
+ "assets": {
+ "windows_80_ie_10_tile": false,
+ "windows_10_ie_11_edge_tiles": {
+ "small": false,
+ "medium": true,
+ "big": false,
+ "rectangle": false
+ }
+ }
+ },
+ "android_chrome": {
+ "picture_aspect": "no_change",
+ "theme_color": "#ffffff",
+ "manifest": {
+ "display": "standalone",
+ "orientation": "not_set",
+ "on_conflict": "override",
+ "declared": true
+ },
+ "assets": {
+ "legacy_icon": false,
+ "low_resolution_icons": false
+ }
+ },
+ "safari_pinned_tab": {
+ "picture_aspect": "silhouette",
+ "theme_color": "#5bbad5"
+ }
+ },
+ "settings": {
+ "scaling_algorithm": "Mitchell",
+ "error_on_image_too_small": false
+ }
+}
\ No newline at end of file
diff --git a/config/initializers/application_controller_renderer.rb b/config/initializers/application_controller_renderer.rb
new file mode 100644
index 0000000..51639b6
--- /dev/null
+++ b/config/initializers/application_controller_renderer.rb
@@ -0,0 +1,6 @@
+# Be sure to restart your server when you modify this file.
+
+# ApplicationController.renderer.defaults.merge!(
+# http_host: 'example.org',
+# https: false
+# )
diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb
new file mode 100644
index 0000000..bb04222
--- /dev/null
+++ b/config/initializers/assets.rb
@@ -0,0 +1,11 @@
+# Be sure to restart your server when you modify this file.
+
+# Version of your assets, change this if you want to expire all your assets.
+Rails.application.config.assets.version = '1.0'
+
+# Add additional assets to the asset load path
+# Rails.application.config.assets.paths << Emoji.images_path
+
+# Precompile additional assets.
+# application.js, application.css.scss, and all non-JS/CSS in app/assets folder are already added.
+# Rails.application.config.assets.precompile += %w( search.js )
diff --git a/config/initializers/backtrace_silencers.rb b/config/initializers/backtrace_silencers.rb
new file mode 100644
index 0000000..59385cd
--- /dev/null
+++ b/config/initializers/backtrace_silencers.rb
@@ -0,0 +1,7 @@
+# Be sure to restart your server when you modify this file.
+
+# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
+# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
+
+# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
+# Rails.backtrace_cleaner.remove_silencers!
diff --git a/config/initializers/bugsnag.rb b/config/initializers/bugsnag.rb
new file mode 100644
index 0000000..85462bc
--- /dev/null
+++ b/config/initializers/bugsnag.rb
@@ -0,0 +1,4 @@
+Bugsnag.configure do |config|
+ config.api_key = Rails.application.secrets.BUGSNAG_API_KEY
+ config.notify_release_stages = ['production']
+end
diff --git a/config/initializers/commontator.rb b/config/initializers/commontator.rb
new file mode 100644
index 0000000..1f1d423
--- /dev/null
+++ b/config/initializers/commontator.rb
@@ -0,0 +1,284 @@
+# Change the settings below to suit your needs
+# All settings are initially set to their default values
+
+# Note: Do not "return" from a Proc, use "next" instead if necessary
+Commontator.configure do |config|
+ # Engine Configuration
+
+ # current_user_proc
+ # Type: Proc
+ # Arguments: the current controller (ActionController::Base)
+ # Returns: the current user (acts_as_commontator)
+ # The default works for Devise and similar authentication plugins
+ # Default: ->(controller) { controller.current_user }
+ config.current_user_proc = ->(controller) { controller.current_user }
+
+ # javascript_proc
+ # Type: Proc
+ # Arguments: a view (ActionView::Base)
+ # Returns: a String that is appended to Commontator JS views
+ # Can be used, for example, to display/clear Rails error messages
+ # or to reapply JQuery UI styles after Ajax calls
+ # Objects visible in view templates can be accessed
+ # through the view object (for example, view.flash)
+ # However, the view does not include the main application's helpers
+ # Default: ->(view) { '$("#error_explanation").remove();' }
+ config.javascript_proc = ->(view) { '$("#error_explanation").remove();' }
+
+
+
+ # User (acts_as_commontator) Configuration
+
+ # user_name_proc
+ # Type: Proc
+ # Arguments: a user (acts_as_commontator)
+ # Returns: the user's name (String)
+ # Default: ->(user) { I18n.t('commontator.anonymous') } (all users are anonymous)
+ config.user_name_proc = lambda { |user| user.display_name }
+
+ # user_link_proc
+ # Type: Proc
+ # Arguments: a user (acts_as_commontator),
+ # the app_routes (ActionDispatch::Routing::RoutesProxy)
+ # Returns: a path to the user's `show` page (String)
+ # If anything non-blank is returned, the user's name in comments
+ # comments will become a hyperlink pointing to this path
+ # The main application's routes can be accessed through the app_routes object
+ # Default: ->(user, app_routes) { '' } (no link)
+ config.user_link_proc = ->(user, app_routes) { '' }
+
+ # user_avatar_proc
+ # Type: Proc
+ # Arguments: a user (acts_as_commontator), a view (ActionView::Base)
+ # Returns: a String containing a HTML tag pointing to the user's avatar image
+ # The commontator_gravatar_image_tag helper takes a user object,
+ # a border size and an options hash for Gravatar, and produces a Gravatar image tag
+ # See available options at http://en.gravatar.com/site/implement/images/)
+ # Note: Gravatar has several security implications for your users
+ # It makes your users trackable across different sites and
+ # allows de-anonymization attacks against their email addresses
+ # If you absolutely want to keep users' email addresses or identities secret,
+ # do not use Gravatar or similar services
+ # Default: ->(user, view) {
+ # view.commontator_gravatar_image_tag(user, 1, s: 60, d: 'mm')
+ # }
+ config.user_avatar_proc = ->(user, view) {
+ view.commontator_gravatar_image_tag(user, 1, s: 60, d: 'mm')
+ }
+
+ # user_email_proc
+ # Type: Proc
+ # Arguments: a user (acts_as_commontator), a mailer (ActionMailer::Base)
+ # Returns: the user's email address (String)
+ # The default works for Devise's defaults
+ # If the mailer argument is nil, Commontator intends to hash the email and send the hash
+ # to Gravatar, so you should always return the user's email address (if using Gravatar)
+ # If the mailer argument is not nil, then Commontator intends to send an email to
+ # the address returned; you can prevent it from being sent by returning a blank String
+ # Default: ->(user, mailer) { user.try(:email) || '' }
+ config.user_email_proc = ->(user, mailer) { user.try(:email) || '' }
+
+
+
+ # Thread/Commontable (acts_as_commontable) Configuration
+
+ # comment_filter
+ # Type: Arel node (Arel::Nodes::Node) or nil
+ # Arel that filters visible comments
+ # If specified, visible comments will be filtered according to this Arel node
+ # A value of nil will cause no filtering to be done
+ # Moderators can manually override this filter for themselves
+ # Example: Commontator::Comment.arel_table[:deleted_at].eq(nil) (hides deleted comments)
+ # This is not recommended, as it can cause confusion over deleted comments
+ # If using pagination, it can also cause comments to change pages
+ # Default: nil (no filtering - all comments are visible)
+ config.comment_filter = nil
+
+ # thread_read_proc
+ # Type: Proc
+ # Arguments: a thread (Commontator::Thread), a user (acts_as_commontator)
+ # Returns: a Boolean, true if and only if the user should be allowed to read that thread
+ # Note: can be called with a user object that is nil (if they are not logged in)
+ # Default: ->(thread, user) { true } (anyone can read any thread)
+ config.thread_read_proc = ->(thread, user) { true }
+
+ # thread_moderator_proc
+ # Type: Proc
+ # Arguments: a thread (Commontator::Thread), a user (acts_as_commontator)
+ # Returns: a Boolean, true if and only if the user is a moderator for that thread
+ # If you want global moderators, make this proc true for them regardless of thread
+ # Default: ->(thread, user) { false } (no moderators)
+ config.thread_moderator_proc = ->(thread, user) { false }
+
+ # comment_editing
+ # Type: Symbol
+ # Whether users can edit their own comments
+ # Valid options:
+ # :a (always)
+ # :l (only if it's the latest comment)
+ # :n (never)
+ # Default: :l
+ config.comment_editing = :l
+
+ # comment_deletion
+ # Type: Symbol
+ # Whether users can delete their own comments
+ # Valid options:
+ # :a (always)
+ # :l (only if it's the latest comment)
+ # :n (never)
+ # Note: For moderators, see the next option
+ # Default: :l
+ config.comment_deletion = :l
+
+ # moderator_permissions
+ # Type: Symbol
+ # What permissions moderators have
+ # Valid options:
+ # :e (delete and edit comments and close threads)
+ # :d (delete comments and close threads)
+ # :c (close threads only)
+ # Default: :d
+ config.moderator_permissions = :d
+
+ # comment_voting
+ # Type: Symbol
+ # Whether users can vote on other users' comments
+ # Valid options:
+ # :n (no voting)
+ # :l (likes - requires acts_as_votable gem)
+ # :ld (likes/dislikes - requires acts_as_votable gem)
+ # Not yet implemented:
+ # :s (star ratings)
+ # :r (reputation system)
+ # Default: :n
+ config.comment_voting = :n
+
+ # vote_count_proc
+ # Type: Proc
+ # Arguments: a thread (Commontator::Thread), pos (Fixnum), neg (Fixnum)
+ # Returns: vote count to be displayed (String)
+ # pos is the number of likes, or the rating, or the reputation
+ # neg is the number of dislikes, if applicable, or 0 otherwise
+ # Default: ->(thread, pos, neg) { "%+d" % (pos - neg) }
+ config.vote_count_proc = ->(thread, pos, neg) { "%+d" % (pos - neg) }
+
+ # comment_order
+ # Type: Symbol
+ # What order to use for comments
+ # Valid options:
+ # :e (earliest comment first)
+ # :l (latest comment first)
+ # :ve (highest voted first; earliest first if tied)
+ # :vl (highest voted first; latest first if tied)
+ # Notes:
+ # :e is usually used in forums (discussions)
+ # :l is usually used in blogs (opinions)
+ # :ve and :vl are usually used where it makes sense to rate comments
+ # based on usefulness (q&a, reviews, guides, etc.)
+ # If :l is selected, the "reply to thread" form will appear before the comments
+ # Otherwise, it will appear after the comments
+ # Default: :e
+ config.comment_order = :e
+
+ # new_comment_style
+ # Type: Symbol
+ # How to display the "new comment" form
+ # Valid options:
+ # :t (always present in the thread's page)
+ # :l (link to the form; opens in the same page using JS)
+ # Not yet implemented:
+ # :n (link to the form; opens in a new window)
+ # Default: :l
+ config.new_comment_style = :l
+
+ # comments_per_page
+ # Type: Fixnum or nil
+ # Number of comments to display in each page
+ # Set to nil to disable pagination
+ # Any other value requires the will_paginate gem
+ # Default: nil (no pagination)
+ config.comments_per_page = nil
+
+ # thread_subscription
+ # Type: Symbol
+ # Whether users can subscribe to threads to receive activity email notifications
+ # Valid options:
+ # :n (no subscriptions)
+ # :a (automatically subscribe when you comment; cannot do it manually)
+ # :m (manual subscriptions only)
+ # :b (both automatic, when commenting, and manual)
+ # Default: :n
+ config.thread_subscription = :n
+
+ # email_from_proc
+ # Type: Proc
+ # Arguments: a thread (Commontator::Thread)
+ # Returns: the address emails are sent "from" (String)
+ # Important: If using subscriptions, change this to at least match your domain name
+ # Default: ->(thread) {
+ # "no-reply@#{Rails.application.class.parent.to_s.downcase}.com"
+ # }
+ config.email_from_proc = ->(thread) {
+ "no-reply@#{Rails.application.class.parent.to_s.downcase}.com"
+ }
+
+ # commontable_name_proc
+ # Type: Proc
+ # Arguments: a thread (Commontator::Thread)
+ # Returns: a name that refers to the commontable object (String)
+ # If you have multiple commontable models, you can also pass this
+ # configuration value as an argument to acts_as_commontable for each one
+ # Default: ->(thread) {
+ # "#{thread.commontable.class.name} ##{thread.commontable.id}" }
+ config.commontable_name_proc = ->(thread) {
+ "#{thread.commontable.class.name} ##{thread.commontable.id}" }
+
+ # comment_url_proc
+ # Type: Proc
+ # Arguments: a comment (Commontator::Comment),
+ # the app_routes (ActionDispatch::Routing::RoutesProxy)
+ # Returns: a String containing the url of the view that displays the given comment
+ # This usually is the commontable's "show" page
+ # The main application's routes can be accessed through the app_routes object
+ # Default: ->(comment, app_routes) {
+ # app_routes.polymorphic_url(comment.thread.commontable, anchor: "comment_#{comment.id}_div")
+ # }
+ # (defaults to the commontable's show url with an anchor pointing to the comment's div)
+ config.comment_url_proc = ->(comment, app_routes) {
+ app_routes.polymorphic_url(comment.thread.commontable, anchor: "comment_#{comment.id}_div")
+ }
+
+ # mentions_enabled
+ # Type: Boolean
+ # Whether users can mention other users to subscribe them to the thread
+ # Valid options:
+ # false (no mentions)
+ # true (mentions enabled)
+ # Default: false
+ config.mentions_enabled = false
+
+ # user_mentions_proc
+ # Type: Proc
+ # Arguments:
+ # the current user (acts_as_commontator)
+ # the current thread (Commontator::Thread)
+ # the search query inputted by user (String)
+ # Returns: an ActiveRecord Relation object
+ # Important notes:
+ #
+ # - The proc will be called internally with an empty search string.
+ # In that case, it MUST return all users that can be mentioned.
+ #
+ # - With mentions enabled, any registered user in your app is able
+ # to call this proc with any search query >= 3 characters.
+ # Make sure to handle SQL escaping properly and that the
+ # attribute being searched does not contain sensitive information.
+ #
+ # Default: ->(current_user, query) {
+ # current_user.class.where('username LIKE ?', "#{query}%")
+ # }
+ config.user_mentions_proc = ->(current_user, thread, query) {
+ current_user.class.where('username LIKE ?', "#{query}%")
+ }
+end
diff --git a/config/initializers/cookies_serializer.rb b/config/initializers/cookies_serializer.rb
new file mode 100644
index 0000000..17b7e4e
--- /dev/null
+++ b/config/initializers/cookies_serializer.rb
@@ -0,0 +1,5 @@
+# Be sure to restart your server when you modify this file.
+
+# Specify a serializer for the signed and encrypted cookie jars.
+# Valid options are :json, :marshal, and :hybrid.
+Rails.application.config.action_dispatch.cookies_serializer = :json
\ No newline at end of file
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb
new file mode 100644
index 0000000..1587dd5
--- /dev/null
+++ b/config/initializers/devise.rb
@@ -0,0 +1,275 @@
+# Use this hook to configure devise mailer, warden hooks and so forth.
+# Many of these configuration options can be set straight in your model.
+Devise.setup do |config|
+ if VizzyConfig.instance.is_ldap_auth
+ config.warden do |manager|
+ manager.default_strategies(:scope => :user).unshift :ldap_authenticatable
+ end
+ end
+ # ==> Mailer Configuration
+ # Configure the e-mail address which will be shown in Devise::Mailer,
+ # note that it will be overwritten if you use your own mailer class
+ # with default "from" parameter.
+ config.mailer_sender = 'vizzy@gmail.com'
+
+ # Configure the class responsible to send e-mails.
+ # config.mailer = 'Devise::Mailer'
+
+ # Configure the parent class responsible to send e-mails.
+ # config.parent_mailer = 'ActionMailer::Base'
+
+ # ==> ORM configuration
+ # Load and configure the ORM. Supports :active_record (default) and
+ # :mongoid (bson_ext recommended) by default. Other ORMs may be
+ # available as additional gems.
+ require 'devise/orm/active_record'
+
+ # ==> Configuration for any authentication mechanism
+ # Configure which keys are used when authenticating a user. The default is
+ # just :email. You can configure it to use [:username, :subdomain], so for
+ # authenticating a user, both parameters are required. Remember that those
+ # parameters are used only when authenticating and not when retrieving from
+ # session. If you need permissions, you should implement that in a before filter.
+ # You can also supply a hash where the value is a boolean determining whether
+ # or not authentication should be aborted when the value is not present.
+ config.authentication_keys = [:email]
+
+ # Configure parameters from the request object used for authentication. Each entry
+ # given should be a request method and it will automatically be passed to the
+ # find_for_authentication method and considered in your model lookup. For instance,
+ # if you set :request_keys to [:subdomain], :subdomain will be used on authentication.
+ # The same considerations mentioned for authentication_keys also apply to request_keys.
+ # config.request_keys = []
+
+ # Configure which authentication keys should be case-insensitive.
+ # These keys will be downcased upon creating or modifying a user and when used
+ # to authenticate or find a user. Default is :email.
+ config.case_insensitive_keys = [:email]
+
+ # Configure which authentication keys should have whitespace stripped.
+ # These keys will have whitespace before and after removed upon creating or
+ # modifying a user and when used to authenticate or find a user. Default is :email.
+ config.strip_whitespace_keys = [:email]
+
+ # Tell if authentication through request.params is enabled. True by default.
+ # It can be set to an array that will enable params authentication only for the
+ # given strategies, for example, `config.params_authenticatable = [:database]` will
+ # enable it only for database (email + password) authentication.
+ # config.params_authenticatable = true
+
+ # Tell if authentication through HTTP Auth is enabled. False by default.
+ # It can be set to an array that will enable http authentication only for the
+ # given strategies, for example, `config.http_authenticatable = [:database]` will
+ # enable it only for database authentication. The supported strategies are:
+ # :database = Support basic authentication with authentication key + password
+ # config.http_authenticatable = false
+
+ # If 401 status code should be returned for AJAX requests. True by default.
+ # config.http_authenticatable_on_xhr = true
+
+ # The realm used in Http Basic Authentication. 'Application' by default.
+ # config.http_authentication_realm = 'Application'
+
+ # It will change confirmation, password recovery and other workflows
+ # to behave the same regardless if the e-mail provided was right or wrong.
+ # Does not affect registerable.
+ # config.paranoid = true
+
+ # By default Devise will store the user in session. You can skip storage for
+ # particular strategies by setting this option.
+ # Notice that if you are skipping storage for all authentication paths, you
+ # may want to disable generating routes to Devise's sessions controller by
+ # passing skip: :sessions to `devise_for` in your config/routes.rb
+ config.skip_session_storage = [:http_auth]
+
+ # By default, Devise cleans up the CSRF token on authentication to
+ # avoid CSRF token fixation attacks. This means that, when using AJAX
+ # requests for sign in and sign up, you need to get a new CSRF token
+ # from the server. You can disable this option at your own risk.
+ # config.clean_up_csrf_token_on_authentication = true
+
+ # When false, Devise will not attempt to reload routes on eager load.
+ # This can reduce the time taken to boot the app but if your application
+ # requires the Devise mappings to be loaded during boot time the application
+ # won't boot properly.
+ # config.reload_routes = true
+
+ # ==> Configuration for :database_authenticatable
+ # For bcrypt, this is the cost for hashing the password and defaults to 11. If
+ # using other algorithms, it sets how many times you want the password to be hashed.
+ #
+ # Limiting the stretches to just one in testing will increase the performance of
+ # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use
+ # a value less than 10 in other environments. Note that, for bcrypt (the default
+ # algorithm), the cost increases exponentially with the number of stretches (e.g.
+ # a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation).
+ config.stretches = Rails.env.test? ? 1 : 11
+
+ # Set up a pepper to generate the hashed password.
+ # config.pepper = '49582d592eff81dca5f887c3a52e64047500a58004c61dbb5f107a5d1eff9683c771b1a94f22c40c21305da719ead84ce4ac79c67641ef02235f162247554ffa'
+
+ # Send a notification to the original email when the user's email is changed.
+ # config.send_email_changed_notification = false
+
+ # Send a notification email when the user's password is changed.
+ # config.send_password_change_notification = false
+
+ # ==> Configuration for :confirmable
+ # A period that the user is allowed to access the website even without
+ # confirming their account. For instance, if set to 2.days, the user will be
+ # able to access the website for two days without confirming their account,
+ # access will be blocked just in the third day. Default is 0.days, meaning
+ # the user cannot access the website without confirming their account.
+ # config.allow_unconfirmed_access_for = 2.days
+
+ # A period that the user is allowed to confirm their account before their
+ # token becomes invalid. For example, if set to 3.days, the user can confirm
+ # their account within 3 days after the mail was sent, but on the fourth day
+ # their account can't be confirmed with the token any more.
+ # Default is nil, meaning there is no restriction on how long a user can take
+ # before confirming their account.
+ # config.confirm_within = 3.days
+
+ # If true, requires any email changes to be confirmed (exactly the same way as
+ # initial account confirmation) to be applied. Requires additional unconfirmed_email
+ # db field (see migrations). Until confirmed, new email is stored in
+ # unconfirmed_email column, and copied to email column on successful confirmation.
+ config.reconfirmable = true
+
+ # Defines which key will be used when confirming an account
+ # config.confirmation_keys = [:email]
+
+ # ==> Configuration for :rememberable
+ # The time the user will be remembered without asking for credentials again.
+ # config.remember_for = 2.weeks
+
+ # Invalidates all the remember me tokens when the user signs out.
+ config.expire_all_remember_me_on_sign_out = true
+
+ # If true, extends the user's remember period when remembered via cookie.
+ # config.extend_remember_period = false
+
+ # Options to be passed to the created cookie. For instance, you can set
+ # secure: true in order to force SSL only cookies.
+ # config.rememberable_options = {}
+
+ # ==> Configuration for :validatable
+ # Range for password length.
+ config.password_length = 12..72
+
+ # Email regex used to validate email formats. It simply asserts that
+ # one (and only one) @ exists in the given string. This is mainly
+ # to give user feedback and not to assert the e-mail validity.
+ config.email_regexp = /\A[^@\s]+@[^@\s]+\z/
+
+ # ==> Configuration for :timeoutable
+ # The time you want to timeout the user session without activity. After this
+ # time the user will be asked for credentials again. Default is 30 minutes.
+ # config.timeout_in = 30.minutes
+
+ # ==> Configuration for :lockable
+ # Defines which strategy will be used to lock an account.
+ # :failed_attempts = Locks an account after a number of failed attempts to sign in.
+ # :none = No lock strategy. You should handle locking by yourself.
+ # config.lock_strategy = :failed_attempts
+
+ # Defines which key will be used when locking and unlocking an account
+ # config.unlock_keys = [:email]
+
+ # Defines which strategy will be used to unlock an account.
+ # :email = Sends an unlock link to the user email
+ # :time = Re-enables login after a certain amount of time (see :unlock_in below)
+ # :both = Enables both strategies
+ # :none = No unlock strategy. You should handle unlocking by yourself.
+ # config.unlock_strategy = :both
+
+ # Number of authentication tries before locking an account if lock_strategy
+ # is failed attempts.
+ # config.maximum_attempts = 20
+
+ # Time interval to unlock the account if :time is enabled as unlock_strategy.
+ # config.unlock_in = 1.hour
+
+ # Warn on the last attempt before the account is locked.
+ # config.last_attempt_warning = true
+
+ # ==> Configuration for :recoverable
+ #
+ # Defines which key will be used when recovering the password for an account
+ # config.reset_password_keys = [:email]
+
+ # Time interval you can reset your password with a reset password key.
+ # Don't put a too small interval or your users won't have the time to
+ # change their passwords.
+ config.reset_password_within = 6.hours
+
+ # When set to false, does not sign a user in automatically after their password is
+ # reset. Defaults to true, so a user is signed in automatically after a reset.
+ # config.sign_in_after_reset_password = true
+
+ # ==> Configuration for :encryptable
+ # Allow you to use another hashing or encryption algorithm besides bcrypt (default).
+ # You can use :sha1, :sha512 or algorithms from others authentication tools as
+ # :clearance_sha1, :authlogic_sha512 (then you should set stretches above to 20
+ # for default behavior) and :restful_authentication_sha1 (then you should set
+ # stretches to 10, and copy REST_AUTH_SITE_KEY to pepper).
+ #
+ # Require the `devise-encryptable` gem when using anything other than bcrypt
+ # config.encryptor = :sha512
+
+ # ==> Scopes configuration
+ # Turn scoped views on. Before rendering "sessions/new", it will first check for
+ # "users/sessions/new". It's turned off by default because it's slower if you
+ # are using only default views.
+ # config.scoped_views = false
+
+ # Configure the default scope given to Warden. By default it's the first
+ # devise role declared in your routes (usually :user).
+ # config.default_scope = :user
+
+ # Set this configuration to false if you want /users/sign_out to sign out
+ # only the current scope. By default, Devise signs out all scopes.
+ # config.sign_out_all_scopes = true
+
+ # ==> Navigation configuration
+ # Lists the formats that should be treated as navigational. Formats like
+ # :html, should redirect to the sign in page when the user does not have
+ # access, but formats like :xml or :json, should return 401.
+ #
+ # If you have any extra navigational formats, like :iphone or :mobile, you
+ # should add them to the navigational formats lists.
+ #
+ # The "*/*" below is required to match Internet Explorer requests.
+ # config.navigational_formats = ['*/*', :html]
+
+ # The default HTTP method used to sign out a resource. Default is :delete.
+ config.sign_out_via = :delete
+
+ # ==> OmniAuth
+ # Add a new OmniAuth provider. Check the wiki for more information on setting
+ # up on your models and hooks.
+ # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo'
+
+ # ==> Warden configuration
+ # If you want to use other strategies, that are not supported by Devise, or
+ # change the failure app, you can configure them inside the config.warden block.
+ #
+ # config.warden do |manager|
+ # manager.intercept_401 = false
+ # manager.default_strategies(scope: :user).unshift :some_external_strategy
+ # end
+
+ # ==> Mountable engine configurations
+ # When using Devise inside an engine, let's call it `MyEngine`, and this engine
+ # is mountable, there are some extra configurations to be taken into account.
+ # The following options are available, assuming the engine is mounted as:
+ #
+ # mount MyEngine, at: '/my_engine'
+ #
+ # The router that invoked `devise_for`, in the example above, would be:
+ # config.router_name = :my_engine
+ #
+ # When using OmniAuth, Devise cannot automatically set OmniAuth path,
+ # so you need to do it manually. For the users scope, it would be:
+ # config.omniauth_path_prefix = '/my_engine/users/auth'
+end
diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb
new file mode 100644
index 0000000..1d2be06
--- /dev/null
+++ b/config/initializers/filter_parameter_logging.rb
@@ -0,0 +1,4 @@
+# Be sure to restart your server when you modify this file.
+
+# Configure sensitive parameters which will be filtered from the log file.
+Rails.application.config.filter_parameters += [:password, :authentication_token]
diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb
new file mode 100644
index 0000000..ac033bf
--- /dev/null
+++ b/config/initializers/inflections.rb
@@ -0,0 +1,16 @@
+# Be sure to restart your server when you modify this file.
+
+# Add new inflection rules using the following format. Inflections
+# are locale specific, and you may define rules for as many different
+# locales as you wish. All of these examples are active by default:
+# ActiveSupport::Inflector.inflections(:en) do |inflect|
+# inflect.plural /^(ox)$/i, '\1en'
+# inflect.singular /^(ox)en/i, '\1'
+# inflect.irregular 'person', 'people'
+# inflect.uncountable %w( fish sheep )
+# end
+
+# These inflection rules are supported but not enabled by default:
+# ActiveSupport::Inflector.inflections(:en) do |inflect|
+# inflect.acronym 'RESTful'
+# end
diff --git a/config/initializers/ldap_authenticatable.rb b/config/initializers/ldap_authenticatable.rb
new file mode 100644
index 0000000..b3dc8a7
--- /dev/null
+++ b/config/initializers/ldap_authenticatable.rb
@@ -0,0 +1,60 @@
+require 'net/ldap'
+require 'devise/strategies/authenticatable'
+
+module Devise
+ module Strategies
+ class LdapAuthenticatable < Authenticatable
+ def authenticate!
+ if params[:user]
+ user = nil
+ auth_config = VizzyConfig.instance.get_config_value(['devise'])
+ if Rails.env.development? || Rails.env.test? || Rails.env.ci_test?
+ # No need to connect to ldap for development environment, just let them sign in
+ user = User.find_or_create_by(email: email)
+ elsif !password.blank?
+ # Use LDAP for production environment
+ ldap = Net::LDAP.new
+ ldap.host = auth_config['ldap_host']
+ ldap.port = auth_config['ldap_port']
+ ldap.base = auth_config['ldap_base']
+ internal_domain = auth_config['ldap_email_internal_domain']
+ ldap.auth username_from_email + internal_domain, password
+
+ if ldap.host.blank? || ldap.port.blank? || ldap.base.blank? || internal_domain.blank?
+ raise "ldap auth configuration missing -- host: #{ldap.host}, port: #{ldap.port}, base: #{ldap.base}, internal_domain: #{internal_domain}"
+ end
+
+ if ldap.bind
+ user = User.find_or_create_by(email: email)
+ else
+ fail(:invalid_login)
+ return
+ end
+ end
+ if user.nil?
+ fail(:invalid_login)
+ else
+ if user.new_record?
+ user.save!
+ end
+ success!(user)
+ end
+ end
+ end
+
+ def username_from_email
+ email.split('@').first
+ end
+
+ def email
+ params[:user][:email]
+ end
+
+ def password
+ params[:user][:password]
+ end
+ end
+ end
+end
+
+Warden::Strategies.add(:ldap_authenticatable, Devise::Strategies::LdapAuthenticatable)
\ No newline at end of file
diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb
new file mode 100644
index 0000000..dc18996
--- /dev/null
+++ b/config/initializers/mime_types.rb
@@ -0,0 +1,4 @@
+# Be sure to restart your server when you modify this file.
+
+# Add new mime types for use in respond_to blocks:
+# Mime::Type.register "text/richtext", :rtf
diff --git a/config/initializers/new_framework_defaults.rb b/config/initializers/new_framework_defaults.rb
new file mode 100644
index 0000000..a54e903
--- /dev/null
+++ b/config/initializers/new_framework_defaults.rb
@@ -0,0 +1,18 @@
+# Be sure to restart your server when you modify this file.
+#
+# This file contains migration options to ease your Rails 5.0 upgrade.
+#
+# Read the Rails 5.0 release notes for more info on each option.
+
+# Enable per-form CSRF tokens. Previous versions had false.
+Rails.application.config.action_controller.per_form_csrf_tokens = true
+
+# Enable origin-checking CSRF mitigation. Previous versions had false.
+Rails.application.config.action_controller.forgery_protection_origin_check = true
+
+# Make Ruby 2.4 preserve the timezone of the receiver when calling `to_time`.
+# Previous versions had false.
+ActiveSupport.to_time_preserves_timezone = true
+
+# Require `belongs_to` associations by default. Previous versions had false.
+Rails.application.config.active_record.belongs_to_required_by_default = true
\ No newline at end of file
diff --git a/config/initializers/paperclip_media_type_spoof_detector_override.rb b/config/initializers/paperclip_media_type_spoof_detector_override.rb
new file mode 100644
index 0000000..da88aa2
--- /dev/null
+++ b/config/initializers/paperclip_media_type_spoof_detector_override.rb
@@ -0,0 +1,8 @@
+require 'paperclip/media_type_spoof_detector'
+module Paperclip
+ class MediaTypeSpoofDetector
+ def spoofed?
+ false
+ end
+ end
+end
\ No newline at end of file
diff --git a/config/initializers/plugin.rb b/config/initializers/plugin.rb
new file mode 100644
index 0000000..30365e4
--- /dev/null
+++ b/config/initializers/plugin.rb
@@ -0,0 +1,14 @@
+require 'find'
+
+def all_plugins
+ plugin_folder = Rails.root.join('app', 'plugins')
+ Find.find(plugin_folder).select { |path| path =~ /.*\.rb/ }
+end
+
+def load_all_plugins
+ all_plugins.each do |plugin|
+ require plugin
+ end
+end
+
+load_all_plugins
\ No newline at end of file
diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb
new file mode 100644
index 0000000..84258d1
--- /dev/null
+++ b/config/initializers/session_store.rb
@@ -0,0 +1,3 @@
+# Be sure to restart your server when you modify this file.
+
+Rails.application.config.session_store :cookie_store, key: '_visual_automation_session'
diff --git a/config/initializers/simple_token_authentication.rb b/config/initializers/simple_token_authentication.rb
new file mode 100644
index 0000000..4fa5614
--- /dev/null
+++ b/config/initializers/simple_token_authentication.rb
@@ -0,0 +1,62 @@
+SimpleTokenAuthentication.configure do |config|
+
+ # Configure the session persistence policy after a successful sign in,
+ # in other words, if the authentication token acts as a signin token.
+ # If true, user is stored in the session and the authentication token and
+ # email may be provided only once.
+ # If false, users must provide their authentication token and email at every request.
+ # config.sign_in_token = false
+
+ # Configure the name of the HTTP headers watched for authentication.
+ #
+ # Default header names for a given token authenticatable entity follow the pattern:
+ # { entity: { authentication_token: 'X-Entity-Token', email: 'X-Entity-Email'} }
+ #
+ # When several token authenticatable models are defined, custom header names
+ # can be specified for none, any, or all of them.
+ #
+ # Note: when using the identifiers options, this option behaviour is modified.
+ # Please see the example below.
+ #
+ # Examples
+ #
+ # Given User and SuperAdmin are token authenticatable,
+ # When the following configuration is used:
+ # `config.header_names = { super_admin: { authentication_token: 'X-Admin-Auth-Token' } }`
+ # Then the token authentification handler for User watches the following headers:
+ # `X-User-Token, X-User-Email`
+ # And the token authentification handler for SuperAdmin watches the following headers:
+ # `X-Admin-Auth-Token, X-SuperAdmin-Email`
+ #
+ # When the identifiers option is set:
+ # `config.identifiers = { super_admin: :phone_number }`
+ # Then both the header names identifier key and default value are modified accordingly:
+ # `config.header_names = { super_admin: { phone_number: 'X-SuperAdmin-PhoneNumber' } }`
+ #
+ # Configure the name of the attribute used to identify the user for authentication.
+ # That attribute must exist in your model.
+ #
+ # The default identifiers follow the pattern:
+ # { entity: 'email' }
+ #
+ # Note: the identifer must match your Devise configuration,
+ # see https://github.com/plataformatec/devise/wiki/How-To:-Allow-users-to-sign-in-using-their-username-or-email-address#tell-devise-to-use-username-in-the-authentication_keys
+ #
+ # Note: setting this option does modify the header_names behaviour,
+ # see the header_names section above.
+ #
+ # Example:
+ #
+ # `config.identifiers = { super_admin: 'phone_number', user: 'uuid' }`
+ #
+
+ # Configure the Devise trackable strategy integration.
+ #
+ # If true, tracking is disabled for token authentication: signing in through
+ # token authentication won't modify the Devise trackable statistics.
+ #
+ # If false, given Devise trackable is configured for the relevant model,
+ # then signing in through token authentication will be tracked as any other sign in.
+ #
+ # config.skip_devise_trackable = true
+end
\ No newline at end of file
diff --git a/config/initializers/wrap_parameters.rb b/config/initializers/wrap_parameters.rb
new file mode 100644
index 0000000..cf733ef
--- /dev/null
+++ b/config/initializers/wrap_parameters.rb
@@ -0,0 +1,14 @@
+# Be sure to restart your server when you modify this file.
+
+# This file contains settings for ActionController::ParamsWrapper which
+# is enabled by default.
+
+# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
+ActiveSupport.on_load(:action_controller) do
+ wrap_parameters format: [:json]
+end
+
+# To enable root element in JSON for ActiveRecord objects.
+# ActiveSupport.on_load(:active_record) do
+# self.include_root_in_json = true
+# end
diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml
new file mode 100644
index 0000000..0b8f130
--- /dev/null
+++ b/config/locales/devise.en.yml
@@ -0,0 +1,64 @@
+# Additional translations at https://github.com/plataformatec/devise/wiki/I18n
+
+en:
+ devise:
+ confirmations:
+ confirmed: "Your email address has been successfully confirmed."
+ send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes."
+ send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes."
+ failure:
+ already_authenticated: "You are already signed in."
+ inactive: "Your account is not activated yet."
+ invalid: "Invalid %{authentication_keys} or password."
+ locked: "Your account is locked."
+ last_attempt: "You have one more attempt before your account is locked."
+ not_found_in_database: "Invalid %{authentication_keys} or password."
+ timeout: "Your session expired. Please sign in again to continue."
+ unauthenticated: "You need to sign in or sign up before continuing."
+ unconfirmed: "You have to confirm your email address before continuing."
+ mailer:
+ confirmation_instructions:
+ subject: "Confirmation instructions"
+ reset_password_instructions:
+ subject: "Reset password instructions"
+ unlock_instructions:
+ subject: "Unlock instructions"
+ email_changed:
+ subject: "Email Changed"
+ password_change:
+ subject: "Password Changed"
+ omniauth_callbacks:
+ failure: "Could not authenticate you from %{kind} because \"%{reason}\"."
+ success: "Successfully authenticated from %{kind} account."
+ passwords:
+ no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided."
+ send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes."
+ send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes."
+ updated: "Your password has been changed successfully. You are now signed in."
+ updated_not_active: "Your password has been changed successfully."
+ registrations:
+ destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon."
+ signed_up: "Welcome! You have signed up successfully."
+ signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated."
+ signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked."
+ signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account."
+ update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirm link to confirm your new email address."
+ updated: "Your account has been updated successfully."
+ sessions:
+ signed_in: "Signed in successfully."
+ signed_out: "Signed out successfully."
+ already_signed_out: "Signed out successfully."
+ unlocks:
+ send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes."
+ send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes."
+ unlocked: "Your account has been unlocked successfully. Please sign in to continue."
+ errors:
+ messages:
+ already_confirmed: "was already confirmed, please try signing in"
+ confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one"
+ expired: "has expired, please request a new one"
+ not_found: "not found"
+ not_locked: "was not locked"
+ not_saved:
+ one: "1 error prohibited this %{resource} from being saved:"
+ other: "%{count} errors prohibited this %{resource} from being saved:"
diff --git a/config/locales/en.yml b/config/locales/en.yml
new file mode 100644
index 0000000..0653957
--- /dev/null
+++ b/config/locales/en.yml
@@ -0,0 +1,23 @@
+# Files in the config/locales directory are used for internationalization
+# and are automatically loaded by Rails. If you want to use locales other
+# than English, add the necessary files in this directory.
+#
+# To use the locales, use `I18n.t`:
+#
+# I18n.t 'hello'
+#
+# In views, this is aliased to just `t`:
+#
+# <%= t('hello') %>
+#
+# To use a different locale, set it with `I18n.locale`:
+#
+# I18n.locale = :es
+#
+# This would use the information in config/locales/es.yml.
+#
+# To learn more, please read the Rails Internationalization guide
+# available at http://guides.rubyonrails.org/i18n.html.
+
+en:
+ hello: "Hello world"
diff --git a/config/puma.rb b/config/puma.rb
new file mode 100644
index 0000000..c7f311f
--- /dev/null
+++ b/config/puma.rb
@@ -0,0 +1,47 @@
+# Puma can serve each request in a thread from an internal thread pool.
+# The `threads` method setting takes two numbers a minimum and maximum.
+# Any libraries that use thread pools should be configured to match
+# the maximum value specified for Puma. Default is set to 5 threads for minimum
+# and maximum, this matches the default thread size of Active Record.
+#
+threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i
+threads threads_count, threads_count
+
+# Specifies the `port` that Puma will listen on to receive requests, default is 3000.
+#
+port ENV.fetch("PORT") { 3000 }
+
+# Specifies the `environment` that Puma will run in.
+#
+environment ENV.fetch("RAILS_ENV") { "development" }
+
+# Specifies the number of `workers` to boot in clustered mode.
+# Workers are forked webserver processes. If using threads and workers together
+# the concurrency of the application would be max `threads` * `workers`.
+# Workers do not work on JRuby or Windows (both of which do not support
+# processes).
+#
+# workers ENV.fetch("WEB_CONCURRENCY") { 2 }
+
+# Use the `preload_app!` method when specifying a `workers` number.
+# This directive tells Puma to first boot the application and load code
+# before forking the application. This takes advantage of Copy On Write
+# process behavior so workers use less memory. If you use this option
+# you need to make sure to reconnect any threads in the `on_worker_boot`
+# block.
+#
+# preload_app!
+
+# The code in the `on_worker_boot` will be called if you are using
+# clustered mode by specifying a number of `workers`. After each worker
+# process is booted this block will be run, if you are using `preload_app!`
+# option you will want to use this block to reconnect to any threads
+# or connections that may have been created at application boot, Ruby
+# cannot share connections between processes.
+#
+# on_worker_boot do
+# ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
+# end
+
+# Allow puma to be restarted by `rails restart` command.
+plugin :tmp_restart
diff --git a/config/routes.rb b/config/routes.rb
new file mode 100644
index 0000000..599f391
--- /dev/null
+++ b/config/routes.rb
@@ -0,0 +1,63 @@
+Rails.application.routes.draw do
+ # The priority is based upon order of creation: first created -> highest priority.
+
+ devise_for :users
+
+ root 'projects#index'
+
+ resources :projects do
+ member do
+ post 'remove_all_base_images'
+ post 'clean_base_image_state'
+ post 'cleanup_uncommitted_builds'
+ post 'base_images'
+ get 'base_images'
+ get 'base_images_test_images'
+ end
+ end
+
+ resources :builds do
+ member do
+ get 'unapproved_diffs'
+ get 'approved_diffs'
+ get 'new_tests'
+ get 'missing_tests'
+ get 'successful_tests'
+ post 'approve_all_images'
+ post 'add_md5s'
+ post 'commit'
+ get 'commit'
+ post 'fail'
+ end
+ end
+
+ resources :diffs do
+ member do
+ post 'approve'
+ post 'unapprove'
+ post 'next'
+ post 'multiple_diff_approval'
+ post 'create_jira'
+ end
+ end
+
+ resources :jiras
+
+ resources :tests do
+ member do
+ post 'remove_base_images'
+ end
+ end
+
+ resources :test_images
+
+ resources :users do
+ member do
+ post 'show_authentication_token'
+ post 'revoke_authentication_token'
+ end
+ end
+
+ # Commontator stuff
+ mount Commontator::Engine => '/commontator'
+end
diff --git a/config/schedule.rb b/config/schedule.rb
new file mode 100644
index 0000000..92a06d5
--- /dev/null
+++ b/config/schedule.rb
@@ -0,0 +1,35 @@
+# Use this file to easily define all of your cron jobs.
+#
+# It's helpful, but not entirely necessary to understand cron before proceeding.
+# http://en.wikipedia.org/wiki/Cron
+
+# Example:
+#
+# set :output, "/path/to/my/cron_log.log"
+#
+# every 2.hours do
+# command "/usr/bin/some_great_command"
+# runner "MyModel.some_method"
+# rake "some:great:rake:task"
+# end
+#
+# every 4.days do
+# runner "AnotherModel.prune_old_records"
+# end
+
+# Learn more: http://github.com/javan/whenever
+
+# Destroy all dev builds every day
+every 1.day do
+ rake 'dev_builds:destroy', environment: 'production'
+end
+
+# Destroy leaf pr builds every day
+every 1.day do
+ rake 'orphaned_pr_builds:destroy', environment: 'production'
+end
+
+# Destroy images that are not attached to a record every month
+every 1.month do
+ rake 'paperclip:clean_orphan_files', environment: 'production'
+end
diff --git a/config/spring.rb b/config/spring.rb
new file mode 100644
index 0000000..c9119b4
--- /dev/null
+++ b/config/spring.rb
@@ -0,0 +1,6 @@
+%w(
+ .ruby-version
+ .rbenv-vars
+ tmp/restart.txt
+ tmp/caching-dev.txt
+).each { |path| Spring.watch(path) }
diff --git a/config/vizzy.yaml b/config/vizzy.yaml
new file mode 100644
index 0000000..2ec823f
--- /dev/null
+++ b/config/vizzy.yaml
@@ -0,0 +1,39 @@
+defaults: &default
+ devise:
+ auth_strategy: 'local'
+ jira:
+ create_issues: yes
+ slack:
+ send_messages: yes
+ bamboo:
+ comment_on_build: yes
+ jenkins:
+ update_description: yes
+
+development:
+ <<: *default
+
+test:
+ <<: *default
+ jira:
+ create_issues: no
+ slack:
+ send_messages: no
+ bamboo:
+ comment_on_build: no
+ jenkins:
+ update_description: no
+
+ci_test:
+ <<: *default
+ jira:
+ create_issues: no
+ slack:
+ send_messages: no
+ bamboo:
+ comment_on_build: no
+ jenkins:
+ update_description: no
+
+production:
+ <<: *default
\ No newline at end of file
diff --git a/db/migrate/20180326171851_init_schema.rb b/db/migrate/20180326171851_init_schema.rb
new file mode 100644
index 0000000..93627e5
--- /dev/null
+++ b/db/migrate/20180326171851_init_schema.rb
@@ -0,0 +1,177 @@
+class InitSchema < ActiveRecord::Migration[5.1]
+ def up
+ # These are extensions that must be enabled in order to support this database
+ enable_extension "plpgsql"
+ create_table "builds", id: :serial, force: :cascade do |t|
+ t.string "url"
+ t.boolean "temporary", default: false
+ t.string "title"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.integer "project_id"
+ t.string "commit_sha"
+ t.string "pull_request_number"
+ t.integer "num_of_images_in_build", default: 0, null: false
+ t.text "image_md5s"
+ t.string "username", default: "Pull Request"
+ t.string "branch_name", default: "Branch"
+ t.text "associated_commit_shas"
+ t.text "full_list_of_image_md5s"
+ t.string "failure_message"
+ t.boolean "dev_build", default: false
+ t.index ["project_id"], name: "index_builds_on_project_id"
+ end
+ create_table "builds_base_images", id: :serial, force: :cascade do |t|
+ t.integer "build_id"
+ t.integer "test_image_id"
+ t.index ["build_id"], name: "index_builds_base_images_on_build_id"
+ t.index ["test_image_id"], name: "index_builds_base_images_on_test_image_id"
+ end
+ create_table "builds_successful_tests", id: :serial, force: :cascade do |t|
+ t.integer "build_id"
+ t.integer "test_image_id"
+ t.index ["build_id"], name: "index_builds_successful_tests_on_build_id"
+ t.index ["test_image_id"], name: "index_builds_successful_tests_on_test_image_id"
+ end
+ create_table "commontator_comments", id: :serial, force: :cascade do |t|
+ t.string "creator_type"
+ t.integer "creator_id"
+ t.string "editor_type"
+ t.integer "editor_id"
+ t.integer "thread_id", null: false
+ t.text "body", null: false
+ t.datetime "deleted_at"
+ t.integer "cached_votes_up", default: 0
+ t.integer "cached_votes_down", default: 0
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.index ["cached_votes_down"], name: "index_commontator_comments_on_cached_votes_down"
+ t.index ["cached_votes_up"], name: "index_commontator_comments_on_cached_votes_up"
+ t.index ["creator_id", "creator_type", "thread_id"], name: "index_commontator_comments_on_c_id_and_c_type_and_t_id"
+ t.index ["thread_id", "created_at"], name: "index_commontator_comments_on_thread_id_and_created_at"
+ end
+ create_table "commontator_subscriptions", id: :serial, force: :cascade do |t|
+ t.string "subscriber_type", null: false
+ t.integer "subscriber_id", null: false
+ t.integer "thread_id", null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.index ["subscriber_id", "subscriber_type", "thread_id"], name: "index_commontator_subscriptions_on_s_id_and_s_type_and_t_id", unique: true
+ t.index ["thread_id"], name: "index_commontator_subscriptions_on_thread_id"
+ end
+ create_table "commontator_threads", id: :serial, force: :cascade do |t|
+ t.string "commontable_type"
+ t.integer "commontable_id"
+ t.datetime "closed_at"
+ t.string "closer_type"
+ t.integer "closer_id"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.index ["commontable_id", "commontable_type"], name: "index_commontator_threads_on_c_id_and_c_type", unique: true
+ end
+ create_table "diffs", id: :serial, force: :cascade do |t|
+ t.integer "old_image_id"
+ t.integer "new_image_id"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.string "differences_file_name"
+ t.string "differences_content_type"
+ t.integer "differences_file_size"
+ t.datetime "differences_updated_at"
+ t.integer "build_id"
+ t.integer "approved_by_id"
+ t.boolean "approved", default: false
+ t.index ["approved_by_id"], name: "index_diffs_on_approved_by_id"
+ t.index ["build_id"], name: "index_diffs_on_build_id"
+ t.index ["new_image_id"], name: "index_diffs_on_new_image_id"
+ t.index ["old_image_id"], name: "index_diffs_on_old_image_id"
+ end
+ create_table "jiras", id: :serial, force: :cascade do |t|
+ t.string "title"
+ t.string "project"
+ t.string "component"
+ t.string "description"
+ t.string "issue_type"
+ t.string "jira_link"
+ t.string "jira_key"
+ t.integer "diff_id"
+ t.string "assignee"
+ t.string "priority"
+ t.string "jira_base_url"
+ t.index ["diff_id"], name: "index_jiras_on_diff_id"
+ end
+ create_table "projects", id: :serial, force: :cascade do |t|
+ t.string "name"
+ t.string "description"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.string "github_root_url"
+ t.string "github_repo"
+ t.string "github_status_context", default: "continuous-integration/vizzy"
+ t.jsonb "plugin_settings", default: {}, null: false
+ t.string "vizzy_server_url"
+ t.index ["plugin_settings"], name: "index_projects_on_plugin_settings", using: :gin
+ end
+ create_table "test_images", id: :serial, force: :cascade do |t|
+ t.string "image_file_name"
+ t.string "image_content_type"
+ t.integer "image_file_size"
+ t.datetime "image_updated_at"
+ t.string "branch"
+ t.boolean "approved", default: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.integer "build_id"
+ t.integer "test_id"
+ t.boolean "user_approved_this_build", default: false
+ t.boolean "image_created_this_build", default: false
+ t.string "image_pull_request_sha"
+ t.string "image_pull_request_number"
+ t.string "md5"
+ t.string "test_key"
+ t.index ["build_id"], name: "index_test_images_on_build_id"
+ t.index ["test_id"], name: "index_test_images_on_test_id"
+ end
+ create_table "tests", id: :serial, force: :cascade do |t|
+ t.string "name"
+ t.string "description"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.string "comment"
+ t.string "jira"
+ t.string "pull_request_link"
+ t.string "comment_user"
+ t.string "ancestry"
+ t.string "ancestry_key"
+ t.integer "project_id"
+ t.index ["ancestry"], name: "index_tests_on_ancestry"
+ t.index ["project_id"], name: "index_tests_on_project_id"
+ end
+ create_table "users", id: :serial, force: :cascade do |t|
+ t.string "email", default: "", null: false
+ t.string "encrypted_password", default: "", null: false
+ t.string "reset_password_token"
+ t.datetime "reset_password_sent_at"
+ t.datetime "remember_created_at"
+ t.integer "sign_in_count", default: 0, null: false
+ t.datetime "current_sign_in_at"
+ t.datetime "last_sign_in_at"
+ t.string "current_sign_in_ip"
+ t.string "last_sign_in_ip"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.boolean "admin", default: false
+ t.string "username"
+ t.string "authentication_token", limit: 30
+ t.index ["authentication_token"], name: "index_users_on_authentication_token", unique: true
+ t.index ["email"], name: "index_users_on_email", unique: true
+ t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
+ t.index ["username"], name: "index_users_on_username", unique: true
+ end
+ add_foreign_key "tests", "projects", on_delete: :cascade
+ end
+
+ def down
+ raise ActiveRecord::IrreversibleMigration, "The initial migration is not revertable"
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
new file mode 100644
index 0000000..8bd408c
--- /dev/null
+++ b/db/schema.rb
@@ -0,0 +1,197 @@
+# This file is auto-generated from the current state of the database. Instead
+# of editing this file, please use the migrations feature of Active Record to
+# incrementally modify your database, and then regenerate this schema definition.
+#
+# Note that this schema.rb definition is the authoritative source for your
+# database schema. If you need to create the application database on another
+# system, you should be using db:schema:load, not running all the migrations
+# from scratch. The latter is a flawed and unsustainable approach (the more migrations
+# you'll amass, the slower it'll run and the greater likelihood for issues).
+#
+# It's strongly recommended that you check this file into your version control system.
+
+ActiveRecord::Schema.define(version: 20180326171851) do
+
+ # These are extensions that must be enabled in order to support this database
+ enable_extension "plpgsql"
+
+ create_table "builds", id: :serial, force: :cascade do |t|
+ t.string "url"
+ t.boolean "temporary", default: false
+ t.string "title"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.integer "project_id"
+ t.string "commit_sha"
+ t.string "pull_request_number"
+ t.integer "num_of_images_in_build", default: 0, null: false
+ t.text "image_md5s"
+ t.string "username", default: "Pull Request"
+ t.string "branch_name", default: "Branch"
+ t.text "associated_commit_shas"
+ t.text "full_list_of_image_md5s"
+ t.string "failure_message"
+ t.boolean "dev_build", default: false
+ t.index ["project_id"], name: "index_builds_on_project_id"
+ end
+
+ create_table "builds_base_images", id: :serial, force: :cascade do |t|
+ t.integer "build_id"
+ t.integer "test_image_id"
+ t.index ["build_id"], name: "index_builds_base_images_on_build_id"
+ t.index ["test_image_id"], name: "index_builds_base_images_on_test_image_id"
+ end
+
+ create_table "builds_successful_tests", id: :serial, force: :cascade do |t|
+ t.integer "build_id"
+ t.integer "test_image_id"
+ t.index ["build_id"], name: "index_builds_successful_tests_on_build_id"
+ t.index ["test_image_id"], name: "index_builds_successful_tests_on_test_image_id"
+ end
+
+ create_table "commontator_comments", id: :serial, force: :cascade do |t|
+ t.string "creator_type"
+ t.integer "creator_id"
+ t.string "editor_type"
+ t.integer "editor_id"
+ t.integer "thread_id", null: false
+ t.text "body", null: false
+ t.datetime "deleted_at"
+ t.integer "cached_votes_up", default: 0
+ t.integer "cached_votes_down", default: 0
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.index ["cached_votes_down"], name: "index_commontator_comments_on_cached_votes_down"
+ t.index ["cached_votes_up"], name: "index_commontator_comments_on_cached_votes_up"
+ t.index ["creator_id", "creator_type", "thread_id"], name: "index_commontator_comments_on_c_id_and_c_type_and_t_id"
+ t.index ["thread_id", "created_at"], name: "index_commontator_comments_on_thread_id_and_created_at"
+ end
+
+ create_table "commontator_subscriptions", id: :serial, force: :cascade do |t|
+ t.string "subscriber_type", null: false
+ t.integer "subscriber_id", null: false
+ t.integer "thread_id", null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.index ["subscriber_id", "subscriber_type", "thread_id"], name: "index_commontator_subscriptions_on_s_id_and_s_type_and_t_id", unique: true
+ t.index ["thread_id"], name: "index_commontator_subscriptions_on_thread_id"
+ end
+
+ create_table "commontator_threads", id: :serial, force: :cascade do |t|
+ t.string "commontable_type"
+ t.integer "commontable_id"
+ t.datetime "closed_at"
+ t.string "closer_type"
+ t.integer "closer_id"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.index ["commontable_id", "commontable_type"], name: "index_commontator_threads_on_c_id_and_c_type", unique: true
+ end
+
+ create_table "diffs", id: :serial, force: :cascade do |t|
+ t.integer "old_image_id"
+ t.integer "new_image_id"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.string "differences_file_name"
+ t.string "differences_content_type"
+ t.integer "differences_file_size"
+ t.datetime "differences_updated_at"
+ t.integer "build_id"
+ t.integer "approved_by_id"
+ t.boolean "approved", default: false
+ t.index ["approved_by_id"], name: "index_diffs_on_approved_by_id"
+ t.index ["build_id"], name: "index_diffs_on_build_id"
+ t.index ["new_image_id"], name: "index_diffs_on_new_image_id"
+ t.index ["old_image_id"], name: "index_diffs_on_old_image_id"
+ end
+
+ create_table "jiras", id: :serial, force: :cascade do |t|
+ t.string "title"
+ t.string "project"
+ t.string "component"
+ t.string "description"
+ t.string "issue_type"
+ t.string "jira_link"
+ t.string "jira_key"
+ t.integer "diff_id"
+ t.string "assignee"
+ t.string "priority"
+ t.string "jira_base_url"
+ t.index ["diff_id"], name: "index_jiras_on_diff_id"
+ end
+
+ create_table "projects", id: :serial, force: :cascade do |t|
+ t.string "name"
+ t.string "description"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.string "github_root_url"
+ t.string "github_repo"
+ t.string "github_status_context", default: "continuous-integration/vizzy"
+ t.jsonb "plugin_settings", default: {}, null: false
+ t.string "vizzy_server_url"
+ t.index ["plugin_settings"], name: "index_projects_on_plugin_settings", using: :gin
+ end
+
+ create_table "test_images", id: :serial, force: :cascade do |t|
+ t.string "image_file_name"
+ t.string "image_content_type"
+ t.integer "image_file_size"
+ t.datetime "image_updated_at"
+ t.string "branch"
+ t.boolean "approved", default: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.integer "build_id"
+ t.integer "test_id"
+ t.boolean "user_approved_this_build", default: false
+ t.boolean "image_created_this_build", default: false
+ t.string "image_pull_request_sha"
+ t.string "image_pull_request_number"
+ t.string "md5"
+ t.string "test_key"
+ t.index ["build_id"], name: "index_test_images_on_build_id"
+ t.index ["test_id"], name: "index_test_images_on_test_id"
+ end
+
+ create_table "tests", id: :serial, force: :cascade do |t|
+ t.string "name"
+ t.string "description"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.string "comment"
+ t.string "jira"
+ t.string "pull_request_link"
+ t.string "comment_user"
+ t.string "ancestry"
+ t.string "ancestry_key"
+ t.integer "project_id"
+ t.index ["ancestry"], name: "index_tests_on_ancestry"
+ t.index ["project_id"], name: "index_tests_on_project_id"
+ end
+
+ create_table "users", id: :serial, force: :cascade do |t|
+ t.string "email", default: "", null: false
+ t.string "encrypted_password", default: "", null: false
+ t.string "reset_password_token"
+ t.datetime "reset_password_sent_at"
+ t.datetime "remember_created_at"
+ t.integer "sign_in_count", default: 0, null: false
+ t.datetime "current_sign_in_at"
+ t.datetime "last_sign_in_at"
+ t.string "current_sign_in_ip"
+ t.string "last_sign_in_ip"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.boolean "admin", default: false
+ t.string "username"
+ t.string "authentication_token", limit: 30
+ t.index ["authentication_token"], name: "index_users_on_authentication_token", unique: true
+ t.index ["email"], name: "index_users_on_email", unique: true
+ t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
+ t.index ["username"], name: "index_users_on_username", unique: true
+ end
+
+ add_foreign_key "tests", "projects", on_delete: :cascade
+end
diff --git a/db/seeds.rb b/db/seeds.rb
new file mode 100644
index 0000000..c24d027
--- /dev/null
+++ b/db/seeds.rb
@@ -0,0 +1,7 @@
+# This file should contain all the record creation needed to seed the database with its default values.
+# The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
+
+require 'factory_bot'
+
+FactoryBot.create(:project)
+FactoryBot.create(:user)
\ No newline at end of file
diff --git a/k8s/deploy-vizzy.sh b/k8s/deploy-vizzy.sh
new file mode 100755
index 0000000..82617cb
--- /dev/null
+++ b/k8s/deploy-vizzy.sh
@@ -0,0 +1,107 @@
+#!/bin/sh -x
+
+while [ $# -gt 0 ]; do
+ case "$1" in
+ --api-server=*)
+ api_server="${1#*=}"
+ ;;
+ --bearer-token=*)
+ bearer_token="${1#*=}"
+ ;;
+ --rails-env=*)
+ rails_env="${1#*=}"
+ ;;
+ --vizzy-version=*)
+ vizzy_version="${1#*=}"
+ ;;
+ --namespace=*)
+ namespace="${1#*=}"
+ ;;
+ --vizzy-uri=*)
+ vizzy_uri="${1#*=}"
+ ;;
+ --replica-pods=*)
+ replica_pods="${1#*=}"
+ ;;
+ --memory=*)
+ memory="${1#*=}"
+ ;;
+ --docker-registry=*)
+ docker_registry="${1#*=}"
+ ;;
+ --run-tests=*)
+ run_tests="${1#*=}"
+ ;;
+ *)
+ printf "***************************\n"
+ printf "* Error: Invalid argument.*\n"
+ printf "***************************\n"
+ exit 1
+ esac
+ shift
+done
+
+export VIZZY_VERSION=$vizzy_version
+export VIZZY_URI=$vizzy_uri
+export VIZZY_REPLICA_PODS=$replica_pods
+export VIZZY_REQUESTS_MEMORY=$memory
+export VIZZY_LIMITS_MEMORY=$memory
+export RAILS_ENV=$rails_env
+export DOCKER_REGISTRY=$docker_registry
+
+kubectl config --kubeconfig=k8sconfig set-cluster k8s --server=$api_server
+kubectl config --kubeconfig=k8sconfig set-credentials jenkins --token=$bearer_token
+kubectl config --kubeconfig=k8sconfig set-context k8s --cluster=k8s --user=jenkins
+kubectl config --kubeconfig=k8sconfig use-context k8s
+
+./render-template.sh kubernetes-vizzy-config.template.yaml > vizzy-config.yaml
+./render-template.sh kubernetes-postgres-config.template.yaml > postgres-config.yaml
+
+kubectl --kubeconfig=k8sconfig --namespace=$namespace apply -f vizzy-pvc.yaml
+kubectl --kubeconfig=k8sconfig --namespace=$namespace apply -f postgres-config.yaml
+kubectl --kubeconfig=k8sconfig --namespace=$namespace apply -f vizzy-config.yaml
+
+rm vizzy-config.yaml
+rm postgres-config.yaml
+rm k8sconfig
+
+sleep 30
+
+# Get the running visual pod and run the tests inside the pod
+TEST_POD=$(kubectl get pods --namespace=$namespace | grep -m1 vizzy | awk '{print $1}')
+echo ${TEST_POD}
+kubectl exec ${TEST_POD} rake db:migrate --namespace=$namespace
+
+if [ $? -ne 0 ]; then
+ echo "Could not run migrations, pod does not exist. Exiting 1..."
+ exit 1
+fi
+
+if [ "$run_tests" = true ] ; then
+ echo 'Running unit tests...'
+ kubectl exec ${TEST_POD} rake test --namespace=$namespace
+ TEST_STATUS=$?
+
+ if [ ${TEST_STATUS} -ne 0 ]; then
+ echo "Unit Tests Failed!"
+ # exit ${TEST_STATUS}
+ else
+ echo "Unit Tests Passed!"
+ fi
+
+ # TODO: Enable system tests on CI
+ #kubectl exec ${TEST_POD} rails test:system --namespace=$namespace
+ #TEST_STATUS=$?
+ #
+ #if [ ${TEST_STATUS} -ne 0 ]; then
+ # echo "System Tests Failed!"
+ #else
+ # echo "System Tests Passed!"
+ #fi
+
+ # Clean up deployment so nothing is left running
+ kubectl delete deployment vizzy --namespace=$namespace
+ kubectl delete deployment postgres --namespace=$namespace
+
+ exit ${TEST_STATUS}
+fi
\ No newline at end of file
diff --git a/k8s/kubernetes-postgres-config.template.yaml b/k8s/kubernetes-postgres-config.template.yaml
new file mode 100644
index 0000000..52dfa77
--- /dev/null
+++ b/k8s/kubernetes-postgres-config.template.yaml
@@ -0,0 +1,71 @@
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: postgres-storage
+spec:
+ accessModes:
+ - ReadWriteMany
+ resources:
+ requests:
+ storage: 5000Mi
+
+---
+
+apiVersion: v1
+kind: Service
+metadata:
+ labels:
+ name: postgres
+ role: service
+ name: postgres
+spec:
+ ports:
+ - port: 5432
+ targetPort: 5432
+ type: NodePort
+ selector:
+ name: postgres
+
+---
+
+apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+ name: postgres
+spec:
+ replicas: 1
+ template:
+ metadata:
+ labels:
+ name: postgres
+ spec:
+ containers:
+ - name: postgres
+ image: ${DOCKER_REGISTRY}/visual/postgres:9.5.3
+ resources:
+ requests:
+ memory: 300Mi
+ limits:
+ memory: 300Mi
+ ports:
+ - containerPort: 5432
+ volumeMounts:
+ - name: postgres-storage
+ mountPath: /var/lib/postgresql/data
+ env:
+ - name: POSTGRES_USER
+ valueFrom:
+ secretKeyRef:
+ name: vizzy-postgres-secret
+ key: username
+ - name: POSTGRES_PASSWORD
+ valueFrom:
+ secretKeyRef:
+ name: vizzy-postgres-secret
+ key: password
+ imagePullSecrets:
+ - name: docker-dev
+ volumes:
+ - name: postgres-storage
+ persistentVolumeClaim:
+ claimName: postgres-storage
\ No newline at end of file
diff --git a/k8s/kubernetes-vizzy-config.template.yaml b/k8s/kubernetes-vizzy-config.template.yaml
new file mode 100644
index 0000000..801ee9a
--- /dev/null
+++ b/k8s/kubernetes-vizzy-config.template.yaml
@@ -0,0 +1,82 @@
+apiVersion: extensions/v1beta1
+kind: Ingress
+metadata:
+ name: vizzy
+spec:
+ rules:
+ - host: ${VIZZY_URI}
+ http:
+ paths:
+ - backend:
+ serviceName: vizzy
+ servicePort: 3000
+
+---
+
+apiVersion: v1
+kind: Service
+metadata:
+ labels:
+ name: vizzy
+ role: service
+ name: vizzy
+spec:
+ ports:
+ - port: 3000
+ targetPort: 3000
+ selector:
+ name: vizzy
+
+---
+
+apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+ name: vizzy
+spec:
+ replicas: ${VIZZY_REPLICA_PODS}
+ template:
+ metadata:
+ labels:
+ name: vizzy
+ spec:
+ containers:
+ - name: visual
+ image: ${DOCKER_REGISTRY}/visual/vizzy:${VIZZY_VERSION}
+ resources:
+ requests:
+ memory: ${VIZZY_REQUESTS_MEMORY}
+ limits:
+ memory: ${VIZZY_LIMITS_MEMORY}
+ ports:
+ - name: web
+ containerPort: 3000
+ volumeMounts:
+ - name: visual-data
+ mountPath: /app/public/visual_images
+ env:
+ - name: VIZZY_URI
+ value: ${VIZZY_URI}
+ - name: RAILS_ENV
+ value: ${RAILS_ENV}
+ - name: POSTGRES_USER
+ valueFrom:
+ secretKeyRef:
+ name: vizzy-postgres-secret
+ key: username
+ - name: POSTGRES_PASSWORD
+ valueFrom:
+ secretKeyRef:
+ name: vizzy-postgres-secret
+ key: password
+ - name: RAILS_MASTER_KEY
+ valueFrom:
+ secretKeyRef:
+ name: vizzy-rails-master-key-secret
+ key: token
+ imagePullSecrets:
+ - name: docker-dev
+ volumes:
+ - name: visual-data
+ persistentVolumeClaim:
+ claimName: visual-automation-home
diff --git a/k8s/render-template.sh b/k8s/render-template.sh
new file mode 100755
index 0000000..f30f86f
--- /dev/null
+++ b/k8s/render-template.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+render_template() {
+ eval "echo \"$(cat $1)\""
+}
+
+render_template $1
diff --git a/k8s/vizzy-pvc.yaml b/k8s/vizzy-pvc.yaml
new file mode 100644
index 0000000..b7d8bb6
--- /dev/null
+++ b/k8s/vizzy-pvc.yaml
@@ -0,0 +1,10 @@
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: visual-automation-home
+spec:
+ accessModes:
+ - ReadWriteMany
+ resources:
+ requests:
+ storage: 10G
\ No newline at end of file
diff --git a/lib/assets/.keep b/lib/assets/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/lib/tasks/.keep b/lib/tasks/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/lib/tasks/dev_builds.rake b/lib/tasks/dev_builds.rake
new file mode 100644
index 0000000..3556c2c
--- /dev/null
+++ b/lib/tasks/dev_builds.rake
@@ -0,0 +1,14 @@
+desc 'Find dev builds and destroy them'
+namespace :dev_builds do
+ task destroy: :environment do
+ puts 'Destroying dev builds older than 30 days...'
+ dev_builds = Build.where(dev_build: true).where('created_at < ?', 30.days.ago)
+ dev_build_count = dev_builds.size
+ if dev_build_count == 0
+ puts 'No dev builds found, nothing to destroy.'
+ else
+ puts "Destroying #{dev_build_count} dev builds..."
+ dev_builds.destroy_all
+ end
+ end
+end
diff --git a/lib/tasks/fix_md5s.rake b/lib/tasks/fix_md5s.rake
new file mode 100644
index 0000000..96cfc3d
--- /dev/null
+++ b/lib/tasks/fix_md5s.rake
@@ -0,0 +1,24 @@
+desc 'Verify that the md5s in the database match the actual md5 of the image'
+task :fix_md5s => :environment do
+ @dry_run = %w(true 1).include? ENV['DRY_RUN']
+
+ puts "Finding images with mismatched md5s"
+ images_updated = 0
+ TestImage.find_each do |test_image|
+ file_md5 = Digest::MD5.file(test_image.image.path).hexdigest
+ db_md5 = test_image.md5
+ if db_md5 != file_md5
+ images_updated += 1
+ if !@dry_run
+ test_image.md5 = file_md5
+ test_image.save
+ end
+ end
+ end
+ if @dry_run
+ puts "Would have updated #{images_updated} md5s"
+ else
+ puts "Updated #{images_updated} md5s"
+ end
+end
+
diff --git a/lib/tasks/invalid_images.rake b/lib/tasks/invalid_images.rake
new file mode 100644
index 0000000..b514278
--- /dev/null
+++ b/lib/tasks/invalid_images.rake
@@ -0,0 +1,22 @@
+namespace :invalid_images do
+ desc 'Delete all invalid test images -- images that are not associated with a build'
+ task :destroy => :environment do
+
+ @dry_run = %w(true 1).include? ENV['DRY_RUN']
+
+ puts "Starting invalid_images:destroy"
+ test_images = TestImage.where(build: nil).order("id DESC")
+ test_image_count = test_images.size
+
+ if test_image_count == 0
+ abort "No invalid images found. Exiting..."
+ end
+
+ if @dry_run
+ puts "Invalid Images That Would Have Been Deleted: #{test_image_count}"
+ else
+ test_images.destroy_all
+ puts "Successfully Destroyed #{test_image_count} Invalid Test Images"
+ end
+ end
+end
diff --git a/lib/tasks/paperclip.rake b/lib/tasks/paperclip.rake
new file mode 100644
index 0000000..3fae199
--- /dev/null
+++ b/lib/tasks/paperclip.rake
@@ -0,0 +1,79 @@
+namespace :paperclip do
+
+ desc 'Destroy paperclip attachment files that are not attached to any record'
+ task :clean_orphan_files => :environment do
+ @last_path = nil
+ @dry_run = %w(true 1).include? ENV['DRY_RUN']
+
+ Signal.trap('USR1') do
+ puts "#{Time.now.strftime('%Y-%m-%d %H:%M:%S')} #{@last_path}"
+ end
+
+ def reverse_id_partition(path)
+ parts = path.to_s.split('/')[-4..-2]
+ puts "Parts: #{parts}"
+ if parts.all? {|e| e =~ /^\d{3}$/}
+ parts.join.to_i
+ end
+ end
+
+ def is_orphan?(model, id)
+ !model.exists?(id)
+ end
+
+ def move_to_deleted_directory(old_path)
+ parts = old_path.to_s.split('/')
+ if parts.include?('images')
+ new_dir = old_path.to_s.gsub(/\bimages\b/, 'images_deleted')
+ new_path = Pathname.new(new_dir)
+ new_path.mkpath
+
+ old_path.rename new_path
+ end
+ end
+
+ def delete_dir_if_empty(dir)
+ if dir.children.none? {|e| (e.file? && e.extname != '') || e.directory?}
+ if @dry_run
+ puts "delete #{dir}"
+ else
+ dir.rmtree
+ end
+ end
+ end
+
+ def move_dir_if_orphan(dir, model)
+ id = reverse_id_partition(dir)
+ puts "id: #{id} model: #{model}"
+ if id && is_orphan?(model, id)
+ if @dry_run
+ puts "#{model}##{id} : orphan"
+ else
+ puts "Moving #{model}##{id} : orphan"
+ move_to_deleted_directory(dir)
+ end
+ end
+ end
+
+ def verify_directory(start_dir, model)
+ return unless start_dir.directory?
+ @last_path = start_dir.to_s
+ if start_dir.children.none? {|e| e.directory?}
+ move_dir_if_orphan(start_dir, model)
+ else
+ start_dir.children.sort.each do |entry|
+ full_path = (start_dir + entry)
+ if full_path.directory?
+ verify_directory(full_path, model)
+ end
+ end
+ delete_dir_if_empty(start_dir)
+ end
+ end
+
+ start_dir = Pathname.new(TestImage::PAPERCLIP_BASEDIR + 'visual_images/test_images/images')
+ puts "start_dir: #{start_dir}"
+ verify_directory(start_dir, TestImage)
+ end
+
+end
\ No newline at end of file
diff --git a/lib/tasks/purge_pr_builds.rake b/lib/tasks/purge_pr_builds.rake
new file mode 100644
index 0000000..c7fef3c
--- /dev/null
+++ b/lib/tasks/purge_pr_builds.rake
@@ -0,0 +1,27 @@
+namespace :orphaned_pr_builds do
+ desc 'Delete builds that are orphaned, aka have no approved diffs (preapprovals) and no new tests (auto approved images)'
+ task :destroy => :environment do
+ @dry_run = %w(true 1).include? ENV['DRY_RUN']
+ builds_destroyed = 0
+
+ non_temporary_pull_requests_older_than_30_days.find_each(batch_size: 500) do |build|
+ no_approved_diffs = build.diffs.where(approved: true).size == 0
+ no_new_tests = build.new_tests.size == 0
+ if no_approved_diffs && no_new_tests
+ puts "Deleting build with id: #{build.id}"
+ builds_destroyed += 1
+ build.destroy unless @dry_run
+ end
+ end
+
+ if @dry_run
+ puts "#{builds_destroyed} builds would have been destroyed"
+ else
+ puts "#{builds_destroyed} builds successfully destroyed"
+ end
+ end
+end
+
+def non_temporary_pull_requests_older_than_30_days
+ Build.where.not(pull_request_number: -1).where(temporary: false).where('created_at < ?', 30.days.ago)
+end
\ No newline at end of file
diff --git a/lib/tasks/rdoc.rake b/lib/tasks/rdoc.rake
new file mode 100644
index 0000000..fa8e3cd
--- /dev/null
+++ b/lib/tasks/rdoc.rake
@@ -0,0 +1,11 @@
+# Rakefile
+require 'sdoc' # and use your RDoc task the same way you used it before
+require 'rdoc/task' # ensure this file is also required in order to use `RDoc::Task`
+
+RDoc::Task.new do |rdoc|
+ rdoc.rdoc_dir = 'doc/rdoc' # name of output directory
+ rdoc.generator = 'sdoc' # explictly set the sdoc generator
+ rdoc.template = 'rails' # template used on api.rubyonrails.org
+ rdoc.main = 'README.md'
+ rdoc.rdoc_files.include('README.md', 'app/', 'lib/')
+end
diff --git a/lib/tasks/run_test_build.rake b/lib/tasks/run_test_build.rake
new file mode 100644
index 0000000..231c6ab
--- /dev/null
+++ b/lib/tasks/run_test_build.rake
@@ -0,0 +1,46 @@
+def run_build(params, args)
+ Dir.chdir Rails.root.join('test-image-upload')
+ build_command = "ruby run_test_push.rb #{params} #{args[:host_with_port]}"
+ git_sha = args[:git_sha]
+ build_command.concat(" #{git_sha}") unless git_sha.nil?
+ pull_request_number = args[:pull_request_number]
+ build_command.concat(" #{pull_request_number}") unless pull_request_number.nil?
+ system(build_command)
+end
+
+namespace :run_test_build do
+############## BUILD DEVELOPS ##############
+
+ desc 'run a ruby script'
+ task :develop_1, [:host_with_port, :git_sha] => [:environment] do |_, args|
+ puts 'running develop 1...'
+ run_build('1 1', args)
+ end
+
+ task :develop_2, [:host_with_port, :git_sha] => [:environment] do |_, args|
+ puts 'running develop 2...'
+ run_build('2 1', args)
+ end
+
+ task :develop_3, [:host_with_port, :git_sha] => [:environment] do |_, args|
+ puts 'running develop 3...'
+ run_build('3 1', args)
+ end
+
+ ############## PULL REQUESTS ##############
+
+ task :pull_request_1, [:host_with_port, :git_sha, :pull_request_number] => [:environment] do |_, args|
+ puts 'running pull request 1...'
+ run_build('1 2', args)
+ end
+
+ task :pull_request_2, [:host_with_port, :git_sha, :pull_request_number] => [:environment] do |_, args|
+ puts 'running pull request 2...'
+ run_build('2 2', args)
+ end
+
+ task :pull_request_3, [:host_with_port, :git_sha, :pull_request_number] => [:environment] do |_, args|
+ puts 'running pull request 3...'
+ run_build('3 2', args)
+ end
+end
\ No newline at end of file
diff --git a/lib/tasks/users.rake b/lib/tasks/users.rake
new file mode 100644
index 0000000..1b74bcf
--- /dev/null
+++ b/lib/tasks/users.rake
@@ -0,0 +1,19 @@
+namespace :users do
+ desc "Destory all user accounts"
+ task destroy_all: :environment do
+ User.destroy_all
+ end
+
+ desc "Destroy a specific user account"
+ task :destroy_user, [:user_email] => [:environment] do |t, args|
+ user_email = args[:user_email].to_s
+ puts "Destroying user with email: #{user_email}"
+ user = User.find_by_email(user_email)
+ if user
+ puts "User found, destroying user account"
+ user.destroy
+ else
+ puts "User does not exist"
+ end
+ end
+end
\ No newline at end of file
diff --git a/lib/tasks/wipeproject.rake b/lib/tasks/wipeproject.rake
new file mode 100644
index 0000000..14c13c3
--- /dev/null
+++ b/lib/tasks/wipeproject.rake
@@ -0,0 +1,27 @@
+namespace :wipeproject do
+ desc 'Delete everything associated with the project id passed into the this rake task'
+ task :destroy, [:project_id] => [:environment] do |t, args|
+
+ project_id = args[:project_id].to_i
+ @dry_run = %w(true 1).include? ENV['DRY_RUN']
+
+ puts "Starting wipeprojects:destroy with project id #{project_id}"
+ project = Project.find(project_id)
+ builds_count = project.builds.size
+ tests_count = project.tests.size
+
+ if builds_count == 0 && tests_count == 0
+ abort "Project with project id: #{project_id} has already been wiped clean. Exiting..."
+ end
+
+ if @dry_run
+ puts "Builds That Would Have Been Deleted: #{builds_count}"
+ puts "Test That Would Have Been Deleted: #{tests_count}"
+ else
+ project.builds.order("id DESC").destroy_all
+ puts "Successfully Deleted #{builds_count} Builds From Project: #{project_id}"
+ project.tests.order("id DESC").destroy_all
+ puts "Successfully Deleted #{tests_count} Tests From Project: #{project_id}"
+ end
+ end
+end
diff --git a/log/development.log b/log/development.log
new file mode 100644
index 0000000..c6188f3
--- /dev/null
+++ b/log/development.log
@@ -0,0 +1 @@
+No valid API key has been set, notifications will not be sent
diff --git a/log/test.log b/log/test.log
new file mode 100644
index 0000000..e69de29
diff --git a/public/404.html b/public/404.html
new file mode 100644
index 0000000..b612547
--- /dev/null
+++ b/public/404.html
@@ -0,0 +1,67 @@
+
+
+
+ The page you were looking for doesn't exist (404)
+
+
+
+
+
+
+
+
+
The page you were looking for doesn't exist.
+
You may have mistyped the address or the page may have moved.
+
+
If you are the application owner check the logs for more information.
+
+
+
diff --git a/public/422.html b/public/422.html
new file mode 100644
index 0000000..a21f82b
--- /dev/null
+++ b/public/422.html
@@ -0,0 +1,67 @@
+
+
+
+ The change you wanted was rejected (422)
+
+
+
+
+
+
+
+
+
The change you wanted was rejected.
+
Maybe you tried to change something you didn't have access to.
+
+
If you are the application owner check the logs for more information.
+
+
+
diff --git a/public/500.html b/public/500.html
new file mode 100644
index 0000000..061abc5
--- /dev/null
+++ b/public/500.html
@@ -0,0 +1,66 @@
+
+
+
+ We're sorry, but something went wrong (500)
+
+
+
+
+
+
+
+
+
We're sorry, but something went wrong.
+
+
If you are the application owner check the logs for more information.
+
+
+
diff --git a/public/apple-touch-icon-precomposed.png b/public/apple-touch-icon-precomposed.png
new file mode 100644
index 0000000..e69de29
diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png
new file mode 100644
index 0000000..e69de29
diff --git a/public/robots.txt b/public/robots.txt
new file mode 100644
index 0000000..3c9c7c0
--- /dev/null
+++ b/public/robots.txt
@@ -0,0 +1,5 @@
+# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
+#
+# To ban all spiders from the entire site uncomment the next two lines:
+# User-agent: *
+# Disallow: /
diff --git a/public/upload_images_to_server.rb b/public/upload_images_to_server.rb
new file mode 100755
index 0000000..3eeeca6
--- /dev/null
+++ b/public/upload_images_to_server.rb
@@ -0,0 +1,685 @@
+#!/usr/bin/ruby
+# This script uploads images to the visual automation server. It calculates the image md5s, sends them with the build id request, the md5s are compared to the base image md5 on
+# the server, generating a list of images that need to be uploaded that is returned with the build id. For usage of this script run with the --help flag.
+require 'rubygems'
+require 'json'
+require 'digest/md5'
+require 'net/http'
+require 'net/https'
+require 'net/http/post/multipart'
+require 'benchmark'
+require 'optparse'
+require 'find'
+require 'fileutils'
+require 'pathname'
+
+# --------------- Server ---------------- #
+
+def create_server_client
+ url = URI(@server_url)
+ http = Net::HTTP.new(url.host, url.port)
+ if @server_url.start_with?('https')
+ http.use_ssl = true
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
+ http.read_timeout = 300
+ end
+ http
+end
+
+def setup_shared_server_client
+ @http = create_server_client
+end
+
+def setup_auth_creds(options)
+ @vizzy_auth_email = options.user_email
+ @vizzy_auth_token = options.user_token
+end
+
+def print_vizzy_link(build_id = nil)
+ vizzy_build_link = @server_url
+ vizzy_build_link += "/builds/#{build_id}" unless build_id.nil?
+ puts "Vizzy Link: #{vizzy_build_link}"
+end
+
+def do_post(uri, body)
+ perform_request(uri, body, false)
+end
+
+def do_get(uri)
+ perform_request(uri, nil, true)
+end
+
+def add_vizzy_auth_headers(request)
+ request.add_field 'X-User-Email', @vizzy_auth_email
+ request.add_field 'X-User-Token', @vizzy_auth_token
+end
+
+def perform_request(uri, body, is_get)
+ request = is_get ? Net::HTTP::Get.new(uri) : Net::HTTP::Post.new(uri)
+ request.add_field 'Content-Type', 'application/json'
+ request.add_field 'Accept', 'application/json'
+ add_vizzy_auth_headers(request)
+ request.body = JSON.dump(body) unless body.nil?
+ fetch_request(request)
+end
+
+HTTP_RETRY_ERRORS = [
+ Net::ReadTimeout,
+ Net::HTTPBadGateway,
+ Errno::ECONNRESET,
+ Errno::EPIPE,
+ Errno::ETIMEDOUT,
+ Net::HTTPServiceUnavailable,
+]
+
+# Fetch Request And Handle Errors
+def fetch_request(request)
+ max_retries = 3
+ times_retried = 0
+
+ begin
+ response = @http.request(request)
+ puts "Response HTTP Status Code: #{response.code}"
+ puts "Response HTTP Response Body: #{response.body}"
+ response
+ rescue *HTTP_RETRY_ERRORS => e
+ if times_retried < max_retries
+ times_retried += 1
+ puts "Error: #{e}, retry #{times_retried}/#{max_retries}"
+ retry
+ else
+ puts "Error: #{e}, HTTP Request Failed: #{e.message}"
+ end
+ rescue StandardError => e
+ puts "HTTP Request failed (#{e.message})"
+ end
+end
+
+# --------------- Actions --------------- #
+
+def fail_build(message, build_id = nil)
+ puts "Build was not successful. Error: #{message}"
+ unless build_id.nil?
+ print_vizzy_link(build_id)
+ body = { failure_message: message }
+ do_post("/builds/#{build_id}/fail.json", body)
+ end
+ exit(1)
+end
+
+# Request (POST)
+# Upload file to test_images controller with build id provided
+def push_image_to_server(file_path, ancestry, build_id, thread_id, http)
+ max_retries = 3
+ times_retried = 0
+
+ thread_puts = lambda do |msg|
+ puts "||#{thread_id}|| #{msg}"
+ end
+
+ begin
+ time = Benchmark.realtime do
+ File.open(File.expand_path(file_path)) do |png|
+ request = Net::HTTP::Post::Multipart.new '/test_images.json',
+ image: UploadIO.new(png, 'image/png'),
+ build_id: build_id,
+ test_image_ancestry: ancestry
+
+ add_vizzy_auth_headers(request)
+ response = http.request(request)
+
+ thread_puts.call "Response HTTP Status Code: #{response.code}"
+ thread_puts.call "Response HTTP Response Body: #{response.body}"
+
+ if response.code == '502'
+ # retry upload when we see a 502 server gateway error. Treat it as a read timeout.
+ raise Net::ReadTimeout
+ end
+ response
+ end
+ end
+ thread_puts.call "Time Spent: #{time}"
+ rescue *HTTP_RETRY_ERRORS => e
+ if times_retried < max_retries
+ times_retried += 1
+ thread_puts.call "Error: #{e}, retry #{times_retried}/#{max_retries}"
+ retry
+ else
+ thread_puts.call "Error: #{e}, HTTP Request Failed: #{e.message}"
+ end
+ rescue StandardError => e
+ thread_puts.call "HTTP Request failed (#{e.message})"
+ end
+end
+
+# Request (POST)
+# Gets build id and images to upload
+def get_build_id(create_options)
+ puts 'Requesting build id...'
+ body = {
+ title: create_options.title,
+ project_id: create_options.project,
+ commit_sha: create_options.commit,
+ pull_request_number: create_options.pull_request_number,
+ url: create_options.url,
+ dev_build: create_options.dev_build
+ }
+ do_post('/builds.json', body)
+end
+
+# Request (POST)
+# Sends a request to upload the generated md5s
+def send_image_md5s(build_id, image_md5s)
+ puts 'Sending md5s for the build'
+ body = { image_md5s: image_md5s }
+ response = do_post("/builds/#{build_id}/add_md5s.json", body)
+ JSON.parse(response.body)
+end
+
+# Request (POST)
+# Sends a request, finalizing the build transaction
+def commit_build(build_id, total_upload_count)
+ puts 'Committing the build'
+ commit_response = do_post("/builds/#{build_id}/commit.json", nil)
+ if commit_response.code != '200'
+ uncommited_json = JSON.parse(commit_response.body)
+ fail_build(uncommited_json['error'], build_id)
+ else
+ poll_for_commit(build_id, total_upload_count)
+ end
+end
+
+def poll_for_commit(build_id, images_uploaded)
+ puts 'Waiting for commit to finalize'
+ sleep 2 # Sleep 2 before the first check so we can catch the 'simple' commits that have no diffs quickly
+
+ # floor: 60 tries * 10 seconds == 10 minutes
+ max_tries = 60
+ # scaling as needed for when there are more than 60 images, 10 seconds per image
+ max_tries = images_uploaded if images_uploaded > max_tries
+ # ceiling 360 * 10 seconds = 3600 seconds = 1 hour
+ max_tries = 360 if images_uploaded > 360
+
+ time_in_seconds = max_tries * 10
+ time_in_minutes = time_in_seconds / 60
+ puts "Polling Timeout Horizon: #{time_in_seconds} seconds == #{time_in_minutes} minutes"
+
+ wait_time = 10
+ total_wait_time = 0
+ max_tries.times do
+ response = do_get("/builds/#{build_id}/commit.json")
+ if response.code != '200'
+ fail_build('Failed to poll for commit status.', build_id)
+ return nil
+ else
+ result = JSON.parse(response.body)
+ return result if result['committed']
+ puts "Build not committed, checking again in #{wait_time} seconds. Have already waited #{total_wait_time} seconds."
+ total_wait_time += wait_time
+ sleep wait_time
+ end
+ end
+ fail_build("Build not committed after #{total_wait_time} seconds.", build_id)
+end
+
+def upload_images_to_build_with_id(id, images_to_upload, test_image_folder)
+ return if images_to_upload.nil?
+ total_upload_count = images_to_upload.size
+ number_of_images_uploaded = 0
+ file_list = traverse_directories(test_image_folder)
+ file_list.select! { |file_info| images_to_upload.key?(file_info[:ancestry]) }
+
+ threads = []
+ semaphore = Mutex.new
+ 3.times do |thread_id|
+ thread = Thread.new do
+ # Create a separate http object for each thread so they don't clobber each others requests
+ http = create_server_client
+ loop do
+ file_info = nil
+ semaphore.synchronize do # Syncronize on the file list / image count
+ next if file_list.empty?
+ file_info = file_list.pop
+ number_of_images_uploaded += 1
+ puts "||#{thread_id}|| Uploading #{number_of_images_uploaded}/#{total_upload_count}: #{file_info[:file]}"
+ end
+ break if file_info.nil?
+ push_image_to_server(file_info[:file], file_info[:ancestry], id, thread_id, http)
+ end
+ end
+ threads.push(thread)
+ end
+ threads.each(&:join)
+end
+
+# Returns a hash containing each image name and the associated md5 for that image, as json
+def compute_image_md5s_json(test_image_folder)
+ image_md5s = {}
+ traverse_directories(test_image_folder) do |ancestry, file|
+ md5 = `md5 -q #{file}`
+ md5 = md5.tr("\n", '').to_s
+ image_md5s[ancestry] = md5
+ end
+ { count: image_md5s.size, json: image_md5s.to_json }
+end
+
+def traverse_directories(test_image_folder)
+ results = []
+ png_file_paths = get_png_file_paths(test_image_folder)
+
+ Dir.chdir(test_image_folder) do
+ test_image_folder_path = Dir.pwd
+ png_file_paths.each do |file_path|
+
+ file_path = file_path.gsub(test_image_folder, '')[1..-1]
+
+ absolute_file_path = file_path
+ unless Pathname.new(file_path).absolute?
+ absolute_file_path = "#{test_image_folder_path}/#{file_path}"
+ end
+
+ filename = File.basename(absolute_file_path)
+
+ if has_special_characters(filename)
+ new_file_name = remove_special_characters(filename)
+ new_absolute_file_path = get_new_file_path(absolute_file_path, new_file_name)
+ absolute_file_path = rename_file(absolute_file_path, new_absolute_file_path)
+ end
+
+ ancestry_file_path = absolute_file_path.gsub(test_image_folder_path, '')[1..-1]
+ ancestry = ancestry_file_path.rpartition('.').first
+
+ if block_given?
+ yield ancestry, absolute_file_path
+ else
+ results.push(ancestry: ancestry, file: absolute_file_path)
+ end
+ end
+ end
+ results unless block_given?
+end
+
+def get_new_file_path(absolute_file_path, new_file_name)
+ path_array = absolute_file_path.split('/')
+ path_array.pop
+ path_array.push(new_file_name)
+ path_array.join('/')
+end
+
+def rename_file(file_path, new_file_path)
+ puts "Renaming file from: #{file_path} -> #{new_file_path}"
+ File.rename(file_path, new_file_path)
+ new_file_path
+end
+
+def get_png_file_paths(test_image_folder)
+ Find.find(test_image_folder).select { |path| get_png_paths(path) }
+end
+
+def get_png_paths(path)
+ path =~ /.*\.png/
+end
+
+# Remove special characters that do not play nicely with the rails filesystem
+# U+0000 (NUL)
+# / (slash)
+# \ (backslash)
+#: (colon)
+# * (asterisk)
+# ? (question mark)
+# " (quote)
+# < (less than)
+# @ (At symbol)
+# >(greater than)
+# | (pipe)
+@special_character_regexp = /[\x00\/\\:\*\?\"@<>\|]/
+
+def remove_special_characters(filename)
+ filename.gsub(@special_character_regexp, '_')
+end
+
+def has_special_characters(filename)
+ filename =~ @special_character_regexp
+end
+
+# Traverses the folder structure and returns the number of images
+def get_test_images_count(folder)
+ Dir[File.join(folder, '**', '*')].count { |file| File.file?(file) }
+end
+
+# -------------- Script Start -------------- #
+
+def validate_arguments(parser, options)
+ unless block_given?
+ puts 'Method requires block for required parameters'
+ exit(1)
+ end
+
+ server_url = nil
+ parser.order!(ARGV) do |arg|
+ unless server_url.nil?
+ puts 'Too many arguments!'
+ puts parser
+ exit(1)
+ end
+ server_url = arg
+ end
+
+ if server_url.nil?
+ puts 'Missing server_url!'
+ puts parser
+ exit(1)
+ end
+
+ required_options = yield options
+
+ required_options.each do |opt|
+ next unless options[opt].nil?
+ puts "Missing required option '--#{opt}'!"
+ puts parser
+ exit(1)
+ end
+
+ setup_auth_creds(options)
+ @server_url = server_url
+end
+
+# Add auth and token to opts. Used for create, upload, and fail
+def add_user_and_token_opts(options, opts)
+ opts.on('--user-email EMAIL', 'Email used for token based authentication with Vizzy. Required') do |user_email|
+ options.user_email = user_email
+ end
+
+ opts.on('--user-token TOKEN', 'Token used for token based authentication with Vizzy. NOTE: Only admins can create tokens. Required') do |user_token|
+ options.user_token = user_token
+ end
+end
+
+def create_command
+ options = OpenStruct.new
+ options.title = nil
+ options.project = nil
+ options.commit = nil
+ options.file = nil
+ options.pull_request_number = -1
+ options.url = nil
+ options.dev_build = false
+ options.user_email = nil
+ options.user_token = nil
+
+ optparse = OptionParser.new do |opts|
+ opts.banner = 'Usage: upload_images_to_server.rb create [options]'
+ opts.on('-t', '--title TITLE', 'Title to supply for this build. Common usage is to match your CIs plan name + number') do |title|
+ options.title = title
+ end
+
+ opts.on('-p', '--project ID', 'The project id to upload to. Required') do |id|
+ options.project = id
+ end
+
+ opts.on('-c', '--commit HASH', 'Current git hash corresponding to this build. Required') do |hash|
+ options.commit = hash
+ end
+
+ opts.on('-f', '--file BUILDFILE', 'File to store the created build information in. Used when running the "upload" command to commit the build. Required') do |file|
+ options.file = file
+ end
+
+ opts.on('--pull-request PR_NUMBER', 'Pull request number to associate with this build. If not supplied build is treated as a "develop" build.') do |prNum|
+ options.pull_request_number = prNum
+ end
+
+ opts.on('-u', '--url URL', 'url to the CI plan that created this build') do |url|
+ options.url = url
+ end
+
+ opts.on('-d', '--developer-build', 'Used when a developer wants to simulate a pull request and check images against the server. Creates a \'dummy\' build where preapprovals will not work. Build deletes itself after 48 hours') do
+ options.dev_build = true
+ end
+
+ add_user_and_token_opts(options, opts)
+
+ opts.on_tail('-h', '--help', 'Show this message') do
+ puts opts
+ exit(0)
+ end
+ end
+
+ validate_arguments(optparse, options) do |parsed_options|
+ required_options = [:user_email, :user_token, :project, :file]
+ unless parsed_options[:dev_build]
+ required_options = required_options + [:title, :commit]
+ end
+ required_options
+ end
+
+ # Remove existing build file
+ FileUtils.rm_f(options.file)
+
+ setup_shared_server_client
+
+ build_id_response = get_build_id(options)
+ if build_id_response.code != '201'
+ error_message = 'Build ID Request Failed'
+ begin
+ parsed_data = JSON.parse(build_id_response)
+ error_message = parsed_data['error'] unless parsed_data['error'].blank?
+ rescue
+ # ignored
+ end
+ fail_build(error_message)
+ else
+ json = build_id_response.body
+ puts "Build Info: #{json}"
+ File.write(options.file, json)
+
+ id = JSON.parse(json)['id']
+ puts "Link to in progress build: #{@server_url}/builds/#{id}"
+ end
+end
+
+def print_build_summary(id, result)
+ new_test_count = result['new_tests_count']
+ unapproved_diffs_count = result['unapproved_diffs_count']
+ missing_tests_count = result['missing_tests_count']
+ successful_tests_count = result['successful_test_count']
+
+ puts "-------------"
+ puts "Build Summary"
+ puts "-------------"
+ puts "New Tests: #{new_test_count}"
+ puts "Missing Tests: #{missing_tests_count}"
+ puts "Successful Tests: #{successful_tests_count}"
+ puts "Unapproved Diffs: #{unapproved_diffs_count}"
+ print_vizzy_link(id)
+ puts "-------------"
+end
+
+def upload_command
+ options = OpenStruct.new
+ options.file = nil
+ options.directory = nil
+ options.exit_0_on_diffs = false
+ options.check_image_count = false
+ options.user_email = nil
+ options.user_token = nil
+
+ optparse = OptionParser.new do |opts|
+ opts.banner = 'Usage: upload_images_to_server.rb upload [options]'
+ opts.on('-f', '--file BUILDFILE', 'File to store the created build information in. Used when running the "upload" command to commit the build.') do |file|
+ options.file = file
+ end
+ opts.on('-d', '--directory IMAGEDIRECTORY', 'Directory to find images to upload.') do |directory|
+ options.directory = directory
+ end
+ opts.on('-e', '--exit-0-on-diffs', 'Script will show as succeeded when diffs are present.') do
+ options.exit_0_on_diffs = true
+ end
+ opts.on('-c', '--check-image-count', 'Script will fail if the number of images generated is less than the number of base images - 200.') do
+ options.check_image_count = true
+ end
+
+ add_user_and_token_opts(options, opts)
+
+ opts.on_tail('-h', '--help', 'Show this message') do
+ puts opts
+ exit(0)
+ end
+ end
+
+ validate_arguments(optparse, options) do
+ [:file, :directory, :user_email, :user_token]
+ end
+ setup_shared_server_client
+
+ build_dict = JSON.parse(File.read(options.file))
+
+ id = build_dict['id']
+ base_image_count = build_dict['base_image_count']
+ puts "Uploading images to Vizzy build with ID: #{id}"
+ puts "Base Image Count: #{base_image_count}"
+
+ fail_build('Image Folder Empty', id) unless File.directory?(options.directory)
+
+ number_of_images_generated = get_test_images_count(options.directory)
+ puts "Number of images generated: #{number_of_images_generated}"
+ image_md5s = compute_image_md5s_json(options.directory)
+ puts "Number of md5s calculated: #{image_md5s[:count]}"
+
+ puts "Listing Images Generated: #{image_md5s[:json]}"
+ images_to_upload = send_image_md5s(id, image_md5s[:json])['image_md5s']
+ total_upload_count = images_to_upload.size
+ puts '=============================='
+ puts "Number of Images to upload: #{total_upload_count}"
+ puts "Images to upload: #{images_to_upload}"
+ puts '=============================='
+
+ upload_images_to_build_with_id(id, images_to_upload, options.directory)
+ result = commit_build(id, total_upload_count)
+ print_build_summary(id, result)
+
+ if options.check_image_count && number_of_images_generated < (base_image_count * 0.8)
+ error_message = "Did not generate enough images, number of images generated: #{number_of_images_generated} < (base image count: #{base_image_count} * 0.8)"
+ fail_build(error_message, id)
+ end
+
+ unapproved_diffs_count = result['unapproved_diffs_count']
+
+ if options.exit_0_on_diffs || unapproved_diffs_count == 0
+ puts 'Exit(0)'
+ exit(0)
+ else
+ puts 'In order to check in your code, please fix the tests/approve them, and rerun'
+ puts 'Exit(1)'
+ exit(1)
+ end
+end
+
+def open_command
+ options = OpenStruct.new
+ options.file = nil
+ optparse = OptionParser.new do |opts|
+ opts.banner = 'Usage: upload_images_to_server.rb open [options]'
+ opts.on('-f', '--file BUILDFILE', 'Build file that contains the build information of the build you want to open.') do |file|
+ options.file = file
+ end
+ opts.on_tail('-h', '--help', 'Show this message') do
+ puts opts
+ exit(0)
+ end
+ end
+
+ validate_arguments(optparse, options) do
+ [:file]
+ end
+
+ unless File.exists?(options.file)
+ puts 'Build file does not exist. You must run \'create\' before running \'open\' for the file to be created properly.'
+ exit(1)
+ end
+
+ build_dict = JSON.parse(File.read(options.file))
+ id = build_dict['id']
+
+ url = "#{@server_url}/builds/#{id}"
+
+ `open #{url}`
+end
+
+def fail_command
+ options = OpenStruct.new
+ options.file = nil
+ options.message = nil
+ options.user_email = nil
+ options.user_token = nil
+
+ optparse = OptionParser.new do |opts|
+ opts.banner = 'Usage: upload_images_to_server.rb fail [options]'
+ opts.on('-f', '--file BUILDFILE', 'File to store the created build information in. Used when running the "upload" command to commit the build.') do |file|
+ options.file = file
+ end
+ opts.on('-m', '--message FAILUREMESSAGE', 'Message to fail the build with. Server will use this message for slack and github status updates.') do |message|
+ options.message = message
+ end
+
+ add_user_and_token_opts(options, opts)
+
+ opts.on_tail('-h', '--help', 'Show this message') do
+ puts opts
+ exit(0)
+ end
+ end
+
+ validate_arguments(optparse, options) do
+ [:file, :message, :user_email, :user_token]
+ end
+ setup_shared_server_client
+
+ build_dict = JSON.parse(File.read(options.file))
+
+ id = build_dict['id']
+
+ fail_build(options.message, id)
+end
+
+STDOUT.sync = true
+STDERR.sync = true
+
+top_level_parser = OptionParser.new do |opts|
+ opts.banner = 'Usage: upload_images_to_server.rb command [options]'
+ opts.separator < 0 && test_case <= 3
+ puts "Unrecognized test case #{test_case}"
+ exit(1)
+end
+
+visual_host = if host_with_port.nil?
+ puts 'host_with_port is nil, falling back to http://localhost:3000'
+ 'http://localhost:3000'
+ else
+ puts "Host with port: #{host_with_port}"
+ host_with_port
+ end
+
+auth_creds = "--user-email john.doe@gmail.com --user-token ht2Cey1i9xbxH5jm-gpx"
+system("curl -O #{visual_host}/upload_images_to_server.rb")
+
+create_arguments = "-p 1 -c #{git_sha} -f ./tmp-buildfile -t ANDROIDGITHUBBUILD-ANDRGITHUBPULLREQUEST-26740"
+unless is_develop == '1'
+ create_arguments += " --pull-request #{pull_request_number}"
+end
+puts "ruby ./upload_images_to_server.rb create #{visual_host} #{create_arguments} #{auth_creds}"
+system("ruby ./upload_images_to_server.rb create #{visual_host} #{create_arguments} #{auth_creds}")
+
+upload_args = "-f ./tmp-buildfile -d image_set#{test_case}"
+upload_args.concat(' -e') if @is_system_test
+puts "ruby ./upload_images_to_server.rb upload #{visual_host} #{upload_args} #{auth_creds}"
+system("ruby ./upload_images_to_server.rb upload #{visual_host} #{upload_args} #{auth_creds}")
+
+# Clean up downloaded files to avoid confusion
+system('rm ./upload_images_to_server.rb')
\ No newline at end of file
diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb
new file mode 100644
index 0000000..d19212a
--- /dev/null
+++ b/test/application_system_test_case.rb
@@ -0,0 +1,5 @@
+require "test_helper"
+
+class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
+ driven_by :selenium, using: :chrome, screen_size: [1400, 1400]
+end
diff --git a/test/controllers/.keep b/test/controllers/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/test/controllers/builds_controller_test.rb b/test/controllers/builds_controller_test.rb
new file mode 100644
index 0000000..2ae5db9
--- /dev/null
+++ b/test/controllers/builds_controller_test.rb
@@ -0,0 +1,66 @@
+require 'test_helper'
+
+class BuildsControllerTest < ActionController::TestCase
+ include Devise::Test::ControllerHelpers
+
+ setup do
+ sign_in with_test_user
+ @build = FactoryBot.create(:build)
+ @build.project = FactoryBot.create(:project)
+ image_md5s_hash = {}
+ image_md5s_hash['AddAndMoveGridRows/AddAndMoveGridRows_01_InitialGridView'] = '12c755932dad470548c5c47708101e3d'
+ image_md5s_hash['AddAndMoveGridRows/AddAndMoveGridRows_02_ActionsModeOpen'] = '18b6543d01070e5502c1d90d9227d8ca'
+ @build.full_list_of_image_md5s = image_md5s_hash
+ @build.save
+ end
+
+ test 'should get index' do
+ get :index
+ assert_response :success
+ assert_not_nil assigns(:builds)
+ end
+
+ test 'should get new' do
+ get :new
+ assert_response :success
+ end
+
+ test 'should create build' do
+ pr_response = {
+ "user": {
+ "ldap_dn": "CN=john.doe,OU=Users,OU=domain,DC=domaininternal,DC=com"
+ },
+ "head": {
+ "ref": "MOBILEANDROID-4659_FakeBranchToTestPullRequest"
+ }
+ }
+
+ stub_request(:post, "#{SystemTestConfig.bamboo_base_url}/rest/api/latest/result/ANDROIDGITHUBBUILD-ANDRGITHUBPULLREQUEST-26740/comment").
+ to_return(status: 200, body: "", headers: {})
+
+ stub_request(:get, "#{SystemTestConfig.github_root_url}/api/v3/repos/mobile/android/pulls/1704").
+ to_return(body: pr_response.to_json)
+
+ stub_request(:post, "#{SystemTestConfig.github_root_url}/api/v3/repos/mobile/android/statuses/d3e552976815db44ac05983a817f7d1c333ef98e").
+ to_return(status: 200, body: "", headers: {})
+
+ assert_difference('Build.count') do
+ post :create, format: :json, params: {build: {title: @build.title, url: @build.title, project_id: @build.project_id, temporary: @build.temporary, commit_sha: @build.commit_sha,
+ pull_request_number: @build.pull_request_number, image_md5s: @build.image_md5s}}
+ end
+
+ assert_response :created
+ end
+
+ test 'should update build' do
+ patch :update, params: { id: @build, build: { title: @build.title, url: @build.url }, authenticity_token: set_form_authenticity_token }
+ assert_redirected_to build_path(assigns(:build))
+ end
+
+ test 'should destroy build' do
+ assert_difference('Build.count', -1) do
+ delete :destroy, params: { id: @build, authenticity_token: set_form_authenticity_token }
+ end
+ assert_redirected_to builds_path
+ end
+end
diff --git a/test/controllers/diffs_controller_test.rb b/test/controllers/diffs_controller_test.rb
new file mode 100644
index 0000000..5a0199a
--- /dev/null
+++ b/test/controllers/diffs_controller_test.rb
@@ -0,0 +1,36 @@
+require 'test_helper'
+
+class DiffsControllerTest < ActionController::TestCase
+ include Devise::Test::ControllerHelpers
+
+ setup do
+ sign_in with_test_user
+ @diff = FactoryBot.create(:diff)
+ end
+
+ test 'should get new' do
+ get :new
+ assert_response :success
+ end
+
+ test 'should create build' do
+ assert_difference('Diff.count') do
+ post :create, params: { diff: { old_image_id: @diff.old_image_id, new_image_id: @diff.new_image_id, build_id: @diff.build_id }, authenticity_token: set_form_authenticity_token }
+ end
+
+ assert_redirected_to diff_path(assigns(:diff))
+ end
+
+ test 'should update diff' do
+ patch :update, params: {id: @diff, diff: { old_image_id: @diff.old_image_id, new_image_id: @diff.new_image_id }, authenticity_token: set_form_authenticity_token }
+ assert_redirected_to diff_path(assigns(:diff))
+ end
+
+
+ test 'should destroy diff' do
+ assert_difference('Diff.count', -1) do
+ delete :destroy, params: { id: @diff, authenticity_token: set_form_authenticity_token }
+ end
+ assert_redirected_to diffs_path
+ end
+end
diff --git a/test/controllers/jiras_controller_test.rb b/test/controllers/jiras_controller_test.rb
new file mode 100644
index 0000000..67aa95b
--- /dev/null
+++ b/test/controllers/jiras_controller_test.rb
@@ -0,0 +1,53 @@
+require 'test_helper'
+
+class JirasControllerTest < ActionController::TestCase
+ include Devise::Test::ControllerHelpers
+
+ setup do
+ sign_in with_test_user
+ @jira = FactoryBot.create(:jira)
+ end
+
+ test 'should get index' do
+ get :index
+ assert_response :success
+ assert_not_nil assigns(:jiras)
+ end
+
+ test 'should get new' do
+ get :new, params: { jira: { title: @jira.title } }
+ assert_response :success
+ end
+
+ test 'should create jira' do
+ hash = { success: SystemTestConfig.jira_base_url}
+ @controller.stub(:create_jira, hash) do
+ assert_difference('Jira.count') do
+ post :create, params: {jira: {title: @jira.title, jira_link: @jira.jira_link}, authenticity_token: set_form_authenticity_token }
+ end
+ end
+
+ assert_redirected_to @jira.jira_link
+ end
+
+ test 'should show jira' do
+ get :show, params: { id: @jira }
+ assert_response :success
+ end
+
+ test 'should update jira' do
+ hash = { success: SystemTestConfig.jira_base_url}
+ @controller.stub(:create_jira, hash) do
+ patch :update, params: {id: @jira, jira: {title: @jira.title}, authenticity_token: set_form_authenticity_token }
+ end
+ assert_redirected_to @jira.jira_link
+ end
+
+ test 'should destroy jira' do
+ assert_difference('Jira.count', -1) do
+ delete :destroy, params: { id: @jira, authenticity_token: set_form_authenticity_token }
+ end
+
+ assert_redirected_to jiras_path
+ end
+end
diff --git a/test/controllers/projects_controller_test.rb b/test/controllers/projects_controller_test.rb
new file mode 100644
index 0000000..0a27ebc
--- /dev/null
+++ b/test/controllers/projects_controller_test.rb
@@ -0,0 +1,43 @@
+require 'test_helper'
+
+class ProjectsControllerTest < ActionController::TestCase
+ include Devise::Test::ControllerHelpers
+
+ setup do
+ # Must be admin to destroy project
+ sign_in with_test_user
+ @project = FactoryBot.create(:project)
+ end
+
+ test 'should get index' do
+ get :index
+ assert_response :success
+ assert_not_nil assigns(:projects)
+ end
+
+ test 'should get new' do
+ get :new
+ assert_response :success
+ end
+
+ test 'should create project' do
+ assert_difference('Project.count') do
+ post :create, params: { project: { name: @project.name, description: @project.description }, authenticity_token: set_form_authenticity_token }
+ end
+
+ assert_redirected_to project_path(assigns(:project))
+ end
+
+ test 'should update project' do
+ patch :update, params: { id: @project, project: { name: @project.name }, authenticity_token: set_form_authenticity_token }
+ assert_redirected_to project_path(assigns(:project))
+ end
+
+ test 'should destroy project' do
+ assert_difference('Project.count', -1) do
+ delete :destroy, params: { id: @project, authenticity_token: set_form_authenticity_token }
+ end
+
+ assert_redirected_to projects_path
+ end
+end
diff --git a/test/controllers/test_images_controller_test.rb b/test/controllers/test_images_controller_test.rb
new file mode 100644
index 0000000..a12d0e9
--- /dev/null
+++ b/test/controllers/test_images_controller_test.rb
@@ -0,0 +1,26 @@
+require 'test_helper'
+
+class TestImagesControllerTest < ActionController::TestCase
+ include Devise::Test::ControllerHelpers
+
+ setup do
+ sign_in with_test_user
+ @test_image = FactoryBot.create(:test_image)
+ @test_image.build = FactoryBot.create(:build)
+ @test_image.save
+ end
+
+ test 'should get index' do
+ get :index
+ assert_response :success
+ assert_not_nil assigns(:test_images)
+ end
+
+ test 'should destroy test_image' do
+ assert_difference('TestImage.count', -1) do
+ delete :destroy, params: { id: @test_image, authenticity_token: set_form_authenticity_token }
+ end
+
+ assert_redirected_to test_images_path
+ end
+end
diff --git a/test/controllers/tests_controller_test.rb b/test/controllers/tests_controller_test.rb
new file mode 100644
index 0000000..61d48da
--- /dev/null
+++ b/test/controllers/tests_controller_test.rb
@@ -0,0 +1,35 @@
+require 'test_helper'
+
+class TestsControllerTest < ActionController::TestCase
+ include Devise::Test::ControllerHelpers
+
+ setup do
+ sign_in with_test_user
+ @test = FactoryBot.create(:test)
+ end
+
+ test "should get index" do
+ get :index
+ assert_response :success
+ assert_not_nil assigns(:tests)
+ end
+
+ test "should get new" do
+ get :new
+ assert_response :success
+ end
+
+ test "should create test" do
+ assert_difference('Test.count') do
+ post :create, params: { test: { name: @test.name, description: @test.description }, authenticity_token: set_form_authenticity_token }
+ end
+ assert_redirected_to test_path(assigns(:test))
+ end
+
+ test "should destroy test" do
+ assert_difference('Test.count', -1) do
+ delete :destroy, params: { id: @test, authenticity_token: set_form_authenticity_token }
+ end
+ assert_redirected_to tests_path
+ end
+end
diff --git a/test/controllers/users_controller_test.rb b/test/controllers/users_controller_test.rb
new file mode 100644
index 0000000..9f603fc
--- /dev/null
+++ b/test/controllers/users_controller_test.rb
@@ -0,0 +1,20 @@
+require 'test_helper'
+
+class UsersControllerTest < ActionDispatch::IntegrationTest
+ include Devise::Test::IntegrationHelpers
+
+ setup do
+ @user = with_test_user
+ sign_in @user
+ end
+
+ test 'should get index user' do
+ get users_path
+ assert_response :success
+ end
+
+ test 'should get show user' do
+ get user_path(id: @user.id)
+ assert_response :success
+ end
+end
diff --git a/test/factories/builds.rb b/test/factories/builds.rb
new file mode 100644
index 0000000..784a82b
--- /dev/null
+++ b/test/factories/builds.rb
@@ -0,0 +1,12 @@
+FactoryBot.define do
+ factory :build do
+ url "#{SystemTestConfig.bamboo_base_url}/browse/ANDROIDGITHUBBUILD-ANDRGITHUBPULLREQUEST-26740"
+ temporary false
+ title 'ANDROIDGITHUBBUILD-ANDRGITHUBPULLREQUEST-26740'
+ project_id 1
+ commit_sha 'd3e552976815db44ac05983a817f7d1c333ef98e'
+ pull_request_number 1704
+ image_md5s '000000.png=>5d06899520fdbe8fccf6e83eb33998d0'
+ username 'john.doe'
+ end
+end
\ No newline at end of file
diff --git a/test/factories/diffs.rb b/test/factories/diffs.rb
new file mode 100644
index 0000000..1ad69de
--- /dev/null
+++ b/test/factories/diffs.rb
@@ -0,0 +1,7 @@
+FactoryBot.define do
+ factory :diff do
+ old_image_id 1
+ new_image_id 2
+ build_id 1
+ end
+end
diff --git a/test/factories/jiras.rb b/test/factories/jiras.rb
new file mode 100644
index 0000000..be25e6f
--- /dev/null
+++ b/test/factories/jiras.rb
@@ -0,0 +1,13 @@
+FactoryBot.define do
+ factory :jira do
+ title 'Visual Automation Test Issue: LightColors_000000'
+ project SystemTestConfig.jira_project
+ component 'Visual Automation'
+ description 'https://vizzy.com/diffs/21839'
+ issue_type 'Bug'
+ jira_link "#{SystemTestConfig.jira_base_url}/browse/MOBILEANDROID-13135"
+ jira_key 'MOBILEANDROID-13135'
+ diff_id 1
+ priority 'Critical'
+ end
+end
diff --git a/test/factories/projects.rb b/test/factories/projects.rb
new file mode 100644
index 0000000..e86b2ca
--- /dev/null
+++ b/test/factories/projects.rb
@@ -0,0 +1,69 @@
+def unique_id_for_plugin(plugin_name)
+ Rails.root.join('app', 'plugins', plugin_name).to_s.to_sym
+end
+
+def bamboo_plugin_settings
+ {
+ enabled: true,
+ base_bamboo_build_url: {
+ value: SystemTestConfig.bamboo_base_url,
+ display_name: 'Base Bamboo Url',
+ placeholder: "Add base bamboo build url (e.g., 'https://bamboo.com')"
+ }
+ }
+end
+
+def slack_plugin_settings
+ {
+ enabled: true,
+ slack_channel: {
+ value: 'test_channel',
+ display_name: 'Slack Channel',
+ placeholder: "Add slack channel to send build results to (e.g., '#build-status')"
+ }
+ }
+end
+
+def jira_plugin_settings
+ {
+ enabled: true,
+ jira_base_url: {
+ value: SystemTestConfig.jira_base_url,
+ display_name: 'Jira Base Url',
+ placeholder: "Add jira root url (e.g., 'https://jira.com')"
+ },
+ jira_project: {
+ value: SystemTestConfig.jira_project,
+ display_name: 'Jira Project',
+ placeholder: "Add jira project to file tickets (e.g., 'MOBILE')"
+ },
+ jira_component: {
+ value: 'Visual Automation',
+ display_name: 'Jira Component',
+ placeholder: "Add jira component to file tickets (e.g., 'Visual Automation')"
+ }
+ }
+end
+
+def jenkins_plugin_settings
+ {
+ enabled: false
+ }
+end
+
+FactoryBot.define do
+ factory :project do
+ name 'Testing'
+ description 'A Testing Plan'
+ github_root_url SystemTestConfig.github_root_url
+ github_repo SystemTestConfig.github_repo
+ github_status_context 'continuous-integration/vizzy'
+ plugin_settings = {
+ unique_id_for_plugin('bamboo_plugin.rb') => bamboo_plugin_settings,
+ unique_id_for_plugin('slack_plugin.rb') => slack_plugin_settings,
+ unique_id_for_plugin('jira_plugin.rb') => jira_plugin_settings,
+ unique_id_for_plugin('jenkins_plugin.rb') => jenkins_plugin_settings
+ }
+ plugin_settings plugin_settings.deep_symbolize_keys!
+ end
+end
diff --git a/test/factories/test_images.rb b/test/factories/test_images.rb
new file mode 100644
index 0000000..1c70a34
--- /dev/null
+++ b/test/factories/test_images.rb
@@ -0,0 +1,10 @@
+FactoryBot.define do
+ factory :test_image do
+ image_file_name '000000.png'
+ approved false
+ build_id 1
+ test_id 2
+ md5 '5d06899520fdbe8fccf6e83eb33998d0'
+ image { File.new(Rails.root.join('test-image-upload/image_set1/dark_colors/000000.png')) }
+ end
+end
diff --git a/test/factories/tests.rb b/test/factories/tests.rb
new file mode 100644
index 0000000..9be0d20
--- /dev/null
+++ b/test/factories/tests.rb
@@ -0,0 +1,7 @@
+FactoryBot.define do
+ factory :test do
+ name 'Initial'
+ description 'First photo of the landing page'
+ association :project_id, factory: :project
+ end
+end
diff --git a/test/factories/users.rb b/test/factories/users.rb
new file mode 100644
index 0000000..2d17df9
--- /dev/null
+++ b/test/factories/users.rb
@@ -0,0 +1,10 @@
+FactoryBot.define do
+ factory :user do
+ email 'john.doe@gmail.com'
+ username 'john.doe'
+ password '123456789012'
+ authentication_token 'ht2Cey1i9xbxH5jm-gpx'
+ admin true
+ initialize_with { User.find_or_create_by(email: email)}
+ end
+end
\ No newline at end of file
diff --git a/test/helpers/.keep b/test/helpers/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/test/helpers/builds_helper_test.rb b/test/helpers/builds_helper_test.rb
new file mode 100644
index 0000000..597641f
--- /dev/null
+++ b/test/helpers/builds_helper_test.rb
@@ -0,0 +1,4 @@
+require 'test_helper'
+
+class BuildsHelperTest < ActionView::TestCase
+end
diff --git a/test/helpers/diffs_helper_test.rb b/test/helpers/diffs_helper_test.rb
new file mode 100644
index 0000000..54dec01
--- /dev/null
+++ b/test/helpers/diffs_helper_test.rb
@@ -0,0 +1,4 @@
+require 'test_helper'
+
+class DiffsHelperTest < ActionView::TestCase
+end
diff --git a/test/helpers/images_helper_test.rb b/test/helpers/images_helper_test.rb
new file mode 100644
index 0000000..f2a0cf2
--- /dev/null
+++ b/test/helpers/images_helper_test.rb
@@ -0,0 +1,4 @@
+require 'test_helper'
+
+class ImagesHelperTest < ActionView::TestCase
+end
diff --git a/test/helpers/jiras_helper_test.rb b/test/helpers/jiras_helper_test.rb
new file mode 100644
index 0000000..04ceb43
--- /dev/null
+++ b/test/helpers/jiras_helper_test.rb
@@ -0,0 +1,4 @@
+require 'test_helper'
+
+class JirasHelperTest < ActionView::TestCase
+end
diff --git a/test/helpers/projects_helper_test.rb b/test/helpers/projects_helper_test.rb
new file mode 100644
index 0000000..a591e4e
--- /dev/null
+++ b/test/helpers/projects_helper_test.rb
@@ -0,0 +1,4 @@
+require 'test_helper'
+
+class ProjectsHelperTest < ActionView::TestCase
+end
diff --git a/test/helpers/tests_helper_test.rb b/test/helpers/tests_helper_test.rb
new file mode 100644
index 0000000..59b6de2
--- /dev/null
+++ b/test/helpers/tests_helper_test.rb
@@ -0,0 +1,4 @@
+require 'test_helper'
+
+class TestsHelperTest < ActionView::TestCase
+end
diff --git a/test/integration/.keep b/test/integration/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/test/jobs/build_background_commit_job_test.rb b/test/jobs/build_background_commit_job_test.rb
new file mode 100644
index 0000000..9d48ddd
--- /dev/null
+++ b/test/jobs/build_background_commit_job_test.rb
@@ -0,0 +1,7 @@
+require 'test_helper'
+
+class BuildBackgroundCommitJobTest < ActiveJob::TestCase
+ # test "the truth" do
+ # assert true
+ # end
+end
diff --git a/test/mailers/.keep b/test/mailers/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/test/models/.keep b/test/models/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/test/models/build_test.rb b/test/models/build_test.rb
new file mode 100644
index 0000000..d995d76
--- /dev/null
+++ b/test/models/build_test.rb
@@ -0,0 +1,21 @@
+require 'test_helper'
+
+class BuildTest < ActiveSupport::TestCase
+
+ setup do
+ @build = FactoryBot.create(:build)
+ end
+
+ test 'build is valid after creation' do
+ assert @build.valid?
+ end
+
+ test 'build is populated correctly' do
+ assert_equal "#{SystemTestConfig.bamboo_base_url}/browse/ANDROIDGITHUBBUILD-ANDRGITHUBPULLREQUEST-26740", @build.url, 'Url not populated correctly'
+ assert_equal false, @build.temporary, 'temporary not populated correctly'
+ assert_equal 'ANDROIDGITHUBBUILD-ANDRGITHUBPULLREQUEST-26740', @build.title, 'title not populated correctly'
+ assert_equal 'd3e552976815db44ac05983a817f7d1c333ef98e', @build.commit_sha, 'commit_sha not populated correctly'
+ assert_equal '1704', @build.pull_request_number, 'pull_request_number not populated correctly'
+ end
+
+end
diff --git a/test/models/diff_test.rb b/test/models/diff_test.rb
new file mode 100644
index 0000000..323ab05
--- /dev/null
+++ b/test/models/diff_test.rb
@@ -0,0 +1,17 @@
+require 'test_helper'
+
+class DiffTest < ActiveSupport::TestCase
+ setup do
+ @diff = FactoryBot.create(:diff)
+ end
+
+ test 'diff is valid after creation' do
+ assert @diff.valid?
+ end
+
+ test 'diff is populated correctly' do
+ assert_equal 1, @diff.old_image_id, 'old_image_id not populated correctly'
+ assert_equal 2, @diff.new_image_id, 'new_image_id not populated correctly'
+ assert_equal 1, @diff.build_id, 'build_id not populated correctly'
+ end
+end
diff --git a/test/models/image_test.rb b/test/models/image_test.rb
new file mode 100644
index 0000000..819476f
--- /dev/null
+++ b/test/models/image_test.rb
@@ -0,0 +1,20 @@
+require 'test_helper'
+require 'digest/md5'
+
+class ImageTest < ActiveSupport::TestCase
+
+ setup do
+ @test_image = FactoryBot.create(:test_image)
+ end
+
+ test 'test image is valid after creation' do
+ assert @test_image.valid?
+ end
+
+ test 'test image is populated correctly' do
+ assert_equal '000000.png', @test_image.image_file_name, 'image_file_name not populated correctly'
+ assert_equal false, @test_image.approved, 'approved not populated correctly'
+ assert_equal 1, @test_image.build_id, 'build_id not populated correctly'
+ assert_equal 2, @test_image.test_id, 'test_id not populated correctly'
+ end
+end
diff --git a/test/models/jira_test.rb b/test/models/jira_test.rb
new file mode 100644
index 0000000..ab1d2c3
--- /dev/null
+++ b/test/models/jira_test.rb
@@ -0,0 +1,25 @@
+require 'test_helper'
+
+class JiraTest < ActiveSupport::TestCase
+
+ setup do
+ @jira = FactoryBot.create(:jira)
+ end
+
+ test 'Jira is valid after creation' do
+ assert @jira.valid?
+ end
+
+ test 'jira is populated correctly' do
+ assert_equal 'Visual Automation Test Issue: LightColors_000000', @jira.title, 'title not populated correctly'
+ assert_equal SystemTestConfig.jira_project, @jira.project, 'project not populated correctly'
+ assert_equal 'Visual Automation', @jira.component, 'component not populated correctly'
+ assert_equal 'https://vizzy.com/diffs/21839', @jira.description, 'description not populated correctly'
+ assert_equal "#{SystemTestConfig.jira_base_url}/browse/MOBILEANDROID-13135", @jira.jira_link, 'jira_link not populated correctly'
+ assert_equal 'Bug', @jira.issue_type, 'issue_type not populated correctly'
+ assert_equal 'MOBILEANDROID-13135', @jira.jira_key, 'jira_key not populated correctly'
+ assert_equal 1, @jira.diff_id, 'diff_id not populated correctly'
+ assert_equal 'Critical', @jira.priority, 'priority not populated correctly'
+ end
+end
+
diff --git a/test/models/project_test.rb b/test/models/project_test.rb
new file mode 100644
index 0000000..00f7b59
--- /dev/null
+++ b/test/models/project_test.rb
@@ -0,0 +1,20 @@
+require 'test_helper'
+
+class ProjectTest < ActiveSupport::TestCase
+
+ setup do
+ @project = FactoryBot.create(:project)
+ end
+
+ test 'project is valid after creation' do
+ assert @project.valid?
+ end
+
+ test 'project is populated correctly' do
+ assert_equal 'Testing', @project.name, 'name not populated correctly'
+ assert_equal 'A Testing Plan', @project.description, 'description not populated correctly'
+ assert_equal SystemTestConfig.github_root_url, @project.github_root_url, 'github_root_url not populated correctly'
+ assert_equal SystemTestConfig.github_repo, @project.github_repo, 'github_repo not populated correctly'
+ assert_equal 'continuous-integration/vizzy', @project.github_status_context, 'github_status_context not populated correctly'
+ end
+end
diff --git a/test/models/test_test.rb b/test/models/test_test.rb
new file mode 100644
index 0000000..a756aaa
--- /dev/null
+++ b/test/models/test_test.rb
@@ -0,0 +1,17 @@
+require 'test_helper'
+
+class TestTest < ActiveSupport::TestCase
+
+ setup do
+ @test = FactoryBot.create(:test)
+ end
+
+ test 'test is valid after creation' do
+ assert @test.valid?
+ end
+
+ test 'test is populated correctly' do
+ assert_equal 'Initial', @test.name, 'name not populated correctly'
+ assert_equal 'First photo of the landing page', @test.description, 'description not populated correctly'
+ end
+end
diff --git a/test/models/user_test.rb b/test/models/user_test.rb
new file mode 100644
index 0000000..9d3738f
--- /dev/null
+++ b/test/models/user_test.rb
@@ -0,0 +1,10 @@
+require 'test_helper'
+
+class UserTest < ActiveSupport::TestCase
+
+ test 'user is populated correctly' do
+ user = FactoryBot.build(:user)
+ assert_equal 'john.doe@gmail.com', user.email, 'email not populated correctly'
+ assert_equal 'john.doe', user.username, 'username not populated correctly'
+ end
+end
diff --git a/test/plugins/bamboo_plugin_test.rb b/test/plugins/bamboo_plugin_test.rb
new file mode 100644
index 0000000..586a7ad
--- /dev/null
+++ b/test/plugins/bamboo_plugin_test.rb
@@ -0,0 +1,40 @@
+require 'test_helper'
+
+class BambooPluginTest < ActiveSupport::TestCase
+
+ def setup
+ @unique_id = Rails.root.join('app', 'plugins', 'bamboo_plugin.rb').to_s.to_sym
+
+ stub_request(:post, "#{SystemTestConfig.bamboo_base_url}/rest/api/latest/result/ANDROIDGITHUBBUILD-ANDRGITHUBPULLREQUEST-26740/comment").
+ to_return(status: 204)
+
+ @build = FactoryBot.create(:build)
+ @build.project = FactoryBot.create(:project)
+ @build.save
+
+ @bamboo_plugin = BambooPlugin.new(@unique_id)
+ @bamboo_plugin.add_plugin_settings_to_project(@build.project)
+ end
+
+ test 'verify hook calls comment on build' do
+ mock = MiniTest::Mock.new
+ mock.expect(:comment_on_build, {success: 'Ok'}, [@build])
+ @bamboo_plugin.build_created_hook(@build)
+ assert_send([mock, :comment_on_build, @build])
+ end
+
+ test 'bamboo build url has value' do
+ project_settings_hash = @build.project.plugin_settings
+ assert_equal(SystemTestConfig.bamboo_base_url, project_settings_hash[@bamboo_plugin.unique_id][:base_bamboo_build_url][:value])
+ end
+
+ test 'bamboo plugin loaded correctly' do
+ plugin_manager = PluginManager.instance.for_project(@build.project)
+ assert_not_nil(plugin_manager.plugins_hash[@unique_id])
+ end
+
+ test 'bamboo plugin credentials are present' do
+ assert_not_empty(Rails.application.secrets.BAMBOO_USERNAME)
+ assert_not_empty(Rails.application.secrets.BAMBOO_PASSWORD)
+ end
+end
\ No newline at end of file
diff --git a/test/plugins/jenkins_plugin_test.rb b/test/plugins/jenkins_plugin_test.rb
new file mode 100644
index 0000000..2890ad0
--- /dev/null
+++ b/test/plugins/jenkins_plugin_test.rb
@@ -0,0 +1,26 @@
+require 'test_helper'
+
+class JenkinsPluginTest < ActiveSupport::TestCase
+
+ def setup
+ @unique_id = Rails.root.join('app', 'plugins', 'jenkins_plugin.rb').to_s.to_sym
+
+ @build = FactoryBot.create(:build)
+ @build.project = FactoryBot.create(:project)
+ @build.save
+
+ @jenkins_plugin = JenkinsPlugin.new(@unique_id)
+ end
+
+ test 'verify hook calls update description' do
+ mock = MiniTest::Mock.new
+ mock.expect(:update_display_name_and_description, {success: 'Ok'}, [@build])
+ @jenkins_plugin.build_created_hook(@build)
+ assert_send([mock, :update_display_name_and_description, @build])
+ end
+
+ test 'jenkins plugin loaded correctly' do
+ plugin_manager = PluginManager.instance.for_project(@build.project)
+ assert_not_nil(plugin_manager.plugins_hash[@unique_id])
+ end
+end
\ No newline at end of file
diff --git a/test/plugins/jira_plugin_test.rb b/test/plugins/jira_plugin_test.rb
new file mode 100644
index 0000000..393f607
--- /dev/null
+++ b/test/plugins/jira_plugin_test.rb
@@ -0,0 +1,41 @@
+require 'test_helper'
+
+class JiraPluginTest < ActiveSupport::TestCase
+
+ def setup
+ @unique_id = Rails.root.join('app', 'plugins', 'jira_plugin.rb').to_s.to_sym
+
+ @build = FactoryBot.create(:build)
+ @build.project = FactoryBot.create(:project)
+ @build.save
+
+ @jira = FactoryBot.create(:jira)
+
+ @jira_plugin = JiraPlugin.new(@unique_id)
+ @jira_plugin.add_plugin_settings_to_project(@build.project)
+
+ @project_settings_hash = @build.project.plugin_settings
+ end
+
+ test 'jira base url has value' do
+ assert_equal(SystemTestConfig.jira_base_url, @project_settings_hash[@jira_plugin.unique_id][:jira_base_url][:value])
+ end
+
+ test 'jira project has value' do
+ assert_equal(SystemTestConfig.jira_project, @project_settings_hash[@jira_plugin.unique_id][:jira_project][:value])
+ end
+
+ test 'jira component has value' do
+ assert_equal('Visual Automation', @project_settings_hash[@jira_plugin.unique_id][:jira_component][:value])
+ end
+
+ test 'jira plugin loaded correctly' do
+ plugin_manager = PluginManager.instance.for_project(@build.project)
+ assert_not_nil(plugin_manager.plugins_hash[@unique_id])
+ end
+
+ test 'jira plugin credentials are present' do
+ assert_not_empty(Rails.application.secrets.JIRA_USERNAME)
+ assert_not_empty(Rails.application.secrets.JIRA_PASSWORD)
+ end
+end
\ No newline at end of file
diff --git a/test/plugins/slack_plugin_test.rb b/test/plugins/slack_plugin_test.rb
new file mode 100644
index 0000000..330770b
--- /dev/null
+++ b/test/plugins/slack_plugin_test.rb
@@ -0,0 +1,42 @@
+require 'test_helper'
+
+class SlackPluginTest < ActiveSupport::TestCase
+
+ def setup
+ @unique_id = Rails.root.join('app', 'plugins', 'slack_plugin.rb').to_s.to_sym
+
+ @build = FactoryBot.create(:build)
+ @build.project = FactoryBot.create(:project)
+ @build.save
+
+ @slack_plugin = SlackPlugin.new(@unique_id)
+ @slack_plugin.add_plugin_settings_to_project(@build.project)
+ end
+
+ test 'create slack plugin and launch post request' do
+ assert_not_empty(Rails.application.secrets.SLACK_WEBHOOK)
+ slack_hook_url = "https://hooks.slack.com#{Rails.application.secrets.SLACK_WEBHOOK}"
+
+ stub_request(:post, slack_hook_url).
+ to_return(status: 200, body: "", headers: {})
+
+ mock = MiniTest::Mock.new
+ mock.expect(:send_build_commit_slack_update, {success: 'Message Sent'}, [@build])
+ @slack_plugin.build_committed_hook(@build)
+ assert_send([mock, :send_build_commit_slack_update, @build])
+
+ build_failed_slack_response = @slack_plugin.build_failed_hook(@build)
+ assert_equal(true, build_failed_slack_response.key?(:success))
+ end
+
+ test 'slack channel plugin setting has correct value' do
+ project_settings_hash = @build.project.plugin_settings
+ value = project_settings_hash[@slack_plugin.unique_id][:slack_channel][:value]
+ assert_equal('test_channel', value)
+ end
+
+ test 'slack plugin loaded correctly' do
+ plugin_manager = PluginManager.instance.for_project(@build.project)
+ assert_not_nil(plugin_manager.plugins_hash[@unique_id])
+ end
+end
\ No newline at end of file
diff --git a/test/system/builds_test.rb b/test/system/builds_test.rb
new file mode 100644
index 0000000..6633a27
--- /dev/null
+++ b/test/system/builds_test.rb
@@ -0,0 +1,134 @@
+require 'application_system_test_case'
+require 'test_helper'
+
+class BuildsTest < ApplicationSystemTestCase
+ test 'develop 1, pr 1 should cause no diffs' do
+ initialize_build_test
+
+ run_test_build('develop_1')
+
+ visit_last_created_build
+ assert_images_checked(7)
+ assert_differences_found(0)
+ assert_new_tests(7)
+ assert_no_visual_differences_found
+
+ run_test_build('pull_request_1')
+
+ visit_last_created_build
+ take_screenshot
+ assert_images_checked(7)
+ assert_differences_found(0)
+ assert_new_tests(0)
+ assert_successful_tests(7)
+ end
+
+ test 'develop 1, pr 2, pr 2, pr 3 should cause diffs' do
+ initialize_build_test
+
+ run_test_build('develop_1')
+ run_test_build('pull_request_2')
+
+ visit_last_created_build
+ assert_images_checked(7)
+ assert_differences_found(7)
+ assert_new_tests(0)
+
+ open_first_unapproved_diff
+ assert_diffs_page_has_all_content
+
+ approve_current_diff
+ click_button('Next')
+ approve_current_diff
+ approve_current_diff
+
+ # verify that fields on diff page work correctly
+ fill_in('test_comment', with: 'Here is a test comment.')
+ fill_in('test_jira', with: "#{SystemTestConfig.jira_base_url}/browse/MOBILEANDROID-1234")
+ fill_in('test_pull_request_link', with: "#{SystemTestConfig.github_root_url}/mobile/android/pulls/2")
+ click_button('Save')
+ visit_last_created_build
+ page.must_have_content('Here is a test comment.')
+ page.must_have_content("#{SystemTestConfig.jira_base_url}/browse/MOBILEANDROID-1234")
+ page.must_have_content("#{SystemTestConfig.github_root_url}/mobile/android/pulls/2")
+
+ # Assert that running the same pr build again gives same state as you left it
+ run_test_build('pull_request_2')
+ visit_last_created_build
+ assert_images_checked(7)
+ assert_differences_found(7)
+ assert_new_tests(0)
+ assert_diffs_approved(3)
+ assert_diffs_waiting_for_approval(4)
+
+ # Assert PR 3 is unrelated to PR 2
+ # Note that it only uploads 6 images instead of 7
+ run_test_build('pull_request_3')
+ visit_last_created_build
+ assert_images_checked(6)
+ assert_differences_found(6)
+ assert_new_tests(0)
+ assert_missing_tests(1)
+ end
+
+ test 'develop 1, pr 2, develop 2 performs preapproval' do
+ initialize_build_test
+
+ run_test_build('develop_1')
+ run_preapproval_pull_request('pull_request_2')
+
+ visit_last_created_build
+ open_first_unapproved_diff
+ approve_current_diff
+ approve_current_diff
+ approve_current_diff
+
+ run_preapproval_develop('develop_2')
+ visit_last_created_build
+ assert_images_checked(7)
+ assert_differences_found(4)
+ assert_new_tests(0)
+ assert_successful_tests(3)
+ assert_diffs_waiting_for_approval(4)
+ end
+
+ test 'develop 1, pr 2, pr3, develop 2 performs multiple preapproval' do
+ initialize_build_test
+ run_test_build('develop_1')
+
+ run_preapproval_pull_request('pull_request_2')
+ visit_last_created_build
+ approve_all_diffs
+
+ run_preapproval_pull_request('pull_request_3')
+ visit_last_created_build
+ approve_all_diffs
+
+ run_preapproval_develop('develop_2')
+ visit_last_created_build
+ assert_images_checked(7)
+ assert_differences_found(12)
+ assert_new_tests(0)
+ assert_successful_tests(1)
+ assert_diffs_waiting_for_approval(12)
+ open_first_unapproved_diff
+
+ # multiple preapproval diff page
+ page.must_have_button('Approve This Old Image')
+ page.must_have_content('Old Image Pre-Approved By:')
+ page.must_have_content('User: john.doe')
+ page.must_have_content("Pull Request: #{SystemTestConfig.github_root_url}/mobile/android/pull/2")
+ page.must_have_content("Pull Request: #{SystemTestConfig.github_root_url}/mobile/android/pull/3")
+
+ approve_current_diff
+ approve_current_diff
+
+ run_preapproval_develop('develop_2')
+ visit_last_created_build
+ assert_images_checked(7)
+ assert_differences_found(8)
+ assert_new_tests(0)
+ assert_successful_tests(3)
+ assert_diffs_waiting_for_approval(8)
+ end
+end
diff --git a/test/system/jiras_test.rb b/test/system/jiras_test.rb
new file mode 100644
index 0000000..871a51c
--- /dev/null
+++ b/test/system/jiras_test.rb
@@ -0,0 +1,27 @@
+require 'application_system_test_case'
+require 'test_helper'
+
+class JirasTest < ApplicationSystemTestCase
+ test "create jira from diff" do
+ initialize_build_test
+
+ run_test_build('develop_1')
+ run_test_build('pull_request_2')
+ visit_last_created_build
+ open_first_unapproved_diff
+ click_on('Create Jira')
+
+ page.must_have_content('New Jira')
+ page.must_have_content('Project')
+ page.must_have_content('Jira Title')
+ page.must_have_content('Component')
+ page.must_have_content('Issue Type')
+ page.must_have_content('Priority')
+ page.must_have_content('Description')
+ page.must_have_content('Images attached')
+ page.must_have_button('Create Jira')
+
+ click_on('Create Jira')
+ page.must_have_content('Creating issues is turned off')
+ end
+end
diff --git a/test/system/projects_test.rb b/test/system/projects_test.rb
new file mode 100644
index 0000000..34f6619
--- /dev/null
+++ b/test/system/projects_test.rb
@@ -0,0 +1,53 @@
+require 'application_system_test_case'
+require 'test_helper'
+
+class ProjectsTest < ApplicationSystemTestCase
+ test 'login, create project, view user' do
+ WebMock.allow_net_connect!
+ clear_database
+
+ visit root_path
+ assert page.must_have_content('Log In')
+ login
+
+ assert page.must_have_content('Signed in successfully.')
+ assert page.must_have_content('Projects')
+ create_project
+ assert page.must_have_content('Project was successfully created.')
+
+ run_test_build('develop_1')
+ run_test_build('pull_request_2')
+ visit project_path(1)
+
+ assert page.must_have_content('A Testing Plan')
+ assert page.must_have_content('Latest Pull Requests')
+ assert page.must_have_content('Latest Builds')
+ assert page.must_have_button('Current Base Images')
+ assert page.must_have_content('MOBILEANDROID-1840_ClearImageCacheOnLogout')
+ assert page.must_have_content('ANDROIDGITHUBBUILD-ANDRGITHUBPULLREQUEST-26740')
+
+ click_on('Current Base Images')
+ assert page.must_have_content('Base Images')
+ assert page.must_have_content('light_colors')
+ assert page.must_have_content('000000')
+ assert page.must_have_content('000001')
+ assert page.must_have_content('000002')
+ click_on('000002')
+
+ assert page.must_have_content('Test Details')
+ assert page.must_have_content('Name: 000002')
+ assert page.must_have_content('Current Base Image')
+ assert page.must_have_content('ANDROIDGITHUBBUILD-ANDRGITHUBPULLREQUEST-26740')
+ assert page.must_have_content('Parent Test: light_colors')
+
+ visit users_path
+ assert page.must_have_content('Users')
+ assert page.must_have_content('Username')
+ assert page.must_have_content('Email')
+ assert page.must_have_content('Role')
+ click_link('Admin')
+ assert page.must_have_content('User Info')
+ assert page.must_have_content('Email: john.doe@gmail.com')
+ assert page.must_have_content('Role: Admin')
+ end
+end
\ No newline at end of file
diff --git a/test/test_helper.rb b/test/test_helper.rb
new file mode 100644
index 0000000..e1424a4
--- /dev/null
+++ b/test/test_helper.rb
@@ -0,0 +1,190 @@
+require File.expand_path('../../config/environment', __FILE__)
+require 'rails/test_help'
+require 'minitest/rails/capybara'
+require 'rake'
+require 'webmock/minitest'
+require 'minitest/mock'
+
+ENV['RAILS_ENV'] ||= 'test'
+ENV['RAILS_SYSTEM_TESTING_SCREENSHOT'] = 'simple'
+
+Rake::Task.clear # necessary to avoid tasks being loaded several times in dev mode
+VisualAutomation::Application.load_tasks # load rake tasks
+
+class ActiveSupport::TestCase
+ include FactoryBot::Syntax::Methods
+
+ # Add more helper methods to be used by all tests here...
+
+ # -------- Generic helper methods -------- #
+
+ def login
+ user = with_test_user
+ fill_in('Email', with: user.email)
+ fill_in('Password', with: user.password)
+ click_button('Log In')
+ end
+
+ def with_test_user
+ # test user must be an admin for all tests to pass
+ user = User.find_by_email('john.doe@gmail.com')
+ if user.nil?
+ puts "User john.doe@gmail.com does not exist, creating new user"
+ end
+ user = user.nil? ? FactoryBot.create(:user) : user
+ puts "User auth token: #{user.authentication_token}, email: #{user.email}, admin: #{user.admin}"
+ user
+ end
+
+ # -------- Project helper methods -------- #
+
+ def create_project
+ click_on('New Project')
+ find("input[id$='project_name']").set 'Testing'
+ find("input[id$='project_description']").set 'A Testing Plan'
+ find("input[id$='project_github_root_url']").set SystemTestConfig.github_root_url
+ find("input[id$='project_github_repo']").set SystemTestConfig.github_repo
+ find("input[id$='project_github_status_context']").set 'continuous-integration/vizzy'
+
+ # plugin settings
+ jira_input_id = get_input_id_for_plugin('jira_plugin.rb')
+ find("input[id='#{jira_input_id}_jira_project_value']").set SystemTestConfig.jira_project
+ find("input[id='#{jira_input_id}_jira_base_url_value']").set SystemTestConfig.jira_base_url
+ find("input[id='#{get_input_id_for_plugin('bamboo_plugin.rb')}_base_bamboo_build_url_value']").set SystemTestConfig.bamboo_base_url
+ find("input[id='#{get_input_id_for_plugin('slack_plugin.rb')}_slack_channel_value']").set 'test_channel'
+
+ click_on('Submit')
+ end
+
+ def get_input_id_for_plugin(plugin)
+ rails_root_join = Rails.root.join('app', 'plugins', plugin).to_s.gsub('/', '_')
+ "plugin_settings_#{rails_root_join}"
+ end
+
+ # -------- Build helper methods -------- #
+
+ def initialize_build_test
+ WebMock.allow_net_connect!
+
+ clear_database
+
+ visit projects_path
+ login
+ FactoryBot.create(:project)
+ end
+
+ def clear_database
+ # clear the database with the current schema
+ system("rails db:environment:set RAILS_ENV=#{ENV['RAILS_ENV']}")
+ system('rake db:schema:load')
+ end
+
+ def run_test_build(build_ref)
+ rake_task, uri_with_port = get_build_params_and_reenable_rake_task(build_ref)
+ Rake.application[rake_task].invoke(uri_with_port)
+ end
+
+ def run_preapproval_pull_request(build_ref)
+ rake_task, uri_with_port = get_build_params_and_reenable_rake_task(build_ref)
+ pull_request_number = build_ref.rpartition('_').last.to_i
+ pull_request_preapproval_git_sha = github_recent_commits[pull_request_number]
+ Rake.application[rake_task].invoke(uri_with_port, pull_request_preapproval_git_sha, pull_request_number)
+ end
+
+ def run_preapproval_develop(build_ref)
+ rake_task, uri_with_port = get_build_params_and_reenable_rake_task(build_ref)
+ develop_most_recent_sha = github_recent_commits.first
+ Rake.application[rake_task].invoke(uri_with_port, develop_most_recent_sha)
+ end
+
+ def visit_last_created_build
+ id = get_build_id
+ puts "Visiting build with id: #{id}"
+ visit build_path(id)
+ end
+
+ def assert_diffs_page_has_all_content
+ page.must_have_button('Approve New Image')
+ page.must_have_button('Create Jira')
+ page.must_have_button('Next')
+ page.must_have_button('Base Images for Test')
+ page.must_have_button('Test History')
+ page.must_have_button('Save')
+ page.must_have_content('Jira')
+ page.must_have_content('Pull Request')
+ page.must_have_content('Comment')
+ end
+
+ def assert_successful_tests(number)
+ page.must_have_content("#{number} successful test(s)")
+ end
+
+ def assert_images_checked(number)
+ page.must_have_content("#{number} images checked")
+ end
+
+ def assert_differences_found(number)
+ page.must_have_content("#{number} difference(s) found")
+ end
+
+ def assert_new_tests(number)
+ page.must_have_content("#{number} new test(s) added")
+ end
+
+ def assert_missing_tests(number)
+ page.must_have_content("#{number} missing test(s)")
+ end
+
+ def assert_no_visual_differences_found
+ page.must_have_content 'No visual differences were found between this build and the base images.'
+ end
+
+ def assert_diffs_waiting_for_approval(number)
+ page.must_have_content("#{number} diffs waiting for approval")
+ end
+
+ def assert_diffs_approved(number)
+ page.must_have_content("#{number} diffs approved")
+ end
+
+ def open_first_unapproved_diff
+ build = Build.find(get_build_id)
+ first_diff = build.unapproved_diffs.first
+ visit diff_path(first_diff.id)
+ end
+
+ def approve_current_diff
+ click_button('Approve New Image')
+ end
+
+ def approve_all_diffs
+ click_button('Approve All Images')
+ end
+
+ private
+ def get_build_id
+ temp_build_file = File.read(Rails.root.join('test-image-upload', 'tmp-buildfile'))
+ data = JSON.parse(temp_build_file)
+ data['id']
+ end
+
+ def get_build_params_and_reenable_rake_task(build_ref)
+ current_url = URI.parse(page.current_url)
+ uri_with_port = "http://#{current_url.host}:#{current_url.port}"
+ rake_task = "run_test_build:#{build_ref}"
+ Rake.application[rake_task].reenable
+ [rake_task, uri_with_port]
+ end
+
+ def github_recent_commits
+ associated_git_shas = []
+ GithubService.run(Project.find(1).github_root_url, Project.find(1).github_repo) do |service|
+ associated_git_shas = service.most_recent_github_commits
+ end
+ associated_git_shas
+ end
+
+ def set_form_authenticity_token
+ session[:_csrf_token] = SecureRandom.base64(32)
+ end
+end