rails_template_18f 0.2.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -0
  3. data/Gemfile +0 -2
  4. data/Gemfile.lock +3 -2
  5. data/README.md +12 -15
  6. data/exe/rails_template_18f +60 -0
  7. data/lib/generators/rails_template18f/active_storage/active_storage_generator.rb +142 -0
  8. data/lib/generators/rails_template18f/active_storage/templates/app/jobs/file_scan_job.rb +33 -0
  9. data/lib/generators/rails_template18f/active_storage/templates/app/models/file_upload.rb +25 -0
  10. data/lib/generators/rails_template18f/active_storage/templates/doc/adr/clamav.md.tt +30 -0
  11. data/lib/generators/rails_template18f/active_storage/templates/spec/jobs/file_scan_job_spec.rb +35 -0
  12. data/lib/generators/rails_template18f/active_storage/templates/spec/models/file_upload_spec.rb +38 -0
  13. data/lib/generators/rails_template18f/circleci/circleci_generator.rb +4 -1
  14. data/lib/generators/rails_template18f/cloud_gov_config/cloud_gov_config_generator.rb +29 -0
  15. data/lib/generators/rails_template18f/cloud_gov_config/templates/app/models/cloud_gov_config.rb +15 -0
  16. data/lib/generators/rails_template18f/cloud_gov_config/templates/spec/models/cloud_gov_config_spec.rb +44 -0
  17. data/lib/generators/rails_template18f/i18n/i18n_generator.rb +106 -0
  18. data/{templates → lib/generators/rails_template18f/i18n/templates}/config/locales/en.yml.tt +3 -3
  19. data/{templates → lib/generators/rails_template18f/i18n/templates}/config/locales/es.yml +3 -3
  20. data/{templates → lib/generators/rails_template18f/i18n/templates}/config/locales/fr.yml +3 -6
  21. data/{templates → lib/generators/rails_template18f/i18n/templates}/config/locales/zh.yml +0 -0
  22. data/lib/generators/rails_template18f/i18n_js/i18n_js_generator.rb +59 -0
  23. data/lib/generators/rails_template18f/i18n_js/templates/lib/tasks/i18n.rake +9 -0
  24. data/lib/generators/rails_template18f/newrelic/newrelic_generator.rb +2 -0
  25. data/lib/generators/rails_template18f/sidekiq/sidekiq_generator.rb +72 -0
  26. data/lib/generators/rails_template18f/sidekiq/templates/config/initializers/redis.rb +14 -0
  27. data/lib/generators/rails_template18f/terraform/templates/terraform/production/main.tf.tt +37 -5
  28. data/lib/generators/rails_template18f/terraform/templates/terraform/shared/clamav/main.tf.tt +50 -0
  29. data/lib/generators/rails_template18f/terraform/templates/terraform/shared/clamav/providers.tf +16 -0
  30. data/lib/generators/rails_template18f/terraform/templates/terraform/shared/clamav/variables.tf +47 -0
  31. data/lib/generators/rails_template18f/terraform/templates/terraform/shared/redis/main.tf.tt +23 -0
  32. data/lib/generators/rails_template18f/terraform/templates/terraform/shared/redis/providers.tf +16 -0
  33. data/lib/generators/rails_template18f/terraform/templates/terraform/shared/redis/variables.tf +42 -0
  34. data/lib/generators/rails_template18f/terraform/templates/terraform/staging/main.tf.tt +37 -5
  35. data/lib/generators/rails_template18f/terraform/terraform_generator.rb +0 -11
  36. data/lib/rails_template18f/app_updater.rb +19 -0
  37. data/lib/rails_template18f/generators/base.rb +37 -5
  38. data/lib/rails_template18f/generators/cloud_gov_options.rb +0 -4
  39. data/lib/rails_template18f/version.rb +1 -1
  40. data/rails-template-18f.gemspec +2 -0
  41. data/template.rb +78 -96
  42. data/templates/config/deployment/staging.yml +1 -1
  43. data/templates/config/environments/ci.rb +0 -1
  44. data/templates/doc/compliance/apps/application.boundary.md.tt +0 -7
  45. metadata +59 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f3b689fcfbc4d7ed1a476adc0b415ed0cbbfc24828937431820b5850db77e564
4
- data.tar.gz: 5f95740853330bc904b23c67059a578b3f616cea6228c7c99a19d4eee5c26518
3
+ metadata.gz: ccf6b140f15f85229d716db5c0b51db5b88b41172000f89a613bdc09975c61f7
4
+ data.tar.gz: 6a3a14efb4cb1373236a53260cf06b161375346d37097563ef65197862e538a8
5
5
  SHA512:
6
- metadata.gz: 6c350c36aeddf44806c2ddbd36dbacd626f361890a9a648ec6e58e4dbeba5b60928ece499542bae318f7f7f644af52b55c7b9e9f6639291f7dd00c00229f5334
7
- data.tar.gz: 75079b3718a90069ae1484b54b86dac37c89be836bd132afc8bd936fb0e1451aa85882a5fafa8a980f60094ce352fb933d7ae0179a557a9b6196d018d2a681c1
6
+ metadata.gz: 8fb2a0c4a4adf9fa72ea8078a6b1001671f2e562ece7b3d74b64ffa39c113b96c280f64a3eacc0233b67c8bc2b6ec160b2867c78e49363100d1cdc7055352516
7
+ data.tar.gz: 48e96e771a2fb4dd8ca03719f923ae2749b6b212577c88c8e72a3e2788d71b1a6a6bda768611509aa2001e129a7a29c833ad99a93433bf954c46552671d40c2b
data/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.4.1] - 2022-02-25
4
+
5
+ - update gem dependencies
6
+ - fix issues when included gem hadn't been previously installed
7
+
8
+ ## [0.4.0] - 2022-02-24
9
+
10
+ - helper script to run rails app:update
11
+ - cloud.gov configuration helper generator
12
+ - activestorage/clamav generator
13
+ - activejob/sidekiq generator
14
+ - i18n-js generator
15
+
16
+ ## [0.3.0] - 2022-02-17
17
+
18
+ - i18n generator
19
+ - helper script to run rails new without cloning repo
20
+
3
21
  ## [0.2.0] - 2022-02-16
4
22
 
5
23
  - terraform generator
data/Gemfile CHANGED
@@ -7,6 +7,4 @@ gemspec
7
7
 
8
8
  gem "rake", "~> 13.0"
9
9
 
10
- gem "colorize", "~> 0.8"
11
-
12
10
  gem "byebug"
data/Gemfile.lock CHANGED
@@ -1,9 +1,11 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rails_template_18f (0.2.0)
4
+ rails_template_18f (0.4.1)
5
5
  activesupport (~> 7.0.0)
6
+ colorize (~> 0.8)
6
7
  railties (~> 7.0.0)
8
+ thor (~> 1.0)
7
9
 
8
10
  GEM
9
11
  remote: https://rubygems.org/
@@ -124,7 +126,6 @@ PLATFORMS
124
126
  DEPENDENCIES
125
127
  ammeter (~> 1.1)
126
128
  byebug
127
- colorize (~> 0.8)
128
129
  rails_template_18f!
129
130
  rake (~> 13.0)
130
131
  rspec (~> 3.11)
data/README.md CHANGED
@@ -7,35 +7,32 @@ See the `rails-6` branch for Rails 6.1.x
7
7
 
8
8
  ## Use for new Rails Project
9
9
 
10
- 1. Clone this repository to your computer
11
- 1. Change directory into the clone
12
- 1. Run `rails new <<PATH_TO_PROJECT>> --rc=<<RC_FILE>>` with the appropriate rc file for your needs. The path should not be a subdirectory of this repository.
10
+ 1. `gem install rails_template_18f`
11
+ 1. `rails_template_18f help new` for usage instructions
13
12
 
14
- ### Choosing which RC file to use
15
-
16
- You should run this template with either `railsrc` or `railsrc-hotwire` depending on your development needs.
13
+ ### Choosing whether to use `--hotwire`
17
14
 
18
15
  #### Server Rendered _or_ Single Page Applications
19
16
 
20
- `rails new <<PATH_TO_PROJECT>> --rc=railsrc`
17
+ `rails_template_18f new <<PATH_TO_PROJECT>>` _or_ `rails_template_18f new <<PATH_TO_PROJECT>> --no-hotwire`
21
18
 
22
- The base `railsrc` file creates a Rails application that is appropriate for both server-rendered applications,
19
+ This creates a Rails application that is appropriate for both server-rendered applications,
23
20
  as well as a basis for installing a separate Single Page Application (SPA) library such as React.
24
21
 
25
22
  #### A bit more JavaScript needed
26
23
 
27
- `rails new <<PATH_TO_PROJECT>> --rc=railsrc-hotwire`
24
+ `rails_template_18f new <<PATH_TO_PROJECT>> --hotwire`
28
25
 
29
- The `railsrc-hotwire` file creates a Rails application that includes the [Hotwire](https://hotwired.dev/) JavaScript framework.
26
+ This creates a Rails application that includes the [Hotwire](https://hotwired.dev/) JavaScript framework.
30
27
 
31
28
  Hotwire can be used to add [a bit of JavaScript](https://engineering.18f.gov/web-architecture/#:~:text=are%20more%20complex-,If%20your%20use%20case%20requires%20a%20bit%20of%20client%2Dside%20interactivity%2C%20use%20the%20above%20options%20with%20a%20bit%20of%20JavaScript.,-You%20might%20use)
32
29
  for more interactivity than server-rendered apps, but less than a full SPA.
33
30
 
34
31
  ### Available Options
35
32
 
36
- The following options can be added after `--rc=<<RC_FILE>>` to change how the template behaves.
33
+ The following options can be added to change how the template behaves.
37
34
 
38
- **Important:** You must not pass `--skip-bundle` or `--skip-javascript` to `rails new` or various aspects of the template will be broken
35
+ **Important:** You must not pass `--skip-bundle` or `--skip-javascript` to `rails_template_18f` or various aspects of the template will be broken
39
36
 
40
37
  #### `--javascript=esbuild`
41
38
 
@@ -46,7 +43,7 @@ maintaining IE11 support with esbuild may be tricky.
46
43
 
47
44
  Each of the skipped frameworks in `railsrc` can be overridden on the command line. For example: `--no-skip-active-storage` will include support for `ActiveStorage` document uploads
48
45
 
49
- ### What `railsrc` does
46
+ ### What default use or `--no-hotwire` does
50
47
 
51
48
  ```
52
49
  --skip-active-storage # don't include ActiveStorage for document upload
@@ -61,9 +58,9 @@ Each of the skipped frameworks in `railsrc` can be overridden on the command lin
61
58
  --database=postgresql # default to PostgreSQL
62
59
  ```
63
60
 
64
- ### What `railsrc-hotwire` does
61
+ ### What `--hotwire` does
65
62
 
66
- `railsrc-hotwire` is identical to `railsrc` except that [Hotwire](https://hotwired.dev/) and [ActionCable](https://guides.rubyonrails.org/action_cable_overview.html) are not skipped.
63
+ Identical to `--no-hotwire` except that [Hotwire](https://hotwired.dev/) and [ActionCable](https://guides.rubyonrails.org/action_cable_overview.html) are not skipped.
67
64
 
68
65
  ActionCable is included to enable the [Turbo Streams](https://turbo.hotwired.dev/handbook/streams) functionality of Hotwire.
69
66
 
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "thor"
5
+ require_relative "../lib/rails_template18f/version"
6
+
7
+ class CLI < Thor
8
+ include Thor::Actions
9
+
10
+ desc "new APP_DIRECTORY [options] [rails new arguments]", "Run rails new with 18F flavor"
11
+ option :hotwire, type: :boolean, default: false, desc: "Enable hotwire JS framework"
12
+ long_desc <<-LONGDESC
13
+ Create a new rails application in <APP_DIRECTORY> as customized by
14
+
15
+ * railsrc: https://github.com/18F/rails-template/blob/main/railsrc
16
+
17
+ * template.rb: https://github.com/18F/rails-template/blob/main/template.rb
18
+
19
+ with --hotwire option, includes the Hotwire JS framework
20
+
21
+ all other arguments will be passed as-is to `rails new`
22
+ LONGDESC
23
+ def new(app_directory, *rails_arguments)
24
+ gem_path = File.expand_path("..", __dir__)
25
+ railsrc = options[:hotwire] ? "railsrc-hotwire" : "railsrc"
26
+ run "rails new #{app_directory} --rc=#{File.join(gem_path, railsrc)} --template=#{File.join(gem_path, "template.rb")} #{rails_arguments.join(" ")}"
27
+ end
28
+
29
+ desc "update", "Run rails app:update with some enhancements"
30
+ long_desc <<-LONGDESC
31
+ Run `rails app:update` with frameworks fully defined by what is commented out at the top
32
+ of config/application.rb
33
+
34
+ Example: to enable ActiveStorage
35
+
36
+ 1) Uncomment `require "active_storage/engine"` in `config/application.rb`
37
+
38
+ 2) Run `bin/rails active_storage:install`
39
+
40
+ 3) Run bundle exec rails_template_18f update
41
+
42
+ 4) Optional: run other rails_template18f generators that may be applicable
43
+ LONGDESC
44
+ def update
45
+ require_relative "../lib/rails_template18f/app_updater"
46
+ require "rails/command"
47
+ Rails::Command.invoke "app:update"
48
+ end
49
+
50
+ desc "version", "Output gem version"
51
+ def version
52
+ puts RailsTemplate18f::VERSION
53
+ end
54
+
55
+ def self.exit_on_failure?
56
+ true
57
+ end
58
+ end
59
+
60
+ CLI.start(ARGV)
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module RailsTemplate18f
6
+ module Generators
7
+ class ActiveStorageGenerator < ::Rails::Generators::Base
8
+ include Base
9
+
10
+ desc <<~DESC
11
+ Description:
12
+ Document use of Clamav as ActiveStorage scanner
13
+ DESC
14
+
15
+ def configure_active_storage
16
+ generate "rails_template18f:cloud_gov_config", inline: true
17
+ rails_command "active_storage:install", inline: true
18
+ comment_lines "config/environments/production.rb", /active_storage\.service/
19
+ insert_into_file "config/environments/production.rb", "\n config.active_storage.service = :amazon", after: /active_storage\.service.*$/
20
+ environment "config.active_storage.service = :local", env: "ci"
21
+ append_to_file "config/storage.yml", <<~EOYAML
22
+
23
+ amazon:
24
+ service: S3
25
+ access_key_id: <%= CloudGovConfig.dig(:s3, :credentials, :access_key_id) %>
26
+ secret_access_key: <%= CloudGovConfig.dig(:s3, :credentials, :secret_access_key) %>
27
+ region: us-gov-west-1
28
+ bucket: <%= CloudGovConfig.dig(:s3, :credentials, :bucket) %>
29
+ EOYAML
30
+ end
31
+
32
+ def install_gems
33
+ faraday_installed = gem_installed?("faraday")
34
+ middleware_installed = gem_installed?("faraday-multipart")
35
+ sdk_installed = gem_installed?("aws-sdk-s3")
36
+ return if faraday_installed && middleware_installed && sdk_installed
37
+ gem "faraday", "~> 2.2" unless faraday_installed
38
+ gem "faraday-multipart", "~> 1.0" unless middleware_installed
39
+ unless sdk_installed
40
+ gem_group :production do
41
+ gem "aws-sdk-s3", "~> 1.112"
42
+ end
43
+ end
44
+ bundle_install
45
+ end
46
+
47
+ def create_scanned_upload_model_and_job
48
+ generate :migration, "CreateFileUploads", "file:attachment", "record:references{polymorphic}", "scan_status:string", inline: true
49
+ migration_file = Dir.glob(File.expand_path(File.join("db", "migrate", "[0-9]*_*.rb"), destination_root)).grep(/\d+_create_file_uploads.rb$/).first
50
+ unless migration_file.nil?
51
+ gsub_file migration_file, ":scan_status", ":scan_status, null: false, default: \"uploaded\""
52
+ end
53
+ directory "app"
54
+ directory "spec"
55
+ end
56
+
57
+ def configure_local_clamav_runner
58
+ append_to_file "Procfile.dev", "clamav: docker run --rm -p 9443:9443 ajilaag/clamav-rest:20211229\n"
59
+ end
60
+
61
+ def configure_clamav_env_var
62
+ append_to_file ".env", <<~EOM
63
+
64
+
65
+ # CLAMAV_API_URL tells FileScanJob where to send files for virus scans
66
+ CLAMAV_API_URL=https://localhost:9443
67
+ EOM
68
+ insert_into_file "manifest.yml", " CLAMAV_API_URL: \"https://#{app_name}-clamapi-((env)).apps.internal:9443\"\n", before: /^\s+processes:/
69
+ insert_into_file "manifest.yml", "\n - #{app_name}-s3-((env))", after: "services:"
70
+ end
71
+
72
+ def update_boundary_diagram
73
+ boundary_filename = "doc/compliance/apps/application.boundary.md"
74
+
75
+ insert_into_file boundary_filename, indent(<<~EOB, 16), after: /ContainerDb\(app_db.*$\n/
76
+ Container(clamav, "File Scanning API", "ClamAV", "Internal application for scanning user uploads")
77
+ ContainerDb(app_s3, "File Storage", "AWS S3", "User-uploaded file storage")
78
+ EOB
79
+ insert_into_file boundary_filename, <<~EOB, before: "@enduml"
80
+ Rel(app, app_s3, "reads/writes file data", "https (443)")
81
+ EOB
82
+ if has_active_job?
83
+ insert_into_file boundary_filename, <<~EOB, before: "@enduml"
84
+ Rel(worker, app_s3, "reads/writes file data", "https (443)")
85
+ Rel(worker, clamav, "scans files", "https (9443)")
86
+ EOB
87
+ end
88
+ end
89
+
90
+ def update_data_model_uml
91
+ insert_into_file "doc/compliance/apps/data.logical.md", data_model_uml, before: "@enduml"
92
+ end
93
+
94
+ def generate_adr
95
+ adr_dir = File.expand_path(File.join("doc", "adr"), destination_root)
96
+ if Dir.exist? adr_dir
97
+ @next_adr_id = `ls #{adr_dir} | tail -n 1 | awk -F '-' '{print $1}'`.strip.to_i + 1
98
+ template "doc/adr/clamav.md", "doc/adr/#{"%04d" % @next_adr_id}-clamav-file-scanning.md"
99
+ end
100
+ end
101
+
102
+ no_tasks do
103
+ def data_model_uml
104
+ <<~UML
105
+ class file_uploads {
106
+ * id : bigint <<generated>>
107
+ * scan_status : string
108
+ * record_id : bigint
109
+ * record_type : string
110
+ }
111
+ class active_storage_attachments {
112
+ * id : bigint <<generated>>
113
+ * name : string
114
+ * record_type : string
115
+ * record_id : bigint
116
+ * blob_id : bigint
117
+ * created_at : timestamp without time zone
118
+ }
119
+ class active_storage_blobs {
120
+ * id : bigint <<generated>>
121
+ * key : string
122
+ * filename : string
123
+ content_type : string
124
+ metadata : text
125
+ * service_name : string
126
+ * byte_size : bigint
127
+ checksum : string
128
+ * created_at : timestamp without time zone
129
+ }
130
+ class active_storage_variant_records {
131
+ * id : bigint <<generated>>
132
+ * variation_digest : string
133
+ }
134
+ file_uploads ||--|| active_storage_attachments
135
+ active_storage_attachments ||--|{ active_storage_blobs
136
+ active_storage_variant_records ||--|{ active_storage_blobs
137
+ UML
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,33 @@
1
+ class FileScanJob < ApplicationJob
2
+ queue_as :default
3
+
4
+ def perform(file_upload)
5
+ return if file_upload.nil? || file_upload.clean?
6
+ file_upload.open do |file|
7
+ payload = {file: Faraday::Multipart::FilePart.new(
8
+ file,
9
+ file_upload.content_type,
10
+ file_upload.filename
11
+ )}
12
+ response = connection.post("/scan", payload)
13
+ if response.success?
14
+ file_upload.update_columns scan_status: "scanned", updated_at: Time.now
15
+ else
16
+ logger.error "File Scan for #{file_upload.id} failed: #{response.body}"
17
+ file_upload.update_columns scan_status: "quarantined", updated_at: Time.now
18
+ end
19
+ end
20
+ rescue => ex
21
+ file_upload&.update_columns scan_status: "scan_failed", updated_at: Time.now
22
+ raise ex
23
+ end
24
+
25
+ def connection
26
+ @connection ||= Faraday.new(
27
+ url: ENV["CLAMAV_API_URL"],
28
+ ssl: {verify: false}
29
+ ) do |f|
30
+ f.request :multipart
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,25 @@
1
+ class FileUpload < ApplicationRecord
2
+ belongs_to :record, polymorphic: true
3
+ has_one_attached :file
4
+
5
+ delegate :open, :content_type, to: :file
6
+
7
+ validates_presence_of :file
8
+ validates_inclusion_of :scan_status, in: %w[uploaded scan_failed scanned quarantined]
9
+
10
+ after_commit :scan
11
+
12
+ def clean?
13
+ scan_status == "scanned"
14
+ end
15
+
16
+ def filename
17
+ file.filename.to_s
18
+ end
19
+
20
+ private
21
+
22
+ def scan
23
+ FileScanJob.perform_later self
24
+ end
25
+ end
@@ -0,0 +1,30 @@
1
+ # <%= @next_adr_id %>. ClamAV File Scanning
2
+
3
+ Date: <%= Date.today.iso8601 %>
4
+
5
+ ## Status
6
+
7
+ Accepted
8
+
9
+ ## Context
10
+
11
+ In order to satisfy the [RA-5](https://nvd.nist.gov/800-53/Rev4/control/RA-5)
12
+ control around vulnerability scanning, we wish to scan all user-uploaded files
13
+ with a malware detection service. These files are user-supplied, and thus
14
+ cannot fit into our other security controls built into the CI/CD pipeline.
15
+
16
+ ## Decision
17
+
18
+ We will run a ClamAV daemon, fronted by a REST API that we will pass all user-uploaded
19
+ files through as part of the upload process.
20
+ <% if terraform_dir_exists? %>
21
+ The ClamAV app is deployed along with other infrastructure by terraform.
22
+ <% else %>
23
+ The ClamAV app is based on a [separate government-controlled repository](https://github.com/18f/clamav-api-cg-app)
24
+ <% end %>
25
+
26
+ ## Consequences
27
+
28
+ While our user-supplied files will now have vulnerability protection, this architecture
29
+ does not provide scanning of the application itself. Therefore we must find other solutions
30
+ to addressing SI-3 or RA-5 in the context of the application.
@@ -0,0 +1,35 @@
1
+ require "rails_helper"
2
+
3
+ RSpec.describe FileScanJob, type: :job do
4
+ subject { described_class.new }
5
+ let(:scanned_file) { double(clean?: true) }
6
+ let(:unscanned_file) { double(id: 1, clean?: false, content_type: "text/plain", filename: "test.txt") }
7
+ let(:success_response) { double(success?: true) }
8
+ let(:error_response) { double(success?: false, body: "Error response body") }
9
+
10
+ it "deals with a nil argument" do
11
+ expect { subject.perform nil }.to_not raise_error
12
+ end
13
+
14
+ it "returns quickly if the file is already scanned" do
15
+ expect { subject.perform scanned_file }.to_not raise_error
16
+ end
17
+
18
+ it "updates the scan_status after scanning the file" do
19
+ now = Time.now
20
+ allow(Time).to receive(:now).and_return now
21
+ allow(unscanned_file).to receive(:open).and_yield __FILE__
22
+ expect(unscanned_file).to receive(:update_columns).with scan_status: "scanned", updated_at: Time.now
23
+ allow(subject).to receive(:connection).and_return double(post: success_response)
24
+ subject.perform unscanned_file
25
+ end
26
+
27
+ it "marks the file as quarantined when dirty" do
28
+ now = Time.now
29
+ allow(Time).to receive(:now).and_return now
30
+ allow(unscanned_file).to receive(:open).and_yield __FILE__
31
+ expect(unscanned_file).to receive(:update_columns).with scan_status: "quarantined", updated_at: Time.now
32
+ allow(subject).to receive(:connection).and_return double(post: error_response)
33
+ subject.perform unscanned_file
34
+ end
35
+ end
@@ -0,0 +1,38 @@
1
+ require "rails_helper"
2
+
3
+ RSpec.describe FileUpload, type: :model do
4
+ subject { described_class.new }
5
+
6
+ describe "validations" do
7
+ before do
8
+ subject.file.attach(io: File.open(__FILE__), filename: "file_upload_spec.rb")
9
+ end
10
+
11
+ %w[uploaded scan_failed scanned quarantined].each do |valid_status|
12
+ it "allows scan_status=#{valid_status}" do
13
+ pending "#{described_class.name} cannot be valid without a record to belong_to"
14
+ subject.scan_status = valid_status
15
+ expect(subject).to be_valid
16
+ end
17
+ end
18
+
19
+ it "is invalid with a bad scan_status" do
20
+ subject.scan_status = "invalid"
21
+ expect(subject).to_not be_valid
22
+ end
23
+ end
24
+
25
+ describe "#clean?" do
26
+ it "returns true when scan_status is scanned" do
27
+ subject.scan_status = "scanned"
28
+ expect(subject).to be_clean
29
+ end
30
+
31
+ it "returns false when scan_status is not scanned" do
32
+ subject.scan_status = "uploaded"
33
+ expect(subject).to_not be_clean
34
+ subject.scan_status = "quarantined"
35
+ expect(subject).to_not be_clean
36
+ end
37
+ end
38
+ end
@@ -14,7 +14,10 @@ module RailsTemplate18f
14
14
  DESC
15
15
 
16
16
  def install_needed_gems
17
- gem "rspec_junit_formatter", "~> 0.5", group: :test
17
+ gem_name = "rspec_junit_formatter"
18
+ return if gem_installed? gem_name
19
+ gem gem_name, "~> 0.5", group: :test
20
+ bundle_install
18
21
  end
19
22
 
20
23
  def install_pipeline
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module RailsTemplate18f
6
+ module Generators
7
+ class CloudGovConfigGenerator < ::Rails::Generators::Base
8
+ include Base
9
+
10
+ desc <<~DESC
11
+ Description:
12
+ Install a helper class to retrieve configuration from ENV["VCAP_SERVICES"]
13
+ DESC
14
+
15
+ def install_climate_control
16
+ return if gem_installed?("climate_control")
17
+ gem_group :test do
18
+ gem "climate_control", "~> 1.0"
19
+ end
20
+ bundle_install
21
+ end
22
+
23
+ def install_model_and_test
24
+ copy_file "app/models/cloud_gov_config.rb"
25
+ copy_file "spec/models/cloud_gov_config_spec.rb"
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CloudGovConfig
4
+ ENV_VARIABLE = "VCAP_SERVICES"
5
+
6
+ def self.dig(*path)
7
+ return nil if ENV[ENV_VARIABLE].blank?
8
+ first, *rest = path
9
+ vcap_services[first]&.first&.dig(*rest)
10
+ end
11
+
12
+ def self.vcap_services
13
+ @vcap_services ||= JSON.parse(ENV[ENV_VARIABLE]).with_indifferent_access
14
+ end
15
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails_helper"
4
+
5
+ RSpec.describe CloudGovConfig, type: :model do
6
+ subject { described_class }
7
+
8
+ describe ".dig" do
9
+ context "VCAP_SERVICES is blank" do
10
+ it "returns nil" do
11
+ expect(subject.dig(:s3, :credentials, :bucket)).to be_nil
12
+ end
13
+ end
14
+
15
+ context "VCAP_SERVICES is set" do
16
+ let(:bucket_name) { "bucket-name" }
17
+ let(:vcap) {
18
+ {
19
+ s3: [
20
+ {
21
+ credentials: {
22
+ bucket: bucket_name
23
+ }
24
+ }
25
+ ]
26
+ }
27
+ }
28
+
29
+ around do |example|
30
+ ClimateControl.modify VCAP_SERVICES: vcap.to_json do
31
+ example.run
32
+ end
33
+ end
34
+
35
+ it "can find a path" do
36
+ expect(subject.dig(:s3, :credentials, :bucket)).to eq bucket_name
37
+ end
38
+
39
+ it "returns nil for a missing path" do
40
+ expect(subject.dig(:s3, :credentials, :other)).to be_nil
41
+ end
42
+ end
43
+ end
44
+ end