diff --git a/.rubocop.yml b/.rubocop.yml index c5d8a6f9..b4d777b5 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,11 +1,152 @@ +require: + - rubocop-rails + - rubocop-performance + AllCops: Exclude: + - bin/* - db/schema.rb + - vendor/ruby/**/* + - node_modules/**/* + TargetRubyVersion: 2.5 + +# Layout + +Layout/AccessModifierIndentation: + Enabled: true + +Layout/AlignParameters: + Description: 'Here we check if the parameters on a multi-line method call or definition are aligned.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-double-indent' + Enabled: true, + EnforcedStyle: with_fixed_indentation + +Layout/ClassStructure: + Categories: + associations: + - belongs_to + - has_one + - has_many + attributes: + - attr_accessor + - attr_reader + - attr_writer + - attr_accessible + callbacks: + - before_validation + - after_validation + - before_save + - around_save + - before_create + - around_create + - after_create + - after_save + - after_commit + module_inclusion: + - include + - prepend + - extend + validations: + - validates + ExpectedOrder: + - module_inclusion + - constants + - public_attributes + - associations + - validations + - callbacks + - public_class_methods + - initializer + - public_methods + - protected_methods + - private_attributes + - private_methods + Enabled: true + +Layout/DotPosition: + Description: 'Checks the position of the dot in multi-line method calls.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#consistent-multi-line-chains' + EnforcedStyle: leading + +Layout/EmptyLinesAroundClassBody: + Enabled: true + EnforcedStyle: empty_lines + +Layout/EmptyLinesAroundModuleBody: + Enabled: true + EnforcedStyle: empty_lines_except_namespace + +Layout/ExtraSpacing: + Description: 'Do not use unnecessary spacing.' + Enabled: true + +Layout/InitialIndentation: + Description: >- + Checks the indentation of the first non-blank non-comment line in a file. + Enabled: false + +Layout/IndentFirstHashElement: + Enabled: true + EnforcedStyle: consistent + +Layout/IndentationConsistency: + EnforcedStyle: indented_internal_methods + Enabled: true + +Layout/MultilineBlockLayout: + Enabled: false + +Layout/MultilineOperationIndentation: + Description: >- + Checks indentation of binary operations that span more than + one line. + Enabled: true + EnforcedStyle: indented + +Layout/MultilineMethodCallIndentation: + Description: >- + Checks indentation of method calls with the dot operator + that span more than one line. + Enabled: true + EnforcedStyle: indented + +Layout/SpaceBeforeBlockBraces: + Description: >- + Checks that block braces have or don't have a space before + the opening brace depending on configuration. + Enabled: false + +# Naming Naming/AccessorMethodName: Description: Check the naming of accessor methods for get_/set_. Enabled: false +Naming/AsciiIdentifiers: + Description: 'Use only ascii symbols in identifiers.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#english-identifiers' + Enabled: true + +Naming/BinaryOperatorParameterName: + Description: 'When defining binary operators, name the argument other.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#other-arg' + Enabled: false + +Naming/FileName: + Description: 'Use snake_case for source file names.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-files' + Enabled: true + +Naming/PredicateName: + Description: 'Check the names of predicate methods.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#bool-methods-qmark' + NamePrefixBlacklist: + - is_ + Exclude: + - spec/**/* + +# Style + Style/Alias: Description: 'Use alias_method instead of alias.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#alias-method' @@ -21,21 +162,11 @@ Style/AsciiComments: StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#english-comments' Enabled: true -Naming/AsciiIdentifiers: - Description: 'Use only ascii symbols in identifiers.' - StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#english-identifiers' - Enabled: true - Style/Attr: Description: 'Checks for uses of Module#attr.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#attr' Enabled: true -Metrics/BlockNesting: - Description: 'Avoid excessive block nesting' - StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#three-is-the-number-thou-shalt-count' - Enabled: true - Style/CaseEquality: Description: 'Avoid explicit use of the case equality operator(===).' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-case-equality' @@ -48,16 +179,8 @@ Style/CharacterLiteral: Style/ClassAndModuleChildren: Description: 'Checks style of children classes and modules.' - Enabled: true - EnforcedStyle: nested - -Metrics/ClassLength: - Description: 'Avoid classes longer than 100 lines of code.' - Enabled: false - -Metrics/ModuleLength: - Description: 'Avoid modules longer than 100 lines of code.' Enabled: false + EnforcedStyle: nested Style/ClassVars: Description: 'Avoid the use of class variables.' @@ -77,11 +200,6 @@ Style/ColonMethodCall: StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#double-colons' Enabled: true -Style/MutableConstant: - Description: 'Avoids assignment of mutable literals to constants..' - StyleGuide: 'http://www.rubydoc.info/github/bbatsov/RuboCop/RuboCop/Cop/Style/MutableConstant' - Enabled: false - Style/CommentAnnotation: Description: >- Checks formatting of special comments @@ -89,27 +207,6 @@ Style/CommentAnnotation: StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#annotate-keywords' Enabled: false -Metrics/AbcSize: - Description: >- - A calculated magnitude based on number of assignments, - branches, and conditions. - Enabled: false - -Metrics/CyclomaticComplexity: - Description: >- - A complexity metric that is strongly correlated to the number - of test cases needed to validate a method. - Enabled: false - -Rails/Delegate: - Description: 'Prefer delegate method for delegations.' - Enabled: false - -Style/PreferredHashMethods: - Description: 'Checks use of `has_key?` and `has_value?` Hash methods.' - StyleGuide: '#hash-key' - Enabled: true - Style/Documentation: Description: 'Document classes and non-namespace modules.' Enabled: false @@ -128,6 +225,9 @@ Style/EmptyLiteral: StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#literal-array-hash' Enabled: true +Style/EmptyMethod: + EnforcedStyle: expanded + # Checks whether the source file has a utf-8 encoding comment or not # AutoCorrectEncodingComment must match the regex # /#.*coding\s?[:=]\s?(?:UTF|utf)-8/ @@ -139,22 +239,12 @@ Style/EvenOdd: StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#predicate-methods' Enabled: true -Naming/FileName: - Description: 'Use snake_case for source file names.' - StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-files' - Enabled: true - Style/FrozenStringLiteralComment: Description: >- Add the frozen_string_literal comment to the top of files to help transition from Ruby 2.3.0 to Ruby 3.0. Enabled: false -Style/FlipFlop: - Description: 'Checks for flip flops' - StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-flip-flops' - Enabled: true - Style/FormatString: Description: 'Enforce the use of Kernel#sprintf, Kernel#format or String#%.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#sprintf' @@ -203,16 +293,11 @@ Style/LineEndConcatenation: line end. Enabled: true -Metrics/LineLength: - Description: 'Limit lines to 80 characters.' - StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#80-character-limits' - Max: 80 - Enabled: false - -Metrics/MethodLength: - Description: 'Avoid methods longer than 10 lines of code.' - StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#short-methods' - Enabled: false +Style/MixinUsage: + Description: 'Checks that `include` statements appaear inside classes.' + Enabled: true + Exclude: + - bin/* Style/ModuleFunction: Description: 'Checks for usage of `extend self` in modules.' @@ -224,6 +309,11 @@ Style/MultilineBlockChain: StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#single-line-blocks' Enabled: false +Style/MutableConstant: + Description: 'Avoids assignment of mutable literals to constants..' + StyleGuide: 'http://www.rubydoc.info/github/bbatsov/RuboCop/RuboCop/Cop/Style/MutableConstant' + Enabled: true + Style/NegatedIf: Description: >- Favor unless over if for negative conditions @@ -256,7 +346,7 @@ Style/NumericLiterals: Add underscores to large numeric literals to improve their readability. StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscores-in-numerics' - Enabled: true + Enabled: false Style/OneLineConditional: Description: >- @@ -265,11 +355,6 @@ Style/OneLineConditional: StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#ternary-operator' Enabled: true -Metrics/ParameterLists: - Description: 'Avoid parameter lists longer than three or four parameters.' - StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#too-many-params' - Enabled: true - Style/PercentLiteralDelimiters: Description: 'Use `%`-literal delimiters consistently' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-literal-braces' @@ -280,13 +365,10 @@ Style/PerlBackrefs: StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-perl-regexp-last-matchers' Enabled: true -Naming/PredicateName: - Description: 'Check the names of predicate methods.' - StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#bool-methods-qmark' - NamePrefixBlacklist: - - is_ - Exclude: - - spec/**/* +Style/PreferredHashMethods: + Description: 'Checks use of `has_key?` and `has_value?` Hash methods.' + StyleGuide: '#hash-key' + Enabled: true Style/Proc: Description: 'Use proc instead of Proc.new.' @@ -303,6 +385,13 @@ Style/RegexpLiteral: StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-r' Enabled: true +Style/Sample: + Description: >- + Use `sample` instead of `shuffle.first`, + `shuffle.last`, and `shuffle[Fixnum]`. + Reference: 'https://github.com/JuanitoFatas/fast-ruby#arrayshufflefirst-vs-arraysample-code' + Enabled: true + Style/SelfAssignment: Description: >- Checks for places where self-assignment shorthand should have @@ -339,15 +428,21 @@ Style/StringLiterals: Style/TrailingCommaInArguments: Description: 'Checks for trailing comma in argument lists.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas' - EnforcedStyleForMultiline: comma + EnforcedStyleForMultiline: no_comma Enabled: true -Style/TrailingCommaInLiteral: - Description: 'Checks for trailing comma in array and hash literals.' +Style/TrailingCommaInArrayLiteral: + Description: 'Checks for trailing comma in array literals.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas' - EnforcedStyleForMultiline: comma + EnforcedStyleForMultiline: no_comma Enabled: false +Style/TrailingCommaInHashLiteral: + Description: 'Checks for trailing comma in hash literals.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas' + EnforcedStyleForMultiline: no_comma + Enabled: true + Style/TrivialAccessors: Description: 'Prefer attr_* methods to trivial readers/writers.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#attr_family' @@ -372,13 +467,75 @@ Style/WhileUntilModifier: StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#while-as-a-modifier' Enabled: true +Style/SymbolArray: + MinSize: 4 + Style/WordArray: Description: 'Use %w or %W for arrays of words.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-w' Enabled: true + MinSize: 4 -Style/SymbolArray: - MinSize: 3 +# Metrics + +Metrics/AbcSize: + Description: >- + A calculated magnitude based on number of assignments, + branches, and conditions. + Enabled: false + +Metrics/BlockLength: + Enabled: true + Exclude: + - config/environments/* + - ./*.gemspec + - lib/generators/disco_app/install/templates/config/environments/* + ExcludedMethods: + - context + - define + - describe + - draw + - factory + - guard + - included + - namespace + - trait + - class_methods + +Metrics/BlockNesting: + Description: 'Avoid excessive block nesting' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#three-is-the-number-thou-shalt-count' + Enabled: true + +Metrics/ClassLength: + Description: 'Avoid classes longer than 100 lines of code.' + Enabled: false + +Metrics/CyclomaticComplexity: + Description: >- + A complexity metric that is strongly correlated to the number + of test cases needed to validate a method. + Enabled: false + +Metrics/LineLength: + Description: 'Limit lines to 80 characters.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#80-character-limits' + Max: 80 + Enabled: false + +Metrics/MethodLength: + Description: 'Avoid methods longer than 10 lines of code.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#short-methods' + Enabled: false + +Metrics/ModuleLength: + Description: 'Avoid modules longer than 100 lines of code.' + Enabled: false + +Metrics/ParameterLists: + Description: 'Avoid parameter lists longer than three or four parameters.' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#too-many-params' + Enabled: true # Lint @@ -404,13 +561,6 @@ Lint/CircularArgumentReference: Description: "Don't refer to the keyword argument in the default value." Enabled: false -Lint/ConditionPosition: - Description: >- - Checks for condition placed in a confusing position relative to - the keyword. - StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#same-line-condition' - Enabled: true - Lint/DeprecatedClassMethods: Description: 'Check for deprecated class method calls.' Enabled: true @@ -427,6 +577,11 @@ Lint/ElseLayout: Description: 'Check for odd code arrangement in an else block.' Enabled: true +Lint/FlipFlop: + Description: 'Checks for flip flops' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-flip-flops' + Enabled: true + Lint/FormatParameterMismatch: Description: 'The number of parameters to format/sprint must match the fields.' Enabled: true @@ -436,13 +591,7 @@ Lint/HandleExceptions: StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#dont-hide-exceptions' Enabled: true -Lint/InvalidCharacterLiteral: - Description: >- - Checks for invalid character literals with a non-escaped - whitespace character. - Enabled: false - -Lint/LiteralInCondition: +Lint/LiteralAsCondition: Description: 'Checks of literals used in conditions.' Enabled: false @@ -483,13 +632,6 @@ Lint/UnderscorePrefixedVariableName: Description: 'Do not use prefix `_` for a variable that is used.' Enabled: false -Lint/UnneededDisable: - Description: >- - Checks for rubocop:disable comments that can be removed. - Note: this cop is not disabled when disabling all cops. - It must be explicitly disabled. - Enabled: false - Lint/Void: Description: 'Possible use of operator/literal/variable in void context.' Enabled: false @@ -514,7 +656,7 @@ Performance/Detect: Use `detect` instead of `select.first`, `find_all.first`, `select.last`, and `find_all.last`. Reference: 'https://github.com/JuanitoFatas/fast-ruby#enumerabledetect-vs-enumerableselectfirst-code' - Enabled: false + Enabled: true Performance/FlatMap: Description: >- @@ -529,13 +671,6 @@ Performance/ReverseEach: Reference: 'https://github.com/JuanitoFatas/fast-ruby#enumerablereverseeach-vs-enumerablereverse_each-code' Enabled: true -Performance/Sample: - Description: >- - Use `sample` instead of `shuffle.first`, - `shuffle.last`, and `shuffle[Fixnum]`. - Reference: 'https://github.com/JuanitoFatas/fast-ruby#arrayshufflefirst-vs-arraysample-code' - Enabled: true - Performance/Size: Description: >- Use `size` instead of `count` for counting @@ -563,6 +698,14 @@ Rails/Date: such as Date.today, Date.current etc. Enabled: true +Rails/Delegate: + Description: 'Prefer delegate method for delegations.' + Enabled: false + +Rails/Exit: + Exclude: + - lib/generators/disco_app/install/templates/spec/rails_helper.rb + Rails/FindBy: Description: 'Prefer find_by over where.first.' Enabled: true @@ -577,7 +720,11 @@ Rails/HasAndBelongsToMany: Rails/Output: Description: 'Checks for calls to puts, print, etc.' - Enabled: true + Enabled: false + +Rails/HttpPositionalArguments: + Description: 'Use keyword arguments instead of positional arguments for http calls' + Enabled: false Rails/ReadWriteAttribute: Description: >- @@ -595,61 +742,18 @@ Rails/TimeZone: Reference: 'http://danilenko.org/2012/7/6/rails_timezones' Enabled: true +Rails/UnknownEnv: + Environments: + - development + - production + - staging + - test + Rails/Validation: Description: 'Use validates :attribute, hash of validations.' Enabled: false -# Layout - -Layout/EmptyLinesAroundClassBody: - Enabled: true - EnforcedStyle: empty_lines - -Layout/AlignParameters: - Description: 'Here we check if the parameters on a multi-line method call or definition are aligned.' - StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-double-indent' - Enabled: true - -Layout/DotPosition: - Description: 'Checks the position of the dot in multi-line method calls.' - StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#consistent-multi-line-chains' - EnforcedStyle: leading - -Layout/ExtraSpacing: - Description: 'Do not use unnecessary spacing.' - Enabled: true - -Layout/MultilineOperationIndentation: - Description: >- - Checks indentation of binary operations that span more than - one line. - Enabled: true - EnforcedStyle: indented - -Layout/MultilineMethodCallIndentation: - Description: >- - Checks indentation of method calls with the dot operator - that span more than one line. - Enabled: true - EnforcedStyle: indented - -Layout/InitialIndentation: - Description: >- - Checks the indentation of the first non-blank non-comment line in a file. - Enabled: false - -Layout/IndentHash: - Enabled: true - EnforcedStyle: consistent - -Layout/AccessModifierIndentation: - Enabled: true - -Layout/IndentationConsistency: - EnforcedStyle: rails - Enabled: true - # Bundler Bundler/OrderedGems: - Enabled: false + Enabled: true \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index d98b4246..768f7868 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,22 @@ # Change Log All notable changes to this project will be documented in this file. +## 0.17.0 - 2019-07-16 +### Added +- Timber logging for generated apps +- rubocop-performance and rubocop-rails plugins +- Support for AppSignal +- Generator for React applications + +### Changed +- Switch from Minitest to RSpec as the testing library for generated apps +- Update rubocop.yml file +- Lock shopify_api version to avoid breaking changes +- Upgrade Rails to 5.2.2 to avoid gem vulnerabilities +- Run rubocop auto-corrections +- Use with_indifferent_access when reading Flow properties +- Renaming of methods and general refactoring + ## 0.16.1 - 2019-01-01 ### Added - Support for Shopify Flow triggers and actions diff --git a/README.md b/README.md index 415be280..c4cfd845 100644 --- a/README.md +++ b/README.md @@ -78,22 +78,15 @@ app via the partner dashboard. Before you can do this, you need to configure a couple of things. #### Create a DiscoApp configuration file in your home directory -First, you'll need to add your partner dashboard and Rollbar credentials to a DiscoApp -configuration file in your home directory, `~/.disco_app.yml`: +First, you'll need to add your partner dashboard credentials to a DiscoApp configuration file in your home directory, `~/.disco_app.yml`: ``` params: PARTNER_EMAIL: "hello@discolabs.com" PARTNER_PASSWORD: "***********" PARTNER_ORGANIZATION: "Disco" - ROLLBAR_ACCOUNT_ACCESS_TOKEN_WRITE: "******************************" - ROLLBAR_ACCOUNT_ACCESS_TOKEN_READ: "******************************" ``` -You can find your tokens in the Rollbar settings under 'Project Access Tokens'. -If you don't yet have a Rollbar account you can leave out the bottom two lines for now. -You'll only need to set this up the one time on your local machine. - #### Configure initial values in local ENV file Next, you'll need to set a few of the basic configuration parameters for your app in `.env.local` in the application directory. The command line utility @@ -349,7 +342,6 @@ There's a number of useful Rake tasks that are baked into the app. They are: - `rake shops:sync`: Synchronises shop data across all installed shops. - `rake users:sync`: Synchronises user data across all installed shops. - `rake generate:partner_app`: Generates an app on the Disco Partner Dashboard -- `rake generate:rollbar_project`: Generates a Rollbar Project ### Background Tasks The `DiscoApp::ShopJob` class inherits from `ActiveJob::Base`, and can be used @@ -955,21 +947,15 @@ Mailgun API in production for sending email. Adds the `MAILGUN_API_KEY` and DiscoApp has support for both exception reporting and application performance monitoring to the application. -[Rollbar][] is used for exception tracking, and will be activated when a -`ROLLBAR_ACCESS_TOKEN` environment variable is present. Rollbar access tokens -are unique to each app. In order to generate a new token run -`rake generate:rollbar_project`. -Make sure you have configured your `~/.disco_app.yml` as per -[the setup guide](#create-a-discoapp-configuration-file-in-your-home-directory) -and that you have the necessary Rollbar permissions to create a project. You can -specify an app name by adding APP_NAME='App Name'. +[Appsignal](https://github.com/appsignal/appsignal-ruby) is used for exception tracking, and will be activated when a +`APPSIGNAL_PUSH_API_KEY` environment variable is present. The Appsignal Push API Key can be found in 1Password. [New Relic][] is used for application performance monitoring, and will be activated when a `NEW_RELIC_LICENSE_KEY` environment variable is present. There is a single New Relic license key across all Disco apps - contact Gavin if you need it to deploy a new application. -[Rollbar]: https://www.rollbar.com +[Appsignal]: https://www.appsignal.com [New Relic]: https://www.newrelic.com diff --git a/Rakefile b/Rakefile index 3e7dfa3e..403de62a 100644 --- a/Rakefile +++ b/Rakefile @@ -14,14 +14,11 @@ RDoc::Task.new(:rdoc) do |rdoc| rdoc.rdoc_files.include('lib/**/*.rb') end -APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__) +APP_RAKEFILE = File.expand_path('test/dummy/Rakefile', __dir__) load 'rails/tasks/engine.rake' - load 'rails/tasks/statistics.rake' - - Bundler::GemHelper.install_tasks require 'rake/testtask' @@ -33,5 +30,4 @@ Rake::TestTask.new(:test) do |t| t.verbose = false end - task default: :test diff --git a/UPGRADING.md b/UPGRADING.md index 3fb14840..d6dc37b8 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -3,6 +3,15 @@ This file contains more detailed instructions on what's required when updating an application between one release version of the gem to the next. It's intended as more in-depth accompaniment to the notes in `CHANGELOG.md` for each version. +## Upgrading from 0.16.1 to 0.17.1 +Upgrade your app to Rails version 5.2.2. + +Set your `shopify_api` version to 6.0: + +```ruby +gem 'shopify_api', '~> 6.0' +``` + ## Upgrading from 0.16.0 to 0.16.1 Ensure new Shopify Flow database migrations are brought across and run: diff --git a/VERSION b/VERSION index 2a0970ca..c5523bd0 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.16.1 +0.17.0 diff --git a/app/clients/disco_app/api_client.rb b/app/clients/disco_app/api_client.rb index 08ac08c6..3c236588 100644 --- a/app/clients/disco_app/api_client.rb +++ b/app/clients/disco_app/api_client.rb @@ -2,7 +2,7 @@ class DiscoApp::ApiClient - SUBSCRIPTION_ENDPOINT = 'app_subscriptions.json' + SUBSCRIPTION_ENDPOINT = 'app_subscriptions.json'.freeze def initialize(shop, url) @shop = shop @@ -10,17 +10,18 @@ def initialize(shop, url) end def create_app_subscription - return unless @url.present? + return if @url.blank? + url = @url + SUBSCRIPTION_ENDPOINT begin - response = RestClient::Request.execute( + RestClient::Request.execute( method: :post, headers: { content_type: :json }, url: url, payload: { shop: @shop, subscription: @shop.current_subscription }.to_json ) rescue RestClient::BadRequest, RestClient::ResourceNotFound => e - raise DiscoApiError.new(e.message) + raise DiscoApiError, e.message end end diff --git a/app/clients/disco_app/graphql_client.rb b/app/clients/disco_app/graphql_client.rb index 418d2880..d15a2776 100644 --- a/app/clients/disco_app/graphql_client.rb +++ b/app/clients/disco_app/graphql_client.rb @@ -43,7 +43,7 @@ def create_flow_trigger(title, resource_name, resource_url, properties) # The double .to_json.to_json below looks odd but is required to properly escape the JSON hash # when inserting it into the GraphQL mutation call. - response = execute(%Q( + response = execute(%( mutation { flowTriggerReceive(body: #{body.to_json.to_json}) { userErrors { diff --git a/app/controllers/disco_app/admin/app_settings_controller.rb b/app/controllers/disco_app/admin/app_settings_controller.rb index bb329931..f937dec4 100644 --- a/app/controllers/disco_app/admin/app_settings_controller.rb +++ b/app/controllers/disco_app/admin/app_settings_controller.rb @@ -1,3 +1,5 @@ class DiscoApp::Admin::AppSettingsController < DiscoApp::Admin::ApplicationController + include DiscoApp::Admin::Concerns::AppSettingsController + end diff --git a/app/controllers/disco_app/admin/application_controller.rb b/app/controllers/disco_app/admin/application_controller.rb index ed519230..77fe00a2 100644 --- a/app/controllers/disco_app/admin/application_controller.rb +++ b/app/controllers/disco_app/admin/application_controller.rb @@ -1,4 +1,5 @@ class DiscoApp::Admin::ApplicationController < ActionController::Base + include DiscoApp::Admin::Concerns::AuthenticatedController private diff --git a/app/controllers/disco_app/admin/concerns/app_settings_controller.rb b/app/controllers/disco_app/admin/concerns/app_settings_controller.rb index f849814e..f4d01703 100644 --- a/app/controllers/disco_app/admin/concerns/app_settings_controller.rb +++ b/app/controllers/disco_app/admin/concerns/app_settings_controller.rb @@ -1,4 +1,5 @@ module DiscoApp::Admin::Concerns::AppSettingsController + extend ActiveSupport::Concern def edit @@ -7,7 +8,7 @@ def edit def update @app_settings = DiscoApp::AppSettings.instance - if @app_settings.update_attributes(app_settings_params) + if @app_settings.update(app_settings_params) flash[:success] = 'Settings updated.' redirect_to edit_admin_app_settings_path else diff --git a/app/controllers/disco_app/admin/concerns/authenticated_controller.rb b/app/controllers/disco_app/admin/concerns/authenticated_controller.rb index 689fb488..af9f55a1 100644 --- a/app/controllers/disco_app/admin/concerns/authenticated_controller.rb +++ b/app/controllers/disco_app/admin/concerns/authenticated_controller.rb @@ -1,19 +1,21 @@ module DiscoApp::Admin::Concerns::AuthenticatedController + extend ActiveSupport::Concern included do - protect_from_forgery with: :exception before_action :authenticate_administrator layout 'admin' - end private def authenticate_administrator authenticate_or_request_with_http_basic do |username, password| - (not username.blank?) && (not password.blank?) && username == ENV['ADMIN_APP_USERNAME'] && password == ENV['ADMIN_APP_PASSWORD'] + username.present? && + password.present? && + username == ENV['ADMIN_APP_USERNAME'] && + password == ENV['ADMIN_APP_PASSWORD'] end end diff --git a/app/controllers/disco_app/admin/concerns/plans_controller.rb b/app/controllers/disco_app/admin/concerns/plans_controller.rb index b994396f..6ed16149 100644 --- a/app/controllers/disco_app/admin/concerns/plans_controller.rb +++ b/app/controllers/disco_app/admin/concerns/plans_controller.rb @@ -1,4 +1,5 @@ module DiscoApp::Admin::Concerns::PlansController + extend ActiveSupport::Concern included do @@ -26,7 +27,7 @@ def edit end def update - if @plan.update_attributes(plan_params) + if @plan.update(plan_params) redirect_to edit_admin_plan_path(@plan) else render 'edit' @@ -47,7 +48,7 @@ def find_plan def plan_params params.require(:plan).permit( :name, :status, :plan_type, :trial_period_days, :amount, - :plan_codes_attributes => [:id, :_destroy, :code, :trial_period_days, :amount] + plan_codes_attributes: %i[id _destroy code trial_period_days amount] ) end diff --git a/app/controllers/disco_app/admin/concerns/shops_controller.rb b/app/controllers/disco_app/admin/concerns/shops_controller.rb index cbba6cb8..55e6eda8 100644 --- a/app/controllers/disco_app/admin/concerns/shops_controller.rb +++ b/app/controllers/disco_app/admin/concerns/shops_controller.rb @@ -1,4 +1,5 @@ module DiscoApp::Admin::Concerns::ShopsController + extend ActiveSupport::Concern def index diff --git a/app/controllers/disco_app/admin/concerns/sources_controller.rb b/app/controllers/disco_app/admin/concerns/sources_controller.rb index 957bcc5f..8d689113 100644 --- a/app/controllers/disco_app/admin/concerns/sources_controller.rb +++ b/app/controllers/disco_app/admin/concerns/sources_controller.rb @@ -1,4 +1,5 @@ module DiscoApp::Admin::Concerns::SourcesController + extend ActiveSupport::Concern included do @@ -26,7 +27,7 @@ def edit end def update - if @source.update_attributes(source_params) + if @source.update(source_params) redirect_to edit_admin_plan_path(@source) else render 'edit' diff --git a/app/controllers/disco_app/admin/concerns/subscriptions_controller.rb b/app/controllers/disco_app/admin/concerns/subscriptions_controller.rb index 794fb74c..c990cb35 100644 --- a/app/controllers/disco_app/admin/concerns/subscriptions_controller.rb +++ b/app/controllers/disco_app/admin/concerns/subscriptions_controller.rb @@ -1,4 +1,5 @@ module DiscoApp::Admin::Concerns::SubscriptionsController + extend ActiveSupport::Concern included do @@ -9,7 +10,7 @@ def edit end def update - if @subscription.update_attributes(subscription_params) + if @subscription.update(subscription_params) redirect_to edit_admin_shop_subscription_path(@subscription.shop, @subscription) else render 'edit' @@ -19,7 +20,7 @@ def update private def find_subscription - @subscription = DiscoApp::Subscription.find_by_id(params[:id]) + @subscription = DiscoApp::Subscription.find_by(id: params[:id]) end def subscription_params diff --git a/app/controllers/disco_app/admin/plans_controller.rb b/app/controllers/disco_app/admin/plans_controller.rb index b0a23a8e..3da3f6e3 100644 --- a/app/controllers/disco_app/admin/plans_controller.rb +++ b/app/controllers/disco_app/admin/plans_controller.rb @@ -1,3 +1,5 @@ class DiscoApp::Admin::PlansController < DiscoApp::Admin::ApplicationController + include DiscoApp::Admin::Concerns::PlansController + end diff --git a/app/controllers/disco_app/admin/resources/shops_controller.rb b/app/controllers/disco_app/admin/resources/shops_controller.rb index 4fc2ff27..b75c6cdb 100644 --- a/app/controllers/disco_app/admin/resources/shops_controller.rb +++ b/app/controllers/disco_app/admin/resources/shops_controller.rb @@ -1,3 +1,5 @@ class DiscoApp::Admin::Resources::ShopsController < JSONAPI::ResourceController + include DiscoApp::Admin::Concerns::AuthenticatedController + end diff --git a/app/controllers/disco_app/admin/shops_controller.rb b/app/controllers/disco_app/admin/shops_controller.rb index 41d87967..01ad8baf 100644 --- a/app/controllers/disco_app/admin/shops_controller.rb +++ b/app/controllers/disco_app/admin/shops_controller.rb @@ -1,3 +1,5 @@ class DiscoApp::Admin::ShopsController < DiscoApp::Admin::ApplicationController + include DiscoApp::Admin::Concerns::ShopsController + end diff --git a/app/controllers/disco_app/admin/sources_controller.rb b/app/controllers/disco_app/admin/sources_controller.rb index 32658dfa..1358f6d5 100644 --- a/app/controllers/disco_app/admin/sources_controller.rb +++ b/app/controllers/disco_app/admin/sources_controller.rb @@ -1,3 +1,5 @@ class DiscoApp::Admin::SourcesController < DiscoApp::Admin::ApplicationController + include DiscoApp::Admin::Concerns::SourcesController + end diff --git a/app/controllers/disco_app/admin/subscriptions_controller.rb b/app/controllers/disco_app/admin/subscriptions_controller.rb index 503b14ed..a4f18d23 100644 --- a/app/controllers/disco_app/admin/subscriptions_controller.rb +++ b/app/controllers/disco_app/admin/subscriptions_controller.rb @@ -1,3 +1,5 @@ class DiscoApp::Admin::SubscriptionsController < DiscoApp::Admin::ApplicationController + include DiscoApp::Admin::Concerns::SubscriptionsController + end diff --git a/app/controllers/disco_app/charges_controller.rb b/app/controllers/disco_app/charges_controller.rb index 1d14c9fc..fad18b3e 100644 --- a/app/controllers/disco_app/charges_controller.rb +++ b/app/controllers/disco_app/charges_controller.rb @@ -1,4 +1,5 @@ class DiscoApp::ChargesController < ApplicationController + include DiscoApp::Concerns::AuthenticatedController skip_before_action :check_active_charge @@ -13,7 +14,7 @@ def new # subscription. If successful, redirect to the (external) charge confirmation # URL. If it fails, redirect back to the new charge page. def create - if(charge = DiscoApp::ChargesService.create(@shop, @subscription)).nil? + if (charge = DiscoApp::ChargesService.create(@shop, @subscription)).nil? redirect_to action: :new else redirect_to charge.confirmation_url @@ -25,8 +26,8 @@ def create # charge wasn't accepted, the flow will start again. def activate # First attempt to find a matching charge. - if(charge = @subscription.charges.find_by(id: params[:id], shopify_id: params[:charge_id])).nil? - redirect_to action: :new and return + if (charge = @subscription.charges.find_by(id: params[:id], shopify_id: params[:charge_id])).nil? + redirect_to(action: :new) && return end if DiscoApp::ChargesService.activate(@shop, charge) redirect_to main_app.root_url @@ -38,10 +39,8 @@ def activate private def find_subscription - @subscription = @shop.subscriptions.find_by_id!(params[:subscription_id]) - unless @subscription.requires_active_charge? and not @subscription.active_charge? - redirect_to main_app.root_url - end + @subscription = @shop.subscriptions.find_by!(id: params[:subscription_id]) + redirect_to main_app.root_url unless @subscription.requires_active_charge? && !@subscription.active_charge? end end diff --git a/app/controllers/disco_app/concerns/app_proxy_controller.rb b/app/controllers/disco_app/concerns/app_proxy_controller.rb index e45c0df6..237078dd 100644 --- a/app/controllers/disco_app/concerns/app_proxy_controller.rb +++ b/app/controllers/disco_app/concerns/app_proxy_controller.rb @@ -1,4 +1,5 @@ module DiscoApp::Concerns::AppProxyController + extend ActiveSupport::Concern included do @@ -6,7 +7,7 @@ module DiscoApp::Concerns::AppProxyController before_action :shopify_shop after_action :add_liquid_header - rescue_from ActiveRecord::RecordNotFound do |exception| + rescue_from ActiveRecord::RecordNotFound do |_exception| render_error 404 end end @@ -14,18 +15,17 @@ module DiscoApp::Concerns::AppProxyController private def verify_proxy_signature - unless proxy_signature_is_valid? - head :unauthorized - end + head :unauthorized unless proxy_signature_is_valid? end def proxy_signature_is_valid? - return true if (Rails.env.development? || Rails.env.test?) and DiscoApp.configuration.skip_proxy_verification? + return true if (Rails.env.development? || Rails.env.test?) && DiscoApp.configuration.skip_proxy_verification? + DiscoApp::ProxyService.proxy_signature_is_valid?(request.query_string, ShopifyApp.configuration.secret) end def shopify_shop - @shop = DiscoApp::Shop.find_by_shopify_domain!(params[:shop]) + @shop = DiscoApp::Shop.find_by!(shopify_domain: params[:shop]) end def add_liquid_header diff --git a/app/controllers/disco_app/concerns/authenticated_controller.rb b/app/controllers/disco_app/concerns/authenticated_controller.rb index c7974172..218436b7 100644 --- a/app/controllers/disco_app/concerns/authenticated_controller.rb +++ b/app/controllers/disco_app/concerns/authenticated_controller.rb @@ -1,4 +1,5 @@ module DiscoApp::Concerns::AuthenticatedController + extend ActiveSupport::Concern include ShopifyApp::LoginProtection @@ -17,12 +18,13 @@ module DiscoApp::Concerns::AuthenticatedController private def auto_login - if shop_session.nil? and request_hmac_valid? - if(shop = DiscoApp::Shop.find_by_shopify_domain(sanitized_shop_name)).present? - session[:shopify] = shop.id - session[:shopify_domain] = sanitized_shop_name - end - end + return unless shop_session.nil? && request_hmac_valid? + + shop = DiscoApp::Shop.find_by(shopify_domain: sanitized_shop_name) + return if shop.blank? + + session[:shopify] = shop.id + session[:shopify_domain] = sanitized_shop_name end def shopify_shop @@ -34,35 +36,32 @@ def shopify_shop end def check_installed - if @shop.awaiting_install? or @shop.installing? + if @shop.awaiting_install? || @shop.installing? redirect_if_not_current_path disco_app.installing_path return end - if @shop.awaiting_uninstall? or @shop.uninstalling? + if @shop.awaiting_uninstall? || @shop.uninstalling? redirect_if_not_current_path disco_app.uninstalling_path return end - unless @shop.installed? - redirect_if_not_current_path disco_app.install_path - end + redirect_if_not_current_path disco_app.install_path unless @shop.installed? end def check_current_subscription - unless @shop.current_subscription? - redirect_if_not_current_path disco_app.new_subscription_path - end + redirect_if_not_current_path disco_app.new_subscription_path unless @shop.current_subscription? end def check_active_charge - if @shop.current_subscription? and @shop.current_subscription.requires_active_charge? and not @shop.development? and not @shop.current_subscription.active_charge? - redirect_if_not_current_path disco_app.new_subscription_charge_path(@shop.current_subscription) - end + return unless @shop.current_subscription? + return unless @shop.current_subscription.requires_active_charge? + return if @shop.development? + return if @shop.current_subscription.active_charge? + + redirect_if_not_current_path disco_app.new_subscription_charge_path(@shop.current_subscription) end def redirect_if_not_current_path(target) - if request.path != target - redirect_to target - end + redirect_to target if request.path != target end def request_hmac_valid? @@ -70,11 +69,11 @@ def request_hmac_valid? end def check_shop_whitelist - if shop_session - if ENV['WHITELISTED_DOMAINS'].present? && !ENV['WHITELISTED_DOMAINS'].include?(shop_session.url) - redirect_to_login - end - end + return unless shop_session + return if ENV['WHITELISTED_DOMAINS'].blank? + return if ENV['WHITELISTED_DOMAINS'].include?(shop_session.url) + + redirect_to_login end end diff --git a/app/controllers/disco_app/concerns/carrier_request_controller.rb b/app/controllers/disco_app/concerns/carrier_request_controller.rb index 52e52f1f..9253f0b4 100644 --- a/app/controllers/disco_app/concerns/carrier_request_controller.rb +++ b/app/controllers/disco_app/concerns/carrier_request_controller.rb @@ -1,4 +1,5 @@ module DiscoApp::Concerns::CarrierRequestController + extend ActiveSupport::Concern included do @@ -10,26 +11,36 @@ module DiscoApp::Concerns::CarrierRequestController private def verify_carrier_request - unless carrier_request_signature_is_valid? - head :unauthorized - end + head :unauthorized unless carrier_request_signature_is_valid? end def carrier_request_signature_is_valid? - return true if Rails.env.development? and DiscoApp.configuration.skip_carrier_request_verification? - DiscoApp::CarrierRequestService.is_valid_hmac?(request.body.read.to_s, ShopifyApp.configuration.secret, request.headers['HTTP_X_SHOPIFY_HMAC_SHA256']) + return true if Rails.env.development? && DiscoApp.configuration.skip_carrier_request_verification? + + DiscoApp::CarrierRequestService.valid_hmac?( + request.body.read.to_s, + ShopifyApp.configuration.secret, + request.headers['HTTP_X_SHOPIFY_HMAC_SHA256'] + ) end def find_shop - unless (@shop = DiscoApp::Shop.find_by_shopify_domain(request.headers['HTTP_X_SHOPIFY_SHOP_DOMAIN'])) - head :unauthorized - end + @shop = DiscoApp::Shop.find_by(shopify_domain: request.headers['HTTP_X_SHOPIFY_SHOP_DOMAIN']) + + head :unauthorized unless @shop end def validate_rate_params - unless params[:rate].present? and params[:rate][:origin].present? and params[:rate][:destination].present? and params[:rate][:items].present? - head :bad_request - end + head :bad_request unless request_is_valid? + end + + def request_is_valid? + return false if params[:rate].blank? + return false if params[:rate][:origin].blank? + return false if params[:rate][:destination].blank? + return false if params[:rate][:items].blank? + + true end end diff --git a/app/controllers/disco_app/concerns/user_authenticated_controller.rb b/app/controllers/disco_app/concerns/user_authenticated_controller.rb index 2ede3c45..629a6c8f 100644 --- a/app/controllers/disco_app/concerns/user_authenticated_controller.rb +++ b/app/controllers/disco_app/concerns/user_authenticated_controller.rb @@ -1,4 +1,5 @@ module DiscoApp::Concerns::UserAuthenticatedController + extend ActiveSupport::Concern include ShopifyApp::LoginProtection diff --git a/app/controllers/disco_app/concerns/webhooks_controller.rb b/app/controllers/disco_app/concerns/webhooks_controller.rb index 5e34242c..d4cfc244 100644 --- a/app/controllers/disco_app/concerns/webhooks_controller.rb +++ b/app/controllers/disco_app/concerns/webhooks_controller.rb @@ -1,4 +1,5 @@ module DiscoApp::Concerns::WebhooksController + extend ActiveSupport::Concern included do @@ -12,20 +13,16 @@ def process_webhook shopify_domain = request.headers['HTTP_X_SHOPIFY_SHOP_DOMAIN'] # Ensure a domain was provided in the headers. - unless shopify_domain - head :bad_request - end + return head :bad_request unless shopify_domain # Try to find a matching background job task for the given topic using class name. job_class = DiscoApp::WebhookService.find_job_class(topic) # Return bad request if we couldn't match a job class. - unless job_class.present? - head :bad_request - end + return head :bad_request if job_class.blank? # Decode the body data and enqueue the appropriate job. - data = ActiveSupport::JSON::decode(request.body.read).with_indifferent_access + data = JSON.parse(request.body.read).with_indifferent_access job_class.perform_later(shopify_domain, data) render body: nil @@ -34,15 +31,19 @@ def process_webhook private def verify_webhook - unless webhook_is_valid? - head :unauthorized - end + return head :unauthorized unless webhook_is_valid? + request.body.rewind end def webhook_is_valid? - return true if Rails.env.development? and DiscoApp.configuration.skip_webhook_verification? - DiscoApp::WebhookService.is_valid_hmac?(request.body.read.to_s, ShopifyApp.configuration.secret, request.headers['HTTP_X_SHOPIFY_HMAC_SHA256']) + return true if Rails.env.development? && DiscoApp.configuration.skip_webhook_verification? + + DiscoApp::WebhookService.valid_hmac?( + request.body.read.to_s, + ShopifyApp.configuration.secret, + request.headers['HTTP_X_SHOPIFY_HMAC_SHA256'] + ) end end diff --git a/app/controllers/disco_app/flow/actions_controller.rb b/app/controllers/disco_app/flow/actions_controller.rb index 96215003..fe5f67a4 100644 --- a/app/controllers/disco_app/flow/actions_controller.rb +++ b/app/controllers/disco_app/flow/actions_controller.rb @@ -1,7 +1,9 @@ module DiscoApp module Flow class ActionsController < ActionController::Base + include DiscoApp::Flow::Concerns::ActionsController + end end end diff --git a/app/controllers/disco_app/flow/concerns/actions_controller.rb b/app/controllers/disco_app/flow/concerns/actions_controller.rb index 25976358..d06342e3 100644 --- a/app/controllers/disco_app/flow/concerns/actions_controller.rb +++ b/app/controllers/disco_app/flow/concerns/actions_controller.rb @@ -25,20 +25,23 @@ def create_flow_action private def verify_flow_action - unless flow_action_is_valid? - head :unauthorized - end + return head :unauthorized unless flow_action_is_valid? + request.body.rewind end # Shopify Flow action endpoints use the same verification method as webhooks, which is why we reuse this # service method here. def flow_action_is_valid? - DiscoApp::WebhookService.is_valid_hmac?(request.body.read.to_s, ShopifyApp.configuration.secret, request.headers['HTTP_X_SHOPIFY_HMAC_SHA256']) + DiscoApp::WebhookService.valid_hmac?( + request.body.read.to_s, + ShopifyApp.configuration.secret, + request.headers['HTTP_X_SHOPIFY_HMAC_SHA256'] + ) end def find_shop - @shop = DiscoApp::Shop.find_by_shopify_domain!(params[:shopify_domain]) + @shop = DiscoApp::Shop.find_by!(shopify_domain: params[:shopify_domain]) end end diff --git a/app/controllers/disco_app/frame_controller.rb b/app/controllers/disco_app/frame_controller.rb index 3d27d544..bfb70ab0 100644 --- a/app/controllers/disco_app/frame_controller.rb +++ b/app/controllers/disco_app/frame_controller.rb @@ -3,7 +3,6 @@ class DiscoApp::FrameController < ActionController::Base layout nil def frame - end end diff --git a/app/controllers/disco_app/install_controller.rb b/app/controllers/disco_app/install_controller.rb index fd895345..8a1dc5a5 100644 --- a/app/controllers/disco_app/install_controller.rb +++ b/app/controllers/disco_app/install_controller.rb @@ -1,4 +1,5 @@ class DiscoApp::InstallController < ApplicationController + include DiscoApp::Concerns::AuthenticatedController skip_before_action :check_current_subscription @@ -12,16 +13,12 @@ def install # Display an "installing" page. def installing - if @shop.installed? - redirect_to main_app.root_path - end + redirect_to main_app.root_path if @shop.installed? end # Display an "uninstalling" page. Should be almost never used. def uninstalling - if @shop.uninstalled? - redirect_to main_app.root_path - end + redirect_to main_app.root_path if @shop.uninstalled? end end diff --git a/app/controllers/disco_app/subscriptions_controller.rb b/app/controllers/disco_app/subscriptions_controller.rb index 29a97cac..b02281d8 100644 --- a/app/controllers/disco_app/subscriptions_controller.rb +++ b/app/controllers/disco_app/subscriptions_controller.rb @@ -1,4 +1,5 @@ class DiscoApp::SubscriptionsController < ApplicationController + include DiscoApp::Concerns::AuthenticatedController skip_before_action :check_current_subscription @@ -10,13 +11,20 @@ def new def create # Get the selected plan. If it's not available or couldn't be found, # redirect back to the plan selection page. - if(plan = DiscoApp::Plan.available.find_by_id(subscription_params[:plan])).nil? - redirect_to action: :new and return - end + plan = DiscoApp::Plan.available.find_by(id: subscription_params[:plan]) + + redirect_to(action: :new) && return unless plan # Subscribe the current shop to the selected plan. Pass along any cookied # plan code and source code. - if(subscription = DiscoApp::SubscriptionService.subscribe(@shop, plan, cookies[DiscoApp::CODE_COOKIE_KEY], cookies[DiscoApp::SOURCE_COOKIE_KEY])).nil? + subscription = DiscoApp::SubscriptionService.subscribe( + @shop, + plan, + cookies[DiscoApp::CODE_COOKIE_KEY], + cookies[DiscoApp::SOURCE_COOKIE_KEY] + ) + + if subscription.nil? redirect_to action: :new else redirect_to main_app.root_path diff --git a/app/controllers/disco_app/user_sessions_controller.rb b/app/controllers/disco_app/user_sessions_controller.rb index 1f4b0d53..50487c1b 100644 --- a/app/controllers/disco_app/user_sessions_controller.rb +++ b/app/controllers/disco_app/user_sessions_controller.rb @@ -1,4 +1,5 @@ class DiscoApp::UserSessionsController < ApplicationController + include DiscoApp::Concerns::AuthenticatedController def new diff --git a/app/controllers/disco_app/webhooks_controller.rb b/app/controllers/disco_app/webhooks_controller.rb index 72616fd7..4a12f1f4 100644 --- a/app/controllers/disco_app/webhooks_controller.rb +++ b/app/controllers/disco_app/webhooks_controller.rb @@ -1,3 +1,5 @@ class DiscoApp::WebhooksController < ActionController::Base + include DiscoApp::Concerns::WebhooksController + end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index dbba900e..5514d77e 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -1,4 +1,5 @@ class SessionsController < ActionController::Base + include ShopifyApp::SessionsConcern def referral @@ -18,14 +19,14 @@ def failure # mode. Skipping OAuth still requires a shop with Shopify domain specified # by the `shop` parameter to be present in the local database. def authenticate - if Rails.env.development? and DiscoApp.configuration.skip_oauth? - shop = DiscoApp::Shop.find_by_shopify_domain!(sanitized_shop_name) + if Rails.env.development? && DiscoApp.configuration.skip_oauth? + shop = DiscoApp::Shop.find_by!(shopify_domain: sanitized_shop_name) sess = ShopifyAPI::Session.new(shop.shopify_domain, shop.shopify_token) session[:shopify] = ShopifyApp::SessionRepository.store(sess) session[:shopify_domain] = sanitized_shop_name - redirect_to disco_app.frame_path and return + redirect_to(disco_app.frame_path) && return end super end diff --git a/app/helpers/disco_app/application_helper.rb b/app/helpers/disco_app/application_helper.rb index 4ab89274..3eed6024 100644 --- a/app/helpers/disco_app/application_helper.rb +++ b/app/helpers/disco_app/application_helper.rb @@ -18,7 +18,7 @@ def link_to_modal(name, path, options = {}) title: options.delete(:modal_title), width: options.delete(:modal_width), height: options.delete(:modal_height), - buttons: options.delete(:modal_buttons), + buttons: options.delete(:modal_buttons) } options[:onclick] = "ShopifyApp.Modal.open(#{modal_options.to_json}); return false;" options[:onclick].gsub!(/"function(.*?)"/, 'function\1') @@ -32,14 +32,14 @@ def react_component_with_content(name, args = {}, options = {}, &block) react_component(name, args, options) end - # Provide link to dynamically add a new nested fields association - def link_to_add_fields(name, f, association) - new_object = f.object.send(association).klass.new + # Provide link to dynamically add a new nested fields association + def link_to_add_fields(name, form, association) + new_object = form.object.send(association).klass.new id = new_object.object_id - fields = f.fields_for(association, new_object, child_index: id) do |builder| - render(association.to_s.singularize + "_fields", f: builder) + fields = form.fields_for(association, new_object, child_index: id) do |builder| + render(association.to_s.singularize + '_fields', f: builder) end - link_to(name, '#', class: "add_fields", data: {id: id, fields: fields.gsub("\n", "")}) + link_to(name, '#', class: 'add_fields', data: { id: id, fields: fields.delete("\n") }) end # Return the props required to instantiate a React ModelForm component for the diff --git a/app/jobs/disco_app/app_installed_job.rb b/app/jobs/disco_app/app_installed_job.rb index 5d3507f5..c6fdbc64 100644 --- a/app/jobs/disco_app/app_installed_job.rb +++ b/app/jobs/disco_app/app_installed_job.rb @@ -1,3 +1,5 @@ class DiscoApp::AppInstalledJob < DiscoApp::ShopJob + include DiscoApp::Concerns::AppInstalledJob + end diff --git a/app/jobs/disco_app/app_uninstalled_job.rb b/app/jobs/disco_app/app_uninstalled_job.rb index 16878d93..2b5a1e8e 100644 --- a/app/jobs/disco_app/app_uninstalled_job.rb +++ b/app/jobs/disco_app/app_uninstalled_job.rb @@ -1,3 +1,5 @@ class DiscoApp::AppUninstalledJob < DiscoApp::ShopJob + include DiscoApp::Concerns::AppUninstalledJob + end diff --git a/app/jobs/disco_app/concerns/app_installed_job.rb b/app/jobs/disco_app/concerns/app_installed_job.rb index 95ebe0e3..29f93271 100644 --- a/app/jobs/disco_app/concerns/app_installed_job.rb +++ b/app/jobs/disco_app/concerns/app_installed_job.rb @@ -1,4 +1,5 @@ module DiscoApp::Concerns::AppInstalledJob + extend ActiveSupport::Concern included do @@ -21,9 +22,7 @@ def perform(_shop, plan_code = nil, source = nil) @shop.reload - if default_plan.present? - DiscoApp::SubscriptionService.subscribe(@shop, default_plan, plan_code, source) - end + DiscoApp::SubscriptionService.subscribe(@shop, default_plan, plan_code, source) if default_plan.present? end # Provide an overridable hook for applications to examine the @shop object diff --git a/app/jobs/disco_app/concerns/app_uninstalled_job.rb b/app/jobs/disco_app/concerns/app_uninstalled_job.rb index 382e8410..255820bd 100644 --- a/app/jobs/disco_app/concerns/app_uninstalled_job.rb +++ b/app/jobs/disco_app/concerns/app_uninstalled_job.rb @@ -1,4 +1,5 @@ module DiscoApp::Concerns::AppUninstalledJob + extend ActiveSupport::Concern included do @@ -12,7 +13,7 @@ module DiscoApp::Concerns::AppUninstalledJob # - Mark any recurring application charges as cancelled. # - Remove any stored sessions for the shop. # - def perform(_shop, shop_data) + def perform(_shop, _shop_data) DiscoApp::ChargesService.cancel_recurring_charges(@shop) DiscoApp::SendSubscriptionJob.perform_later(@shop) @shop.sessions.delete_all diff --git a/app/jobs/disco_app/concerns/render_asset_group_job.rb b/app/jobs/disco_app/concerns/render_asset_group_job.rb index 2b9e7f04..656e595a 100644 --- a/app/jobs/disco_app/concerns/render_asset_group_job.rb +++ b/app/jobs/disco_app/concerns/render_asset_group_job.rb @@ -1,4 +1,5 @@ module DiscoApp::Concerns::RenderAssetGroupJob + extend ActiveSupport::Concern def perform(_shop, instance, asset_group) diff --git a/app/jobs/disco_app/concerns/shop_update_job.rb b/app/jobs/disco_app/concerns/shop_update_job.rb index 6d0b9c26..37b4f8cb 100644 --- a/app/jobs/disco_app/concerns/shop_update_job.rb +++ b/app/jobs/disco_app/concerns/shop_update_job.rb @@ -1,13 +1,20 @@ module DiscoApp::Concerns::ShopUpdateJob + extend ActiveSupport::Concern # Perform an update of the current shop's information. def perform(_shop, shop_data = nil) # If we weren't provided with shop data (eg from a webhook), fetch it. - shop_data ||= ActiveSupport::JSON::decode(ShopifyAPI::Shop.current.to_json) + shop_data ||= JSON.parse(ShopifyAPI::Shop.current.to_json) # Update attributes stored directly on the Shop model, along with the data hash itself. - @shop.update(shop_data.with_indifferent_access.slice(*DiscoApp::Shop.column_names).except(:id, :created_at).merge(data: shop_data)) + @shop.update( + shop_data + .with_indifferent_access + .slice(*DiscoApp::Shop.column_names) + .except(:id, :created_at) + .merge(data: shop_data) + ) end end diff --git a/app/jobs/disco_app/concerns/subscription_changed_job.rb b/app/jobs/disco_app/concerns/subscription_changed_job.rb index 1387d865..7de17a84 100644 --- a/app/jobs/disco_app/concerns/subscription_changed_job.rb +++ b/app/jobs/disco_app/concerns/subscription_changed_job.rb @@ -1,7 +1,8 @@ module DiscoApp::Concerns::SubscriptionChangedJob + extend ActiveSupport::Concern - def perform(_shop, subscription) + def perform(_shop, _subscription) DiscoApp::SendSubscriptionJob.perform_later(@shop) end diff --git a/app/jobs/disco_app/concerns/synchronise_carrier_service_job.rb b/app/jobs/disco_app/concerns/synchronise_carrier_service_job.rb index ca6093c1..c2e84ed3 100644 --- a/app/jobs/disco_app/concerns/synchronise_carrier_service_job.rb +++ b/app/jobs/disco_app/concerns/synchronise_carrier_service_job.rb @@ -1,10 +1,11 @@ module DiscoApp::Concerns::SynchroniseCarrierServiceJob + extend ActiveSupport::Concern # Ensure that any carrier service required by our app is registered. def perform(_shop) # Don't proceed unless we have a name and callback url. - return unless carrier_service_name and callback_url + return unless carrier_service_name && callback_url # Registered the carrier service if it hasn't been registered yet. unless current_carrier_service_names.include?(carrier_service_name) @@ -19,11 +20,11 @@ def perform(_shop) # Ensure any existing carrier services (with the correct name) are active # and have a current callback URL. current_carrier_services.each do |carrier_service| - if carrier_service.name == carrier_service_name - carrier_service.callback_url = callback_url - carrier_service.active = true - carrier_service.save - end + next unless carrier_service.name == carrier_service_name + + carrier_service.callback_url = callback_url + carrier_service.active = true + carrier_service.save end end @@ -49,7 +50,7 @@ def current_carrier_service_names # Return a list of currently registered carrier services. def current_carrier_services - @current_carrier_service ||= ShopifyAPI::CarrierService.find(:all) + @current_carrier_services ||= ShopifyAPI::CarrierService.find(:all) end end diff --git a/app/jobs/disco_app/concerns/synchronise_resources_job.rb b/app/jobs/disco_app/concerns/synchronise_resources_job.rb index 63a159c4..a417a566 100644 --- a/app/jobs/disco_app/concerns/synchronise_resources_job.rb +++ b/app/jobs/disco_app/concerns/synchronise_resources_job.rb @@ -1,4 +1,5 @@ module DiscoApp::Concerns::SynchroniseResourcesJob + extend ActiveSupport::Concern def perform(_shop, class_name, params) diff --git a/app/jobs/disco_app/concerns/synchronise_users_job.rb b/app/jobs/disco_app/concerns/synchronise_users_job.rb index cefb3ba1..0d25647a 100644 --- a/app/jobs/disco_app/concerns/synchronise_users_job.rb +++ b/app/jobs/disco_app/concerns/synchronise_users_job.rb @@ -1,14 +1,17 @@ module DiscoApp::Concerns::SynchroniseUsersJob + extend ActiveSupport::Concern def perform(_shop) begin - users = @shop.with_api_context { + users = @shop.with_api_context do ShopifyAPI::User.all - } + end rescue ActiveResource::UnauthorizedAccess => e - Rollbar.error(e) and return + Appsignal.set_error(e) + return end + users.each { |user| DiscoApp::User.create_user(user, @shop) } end diff --git a/app/jobs/disco_app/concerns/synchronise_webhooks_job.rb b/app/jobs/disco_app/concerns/synchronise_webhooks_job.rb index c0a762b1..912db856 100644 --- a/app/jobs/disco_app/concerns/synchronise_webhooks_job.rb +++ b/app/jobs/disco_app/concerns/synchronise_webhooks_job.rb @@ -1,7 +1,8 @@ module DiscoApp::Concerns::SynchroniseWebhooksJob + extend ActiveSupport::Concern - COMMON_WEBHOOKS = [:'app/uninstalled', :'shop/update'] + COMMON_WEBHOOKS = [:'app/uninstalled', :'shop/update'].freeze # Ensure the webhooks registered with our shop are the same as those listed # in our application configuration. @@ -12,16 +13,14 @@ def perform(_shop) ShopifyAPI::Webhook.create( topic: topic, address: webhooks_url, - format: 'json', + format: 'json' ) end end # Remove any extraneous topics. current_webhooks.each do |webhook| - unless expected_topics.include?(webhook.topic.to_sym) - ShopifyAPI::Webhook.delete(webhook.id) - end + ShopifyAPI::Webhook.delete(webhook.id) unless expected_topics.include?(webhook.topic.to_sym) end # Ensure webhook addresses are current. diff --git a/app/jobs/disco_app/render_asset_group_job.rb b/app/jobs/disco_app/render_asset_group_job.rb index 7075b2ad..f2f7a5b9 100644 --- a/app/jobs/disco_app/render_asset_group_job.rb +++ b/app/jobs/disco_app/render_asset_group_job.rb @@ -1,3 +1,5 @@ class DiscoApp::RenderAssetGroupJob < DiscoApp::ShopJob + include DiscoApp::Concerns::RenderAssetGroupJob + end diff --git a/app/jobs/disco_app/shop_job.rb b/app/jobs/disco_app/shop_job.rb index fb02d51a..8b000002 100644 --- a/app/jobs/disco_app/shop_job.rb +++ b/app/jobs/disco_app/shop_job.rb @@ -18,7 +18,9 @@ class DiscoApp::ShopJob < ApplicationJob private def find_shop(job) - @shop ||= job.arguments.first.is_a?(DiscoApp::Shop) ? job.arguments.first : DiscoApp::Shop.find_by!(shopify_domain: job.arguments.first) + return @shop if @shop + + @shop = job.arguments.first.is_a?(DiscoApp::Shop) ? job.arguments.first : DiscoApp::Shop.find_by!(shopify_domain: job.arguments.first) end def shop_context(job, block) @@ -26,7 +28,6 @@ def shop_context(job, block) shop_id: @shop.id, shopify_domain: @shop.shopify_domain ) - @shop.with_api_context { block.call(job.arguments) } end diff --git a/app/jobs/disco_app/shop_update_job.rb b/app/jobs/disco_app/shop_update_job.rb index 3ae4298c..dfc71610 100644 --- a/app/jobs/disco_app/shop_update_job.rb +++ b/app/jobs/disco_app/shop_update_job.rb @@ -1,3 +1,5 @@ class DiscoApp::ShopUpdateJob < DiscoApp::ShopJob + include DiscoApp::Concerns::ShopUpdateJob + end diff --git a/app/jobs/disco_app/subscription_changed_job.rb b/app/jobs/disco_app/subscription_changed_job.rb index e28beeaf..0800bee0 100644 --- a/app/jobs/disco_app/subscription_changed_job.rb +++ b/app/jobs/disco_app/subscription_changed_job.rb @@ -1,3 +1,5 @@ class DiscoApp::SubscriptionChangedJob < DiscoApp::ShopJob + include DiscoApp::Concerns::SubscriptionChangedJob + end diff --git a/app/jobs/disco_app/synchronise_carrier_service_job.rb b/app/jobs/disco_app/synchronise_carrier_service_job.rb index 543e1c01..4e4533aa 100644 --- a/app/jobs/disco_app/synchronise_carrier_service_job.rb +++ b/app/jobs/disco_app/synchronise_carrier_service_job.rb @@ -1,3 +1,5 @@ class DiscoApp::SynchroniseCarrierServiceJob < DiscoApp::ShopJob + include DiscoApp::Concerns::SynchroniseCarrierServiceJob + end diff --git a/app/jobs/disco_app/synchronise_resources_job.rb b/app/jobs/disco_app/synchronise_resources_job.rb index d97ee3cd..1af79436 100644 --- a/app/jobs/disco_app/synchronise_resources_job.rb +++ b/app/jobs/disco_app/synchronise_resources_job.rb @@ -1,3 +1,5 @@ class DiscoApp::SynchroniseResourcesJob < DiscoApp::ShopJob + include DiscoApp::Concerns::SynchroniseResourcesJob + end diff --git a/app/jobs/disco_app/synchronise_users_job.rb b/app/jobs/disco_app/synchronise_users_job.rb index 381962c9..4d03cf09 100644 --- a/app/jobs/disco_app/synchronise_users_job.rb +++ b/app/jobs/disco_app/synchronise_users_job.rb @@ -1,3 +1,5 @@ class DiscoApp::SynchroniseUsersJob < DiscoApp::ShopJob + include DiscoApp::Concerns::SynchroniseUsersJob + end diff --git a/app/jobs/disco_app/synchronise_webhooks_job.rb b/app/jobs/disco_app/synchronise_webhooks_job.rb index df5098a6..0920c62a 100644 --- a/app/jobs/disco_app/synchronise_webhooks_job.rb +++ b/app/jobs/disco_app/synchronise_webhooks_job.rb @@ -1,3 +1,5 @@ class DiscoApp::SynchroniseWebhooksJob < DiscoApp::ShopJob + include DiscoApp::Concerns::SynchroniseWebhooksJob + end diff --git a/app/models/application_record.rb b/app/models/application_record.rb index 10a4cba8..c6ae68f2 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -1,3 +1,5 @@ class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true + end diff --git a/app/models/disco_app/app_settings.rb b/app/models/disco_app/app_settings.rb index 70a19597..86817b00 100644 --- a/app/models/disco_app/app_settings.rb +++ b/app/models/disco_app/app_settings.rb @@ -1,3 +1,5 @@ class DiscoApp::AppSettings < ApplicationRecord + include DiscoApp::Concerns::AppSettings + end diff --git a/app/models/disco_app/application_charge.rb b/app/models/disco_app/application_charge.rb index 0fe3720c..c7656828 100644 --- a/app/models/disco_app/application_charge.rb +++ b/app/models/disco_app/application_charge.rb @@ -8,7 +8,7 @@ class DiscoApp::ApplicationCharge < ApplicationRecord accepted: 1, declined: 2, expired: 3, - active: 4, + active: 4 } scope :active, -> { where status: statuses[:active] } diff --git a/app/models/disco_app/concerns/app_settings.rb b/app/models/disco_app/concerns/app_settings.rb index 17a41bef..ce90afe7 100644 --- a/app/models/disco_app/concerns/app_settings.rb +++ b/app/models/disco_app/concerns/app_settings.rb @@ -1,7 +1,9 @@ module DiscoApp::Concerns::AppSettings + extend ActiveSupport::Concern included do acts_as_singleton end + end diff --git a/app/models/disco_app/concerns/can_be_liquified.rb b/app/models/disco_app/concerns/can_be_liquified.rb index 973f6581..b93a6870 100644 --- a/app/models/disco_app/concerns/can_be_liquified.rb +++ b/app/models/disco_app/concerns/can_be_liquified.rb @@ -1,10 +1,11 @@ module DiscoApp::Concerns::CanBeLiquified + extend ActiveSupport::Concern - SPLIT_ARRAY_SEPARATOR = '@!@' + SPLIT_ARRAY_SEPARATOR = '@!@'.freeze + NIL_VALUE = 'nil'.freeze included do - # Return this model as a hash for use with `to_liquid`. Returns `as_json` by default but is provided here as a hook # for potential overrides. def as_liquid @@ -26,21 +27,35 @@ def liquid_model_name # Return given value as a string expression that will be evaluated in Liquid to result in the correct value type. def as_liquid_value(key, value) - if value.is_a? Numeric or (!!value == value) - return value.to_s - end - if value.nil? - return 'nil' - end - if value.is_a? Array - return "'#{value.map { |e| (e.is_a? String) ? CGI::escapeHTML(e) : e }.join(SPLIT_ARRAY_SEPARATOR)}' | split: '#{SPLIT_ARRAY_SEPARATOR}'" - end - if value.is_a? String and key.end_with? '_html' - return "'#{value.to_s.gsub("'", "'")}'" + html_string = ->(val) { val.is_a?(String) && key.end_with?('_html') } + + case value + when Numeric, TrueClass, FalseClass + value.to_s + when NilClass + NIL_VALUE + when Array + as_liquid_array_value(value) + when html_string + as_liquid_html_value(value) + else + "'#{CGI.escapeHTML(value.to_s)}'" end - "'#{CGI::escapeHTML(value.to_s)}'" end + def as_liquid_array_value(value) + split_array = value.map do |element| + CGI.escapeHTML(element) if element.is_a? String + + element + end.join(SPLIT_ARRAY_SEPARATOR) + + "'#{split_array}' | split: '#{SPLIT_ARRAY_SEPARATOR}'" + end + + def as_liquid_html_value(value) + "'#{value.to_s.gsub("'", ''')}'" + end end end diff --git a/app/models/disco_app/concerns/has_metafields.rb b/app/models/disco_app/concerns/has_metafields.rb index 36423078..6db95511 100644 --- a/app/models/disco_app/concerns/has_metafields.rb +++ b/app/models/disco_app/concerns/has_metafields.rb @@ -1,8 +1,8 @@ module DiscoApp::Concerns::HasMetafields + extend ActiveSupport::Concern included do - # Write multiple metafields for this object in a single call. # # Expects an argument in a nested hash structure with :namespace => :key => @@ -42,7 +42,6 @@ def build_metafields(metafields) end end.flatten end - end end diff --git a/app/models/disco_app/concerns/plan.rb b/app/models/disco_app/concerns/plan.rb index b89cd5b1..c5280f19 100644 --- a/app/models/disco_app/concerns/plan.rb +++ b/app/models/disco_app/concerns/plan.rb @@ -1,9 +1,9 @@ module DiscoApp::Concerns::Plan + extend ActiveSupport::Concern included do - - has_many :subscriptions + has_many :subscriptions, dependent: :restrict_with_exception has_many :shops, through: :subscriptions has_many :plan_codes, dependent: :destroy @@ -25,11 +25,10 @@ module DiscoApp::Concerns::Plan scope :available, -> { where status: statuses[:available] } validates_presence_of :name - end def has_trial? - trial_period_days.present? and trial_period_days > 0 + trial_period_days.present? && trial_period_days.positive? end end diff --git a/app/models/disco_app/concerns/plan_code.rb b/app/models/disco_app/concerns/plan_code.rb index b7892653..0e1b68a2 100644 --- a/app/models/disco_app/concerns/plan_code.rb +++ b/app/models/disco_app/concerns/plan_code.rb @@ -1,8 +1,8 @@ module DiscoApp::Concerns::PlanCode + extend ActiveSupport::Concern included do - belongs_to :plan enum status: { @@ -12,7 +12,6 @@ module DiscoApp::Concerns::PlanCode validates_presence_of :code validates_presence_of :amount - end end diff --git a/app/models/disco_app/concerns/renders_assets.rb b/app/models/disco_app/concerns/renders_assets.rb index b3333c48..6c59229e 100644 --- a/app/models/disco_app/concerns/renders_assets.rb +++ b/app/models/disco_app/concerns/renders_assets.rb @@ -2,6 +2,7 @@ require 'uglifier' module DiscoApp::Concerns::RendersAssets + extend ActiveSupport::Concern included do @@ -10,7 +11,6 @@ module DiscoApp::Concerns::RendersAssets end class_methods do - # Ruby "macro" that allows the definition of a number of Shopify assets that # should be rendered and uploaded when certain attributes on the including # class change. This assumes that the including class (1) is an ActiveRecord @@ -70,7 +70,6 @@ def renders_assets_default_options minify: Rails.env.production? || Rails.env.staging? } end - end # Callback, triggered after a model save. Iterates through each asset group @@ -78,9 +77,7 @@ def renders_assets_default_options # attributes are found in the asset group's triggered_by list. def queue_render_asset_group_job renderable_asset_groups.each do |asset_group, options| - unless (previous_changes.keys & options[:triggered_by]).empty? - DiscoApp::RenderAssetGroupJob.perform_later(shop, self, asset_group.to_s) - end + DiscoApp::RenderAssetGroupJob.perform_later(shop, self, asset_group.to_s) unless (previous_changes.keys & options[:triggered_by]).empty? end end @@ -97,26 +94,22 @@ def render_asset_group(asset_group) options[:assets].each do |asset| # Create/replace the asset via the Shopify API. - shopify_asset = shop.with_api_context { + shopify_asset = shop.with_api_context do ShopifyAPI::Asset.create( key: asset, value: render_asset_group_asset(asset, public_urls, options) ) - } + end # Store the public URL to this asset, so that we're able to use it in # subsequent template renders. Adds a .css suffix to .scss assets, so that # we use the Shopify-compiled version of any SCSS stylesheets. - if shopify_asset.public_url.present? - public_urls[asset] = shopify_asset.public_url.gsub(/\.scss\?/, '.scss.css?') - end + public_urls[asset] = shopify_asset.public_url.gsub(/\.scss\?/, '.scss.css?') if shopify_asset.public_url.present? end # If we specified the creation of any script tags based on newly rendered # assets, do that now. - unless options[:script_tags].empty? - render_asset_script_tags(options, public_urls) - end + render_asset_script_tags(options, public_urls) unless options[:script_tags].empty? end private @@ -141,7 +134,7 @@ def render_asset_group_asset(asset, public_urls, options) # Return true if the given asset should be minified with Uglifier. def should_be_minified?(asset, options) - asset.to_s.end_with?('.js') and options[:minify] + asset.to_s.end_with?('.js') && options[:minify] end def render_asset_renderer @@ -157,9 +150,12 @@ def render_asset_script_tags(options, public_urls) # Iterate each script tag for which we have a known public URL and create # or update the corresponding script tag resource. public_urls.slice(*options[:script_tags]).each do |asset, public_url| - script_tag = current_script_tags.find(lambda { ShopifyAPI::ScriptTag.new(event: 'onload') }) { |script_tag| script_tag.src.include?("#{asset}?") } - script_tag.src = public_url - shop.with_api_context { script_tag.save } + generate_new_script_tag = -> { ShopifyAPI::ScriptTag.new(event: 'onload') } + + current_script_tags + .find(generate_new_script_tag) { |script_tag| script_tag.src.include?("#{asset}?") } + .tap { |script_tag| script_tag.src = public_url } + .yield_self { |script_tag| shop.with_api_context { script_tag.save } } end end diff --git a/app/models/disco_app/concerns/shop.rb b/app/models/disco_app/concerns/shop.rb index 96a4f858..2c1713ee 100644 --- a/app/models/disco_app/concerns/shop.rb +++ b/app/models/disco_app/concerns/shop.rb @@ -1,4 +1,5 @@ module DiscoApp::Concerns::Shop + extend ActiveSupport::Concern included do @@ -6,11 +7,11 @@ module DiscoApp::Concerns::Shop include ActionView::Helpers::DateHelper # Define relationships to plans and subscriptions. - has_many :subscriptions + has_many :subscriptions, dependent: :restrict_with_exception has_many :plans, through: :subscriptions # Define relationship to users. - has_many :users + has_many :users, dependent: :restrict_with_exception # Define relationship to sessions. has_many :sessions, class_name: 'DiscoApp::Session', dependent: :destroy @@ -31,7 +32,7 @@ module DiscoApp::Concerns::Shop } # Define some useful scopes. - scope :status, -> (status) { where status: status } + scope :status, ->(status) { where status: status } scope :installed, -> { where status: statuses[:installed] } scope :has_active_shopify_plan, -> { where.not(plan_name: [:cancelled, :frozen, :fraudulent]) } scope :shopify_plus, -> { where(plan_name: :shopify_plus) } @@ -86,8 +87,8 @@ def installed_duration def time_zone @time_zone ||= begin Time.find_zone!(data[:timezone].to_s.gsub(/^\(.+\)\s/, '')) - rescue ArgumentError - Time.zone + rescue ArgumentError + Time.zone end end @@ -99,14 +100,13 @@ def locale # Return an instance of the Disco API client. def disco_api_client - @api_client ||= DiscoApp::ApiClient.new(self, ENV['DISCO_API_URL']) + @disco_api_client ||= DiscoApp::ApiClient.new(self, ENV['DISCO_API_URL']) end # Override the "read" data attribute to allow indifferent access. def data read_attribute(:data).with_indifferent_access end - end end diff --git a/app/models/disco_app/concerns/source.rb b/app/models/disco_app/concerns/source.rb index 37651091..be0290f2 100644 --- a/app/models/disco_app/concerns/source.rb +++ b/app/models/disco_app/concerns/source.rb @@ -1,14 +1,13 @@ module DiscoApp::Concerns::Source + extend ActiveSupport::Concern included do - - has_many :subscriptions + has_many :subscriptions, dependent: :restrict_with_exception has_many :shops, through: :subscriptions validates_presence_of :source validates_presence_of :name - end end diff --git a/app/models/disco_app/concerns/subscription.rb b/app/models/disco_app/concerns/subscription.rb index dcd003ef..eeb7490c 100644 --- a/app/models/disco_app/concerns/subscription.rb +++ b/app/models/disco_app/concerns/subscription.rb @@ -1,8 +1,8 @@ module DiscoApp::Concerns::Subscription + extend ActiveSupport::Concern included do - belongs_to :shop belongs_to :plan belongs_to :plan_code, optional: true @@ -23,12 +23,11 @@ module DiscoApp::Concerns::Subscription scope :current, -> { where status: [statuses[:trial], statuses[:active]] } after_commit :cancel_charge - end # Only require an active charge if the amount to be charged is > 0. def requires_active_charge? - amount > 0 + amount.positive? end # Convenience method to check if this subscription has an active charge. @@ -67,6 +66,7 @@ def as_json(options = {}) def cancel_charge return if (previous_changes.keys & ['amount', 'trial_period_days']).empty? return unless active_charge? + active_charge.cancelled! end diff --git a/app/models/disco_app/concerns/synchronises.rb b/app/models/disco_app/concerns/synchronises.rb index 2cce9b05..7ec4ad30 100644 --- a/app/models/disco_app/concerns/synchronises.rb +++ b/app/models/disco_app/concerns/synchronises.rb @@ -1,31 +1,29 @@ module DiscoApp::Concerns::Synchronises + extend ActiveSupport::Concern class_methods do - # Define the number of resources per page to fetch. SYNCHRONISES_PAGE_LIMIT = 250 - def should_synchronise?(shop, data) + def should_synchronise?(_shop, _data) true end - def synchronise_by(shop, data) + def synchronise_by(_shop, data) { id: data[:id] } end def synchronise(shop, data) - if data.is_a?(ShopifyAPI::Base) - data = ActiveSupport::JSON.decode(data.to_json) - end + data = JSON.parse(data.to_json) if data.is_a?(ShopifyAPI::Base) data = data.with_indifferent_access return unless should_synchronise?(shop, data) begin - instance = self.find_or_create_by!(self.synchronise_by(shop, data)) do |instance| - instance.shop = shop - instance.data = data + instance = find_or_create_by!(synchronise_by(shop, data)) do |new_instance| + new_instance.shop = shop + new_instance.data = data end rescue ActiveRecord::RecordNotUnique, PG::UniqueViolation retry @@ -36,7 +34,7 @@ def synchronise(shop, data) instance end - def should_synchronise_deletion?(shop, data) + def should_synchronise_deletion?(_shop, _data) true end @@ -45,26 +43,23 @@ def synchronise_deletion(shop, data) return unless should_synchronise_deletion?(shop, data) - self.where(shop: shop, id: data[:id]).destroy_all + where(shop: shop, id: data[:id]).destroy_all end def synchronise_all(shop, params = {}) resource_count = shop.with_api_context { self::SHOPIFY_API_CLASS.count(params) } (1..(resource_count / SYNCHRONISES_PAGE_LIMIT.to_f).ceil).each do |page| - DiscoApp::SynchroniseResourcesJob.perform_later(shop, self.name, params.merge(page: page, limit: SYNCHRONISES_PAGE_LIMIT)) + DiscoApp::SynchroniseResourcesJob.perform_later(shop, name, params.merge(page: page, limit: SYNCHRONISES_PAGE_LIMIT)) end end - end included do - # Override the "read" data attribute to allow indifferent access. def data read_attribute(:data).with_indifferent_access end - end end diff --git a/app/models/disco_app/concerns/taggable.rb b/app/models/disco_app/concerns/taggable.rb index 6ce067d5..737b62f3 100644 --- a/app/models/disco_app/concerns/taggable.rb +++ b/app/models/disco_app/concerns/taggable.rb @@ -1,4 +1,5 @@ module DiscoApp::Concerns::Taggable + extend ActiveSupport::Concern def tags diff --git a/app/models/disco_app/concerns/user.rb b/app/models/disco_app/concerns/user.rb index d0523c55..b524acf8 100644 --- a/app/models/disco_app/concerns/user.rb +++ b/app/models/disco_app/concerns/user.rb @@ -1,11 +1,12 @@ module DiscoApp::Concerns::User + extend ActiveSupport::Concern included do belongs_to :shop def self.create_user(shopify_user, shop) - user = self.find_or_create_by!(id: shopify_user.id, shop: shop) + user = find_or_create_by!(id: shopify_user.id, shop: shop) user.update( first_name: shopify_user.first_name || '', last_name: shopify_user.last_name || '', @@ -15,6 +16,6 @@ def self.create_user(shopify_user, shop) rescue ActiveRecord::RecordNotUnique, PG::UniqueViolation retry end - end + end diff --git a/app/models/disco_app/flow/action.rb b/app/models/disco_app/flow/action.rb index 58c2c5c9..3ed97ac1 100644 --- a/app/models/disco_app/flow/action.rb +++ b/app/models/disco_app/flow/action.rb @@ -1,7 +1,9 @@ module DiscoApp module Flow class Action < ApplicationRecord + include DiscoApp::Flow::Concerns::Action + end end end diff --git a/app/models/disco_app/flow/concerns/action.rb b/app/models/disco_app/flow/concerns/action.rb index b4fae459..820be067 100644 --- a/app/models/disco_app/flow/concerns/action.rb +++ b/app/models/disco_app/flow/concerns/action.rb @@ -2,10 +2,10 @@ module DiscoApp module Flow module Concerns module Action + extend ActiveSupport::Concern included do - belongs_to :shop self.table_name = :disco_app_flow_actions @@ -16,6 +16,9 @@ module Action failed: 2 } + def properties + read_attribute(:properties).with_indifferent_access + end end end diff --git a/app/models/disco_app/flow/concerns/trigger.rb b/app/models/disco_app/flow/concerns/trigger.rb index 49edc651..a1c48ca1 100644 --- a/app/models/disco_app/flow/concerns/trigger.rb +++ b/app/models/disco_app/flow/concerns/trigger.rb @@ -2,10 +2,10 @@ module DiscoApp module Flow module Concerns module Trigger + extend ActiveSupport::Concern included do - belongs_to :shop self.table_name = :disco_app_flow_triggers @@ -16,6 +16,9 @@ module Trigger failed: 2 } + def properties + read_attribute(:properties).with_indifferent_access + end end end diff --git a/app/models/disco_app/flow/trigger.rb b/app/models/disco_app/flow/trigger.rb index 4b7a9b23..0e623fa4 100644 --- a/app/models/disco_app/flow/trigger.rb +++ b/app/models/disco_app/flow/trigger.rb @@ -1,7 +1,9 @@ module DiscoApp module Flow class Trigger < ApplicationRecord + include DiscoApp::Flow::Concerns::Trigger + end end end diff --git a/app/models/disco_app/plan.rb b/app/models/disco_app/plan.rb index 514202da..1c547879 100644 --- a/app/models/disco_app/plan.rb +++ b/app/models/disco_app/plan.rb @@ -1,3 +1,5 @@ class DiscoApp::Plan < ApplicationRecord + include DiscoApp::Concerns::Plan + end diff --git a/app/models/disco_app/plan_code.rb b/app/models/disco_app/plan_code.rb index 0ccf33e8..b51e5182 100644 --- a/app/models/disco_app/plan_code.rb +++ b/app/models/disco_app/plan_code.rb @@ -1,3 +1,5 @@ class DiscoApp::PlanCode < ApplicationRecord + include DiscoApp::Concerns::PlanCode + end diff --git a/app/models/disco_app/session_storage.rb b/app/models/disco_app/session_storage.rb index c60a3842..b0021feb 100644 --- a/app/models/disco_app/session_storage.rb +++ b/app/models/disco_app/session_storage.rb @@ -1,5 +1,6 @@ module DiscoApp class SessionStorage + def self.store(session) shop = Shop.find_or_initialize_by(shopify_domain: session.url) shop.shopify_token = session.token @@ -9,10 +10,12 @@ def self.store(session) def self.retrieve(id) return unless id + shop = Shop.find(id) ShopifyAPI::Session.new(shop.shopify_domain, shop.shopify_token) rescue ActiveRecord::RecordNotFound nil end + end end diff --git a/app/models/disco_app/shop.rb b/app/models/disco_app/shop.rb index 1395c25a..74c55964 100644 --- a/app/models/disco_app/shop.rb +++ b/app/models/disco_app/shop.rb @@ -1,3 +1,5 @@ class DiscoApp::Shop < ApplicationRecord + include DiscoApp::Concerns::Shop + end diff --git a/app/models/disco_app/source.rb b/app/models/disco_app/source.rb index 2da69244..eebb0ec2 100644 --- a/app/models/disco_app/source.rb +++ b/app/models/disco_app/source.rb @@ -1,3 +1,5 @@ class DiscoApp::Source < ApplicationRecord + include DiscoApp::Concerns::Source + end diff --git a/app/models/disco_app/subscription.rb b/app/models/disco_app/subscription.rb index 8a598db6..38a6525e 100644 --- a/app/models/disco_app/subscription.rb +++ b/app/models/disco_app/subscription.rb @@ -1,3 +1,5 @@ class DiscoApp::Subscription < ApplicationRecord + include DiscoApp::Concerns::Subscription + end diff --git a/app/models/disco_app/user.rb b/app/models/disco_app/user.rb index e8d79be1..158d4366 100644 --- a/app/models/disco_app/user.rb +++ b/app/models/disco_app/user.rb @@ -1,3 +1,5 @@ class DiscoApp::User < ApplicationRecord + include DiscoApp::Concerns::User + end diff --git a/app/resources/disco_app/admin/resources/concerns/shop_resource.rb b/app/resources/disco_app/admin/resources/concerns/shop_resource.rb index 646f3f37..3dc4ac70 100644 --- a/app/resources/disco_app/admin/resources/concerns/shop_resource.rb +++ b/app/resources/disco_app/admin/resources/concerns/shop_resource.rb @@ -1,10 +1,10 @@ require 'jsonapi/resource' module DiscoApp::Admin::Resources::Concerns::ShopResource + extend ActiveSupport::Concern included do - attributes :domain, :status, :created_at attributes :email, :country_name, :currency, :plan_display_name attributes :current_subscription_id, :current_subscription_display_amount, :current_subscription_display_plan, :current_subscription_display_plan_code, :current_subscription_source @@ -15,33 +15,33 @@ module DiscoApp::Admin::Resources::Concerns::ShopResource filters :query, :status # Adjust the base records method to ensure only models for the authenticated domain are retrieved. - def self.records(options = {}) + def self.records(_options = {}) records = DiscoApp::Shop.order(created_at: :desc) records end # Apply filters. - def self.apply_filter(records, filter, value, options) + def self.apply_filter(records, filter, value, _options) return records if value.blank? # Perform appropriate filtering. case filter - when :query - return records.where('name LIKE ? OR shopify_domain LIKE ? OR domain LIKE ?', "%#{value.first}%", "%#{value.first}%", "%#{value.first}%") - when :status - return records.where(status: value.map { |v| DiscoApp::Shop.statuses[v.to_sym] } ) - else - return super(records, filter, value) + when :query + return records.where('name LIKE ? OR shopify_domain LIKE ? OR domain LIKE ?', "%#{value.first}%", "%#{value.first}%", "%#{value.first}%") + when :status + return records.where(status: value.map { |v| DiscoApp::Shop.statuses[v.to_sym] }) + else + return super(records, filter, value) end end # Don't allow the update of any fields via the API. - def self.updatable_fields(context) + def self.updatable_fields(_context) [] end # Don't allow the creation of any fields via the API. - def self.creatable_fields(context) + def self.creatable_fields(_context) [] end @@ -62,9 +62,7 @@ def plan_display_name end def current_subscription_id - if @model.current_subscription? - @model.current_subscription.id - end + @model.current_subscription.id if @model.current_subscription? end def current_subscription_display_amount @@ -94,7 +92,6 @@ def current_subscription_source '-' end end - end end diff --git a/app/resources/disco_app/admin/resources/shop_resource.rb b/app/resources/disco_app/admin/resources/shop_resource.rb index 22924431..117a9115 100644 --- a/app/resources/disco_app/admin/resources/shop_resource.rb +++ b/app/resources/disco_app/admin/resources/shop_resource.rb @@ -1,4 +1,5 @@ class DiscoApp::Admin::Resources::ShopResource < JSONAPI::Resource + include DiscoApp::Admin::Resources::Concerns::ShopResource end diff --git a/app/services/disco_app/carrier_request_service.rb b/app/services/disco_app/carrier_request_service.rb index 0fe3a4d9..ad0b98f1 100644 --- a/app/services/disco_app/carrier_request_service.rb +++ b/app/services/disco_app/carrier_request_service.rb @@ -2,13 +2,13 @@ class DiscoApp::CarrierRequestService # Return true iff the provided hmac_to_verify matches that calculated from the # given data and secret. - def self.is_valid_hmac?(body, secret, hmac_to_verify) - ActiveSupport::SecurityUtils.secure_compare(self.calculated_hmac(body, secret), hmac_to_verify.to_s) + def self.valid_hmac?(body, secret, hmac_to_verify) + ActiveSupport::SecurityUtils.secure_compare(calculated_hmac(body, secret), hmac_to_verify.to_s) end # Calculate the HMAC for the given data and secret. def self.calculated_hmac(body, secret) - digest = OpenSSL::Digest.new('sha256') + digest = OpenSSL::Digest.new('sha256') Base64.encode64(OpenSSL::HMAC.digest(digest, secret, body)).strip end diff --git a/app/services/disco_app/charges_service.rb b/app/services/disco_app/charges_service.rb index 97466b99..935c271a 100644 --- a/app/services/disco_app/charges_service.rb +++ b/app/services/disco_app/charges_service.rb @@ -6,24 +6,22 @@ def self.create(shop, subscription) # Create the charge object locally first. charge = subscription.charge_class.create!( shop: shop, - subscription: subscription, + subscription: subscription ) # Create the charge object on Shopify. - shopify_charge = shop.with_api_context { + shopify_charge = shop.with_api_context do subscription.shopify_charge_class.create( name: subscription.plan.name, - price: '%.2f' % (subscription.amount.to_f / 100.0), + price: format('%.2f', (subscription.amount.to_f / 100.0)), trial_days: subscription.plan.has_trial? ? subscription.trial_period_days : nil, return_url: charge.activate_url, test: !DiscoApp.configuration.real_charges? ) - } + end # If we couldn't create the charge on Shopify, return nil. - if shopify_charge.nil? - return nil - end + return nil if shopify_charge.nil? # Update the local record of the charge from Shopify's created charge, then # return it. @@ -37,45 +35,39 @@ def self.create(shop, subscription) # Attempt to activate the given Shopify charge for the given Shop using the # Shopify API. Returns true on successful activation, false otherwise. def self.activate(shop, charge) - begin - # Start by fetching the Shopify charge to check that it was accepted. - shopify_charge = shop.with_api_context { - charge.subscription.shopify_charge_class.find(charge.shopify_id) - } + # Start by fetching the Shopify charge to check that it was accepted. + shopify_charge = shop.with_api_context do + charge.subscription.shopify_charge_class.find(charge.shopify_id) + end - # Update the status of the local charge based on the Shopify charge. - charge.send("#{shopify_charge.status}!") if charge.respond_to? "#{shopify_charge.status}!" + # Update the status of the local charge based on the Shopify charge. + charge.send("#{shopify_charge.status}!") if charge.respond_to? "#{shopify_charge.status}!" - # If the charge wasn't accepted, fail and return. - return false unless charge.accepted? + # If the charge wasn't accepted, fail and return. + return false unless charge.accepted? - # If the charge was indeed accepted, activate it via Shopify. - charge.shop.with_api_context { - shopify_charge.activate - } + # If the charge was indeed accepted, activate it via Shopify. + charge.shop.with_api_context do + shopify_charge.activate + end - # If the charge was recurring, make sure that all other local recurring - # charges are marked inactive. - if charge.recurring? - self.cancel_recurring_charges(shop, charge) - end + # If the charge was recurring, make sure that all other local recurring + # charges are marked inactive. + cancel_recurring_charges(shop, charge) if charge.recurring? - charge.active! + charge.active! - true - rescue - false - end + true + rescue StandardError + false end # Cancel all recurring charges for the given shop. If the optional charge # parameter is given, it will be excluded from the cancellation. def self.cancel_recurring_charges(shop, charge = nil) charges = DiscoApp::RecurringApplicationCharge.where(shop: shop) - if charge.present? - charges = charges.where.not(id: charge) - end - charges.update_all(status: DiscoApp::RecurringApplicationCharge.statuses[:cancelled]) + charges = charges.where.not(id: charge) if charge.present? + charges.update_all(status: DiscoApp::RecurringApplicationCharge.statuses[:cancelled]) # rubocop:disable Rails/SkipsModelValidations end end diff --git a/app/services/disco_app/flow/process_action.rb b/app/services/disco_app/flow/process_action.rb index c5e8765f..cf64cfc3 100644 --- a/app/services/disco_app/flow/process_action.rb +++ b/app/services/disco_app/flow/process_action.rb @@ -24,12 +24,12 @@ def validate_action def find_action_service_class context.action_service_class = action.action_id.classify.safe_constantize || - %Q(Flow::Actions::#{("#{action.action_id}".classify)}).safe_constantize + %(Flow::Actions::#{action.action_id.to_s.classify}).safe_constantize - if action_service_class.nil? - update_action(false, ["Could not find service class for #{action.action_id}"]) - context.fail! - end + return unless action_service_class.nil? + + update_action(false, ["Could not find service class for #{action.action_id}"]) + context.fail! end def execute_action_service_class diff --git a/app/services/disco_app/partner_app_service.rb b/app/services/disco_app/partner_app_service.rb index 30ec6f96..bc4060b3 100644 --- a/app/services/disco_app/partner_app_service.rb +++ b/app/services/disco_app/partner_app_service.rb @@ -79,7 +79,7 @@ def create_partner_app(apps_page) form['create_form[application_url]'] = @app_url # Accept TOS - unless form['create_form[accepted]'].blank? + if form['create_form[accepted]'].present? form['create_form[accepted]'] = '1' form.hiddens.last.value = 1 end diff --git a/app/services/disco_app/proxy_service.rb b/app/services/disco_app/proxy_service.rb index dda3aea2..71eb3459 100644 --- a/app/services/disco_app/proxy_service.rb +++ b/app/services/disco_app/proxy_service.rb @@ -5,12 +5,12 @@ class DiscoApp::ProxyService def self.proxy_signature_is_valid?(query_string, secret) query_hash = Rack::Utils.parse_query(query_string) signature = query_hash.delete('signature').to_s - ActiveSupport::SecurityUtils.secure_compare(self.calculated_signature(query_hash, secret), signature) + ActiveSupport::SecurityUtils.secure_compare(calculated_signature(query_hash, secret), signature) end # Return the calculated signature for the given query hash and secret. def self.calculated_signature(query_hash, secret) - sorted_params = query_hash.collect{ |k, v| "#{k}=#{Array(v).join(',')}" }.sort.join + sorted_params = query_hash.map{ |k, v| "#{k}=#{Array(v).join(',')}" }.sort.join OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), secret, sorted_params) end diff --git a/app/services/disco_app/request_validation_service.rb b/app/services/disco_app/request_validation_service.rb index b3ceedda..a9e79ba3 100644 --- a/app/services/disco_app/request_validation_service.rb +++ b/app/services/disco_app/request_validation_service.rb @@ -3,12 +3,12 @@ class DiscoApp::RequestValidationService def self.hmac_valid?(query_string, secret) query_hash = Rack::Utils.parse_query(query_string) hmac = query_hash.delete('hmac').to_s - ActiveSupport::SecurityUtils.secure_compare(self.calculated_hmac(query_hash, secret), hmac) + ActiveSupport::SecurityUtils.secure_compare(calculated_hmac(query_hash, secret), hmac) end # Return the calculated hmac for the given query hash and secret. def self.calculated_hmac(query_hash, secret) - sorted_params = query_hash.collect{ |k, v| "#{k}=#{Array(v).join(',')}" }.sort.join('&') + sorted_params = query_hash.map{ |k, v| "#{k}=#{Array(v).join(',')}" }.sort.join('&') OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), secret, sorted_params) end diff --git a/app/services/disco_app/subscription_service.rb b/app/services/disco_app/subscription_service.rb index 50890806..f7e85508 100644 --- a/app/services/disco_app/subscription_service.rb +++ b/app/services/disco_app/subscription_service.rb @@ -3,50 +3,78 @@ class DiscoApp::SubscriptionService # Subscribe the given shop to the given plan, optionally using the given plan # code and optionally tracking the subscription source. def self.subscribe(shop, plan, plan_code = nil, source_name = nil) + new(shop, plan, plan_code, source_name).subscribe + end + + attr_reader :shop, :plan, :plan_code, :source_name + + def initialize(shop, plan, plan_code = nil, source_name = nil) + @shop = shop + @plan = plan + @plan_code = plan_code + @source_name = source_name + end + + def subscribe + cancel_existing_subscriptions + + # Create the new subscription. + new_subscription = create_new_subscription + + # Enqueue the subscription changed background job. + DiscoApp::SubscriptionChangedJob.perform_later(shop, new_subscription) + + # Return the new subscription. + new_subscription + end + + private # If a plan code was provided, fetch it for the given plan. - plan_code_instance = nil - if plan_code.present? - plan_code_instance = DiscoApp::PlanCode.available.find_by(plan: plan, code: plan_code) + def plan_code_instance + return if plan_code.blank? + + @plan_code_instance ||= DiscoApp::PlanCode.available.find_by(plan: plan, code: plan_code) end # If a source name has been provided, fetch or create it - source_instance = nil - if source_name.present? - source_instance = DiscoApp::Source.find_or_create_by(source: source_name) + def source_instance + return if source_name.blank? + + @source_instance ||= DiscoApp::Source.find_or_create_by(source: source_name) end # Cancel any existing current subscriptions. - shop.subscriptions.current.update_all( - status: DiscoApp::Subscription.statuses[:cancelled], - cancelled_at: Time.now - ) + def cancel_existing_subscriptions + shop.subscriptions.current.update_all( # rubocop:disable Rails/SkipsModelValidations + status: DiscoApp::Subscription.statuses[:cancelled], + cancelled_at: Time.zone.now + ) + end # Get the amount that should be charged for the subscription. - subscription_amount = plan_code_instance.present? ? plan_code_instance.amount : plan.amount + def subscription_amount + plan_code_instance.present? ? plan_code_instance.amount : plan.amount + end # Get the date the subscription trial should end. - subscription_trial_period_days = plan_code_instance.present? ? plan_code_instance.trial_period_days : plan.trial_period_days - - # Create the new subscription. - new_subscription = DiscoApp::Subscription.create!( - shop: shop, - plan: plan, - plan_code: plan_code_instance, - status: DiscoApp::Subscription.statuses[plan.has_trial? ? :trial : :active], - subscription_type: plan.plan_type, - amount: subscription_amount, - trial_period_days: plan.has_trial? ? subscription_trial_period_days : nil, - trial_start_at: plan.has_trial? ? Time.now : nil, - trial_end_at: plan.has_trial? ? subscription_trial_period_days.days.from_now : nil, - source: source_instance - ) - - # Enqueue the subscription changed background job. - DiscoApp::SubscriptionChangedJob.perform_later(shop, new_subscription) + def subscription_trial_period_days + plan_code_instance.present? ? plan_code_instance.trial_period_days : plan.trial_period_days + end - # Return the new subscription. - new_subscription - end + def create_new_subscription + DiscoApp::Subscription.create!( + shop: shop, + plan: plan, + plan_code: plan_code_instance, + status: DiscoApp::Subscription.statuses[plan.has_trial? ? :trial : :active], + subscription_type: plan.plan_type, + amount: subscription_amount, + trial_period_days: plan.has_trial? ? subscription_trial_period_days : nil, + trial_start_at: plan.has_trial? ? Time.zone.now : nil, + trial_end_at: plan.has_trial? ? subscription_trial_period_days.days.from_now : nil, + source: source_instance + ) + end end diff --git a/app/services/disco_app/webhook_service.rb b/app/services/disco_app/webhook_service.rb index c6a3b8ca..75970527 100644 --- a/app/services/disco_app/webhook_service.rb +++ b/app/services/disco_app/webhook_service.rb @@ -2,28 +2,26 @@ class DiscoApp::WebhookService # Return true iff the provided hmac_to_verify matches that calculated from the # given data and secret. - def self.is_valid_hmac?(body, secret, hmac_to_verify) - ActiveSupport::SecurityUtils.secure_compare(self.calculated_hmac(body, secret), hmac_to_verify.to_s) + def self.valid_hmac?(body, secret, hmac_to_verify) + ActiveSupport::SecurityUtils.secure_compare(calculated_hmac(body, secret), hmac_to_verify.to_s) end # Calculate the HMAC for the given data and secret. def self.calculated_hmac(body, secret) - digest = OpenSSL::Digest.new('sha256') + digest = OpenSSL::Digest.new('sha256') Base64.encode64(OpenSSL::HMAC.digest(digest, secret, body)).strip end # Try to find a job class for the given webhook topic. def self.find_job_class(topic) + # First try to find a top-level matching job class. + "#{topic}_job".tr('/', '_').classify.constantize + rescue NameError + # If that fails, try to find a DiscoApp:: prefixed job class. begin - # First try to find a top-level matching job class. - "#{topic}_job".gsub('/', '_').classify.constantize + %(DiscoApp::#{"#{topic}_job".tr('/', '_').classify}).constantize rescue NameError - # If that fails, try to find a DiscoApp:: prefixed job class. - begin - %Q{DiscoApp::#{"#{topic}_job".gsub('/', '_').classify}}.constantize - rescue NameError - nil - end + nil end end diff --git a/config/routes.rb b/config/routes.rb index 0253fd18..f6b4d0ce 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,7 +1,6 @@ require 'sidekiq/web' DiscoApp::Engine.routes.draw do - get 'ref', to: '/sessions#referral' get '/auth/failure', to: '/sessions#failure' @@ -70,5 +69,4 @@ get 'frame' => :frame, as: :frame end end - end diff --git a/db/migrate/20170315062629_add_sources_to_shop_subscriptions.rb b/db/migrate/20170315062629_add_sources_to_shop_subscriptions.rb index 870c6e97..352252d3 100644 --- a/db/migrate/20170315062629_add_sources_to_shop_subscriptions.rb +++ b/db/migrate/20170315062629_add_sources_to_shop_subscriptions.rb @@ -10,7 +10,7 @@ def change end end - remove_column :disco_app_subscriptions, :source + remove_column :disco_app_subscriptions, :source, :string end end diff --git a/disco_app.gemspec b/disco_app.gemspec index 4ec1c585..a6e20d3c 100644 --- a/disco_app.gemspec +++ b/disco_app.gemspec @@ -1,61 +1,63 @@ -$:.push File.expand_path("../lib", __FILE__) +$LOAD_PATH.push File.expand_path('lib', __dir__) # Maintain your gem's version: -require "disco_app/version" +require 'disco_app/version' # Describe your gem and declare its dependencies: Gem::Specification.new do |s| - s.name = "disco_app" + s.name = 'disco_app' s.version = DiscoApp::VERSION - s.authors = ["Gavin Ballard"] - s.email = ["gavin@gavinballard.com"] - s.homepage = "https://github.com/discolabs/disco_app/" - s.summary = "Rails engine for Shopify applications." - s.description = "Rails engine for Shopify applications." - s.license = "None" + s.authors = ['Gavin Ballard'] + s.email = ['gavin@gavinballard.com'] + s.homepage = 'https://github.com/discolabs/disco_app/' + s.summary = 'Rails engine for Shopify applications.' + s.description = 'Rails engine for Shopify applications.' + s.license = 'None' # To build up the list of files, we need to deviate from the standard .gemspec approach to ensure that # a number of "dotfiles"/"dotfolders" that we use as templates files are included in our gem package. - s.files = Dir.glob("{app,config,db,lib}/**/{*,.*}", File::FNM_DOTMATCH).reject { |d| d.end_with?('.') } - s.files += Dir["MIT-LICENSE", "Rakefile", "README.rdoc"] + s.files = Dir.glob('{app,config,db,lib}/**/{*,.*}', File::FNM_DOTMATCH).reject { |d| d.end_with?('.') } + s.files += Dir['MIT-LICENSE', 'Rakefile', 'README.rdoc'] - s.test_files = Dir["test/**/*"] + s.test_files = Dir['test/**/*'] - s.add_runtime_dependency 'rails', '~> 5.2.0' - s.add_runtime_dependency 'sass-rails', '~> 5.0' - s.add_runtime_dependency 'uglifier', '~> 3.2' + s.add_runtime_dependency 'active_link_to', '~> 1.0' + s.add_runtime_dependency 'active_utils', '~> 3.2' + s.add_runtime_dependency 'activemodel-serializers-xml', '~> 1.0' + s.add_runtime_dependency 'activerecord-session_store', '~> 1.0' + s.add_runtime_dependency 'acts_as_singleton', '~> 0.0.8' + s.add_runtime_dependency 'appsignal', '~> 2.7' + s.add_runtime_dependency 'classnames-rails', '~> 2.1' s.add_runtime_dependency 'coffee-rails', '~> 4.2' + s.add_runtime_dependency 'interactor' + s.add_runtime_dependency 'interactor-rails' s.add_runtime_dependency 'jquery-rails', '~> 4.3' - s.add_runtime_dependency 'turbolinks', '~> 5.0' - s.add_runtime_dependency 'shopify_app', '~> 7.2', '>= 7.2.3' - s.add_runtime_dependency 'puma', '~> 3.9' - s.add_runtime_dependency 'sidekiq', '~> 5.0' + s.add_runtime_dependency 'jsonapi-resources', '~> 0.8' + s.add_runtime_dependency 'mailgun_rails', '~> 0.8' + s.add_runtime_dependency 'newrelic_rpm', '~> 3.15' + s.add_runtime_dependency 'nokogiri', '~> 1.7' + s.add_runtime_dependency 'oj', '~> 2.14' s.add_runtime_dependency 'pg', '~> 0.21.0' + s.add_runtime_dependency 'premailer-rails', '~> 1.8' + s.add_runtime_dependency 'puma', '~> 3.9' + s.add_runtime_dependency 'rails', '~> 5.2.2' s.add_runtime_dependency 'rails_12factor', '~> 0.0.3' - s.add_runtime_dependency 'active_utils', '~> 3.2' - s.add_runtime_dependency 'activerecord-session_store', '~> 1.0' - s.add_runtime_dependency 'jsonapi-resources', '~> 0.8' - s.add_runtime_dependency 'acts_as_singleton', '~> 0.0.8' s.add_runtime_dependency 'react-rails', '~> 1.10' - s.add_runtime_dependency 'classnames-rails', '~> 2.1' s.add_runtime_dependency 'render_anywhere', '~> 0.0.12' + s.add_runtime_dependency 'sass-rails', '~> 5.0' + s.add_runtime_dependency 'shopify_api', '~> 6.0' + s.add_runtime_dependency 'shopify_app', '~> 7.2', '>= 7.2.3' + s.add_runtime_dependency 'sidekiq', '~> 5.0' s.add_runtime_dependency 'sinatra', '~> 2.0' - s.add_runtime_dependency 'active_link_to', '~> 1.0' - s.add_runtime_dependency 'premailer-rails', '~> 1.8' - s.add_runtime_dependency 'nokogiri', '~> 1.7' - s.add_runtime_dependency 'appsignal', '~> 2.7' - s.add_runtime_dependency 'oj', '~> 2.14' - s.add_runtime_dependency 'newrelic_rpm', '~> 3.15' - s.add_runtime_dependency 'mailgun_rails', '~> 0.8' - s.add_runtime_dependency 'activemodel-serializers-xml', '~> 1.0' - s.add_runtime_dependency 'interactor' - s.add_runtime_dependency 'interactor-rails' + s.add_runtime_dependency 'turbolinks', '~> 5.0' + s.add_runtime_dependency 'uglifier', '>= 3.2' s.add_development_dependency 'dotenv-rails', '~> 2.0' - s.add_development_dependency 'minitest-reporters', '1.1.9' s.add_development_dependency 'minitest', '5.10.1' - s.add_development_dependency 'webmock', '~> 2.3' + s.add_development_dependency 'minitest-reporters', '1.1.9' + s.add_development_dependency 'rubocop', '~> 0.72' + s.add_development_dependency 'rubocop-performance', '~> 1.4.0' + s.add_development_dependency 'rubocop-rails', '~> 2.2.0' s.add_development_dependency 'vcr', '~> 3.0' - s.add_development_dependency 'rubocop', '~> 0.50' + s.add_development_dependency 'webmock', '~> 2.3' end - diff --git a/initialise.sh b/initialise.sh index 05128212..e8d4981b 100755 --- a/initialise.sh +++ b/initialise.sh @@ -4,7 +4,7 @@ APP_NAME="$1" RAILS_VERSION="${RAILS_VERSION:-5.2.0}" RUBY_VERSION="${RUBY_VERSION:-2.5.0}" -DISCO_APP_VERSION="${DISCO_APP_VERSION:-0.16.1}" +DISCO_APP_VERSION="${DISCO_APP_VERSION:-0.17.0}" if [ -z $APP_NAME ]; then echo "Usage: ./initialise.sh app_name (rails_version) (ruby_version) (disco_app_version)" @@ -21,4 +21,4 @@ bundle install bundle exec rails _"$RAILS_VERSION"_ new . --force --skip-bundle echo "gem 'disco_app', '$DISCO_APP_VERSION', source: \"https://gem.fury.io/discolabs/\"" >> Gemfile bundle update -bundle exec rails generate disco_app --force +bundle exec rails generate disco_app:install --force diff --git a/lib/disco_app/configuration.rb b/lib/disco_app/configuration.rb index 2ef7ebda..7abd05bf 100644 --- a/lib/disco_app/configuration.rb +++ b/lib/disco_app/configuration.rb @@ -20,17 +20,17 @@ class Configuration # Set the below to create real Shopify charges. attr_accessor :real_charges - alias_method :real_charges?, :real_charges + alias real_charges? real_charges # Optional configuration, usually useful for development environments. attr_accessor :skip_proxy_verification - alias_method :skip_proxy_verification?, :skip_proxy_verification + alias skip_proxy_verification? skip_proxy_verification attr_accessor :skip_webhook_verification - alias_method :skip_webhook_verification?, :skip_webhook_verification + alias skip_webhook_verification? skip_webhook_verification attr_accessor :skip_carrier_request_verification - alias_method :skip_carrier_request_verification?, :skip_carrier_request_verification + alias skip_carrier_request_verification? skip_carrier_request_verification attr_accessor :skip_oauth - alias_method :skip_oauth?, :skip_oauth + alias skip_oauth? skip_oauth end diff --git a/lib/disco_app/constants.rb b/lib/disco_app/constants.rb index 68d27c43..8f7bf12f 100644 --- a/lib/disco_app/constants.rb +++ b/lib/disco_app/constants.rb @@ -1,4 +1,6 @@ module DiscoApp - SOURCE_COOKIE_KEY = '_disco_app_source' - CODE_COOKIE_KEY = '_disco_app_code' + + SOURCE_COOKIE_KEY = '_disco_app_source'.freeze + CODE_COOKIE_KEY = '_disco_app_code'.freeze + end diff --git a/lib/disco_app/engine.rb b/lib/disco_app/engine.rb index 1e3973d1..f800f89b 100644 --- a/lib/disco_app/engine.rb +++ b/lib/disco_app/engine.rb @@ -19,7 +19,7 @@ class Engine < ::Rails::Engine # Ensure our frame assets are included for precompilation. initializer 'disco_app.assets.precompile' do |app| - app.config.assets.precompile += %w(disco_app/icon.svg disco_app/admin.css disco_app/frame.css disco_app/frame.js) + app.config.assets.precompile += %w[disco_app/icon.svg disco_app/admin.css disco_app/frame.css disco_app/frame.js] end end diff --git a/lib/disco_app/session.rb b/lib/disco_app/session.rb index 0fa9c9ce..12b2af5d 100644 --- a/lib/disco_app/session.rb +++ b/lib/disco_app/session.rb @@ -7,6 +7,7 @@ class Session < ActiveRecord::SessionStore::Session def set_shop_id! return false unless loaded? + write_attribute(:shop_id, data[:shopify] || data['shopify']) end diff --git a/lib/disco_app/support/file_fixtures.rb b/lib/disco_app/support/file_fixtures.rb index ca37300b..a7d6809a 100644 --- a/lib/disco_app/support/file_fixtures.rb +++ b/lib/disco_app/support/file_fixtures.rb @@ -11,7 +11,8 @@ def xml_fixture(path) def json_fixture(path, dir: 'json', parse: true) filename = Rails.root.join('test', 'fixtures', dir, "#{path}.json") return File.read(filename) unless parse - HashWithIndifferentAccess.new(ActiveSupport::JSON.decode(File.read(filename))) + + HashWithIndifferentAccess.new(JSON.parse(File.read(filename))) end # Webhook fixtures are special-case JSON fixtures. diff --git a/lib/disco_app/version.rb b/lib/disco_app/version.rb index c1cc6d33..d0a19251 100644 --- a/lib/disco_app/version.rb +++ b/lib/disco_app/version.rb @@ -1,3 +1,5 @@ module DiscoApp - VERSION = '0.16.1' + + VERSION = '0.17.0'.freeze + end diff --git a/lib/generators/disco_app/USAGE b/lib/generators/disco_app/USAGE deleted file mode 100644 index 75001248..00000000 --- a/lib/generators/disco_app/USAGE +++ /dev/null @@ -1,5 +0,0 @@ -Description: - Generate a new Shopify application. - -Example: - rails generate disco_app \ No newline at end of file diff --git a/lib/generators/disco_app/disco_app_generator.rb b/lib/generators/disco_app/disco_app_generator.rb deleted file mode 100644 index 1f658541..00000000 --- a/lib/generators/disco_app/disco_app_generator.rb +++ /dev/null @@ -1,252 +0,0 @@ -class DiscoAppGenerator < Rails::Generators::Base - - source_root File.expand_path('../templates', __FILE__) - - # Copy a number of template files to the top-level directory of our application: - # - # - .env and .env.local for settings environment variables in development with dotenv-rails; - # - Slightly customised version of the default Rails .gitignore; - # - Default simple Procfile for Heroku; - # - .editorconfig to help enforce 2-space tabs, newlines and truncated whitespace for editors that support it. - # - README/PULL REQUEST template - # - def copy_root_files - %w(.editorconfig .env .env.local .gitignore .rubocop.yml .codeclimate.yml Procfile CHECKS README.md).each do |file| - copy_file "root/#{file}", file - end - directory 'root/.github' - end - - # Remove a number of root files. - def remove_root_files - %w(README.rdoc).each do |file| - remove_file file - end - end - - # Configure the application's Gemfile. - def configure_gemfile - # Remove sqlite. - gsub_file 'Gemfile', /^# Use sqlite3 as the database for Active Record\ngem 'sqlite3'/m, '' - - # Add gem requirements. - gem 'active_link_to' - gem 'activeresource' - gem 'acts_as_singleton' - gem 'classnames-rails' - gem 'newrelic_rpm' - gem 'nokogiri' - gem 'oj' - gem 'pg' - gem 'premailer-rails' - gem 'react-rails' - gem 'render_anywhere' - gem 'appsignal' - gem 'shopify_app' - gem 'sidekiq' - - # Indicate which gems should only be used in staging and production. - gem_group :staging, :production do - gem 'mailgun_rails' - gem 'rails_12factor' - end - - # Indicate which gems should only be used in development and test. - gem_group :development, :test do - gem 'dotenv-rails' - gem 'mechanize' - gem 'minitest-reporters' - gem 'webmock' - end - end - - # copy template for pg configuration - def update_database_config - template 'config/database.yml.tt' - end - - def update_cable_config - template 'config/cable.yml.tt' - end - - # Run bundle install to add our new gems before running tasks. - def bundle_install - Bundler.with_clean_env do - run 'bundle install' - end - end - - def support_staging_environment - copy_file 'config/environments/staging.rb', 'config/environments/staging.rb' - end - - # Make any required adjustments to the application configuration. - def configure_application - # The force_ssl flag is commented by default for production. - # Uncomment to ensure config.force_ssl = true in production. - uncomment_lines 'config/environments/production.rb', /force_ssl/ - - # Set time zone to UTC - application "config.time_zone = 'UTC'" - application "# Ensure UTC is the default timezone" - - # Set server side rendereing for components.js - application "config.react.server_renderer_options = {\nfiles: ['components.js'], # files to load for prerendering\n}" - application "# Enable server side react rendering" - - # Set defaults for various charge attributes. - application "config.x.shopify_charges_default_trial_days = 14\n" - application "config.x.shopify_charges_default_price = 10.00" - application "config.x.shopify_charges_default_type = :recurring" - application "# Set defaults for charges created by the application" - - # Set the "real charges" config variable to false explicitly by default. - # Only in production do we read from the environment variable and - # potentially have it become true. - application "config.x.shopify_charges_real = false\n" - application "# Explicitly prevent real charges being created by default" - application "config.x.shopify_charges_real = ENV['SHOPIFY_CHARGES_REAL'] == 'true'\n", env: :production - application "# Allow real charges in production with an ENV variable", env: :production - - # Configure session storage. - application "ActiveRecord::SessionStore::Session.table_name = 'disco_app_sessions'" - application "ActionDispatch::Session::ActiveRecordStore.session_class = DiscoApp::Session" - application "# Configure custom session storage" - - # Set Sidekiq as the queue adapter in production. - application "config.active_job.queue_adapter = :sidekiq\n", env: :production - application "# Use Sidekiq as the active job backend", env: :production - - # Set Sidekiq as the queue adapter in staging. - application "config.active_job.queue_adapter = :sidekiq\n", env: :staging - application "# Use Sidekiq as the active job backend", env: :staging - - # Ensure the application configuration uses the DEFAULT_HOST environment - # variable to set up support for reverse routing absolute URLS (needed when - # generating Webhook URLs for example). - application "routes.default_url_options[:host] = ENV['DEFAULT_HOST']\n" - application "# Set the default host for absolute URL routing purposes" - - # Configure React in development, staging, and production. - application "config.react.variant = :development", env: :development - application "# Use development variant of React in development.", env: :development - - application "config.react.variant = :production", env: :staging - application "# Use production variant of React in staging.", env: :staging - - application "config.react.variant = :production", env: :production - application "# Use production variant of React in production.", env: :production - - # Copy over the default puma configuration. - copy_file 'config/puma.rb', 'config/puma.rb' - - # Mail configuration - configuration = <<-CONFIG.strip_heredoc - - # Configure ActionMailer to use MailGun - if ENV['MAILGUN_API_KEY'] - config.action_mailer.delivery_method = :mailgun - config.action_mailer.mailgun_settings = { - api_key: ENV['MAILGUN_API_KEY'], - domain: ENV['MAILGUN_API_DOMAIN'] - } - end - CONFIG - application configuration, env: :production - application configuration, env: :staging - - # Monitoring configuration - copy_file 'config/appsignal.yml', 'config/appsignal.yml' - copy_file 'config/newrelic.yml', 'config/newrelic.yml' - end - - # Add entries to .env and .env.local - def add_env_variables - configuration = <<-CONFIG.strip_heredoc - - MAILGUN_API_KEY= - MAILGUN_API_DOMAIN= - CONFIG - append_to_file '.env', configuration - append_to_file '.env.local', configuration - end - - # Set up routes. - def setup_routes - route "mount DiscoApp::Engine, at: '/'" - end - - # Run generators. - def run_generators - generate 'shopify_app:install' - generate 'shopify_app:home_controller' - generate 'react:install' - end - - # Copy template files to the appropriate location. In some cases, we'll be - # overwriting or removing existing files or those created by ShopifyApp. - def copy_and_remove_files - # Copy initializers - copy_file 'initializers/shopify_app.rb', 'config/initializers/shopify_app.rb' - copy_file 'initializers/disco_app.rb', 'config/initializers/disco_app.rb' - copy_file 'initializers/shopify_session_repository.rb', 'config/initializers/shopify_session_repository.rb' - copy_file 'initializers/session_store.rb', 'config/initializers/session_store.rb' - - # Copy default home controller and view - copy_file 'controllers/home_controller.rb', 'app/controllers/home_controller.rb' - copy_file 'views/home/index.html.erb', 'app/views/home/index.html.erb' - - # Copy assets - copy_file 'assets/javascripts/application.js', 'app/assets/javascripts/application.js' - copy_file 'assets/javascripts/components.js', 'app/assets/javascripts/components.js' - copy_file 'assets/stylesheets/application.scss', 'app/assets/stylesheets/application.scss' - - # Remove application.css - remove_file 'app/assets/stylesheets/application.css' - - # Remove the layout files created by ShopifyApp - remove_file 'app/views/layouts/application.html.erb' - remove_file 'app/views/layouts/embedded_app.html.erb' - end - - # Add the Disco App test helper to test/test_helper.rb - def add_test_helper - inject_into_file 'test/test_helper.rb', "require 'disco_app/test_help'\n", { after: "require 'rails/test_help'\n" } - end - - # Copy engine migrations over. - def install_migrations - rake 'disco_app:install:migrations' - end - - # Create PG database - def create_database - rake 'db:create' - end - - # Run migrations. - def migrate - rake 'db:migrate' - end - - # Lock down the application to a specific Ruby version: - # - # - Via .ruby-version file for rbenv in development; - # - Via a Gemfile line in production. - # - # This should be the last operation, to allow all other operations to run in the initial Ruby version. - def set_ruby_version - copy_file 'root/.ruby-version', '.ruby-version' - prepend_to_file 'Gemfile', "ruby '2.5.0'\n" - end - - private - - # This method of finding the component.js manifest taken from the - # install generator in react-rails. - # See https://github.com/reactjs/react-rails/blob/3f0af13fa755d6e95969c17728d0354c234f3a37/lib/generators/react/install_generator.rb#L53-L55 - def components - Pathname.new(destination_root).join('app/assets/javascripts', 'components.js') - end - -end diff --git a/lib/generators/disco_app/install/USAGE b/lib/generators/disco_app/install/USAGE new file mode 100644 index 00000000..82910c42 --- /dev/null +++ b/lib/generators/disco_app/install/USAGE @@ -0,0 +1,5 @@ +Description: + Generate a new Shopify application. + +Example: + rails generate disco_app:install diff --git a/lib/generators/disco_app/install/install_generator.rb b/lib/generators/disco_app/install/install_generator.rb new file mode 100644 index 00000000..35d4e4fc --- /dev/null +++ b/lib/generators/disco_app/install/install_generator.rb @@ -0,0 +1,287 @@ +module DiscoApp + module Generators + class InstallGenerator < Rails::Generators::Base + + source_root File.expand_path('templates', __dir__) + + # Copy a number of template files to the top-level directory of our application: + # + # - .env and .env.local for settings environment variables in development with dotenv-rails; + # - Slightly customised version of the default Rails .gitignore; + # - Default simple Procfile for Heroku; + # - .editorconfig to help enforce 2-space tabs, newlines and truncated whitespace for editors that support it. + # - README/PULL REQUEST template + # + def copy_root_files + %w[.editorconfig .env .env.local .gitignore .rubocop.yml Procfile CHECKS README.md].each do |file| + copy_file "root/#{file}", file + end + directory 'root/.github' + end + + # Remove a number of root files. + def remove_root_files + %w[README.rdoc].each do |file| + remove_file file + end + end + + # Configure the application's Gemfile. + def configure_gemfile + # Remove sqlite. + gsub_file 'Gemfile', /^# Use sqlite3 as the database for Active Record\ngem 'sqlite3'/m, '' + + # Add gem requirements. + gem 'active_link_to' + gem 'activeresource' + gem 'acts_as_singleton' + gem 'appsignal' + gem 'classnames-rails' + gem 'nokogiri' + gem 'oj' + gem 'pg' + gem 'premailer-rails' + gem 'react-rails' + gem 'render_anywhere' + gem 'shopify_app' + gem 'sidekiq' + gem 'timber', '~> 3.0' + gem 'timber-rails', '~> 1.0' + + # Indicate which gems should only be used in production. + gem_group :production do + gem 'mailgun_rails' + gem 'rails_12factor' + end + + # Indicate which gems should only be used in development. + gem_group :production do + gem 'rb-readline' + end + + gem_group :development do + gem 'rubocop' + gem 'rubocop-performance' + gem 'rubocop-rails' + end + + # Indicate which gems should only be used in development and test. + gem_group :development, :test do + gem 'coveralls' + gem 'dotenv-rails' + gem 'factory_bot_rails' + gem 'faker' + gem 'mechanize' + gem 'rspec-rails' + gem 'vcr' + gem 'webmock' + end + + gem_group :test do + gem 'database_cleaner' + gem 'shoulda-matchers' + end + end + + # copy template for pg configuration + def update_database_config + template 'config/database.yml.tt' + end + + # Run bundle install to add our new gems before running tasks. + def bundle_install + Bundler.with_clean_env do + run 'bundle install' + end + end + + def support_staging_environment + copy_file 'config/environments/staging.rb', 'config/environments/staging.rb' + end + + # Make any required adjustments to the application configuration. + def configure_application + # The force_ssl flag is commented by default for production. + # Uncomment to ensure config.force_ssl = true in production. + uncomment_lines 'config/environments/production.rb', /force_ssl/ + + # Set time zone to UTC + application "config.time_zone = 'UTC'" + application '# Ensure UTC is the default timezone' + + # Set server side rendereing for components.js + application <<~CONFIG + # Enable server side react rendering + config.react.server_renderer_options = { + # files to load for prerendering + files: ['components.js'] + } + CONFIG + + # Set defaults for various charge attributes. + application "config.x.shopify_charges_default_trial_days = 14\n" + application 'config.x.shopify_charges_default_price = 10.00' + application 'config.x.shopify_charges_default_type = :recurring' + application '# Set defaults for charges created by the application' + + # Set the "real charges" config variable to false explicitly by default. + # Only in production do we read from the environment variable and + # potentially have it become true. + application "config.x.shopify_charges_real = false\n" + application '# Explicitly prevent real charges being created by default' + application "config.x.shopify_charges_real = ENV['SHOPIFY_CHARGES_REAL'] == 'true'\n", env: :production + application '# Allow real charges in production with an ENV variable', env: :production + + # Configure session storage. + application "ActiveRecord::SessionStore::Session.table_name = 'disco_app_sessions'" + application 'ActionDispatch::Session::ActiveRecordStore.session_class = DiscoApp::Session' + application '# Configure custom session storage' + + # Set Sidekiq as the queue adapter in production. + application "config.active_job.queue_adapter = :sidekiq\n", env: :production + application '# Use Sidekiq as the active job backend', env: :production + + # Set Sidekiq as the queue adapter in staging. + application "config.active_job.queue_adapter = :sidekiq\n", env: :staging + application '# Use Sidekiq as the active job backend', env: :staging + + # Ensure the application configuration uses the DEFAULT_HOST environment + # variable to set up support for reverse routing absolute URLS (needed when + # generating Webhook URLs for example). + application "routes.default_url_options[:host] = ENV['DEFAULT_HOST']\n" + application '# Set the default host for absolute URL routing purposes' + + # Configure React in development, staging and production. + application 'config.react.variant = :development', env: :development + application '# Use development variant of React in development.', env: :development + application 'config.react.variant = :production', env: :staging + application '# Use production variant of React in staging.', env: :staging + application 'config.react.variant = :production', env: :production + application '# Use production variant of React in production.', env: :production + + # Configure Factory Bot as the Rails testing fixture replacement + application <<~CONFIG + config.generators do |g| + g.test_framework :rspec, fixtures: true, view_specs: false, helper_specs: false, routing_specs: false + g.fixture_replacement :factory_bot, dir: 'spec/factories' + end + CONFIG + + # Copy over the default puma configuration. + copy_file 'config/puma.rb', 'config/puma.rb' + + # Mail configuration + configuration = <<-CONFIG.strip_heredoc + + # Configure ActionMailer to use MailGun + if ENV['MAILGUN_API_KEY'] + config.action_mailer.delivery_method = :mailgun + config.action_mailer.mailgun_settings = { + api_key: ENV['MAILGUN_API_KEY'], + domain: ENV['MAILGUN_API_DOMAIN'] + } + end + CONFIG + application configuration, env: :production + application configuration, env: :staging + + # Monitoring configuration + copy_file 'config/appsignal.yml', 'config/appsignal.yml' + end + + # Add entries to .env and .env.local + def add_env_variables + configuration = <<-CONFIG.strip_heredoc + + MAILGUN_API_KEY= + MAILGUN_API_DOMAIN= + CONFIG + append_to_file '.env', configuration + append_to_file '.env.local', configuration + end + + # Set up routes. + def setup_routes + route "mount DiscoApp::Engine, at: '/'" + end + + # Run generators. + def run_generators + generate 'shopify_app:install' + generate 'shopify_app:home_controller' + generate 'react:install' + end + + def configure_rspec + directory 'spec' + copy_file 'root/.rspec', '.rspec' + end + + # Copy template files to the appropriate location. In some cases, we'll be + # overwriting or removing existing files or those created by ShopifyApp. + def copy_and_remove_files + # Copy initializers + copy_file 'initializers/shopify_app.rb', 'config/initializers/shopify_app.rb' + copy_file 'initializers/disco_app.rb', 'config/initializers/disco_app.rb' + copy_file 'initializers/shopify_session_repository.rb', 'config/initializers/shopify_session_repository.rb' + copy_file 'initializers/session_store.rb', 'config/initializers/session_store.rb' + copy_file 'initializers/timber.rb', 'config/initializers/timber.rb' + + # Copy default home controller and view + copy_file 'controllers/home_controller.rb', 'app/controllers/home_controller.rb' + copy_file 'views/home/index.html.erb', 'app/views/home/index.html.erb' + + # Copy assets + copy_file 'assets/javascripts/application.js', 'app/assets/javascripts/application.js' + copy_file 'assets/javascripts/components.js', 'app/assets/javascripts/components.js' + copy_file 'assets/stylesheets/application.scss', 'app/assets/stylesheets/application.scss' + + # Remove application.css + remove_file 'app/assets/stylesheets/application.css' + + # Remove the layout files created by ShopifyApp + remove_file 'app/views/layouts/application.html.erb' + remove_file 'app/views/layouts/embedded_app.html.erb' + + # Remove the test directory generated by rails new + remove_dir 'test' + end + + # Copy engine migrations over. + def install_migrations + rake 'disco_app:install:migrations' + end + + # Create PG database + def create_database + rake 'db:create' + end + + # Run migrations. + def migrate + rake 'db:migrate' + end + + # Lock down the application to a specific Ruby version: + # + # - Via .ruby-version file for rbenv in development; + # - Via a Gemfile line in production. + # + # This should be the last operation, to allow all other operations to run in the initial Ruby version. + def set_ruby_version + copy_file 'root/.ruby-version', '.ruby-version' + prepend_to_file 'Gemfile', "ruby '2.5.0'\n" + end + + private + + # This method of finding the component.js manifest taken from the + # install generator in react-rails. + # See https://github.com/reactjs/react-rails/blob/3f0af13fa755d6e95969c17728d0354c234f3a37/lib/generators/react/install_generator.rb#L53-L55 + def components + Pathname.new(destination_root).join('app/assets/javascripts', 'components.js') + end + + end + end +end diff --git a/lib/generators/disco_app/templates/assets/javascripts/application.js b/lib/generators/disco_app/install/templates/assets/javascripts/application.js similarity index 100% rename from lib/generators/disco_app/templates/assets/javascripts/application.js rename to lib/generators/disco_app/install/templates/assets/javascripts/application.js diff --git a/lib/generators/disco_app/templates/assets/javascripts/components.js b/lib/generators/disco_app/install/templates/assets/javascripts/components.js similarity index 100% rename from lib/generators/disco_app/templates/assets/javascripts/components.js rename to lib/generators/disco_app/install/templates/assets/javascripts/components.js diff --git a/lib/generators/disco_app/templates/assets/stylesheets/application.scss b/lib/generators/disco_app/install/templates/assets/stylesheets/application.scss similarity index 100% rename from lib/generators/disco_app/templates/assets/stylesheets/application.scss rename to lib/generators/disco_app/install/templates/assets/stylesheets/application.scss diff --git a/lib/generators/disco_app/templates/config/appsignal.yml b/lib/generators/disco_app/install/templates/config/appsignal.yml similarity index 100% rename from lib/generators/disco_app/templates/config/appsignal.yml rename to lib/generators/disco_app/install/templates/config/appsignal.yml diff --git a/lib/generators/disco_app/templates/config/cable.yml.tt b/lib/generators/disco_app/install/templates/config/cable.yml.tt similarity index 100% rename from lib/generators/disco_app/templates/config/cable.yml.tt rename to lib/generators/disco_app/install/templates/config/cable.yml.tt diff --git a/lib/generators/disco_app/templates/config/database.yml.tt b/lib/generators/disco_app/install/templates/config/database.yml.tt similarity index 100% rename from lib/generators/disco_app/templates/config/database.yml.tt rename to lib/generators/disco_app/install/templates/config/database.yml.tt diff --git a/lib/generators/disco_app/templates/config/environments/staging.rb b/lib/generators/disco_app/install/templates/config/environments/staging.rb similarity index 100% rename from lib/generators/disco_app/templates/config/environments/staging.rb rename to lib/generators/disco_app/install/templates/config/environments/staging.rb diff --git a/lib/generators/disco_app/templates/config/puma.rb b/lib/generators/disco_app/install/templates/config/puma.rb similarity index 100% rename from lib/generators/disco_app/templates/config/puma.rb rename to lib/generators/disco_app/install/templates/config/puma.rb diff --git a/lib/generators/disco_app/templates/controllers/home_controller.rb b/lib/generators/disco_app/install/templates/controllers/home_controller.rb similarity index 100% rename from lib/generators/disco_app/templates/controllers/home_controller.rb rename to lib/generators/disco_app/install/templates/controllers/home_controller.rb diff --git a/lib/generators/disco_app/templates/initializers/disco_app.rb b/lib/generators/disco_app/install/templates/initializers/disco_app.rb similarity index 100% rename from lib/generators/disco_app/templates/initializers/disco_app.rb rename to lib/generators/disco_app/install/templates/initializers/disco_app.rb diff --git a/lib/generators/disco_app/templates/initializers/session_store.rb b/lib/generators/disco_app/install/templates/initializers/session_store.rb similarity index 100% rename from lib/generators/disco_app/templates/initializers/session_store.rb rename to lib/generators/disco_app/install/templates/initializers/session_store.rb diff --git a/lib/generators/disco_app/templates/initializers/shopify_app.rb b/lib/generators/disco_app/install/templates/initializers/shopify_app.rb similarity index 100% rename from lib/generators/disco_app/templates/initializers/shopify_app.rb rename to lib/generators/disco_app/install/templates/initializers/shopify_app.rb diff --git a/lib/generators/disco_app/templates/initializers/shopify_session_repository.rb b/lib/generators/disco_app/install/templates/initializers/shopify_session_repository.rb similarity index 100% rename from lib/generators/disco_app/templates/initializers/shopify_session_repository.rb rename to lib/generators/disco_app/install/templates/initializers/shopify_session_repository.rb diff --git a/lib/generators/disco_app/install/templates/initializers/timber.rb b/lib/generators/disco_app/install/templates/initializers/timber.rb new file mode 100644 index 00000000..28aa2711 --- /dev/null +++ b/lib/generators/disco_app/install/templates/initializers/timber.rb @@ -0,0 +1,6 @@ +if Rails.env.production? + http_device = Timber::LogDevices::HTTP.new('YOUR_API_KEY', 'YOUR_SOURCE_ID') + Rails.logger = Timber::Logger.new(http_device) +else + Rails.logger = Timber::Logger.new(STDOUT) +end diff --git a/lib/generators/disco_app/templates/root/.editorconfig b/lib/generators/disco_app/install/templates/root/.editorconfig similarity index 100% rename from lib/generators/disco_app/templates/root/.editorconfig rename to lib/generators/disco_app/install/templates/root/.editorconfig diff --git a/lib/generators/disco_app/templates/root/.env b/lib/generators/disco_app/install/templates/root/.env similarity index 100% rename from lib/generators/disco_app/templates/root/.env rename to lib/generators/disco_app/install/templates/root/.env diff --git a/lib/generators/disco_app/templates/root/.env.local b/lib/generators/disco_app/install/templates/root/.env.local similarity index 84% rename from lib/generators/disco_app/templates/root/.env.local rename to lib/generators/disco_app/install/templates/root/.env.local index bfaeb228..62058f99 100644 --- a/lib/generators/disco_app/templates/root/.env.local +++ b/lib/generators/disco_app/install/templates/root/.env.local @@ -23,3 +23,6 @@ REDIS_PROVIDER= DISCO_API_URL= WHITELISTED_DOMAINS= + +# You can find this listed in 1Password +APPSIGNAL_PUSH_API_KEY= \ No newline at end of file diff --git a/lib/generators/disco_app/templates/root/.github/PULL_REQUEST_TEMPLATE.md b/lib/generators/disco_app/install/templates/root/.github/PULL_REQUEST_TEMPLATE.md similarity index 100% rename from lib/generators/disco_app/templates/root/.github/PULL_REQUEST_TEMPLATE.md rename to lib/generators/disco_app/install/templates/root/.github/PULL_REQUEST_TEMPLATE.md diff --git a/lib/generators/disco_app/templates/root/.gitignore b/lib/generators/disco_app/install/templates/root/.gitignore similarity index 88% rename from lib/generators/disco_app/templates/root/.gitignore rename to lib/generators/disco_app/install/templates/root/.gitignore index 4079c4af..e7f7ea5a 100644 --- a/lib/generators/disco_app/templates/root/.gitignore +++ b/lib/generators/disco_app/install/templates/root/.gitignore @@ -15,6 +15,11 @@ pickle-email-*.html .byebug_history .DS_Store .disco_app +/public/packs +/public/packs-test +/node_modules +yarn-debug.log* +.yarn-integrity ## Environment normalisation: /.bundle @@ -22,6 +27,7 @@ pickle-email-*.html /vendor/ruby /.env.local .envrc +.pryrc # these should all be checked in to normalise the environment: # Gemfile.lock, .ruby-version, .ruby-gemset diff --git a/lib/generators/disco_app/install/templates/root/.rspec b/lib/generators/disco_app/install/templates/root/.rspec new file mode 100644 index 00000000..c99d2e73 --- /dev/null +++ b/lib/generators/disco_app/install/templates/root/.rspec @@ -0,0 +1 @@ +--require spec_helper diff --git a/lib/generators/disco_app/templates/root/.rubocop.yml b/lib/generators/disco_app/install/templates/root/.rubocop.yml similarity index 97% rename from lib/generators/disco_app/templates/root/.rubocop.yml rename to lib/generators/disco_app/install/templates/root/.rubocop.yml index 251bc7df..f5507fbe 100644 --- a/lib/generators/disco_app/templates/root/.rubocop.yml +++ b/lib/generators/disco_app/install/templates/root/.rubocop.yml @@ -1,8 +1,13 @@ +require: + - rubocop-rails + - rubocop-performance + AllCops: Exclude: - bin/* - db/schema.rb - vendor/ruby/**/* + - node_modules/**/* TargetRubyVersion: 2.5 # Layout @@ -46,7 +51,7 @@ Layout/ClassStructure: ExpectedOrder: - module_inclusion - constants - - attributes + - public_attributes - associations - validations - callbacks @@ -54,6 +59,7 @@ Layout/ClassStructure: - initializer - public_methods - protected_methods + - private_attributes - private_methods Enabled: true @@ -79,12 +85,12 @@ Layout/InitialIndentation: Checks the indentation of the first non-blank non-comment line in a file. Enabled: false -Layout/IndentHash: +Layout/IndentFirstHashElement: Enabled: true EnforcedStyle: consistent Layout/IndentationConsistency: - EnforcedStyle: rails + EnforcedStyle: indented_internal_methods Enabled: true Layout/MultilineBlockLayout: @@ -239,11 +245,6 @@ Style/FrozenStringLiteralComment: to help transition from Ruby 2.3.0 to Ruby 3.0. Enabled: false -Style/FlipFlop: - Description: 'Checks for flip flops' - StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-flip-flops' - Enabled: true - Style/FormatString: Description: 'Enforce the use of Kernel#sprintf, Kernel#format or String#%.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#sprintf' @@ -384,6 +385,13 @@ Style/RegexpLiteral: StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-r' Enabled: true +Style/Sample: + Description: >- + Use `sample` instead of `shuffle.first`, + `shuffle.last`, and `shuffle[Fixnum]`. + Reference: 'https://github.com/JuanitoFatas/fast-ruby#arrayshufflefirst-vs-arraysample-code' + Enabled: true + Style/SelfAssignment: Description: >- Checks for places where self-assignment shorthand should have @@ -433,7 +441,7 @@ Style/TrailingCommaInHashLiteral: Description: 'Checks for trailing comma in hash literals.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas' EnforcedStyleForMultiline: no_comma - Enabled: false + Enabled: true Style/TrivialAccessors: Description: 'Prefer attr_* methods to trivial readers/writers.' @@ -480,6 +488,17 @@ Metrics/BlockLength: Enabled: true Exclude: - config/environments/* + ExcludedMethods: + - context + - define + - describe + - draw + - factory + - guard + - included + - namespace + - trait + Metrics/BlockNesting: Description: 'Avoid excessive block nesting' @@ -556,6 +575,11 @@ Lint/ElseLayout: Description: 'Check for odd code arrangement in an else block.' Enabled: true +Lint/FlipFlop: + Description: 'Checks for flip flops' + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-flip-flops' + Enabled: true + Lint/FormatParameterMismatch: Description: 'The number of parameters to format/sprint must match the fields.' Enabled: true @@ -645,13 +669,6 @@ Performance/ReverseEach: Reference: 'https://github.com/JuanitoFatas/fast-ruby#enumerablereverseeach-vs-enumerablereverse_each-code' Enabled: true -Performance/Sample: - Description: >- - Use `sample` instead of `shuffle.first`, - `shuffle.last`, and `shuffle[Fixnum]`. - Reference: 'https://github.com/JuanitoFatas/fast-ruby#arrayshufflefirst-vs-arraysample-code' - Enabled: true - Performance/Size: Description: >- Use `size` instead of `count` for counting @@ -683,6 +700,10 @@ Rails/Delegate: Description: 'Prefer delegate method for delegations.' Enabled: false +Rails/Exit: + Exclude: + - lib/generators/disco_app/install/templates/spec/rails_helper.rb + Rails/FindBy: Description: 'Prefer find_by over where.first.' Enabled: true @@ -715,6 +736,13 @@ Rails/TimeZone: Reference: 'http://danilenko.org/2012/7/6/rails_timezones' Enabled: true +Rails/UnknownEnv: + Environments: + - development + - production + - staging + - test + Rails/Validation: Description: 'Use validates :attribute, hash of validations.' Enabled: false @@ -722,4 +750,4 @@ Rails/Validation: # Bundler Bundler/OrderedGems: - Enabled: true + Enabled: true \ No newline at end of file diff --git a/lib/generators/disco_app/templates/root/.ruby-version b/lib/generators/disco_app/install/templates/root/.ruby-version similarity index 100% rename from lib/generators/disco_app/templates/root/.ruby-version rename to lib/generators/disco_app/install/templates/root/.ruby-version diff --git a/lib/generators/disco_app/templates/root/CHECKS b/lib/generators/disco_app/install/templates/root/CHECKS similarity index 100% rename from lib/generators/disco_app/templates/root/CHECKS rename to lib/generators/disco_app/install/templates/root/CHECKS diff --git a/lib/generators/disco_app/templates/root/Procfile b/lib/generators/disco_app/install/templates/root/Procfile similarity index 100% rename from lib/generators/disco_app/templates/root/Procfile rename to lib/generators/disco_app/install/templates/root/Procfile diff --git a/lib/generators/disco_app/templates/root/README.md b/lib/generators/disco_app/install/templates/root/README.md similarity index 100% rename from lib/generators/disco_app/templates/root/README.md rename to lib/generators/disco_app/install/templates/root/README.md diff --git a/lib/generators/disco_app/install/templates/spec/rails_helper.rb b/lib/generators/disco_app/install/templates/spec/rails_helper.rb new file mode 100644 index 00000000..1e39dda2 --- /dev/null +++ b/lib/generators/disco_app/install/templates/spec/rails_helper.rb @@ -0,0 +1,40 @@ +require 'spec_helper' +ENV['RAILS_ENV'] ||= 'test' +require File.expand_path('../config/environment', __dir__) + +abort('The Rails environment is running in production mode!') if Rails.env.production? + +require 'rspec/rails' + +# Add additional requires below this line. Rails is not loaded until this point! + +# Requires supporting ruby files with custom matchers and macros, etc, in +# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are +# run as spec files by default. This means that files in spec/support that end +# in _spec.rb will both be required and run as specs, causing the specs to be +# run twice. It is recommended that you do not name files matching this glob to +# end with _spec.rb. You can configure this pattern with the --pattern +# option on the command line or in ~/.rspec, .rspec or `.rspec-local`. +# +# The following line is provided for convenience purposes. It has the downside +# of increasing the boot-up time by auto-requiring all files in the support +# directory. Alternatively, in the individual `*_spec.rb` files, manually +# require only the support files necessary. +# +Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f } + +# Checks for pending migrations and applies them before tests are run. +begin + ActiveRecord::Migration.maintain_test_schema! +rescue ActiveRecord::PendingMigrationError => e + puts e.to_s.strip + exit 1 +end + +RSpec.configure do |config| + config.filter_rails_from_backtrace! + config.fixture_path = "#{::Rails.root}/spec/fixtures" + config.infer_spec_type_from_file_location! + config.use_transactional_fixtures = false + config.include Helpers::JsonHelper +end diff --git a/lib/generators/disco_app/install/templates/spec/spec_helper.rb b/lib/generators/disco_app/install/templates/spec/spec_helper.rb new file mode 100644 index 00000000..7d531957 --- /dev/null +++ b/lib/generators/disco_app/install/templates/spec/spec_helper.rb @@ -0,0 +1,24 @@ +require 'support/coveralls' +require 'support/webmock' + +RSpec.configure do |config| + config.expect_with :rspec do |expectations| + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + config.mock_with :rspec do |mocks| + mocks.verify_partial_doubles = true + end + + config.default_formatter = 'doc' if config.files_to_run.one? + config.disable_monkey_patching! + config.example_status_persistence_file_path = 'spec/examples.txt' + config.filter_run_when_matching :focus + config.order = :random + config.shared_context_metadata_behavior = :apply_to_host_groups + + config.filter_run focus: true + config.run_all_when_everything_filtered = true + + Kernel.srand config.seed +end diff --git a/lib/generators/disco_app/install/templates/spec/support/active_job.rb b/lib/generators/disco_app/install/templates/spec/support/active_job.rb new file mode 100644 index 00000000..edff539c --- /dev/null +++ b/lib/generators/disco_app/install/templates/spec/support/active_job.rb @@ -0,0 +1,13 @@ +require 'sidekiq/testing' + +RSpec.configure do |config| + config.include ActiveJob::TestHelper + + config.before(:all) do + Sidekiq::Testing.fake! + end + + config.before(:each) do + Sidekiq::Worker.clear_all + end +end diff --git a/lib/generators/disco_app/install/templates/spec/support/coveralls.rb b/lib/generators/disco_app/install/templates/spec/support/coveralls.rb new file mode 100644 index 00000000..7a960894 --- /dev/null +++ b/lib/generators/disco_app/install/templates/spec/support/coveralls.rb @@ -0,0 +1,3 @@ +require 'coveralls' + +Coveralls.wear!('rails') diff --git a/lib/generators/disco_app/install/templates/spec/support/database_cleaner.rb b/lib/generators/disco_app/install/templates/spec/support/database_cleaner.rb new file mode 100644 index 00000000..32ba6918 --- /dev/null +++ b/lib/generators/disco_app/install/templates/spec/support/database_cleaner.rb @@ -0,0 +1,17 @@ +RSpec.configure do |config| + config.before(:suite) do + DatabaseCleaner.clean_with(:truncation) + end + + config.before(:each) do + DatabaseCleaner.strategy = :transaction + end + + config.before(:each) do + DatabaseCleaner.start + end + + config.after(:each) do + DatabaseCleaner.clean + end +end diff --git a/lib/generators/disco_app/install/templates/spec/support/factory_bot.rb b/lib/generators/disco_app/install/templates/spec/support/factory_bot.rb new file mode 100644 index 00000000..c7890e49 --- /dev/null +++ b/lib/generators/disco_app/install/templates/spec/support/factory_bot.rb @@ -0,0 +1,3 @@ +RSpec.configure do |config| + config.include FactoryBot::Syntax::Methods +end diff --git a/lib/generators/disco_app/install/templates/spec/support/helpers/json_helper.rb b/lib/generators/disco_app/install/templates/spec/support/helpers/json_helper.rb new file mode 100644 index 00000000..1dc22891 --- /dev/null +++ b/lib/generators/disco_app/install/templates/spec/support/helpers/json_helper.rb @@ -0,0 +1,13 @@ +module Helpers + module JsonHelper + + # Return a JSON fixture as an indifferent hash. + def json_fixture(path, dir: 'json', parse: true) + filename = Rails.root.join('spec', 'fixtures', 'files', dir, "#{path}.json") + return File.read(filename) unless parse + + HashWithIndifferentAccess.new(ActiveSupport::JSON.decode(File.read(filename))) + end + + end +end diff --git a/lib/generators/disco_app/install/templates/spec/support/shared_examples/a_synchronise_job.rb b/lib/generators/disco_app/install/templates/spec/support/shared_examples/a_synchronise_job.rb new file mode 100644 index 00000000..c9c9291d --- /dev/null +++ b/lib/generators/disco_app/install/templates/spec/support/shared_examples/a_synchronise_job.rb @@ -0,0 +1,12 @@ +RSpec.shared_examples :a_synchronise_job do |model| + subject(:job) { described_class.perform_later(shop, data) } + + let(:shop) { create(:shop) } + let(:data) { 'data' } + + it "synchronises #{model}" do + expect(model).to receive(:synchronise).with(shop, data, any_args) + + perform_enqueued_jobs { job } + end +end diff --git a/lib/generators/disco_app/install/templates/spec/support/shoulda.rb b/lib/generators/disco_app/install/templates/spec/support/shoulda.rb new file mode 100644 index 00000000..7d045f35 --- /dev/null +++ b/lib/generators/disco_app/install/templates/spec/support/shoulda.rb @@ -0,0 +1,6 @@ +Shoulda::Matchers.configure do |config| + config.integrate do |with| + with.test_framework :rspec + with.library :rails + end +end diff --git a/lib/generators/disco_app/install/templates/spec/support/vcr.rb b/lib/generators/disco_app/install/templates/spec/support/vcr.rb new file mode 100644 index 00000000..88935c9c --- /dev/null +++ b/lib/generators/disco_app/install/templates/spec/support/vcr.rb @@ -0,0 +1,14 @@ +VCR.configure do |config| + config.before_record do |interaction| + interaction.request.headers['X-Shopify-Access-Token'] = [''] + interaction.request.headers['Authorization'] = [''] + end + config.cassette_library_dir = 'spec/vcr' + config.configure_rspec_metadata! + config.default_cassette_options = { + decode_compressed_response: true, + match_requests_on: %i[method uri body], + record: :once + } + config.hook_into :webmock +end diff --git a/lib/generators/disco_app/install/templates/spec/support/webmock.rb b/lib/generators/disco_app/install/templates/spec/support/webmock.rb new file mode 100644 index 00000000..e6cac2a5 --- /dev/null +++ b/lib/generators/disco_app/install/templates/spec/support/webmock.rb @@ -0,0 +1,8 @@ +require 'webmock/rspec' + +RSpec.configure do |config| + config.before(:each) do + WebMock.reset! + WebMock.disable_net_connect! + end +end diff --git a/lib/generators/disco_app/templates/views/home/index.html.erb b/lib/generators/disco_app/install/templates/views/home/index.html.erb similarity index 100% rename from lib/generators/disco_app/templates/views/home/index.html.erb rename to lib/generators/disco_app/install/templates/views/home/index.html.erb diff --git a/lib/generators/disco_app/react/USAGE b/lib/generators/disco_app/react/USAGE new file mode 100644 index 00000000..4d474854 --- /dev/null +++ b/lib/generators/disco_app/react/USAGE @@ -0,0 +1,5 @@ +Description: + Generate boilerplate for a React-flavoured embedded app. + +Example: + rails generate disco_app:react diff --git a/lib/generators/disco_app/react/react_generator.rb b/lib/generators/disco_app/react/react_generator.rb new file mode 100644 index 00000000..3aa953e7 --- /dev/null +++ b/lib/generators/disco_app/react/react_generator.rb @@ -0,0 +1,108 @@ +module DiscoApp + module Generators + class ReactGenerator < Rails::Generators::Base + + source_root File.expand_path('templates', __dir__) + + def prepare_application + copy_file 'root/VERSION', 'VERSION' + end + + def configure_gemfile + gem 'fast_jsonapi' + gem 'multi_json' + gem 'oj' + gem 'olive_branch' + gem 'webpacker' + end + + def bundle_install + Bundler.with_clean_env do + run 'bundle install' + end + end + + def configure_application + application 'config.middleware.use OliveBranch::Middleware' + application '# Camel-case to underscore transformation for JSON requests.' + + copy_file 'config/initializers/mime_types.rb' + copy_file 'config/initializers/omniauth.rb' + template 'config/initializers/version.rb.tt', 'config/initializers/version.rb' + + %w[.env .env.local].each do |file| + append_to_file file, 'BUGSNAG_API_KEY=00000000' + end + end + + def update_routes + routes = <<-ROUTES.gsub(/^ {8}/, '') + # Embedded React routes. + root to: 'embedded/home#index' + + # Embedded API. + namespace :embedded do + namespace :api, constraints: { format: :json }, defaults: { format: :json } do + resource :shop, only: [:show] + + resources :users, only: [] do + get :current, on: :collection + end + end + end + ROUTES + + route routes + + comment_lines 'config/routes.rb', "root to: 'home#index'" + end + + def install_webpacker + rake 'webpacker:install' + end + + def configure_webpack + %w[.eslintignore .eslintrc .prettierrc babel.config.js postcss.config.js].each do |file| + copy_file "root/#{file}", file + end + + template 'root/package.json.tt', 'package.json' + + copy_file 'config/webpacker.yml' + copy_file 'config/webpack/staging.js' + copy_file 'config/webpack/test.js' + + run "if [ -d 'app/javascript' ]; then mv -f app/javascript app/webpack; fi" + end + + def yarn_install + run 'yarn install' + end + + def configure_api + directory 'app/controllers/embedded' + end + + def configure_views + directory 'app/views/embedded' + copy_file 'app/views/layouts/embedded.html.erb' + end + + def configure_serializers + directory 'app/serializers' + end + + def configure_api_response + copy_file 'app/models/api_response.rb' + end + + def configure_react + directory 'app/webpack/javascripts' + directory 'app/webpack/stylesheets' + copy_file 'app/webpack/packs/embedded.js' + remove_file 'app/webpack/packs/application.js' + end + + end + end +end diff --git a/lib/generators/disco_app/react/templates/app/controllers/embedded/api/base_controller.rb b/lib/generators/disco_app/react/templates/app/controllers/embedded/api/base_controller.rb new file mode 100644 index 00000000..3e95fe67 --- /dev/null +++ b/lib/generators/disco_app/react/templates/app/controllers/embedded/api/base_controller.rb @@ -0,0 +1,18 @@ +module Embedded + module Api + class BaseController < ApplicationController + + include DiscoApp::Concerns::AuthenticatedController + include DiscoApp::Concerns::UserAuthenticatedController + + rescue_from ActiveRecord::RecordInvalid, with: :unprocessable_entity + + private + + def unprocessable_entity(exception) + render json: ApiResponse.serialize(exception.record.errors), status: :unprocessable_entity + end + + end + end +end diff --git a/lib/generators/disco_app/react/templates/app/controllers/embedded/api/home_controller.rb b/lib/generators/disco_app/react/templates/app/controllers/embedded/api/home_controller.rb new file mode 100644 index 00000000..f6f28930 --- /dev/null +++ b/lib/generators/disco_app/react/templates/app/controllers/embedded/api/home_controller.rb @@ -0,0 +1,10 @@ +module Embedded + module Api + class HomeController < BaseController + + def show + end + + end + end +end diff --git a/lib/generators/disco_app/react/templates/app/controllers/embedded/api/shops_controller.rb b/lib/generators/disco_app/react/templates/app/controllers/embedded/api/shops_controller.rb new file mode 100644 index 00000000..c61aca44 --- /dev/null +++ b/lib/generators/disco_app/react/templates/app/controllers/embedded/api/shops_controller.rb @@ -0,0 +1,11 @@ +module Embedded + module Api + class ShopsController < BaseController + + def show + render json: ApiResponse.serialize(@shop) + end + + end + end +end diff --git a/lib/generators/disco_app/react/templates/app/controllers/embedded/api/users_controller.rb b/lib/generators/disco_app/react/templates/app/controllers/embedded/api/users_controller.rb new file mode 100644 index 00000000..3ebfdca6 --- /dev/null +++ b/lib/generators/disco_app/react/templates/app/controllers/embedded/api/users_controller.rb @@ -0,0 +1,11 @@ +module Embedded + module Api + class UsersController < BaseController + + def current + render json: ApiResponse.serialize(@user) + end + + end + end +end diff --git a/lib/generators/disco_app/react/templates/app/controllers/embedded/home_controller.rb b/lib/generators/disco_app/react/templates/app/controllers/embedded/home_controller.rb new file mode 100644 index 00000000..b0660bf6 --- /dev/null +++ b/lib/generators/disco_app/react/templates/app/controllers/embedded/home_controller.rb @@ -0,0 +1,13 @@ +module Embedded + class HomeController < ApplicationController + + include DiscoApp::Concerns::AuthenticatedController + include DiscoApp::Concerns::UserAuthenticatedController + + layout 'embedded' + + def index + end + + end +end diff --git a/lib/generators/disco_app/react/templates/app/models/api_response.rb b/lib/generators/disco_app/react/templates/app/models/api_response.rb new file mode 100644 index 00000000..46591106 --- /dev/null +++ b/lib/generators/disco_app/react/templates/app/models/api_response.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true + +class ApiResponse + + EMPTY_SERIALIZER = 'Empty' + ERROR_SERIALIZER = 'Error' + SERIALIZER_SUFFIX = 'Serializer' + SENSITIVE_REQUEST_PARAMS = ['timestamp', 'signature'].freeze + + def initialize(result, custom_serializer = nil) + @result = result + @custom_serializer = custom_serializer + end + + def serialize(options = {}) + request = options.delete(:request) + + serializer.new( + result, options.merge(collection_options(request)) + ).serializable_hash + end + + def self.serialize(result, options = {}) + serializer = options.delete(:serializer) + + new(result, serializer).serialize(options) + end + + private + + attr_accessor :result, :custom_serializer + + def array? + result.is_a?(Array) + end + + def collection? + result.is_a?(ActiveRecord::Relation) + end + + def error? + [ + result.is_a?(ActiveModel::Errors), + result.is_a?(ActiveResource::Errors), + result.is_a?(StandardError), + result.is_a?(String) + ].any? + end + + def empty? + (array? || collection?) && result.empty? + end + + def resource? + !array && !collection? && !error? + end + + def resource_type + return custom_serializer.to_s.classify if custom_serializer + return ERROR_SERIALIZER if error? + return EMPTY_SERIALIZER if empty? + return result.first.class.name if array? + return result.class.to_s.deconstantize if collection? + + result.class.name + end + + def serializer + "#{resource_type}#{SERIALIZER_SUFFIX}".constantize + end + + def collection_options(request = nil) + options = {} + + return options if error? + + options[:is_collection] = array? || collection? + + if collection? && result.respond_to?(:total_count) + options[:meta] = { + page_size: result.limit_value, + total_count: result.total_count, + total_pages: result.total_pages + } + + options[:links] = { + prev: link(result.prev_page, request), + next: link(result.next_page, request) + } + end + + options + end + + def link(page, request) + return page if page.blank? || request.blank? + + uri = URI.parse(request) + params = Rack::Utils.parse_query(uri.query) + params.delete_if{ |key| SENSITIVE_REQUEST_PARAMS.include?(key) } + params['page[number]'] = page + parsed_params = params.map{ |key, value| "#{key}=#{value}" }.join('&') + + "#{uri.scheme}:://#{uri.host}#{uri.path}?#{parsed_params}" + end + +end diff --git a/lib/generators/disco_app/react/templates/app/serializers/disco_app/shop_serializer.rb b/lib/generators/disco_app/react/templates/app/serializers/disco_app/shop_serializer.rb new file mode 100644 index 00000000..0be8594a --- /dev/null +++ b/lib/generators/disco_app/react/templates/app/serializers/disco_app/shop_serializer.rb @@ -0,0 +1,13 @@ +module DiscoApp + class ShopSerializer + + include FastJsonapi::ObjectSerializer + + attributes :id, :name, :shopify_url + + attribute :time_zone do |shop| + shop.time_zone.tzinfo.name + end + + end +end diff --git a/lib/generators/disco_app/react/templates/app/serializers/disco_app/user_serializer.rb b/lib/generators/disco_app/react/templates/app/serializers/disco_app/user_serializer.rb new file mode 100644 index 00000000..7190103a --- /dev/null +++ b/lib/generators/disco_app/react/templates/app/serializers/disco_app/user_serializer.rb @@ -0,0 +1,13 @@ +module DiscoApp + class UserSerializer + + include FastJsonapi::ObjectSerializer + + attributes :email, :first_name, :id, :last_name + + attribute :initials do |user| + [user.first_name, user.last_name].compact.map(&:first).join + end + + end +end diff --git a/lib/generators/disco_app/react/templates/app/serializers/empty_serializer.rb b/lib/generators/disco_app/react/templates/app/serializers/empty_serializer.rb new file mode 100644 index 00000000..e4da4f5b --- /dev/null +++ b/lib/generators/disco_app/react/templates/app/serializers/empty_serializer.rb @@ -0,0 +1,5 @@ +class EmptySerializer + + include FastJsonapi::ObjectSerializer + +end diff --git a/lib/generators/disco_app/react/templates/app/serializers/error_serializer.rb b/lib/generators/disco_app/react/templates/app/serializers/error_serializer.rb new file mode 100644 index 00000000..1078a8d8 --- /dev/null +++ b/lib/generators/disco_app/react/templates/app/serializers/error_serializer.rb @@ -0,0 +1,76 @@ +class ErrorSerializer + + attr_reader :errors, :source, :title + + def initialize(errors, source: nil, title: nil) + @errors = errors + @source = source + @title = title + end + + def serialized_json + serializable_hash.to_json + end + + def serializable_hash + { + errors: formatted_errors + } + end + + private + + def formatted_errors + return errors_from_exception if exception_error? + return errors_from_string if string_error? + + errors_from_active_model + end + + def errors_from_active_model + error_array = [] + + errors.keys.each do |field| + errors.full_messages_for(field).each do |error| + error_array << { + source: { pointer: "/data/attributes/#{field}" }, + title: title || 'Unprocessable entity', + detail: error + } + end + end + + error_array + end + + def errors_from_exception + error = { + title: title || errors.class.name.demodulize || 'Unknown error', + detail: errors.message + } + + error[:source] = { pointer: source } if source + + [error] + end + + def errors_from_string + error = { + title: title || 'Unknown error', + detail: errors + } + + error[:source] = { pointer: source } if source + + [error] + end + + def exception_error? + errors.is_a?(StandardError) + end + + def string_error? + errors.is_a?(String) + end + +end diff --git a/lib/generators/disco_app/react/templates/app/views/embedded/home/index.html.erb b/lib/generators/disco_app/react/templates/app/views/embedded/home/index.html.erb new file mode 100644 index 00000000..dacc1bb5 --- /dev/null +++ b/lib/generators/disco_app/react/templates/app/views/embedded/home/index.html.erb @@ -0,0 +1,12 @@ +
+<%= javascript_pack_tag 'embedded' %> +<%= stylesheet_pack_tag 'embedded' %> diff --git a/lib/generators/disco_app/react/templates/app/views/layouts/embedded.html.erb b/lib/generators/disco_app/react/templates/app/views/layouts/embedded.html.erb new file mode 100644 index 00000000..a9ec5a85 --- /dev/null +++ b/lib/generators/disco_app/react/templates/app/views/layouts/embedded.html.erb @@ -0,0 +1,10 @@ + + + + <%= ENV['SHOPIFY_APP_NAME'] %> + <%= csrf_meta_tags %> + + + <%= yield %> + + diff --git a/lib/generators/disco_app/react/templates/app/webpack/javascripts/embedded/components/App.jsx b/lib/generators/disco_app/react/templates/app/webpack/javascripts/embedded/components/App.jsx new file mode 100644 index 00000000..85747a4f --- /dev/null +++ b/lib/generators/disco_app/react/templates/app/webpack/javascripts/embedded/components/App.jsx @@ -0,0 +1,77 @@ +import axios from 'axios'; +import * as PropTypes from 'prop-types'; +import React from 'react'; +import { Route, Switch } from 'react-router-dom'; +import HomePage from './HomePage'; +import withApi from './withApi'; + +const HomePageWithApi = withApi(HomePage); + +class App extends React.Component { + static propTypes = { + api: PropTypes.func.isRequired, + parseApiResponse: PropTypes.func.isRequired + }; + + static childContextTypes = { + shop: PropTypes.shape({ + id: PropTypes.string, + name: PropTypes.string, + shopifyDomain: PropTypes.string, + timeZone: PropTypes.string + }), + user: PropTypes.shape({ + id: PropTypes.string, + email: PropTypes.string, + firstName: PropTypes.string, + initials: PropTypes.string, + lastName: PropTypes.string + }) + }; + + state = { + shop: null, + user: null + }; + + getChildContext() { + return { + shop: this.state.shop, + user: this.state.user + }; + } + + componentWillMount() { + const { api, parseApiResponse } = this.props; + + axios + .all([ + api.get('/embedded/api/shop'), + api.get('/embedded/api/users/current') + ]) + .then( + axios.spread( + async (shopResponse, usersResponse) => { + this.setState({ + shop: await parseApiResponse(shopResponse), + user: await parseApiResponse(usersResponse) + }); + } + ) + ); + } + + render() { + const { user } = this.state; + + if (!user) return
; + + return ( + + + + ); + } +} + +export default App; diff --git a/lib/generators/disco_app/react/templates/app/webpack/javascripts/embedded/components/HomePage.jsx b/lib/generators/disco_app/react/templates/app/webpack/javascripts/embedded/components/HomePage.jsx new file mode 100644 index 00000000..67aa23c0 --- /dev/null +++ b/lib/generators/disco_app/react/templates/app/webpack/javascripts/embedded/components/HomePage.jsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { EmptyState, FooterHelp, Layout, Link } from '@shopify/polaris'; +import EmbeddedPage from './Shared/EmbeddedPage'; + +const HomePage = props => ( + + +

Time to build a killer UI

+
+ + + Learn more about{' '} + + Disco + + 's{' '} + + [Disco app] + + . + + +
+); + +export default HomePage; diff --git a/lib/generators/disco_app/react/templates/app/webpack/javascripts/embedded/components/Shared/EmbeddedPage.jsx b/lib/generators/disco_app/react/templates/app/webpack/javascripts/embedded/components/Shared/EmbeddedPage.jsx new file mode 100644 index 00000000..62372b4a --- /dev/null +++ b/lib/generators/disco_app/react/templates/app/webpack/javascripts/embedded/components/Shared/EmbeddedPage.jsx @@ -0,0 +1,70 @@ +import _ from 'lodash'; +import * as PropTypes from 'prop-types'; +import React from 'react'; +import ReactRouterPropTypes from 'react-router-prop-types'; +import { History } from '@shopify/app-bridge/actions'; +import { Page } from '@shopify/polaris'; + +class EmbeddedPage extends React.Component { + static contextTypes = { + polaris: PropTypes.object + }; + + static propTypes = { + children: PropTypes.node.isRequired, + history: ReactRouterPropTypes.history.isRequired, + location: ReactRouterPropTypes.location.isRequired, + title: PropTypes.string.isRequired + }; + + componentDidMount() { + window.addEventListener('message', this.handleMessage); + + this.pushHistory(); + } + + componentDidUpdate(prevProps) { + if (prevProps.location.pathname !== this.props.location.pathname) { + this.pushHistory(); + } + } + + componentWillUnmount() { + window.removeEventListener('message', this.handleMessage); + } + + pushHistory = () => { + const history = History.create(this.context.polaris.appBridge); + + history.dispatch(History.Action.PUSH, this.props.history.location.pathname); + }; + + handleMessage = e => { + if (e.isTrusted) { + if (_.isString(e.data)) { + const json = JSON.parse(e.data); + + if (json.message === 'Shopify.API.setWindowLocation') { + const url = new URL(json.data); + this.props.history.push(url.pathname); + } + } + } + }; + + pushHistory = () => { + const history = History.create(this.context.polaris.appBridge); + + history.dispatch(History.Action.PUSH, this.props.history.location.pathname); + }; + + render() { + return ( + + {this.props.children} + + ); + } +} + +export default EmbeddedPage; diff --git a/lib/generators/disco_app/react/templates/app/webpack/javascripts/embedded/components/Shared/ErrorBanner.jsx b/lib/generators/disco_app/react/templates/app/webpack/javascripts/embedded/components/Shared/ErrorBanner.jsx new file mode 100644 index 00000000..5734957b --- /dev/null +++ b/lib/generators/disco_app/react/templates/app/webpack/javascripts/embedded/components/Shared/ErrorBanner.jsx @@ -0,0 +1,58 @@ +import * as PropTypes from 'prop-types'; +import React from 'react'; +import { Banner, TextStyle } from '@shopify/polaris'; + +const ErrorBanner = ({ errors, prologue }) => { + const errorKeys = () => Object.keys(errors).sort(); + + const errorCount = () => errorKeys().length; + + const errorMessage = () => { + let msg = '1 field needs changes'; + + if (errorCount() > 1) { + msg = `${errorCount()} fields need changes`; + } + + return msg; + }; + + const separator = index => { + if (index === 0) return ' '; + + if (index === errorCount() - 1) return ' and '; + + return ', '; + }; + + if (errorCount() === 0) return null; + + return ( + +

+ {prologue},{errorMessage()}: + {errorKeys().map((key, index) => ( + + {separator(index)} + + {key} + + + ))} + . +

+
+ ); +}; + +ErrorBanner.defaultProps = { + errors: {}, + prologue: 'To save this form' +}; + +ErrorBanner.propTypes = { + errors: PropTypes.shape({}), + prologue: PropTypes.string +}; + +export default ErrorBanner; diff --git a/lib/generators/disco_app/react/templates/app/webpack/javascripts/embedded/components/Shared/PaginationWrapper.jsx b/lib/generators/disco_app/react/templates/app/webpack/javascripts/embedded/components/Shared/PaginationWrapper.jsx new file mode 100644 index 00000000..b2dfc968 --- /dev/null +++ b/lib/generators/disco_app/react/templates/app/webpack/javascripts/embedded/components/Shared/PaginationWrapper.jsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { Pagination } from '@shopify/polaris'; + +const PaginationWrapper = props => ( +
+ +
+); + +export default PaginationWrapper; diff --git a/lib/generators/disco_app/react/templates/app/webpack/javascripts/embedded/components/Shared/ScrollToTop.jsx b/lib/generators/disco_app/react/templates/app/webpack/javascripts/embedded/components/Shared/ScrollToTop.jsx new file mode 100644 index 00000000..840fffd8 --- /dev/null +++ b/lib/generators/disco_app/react/templates/app/webpack/javascripts/embedded/components/Shared/ScrollToTop.jsx @@ -0,0 +1,23 @@ +import * as PropTypes from 'prop-types'; +import React from 'react'; +import ReactRouterPropTypes from 'react-router-prop-types'; +import { withRouter } from 'react-router-dom'; + +class ScrollToTop extends React.Component { + static propTypes = { + children: PropTypes.node.isRequired, + location: ReactRouterPropTypes.location.isRequired + }; + + componentDidUpdate(prevProps) { + if (this.props.location !== prevProps.location) { + window.scrollTo(0, 0); + } + } + + render() { + return this.props.children; + } +} + +export default withRouter(ScrollToTop); diff --git a/lib/generators/disco_app/react/templates/app/webpack/javascripts/embedded/components/withApi.jsx b/lib/generators/disco_app/react/templates/app/webpack/javascripts/embedded/components/withApi.jsx new file mode 100644 index 00000000..c339095d --- /dev/null +++ b/lib/generators/disco_app/react/templates/app/webpack/javascripts/embedded/components/withApi.jsx @@ -0,0 +1,125 @@ +import axios from 'axios'; +import { deserialise } from 'kitsu-core'; +import _ from 'lodash'; +import qs from 'qs'; +import React from 'react'; + +function withApi(WrappedComponent) { + class WithApi extends React.Component { + constructor(props) { + super(props); + + this.initApi(); + } + + getErrorsFor = (field, errors) => { + const fieldErrors = errors[field]; + + if (!fieldErrors) return ''; + + return fieldErrors.join(', '); + }; + + initApi = () => { + const csrfToken = document + .getElementsByName('csrf-token')[0] + .getAttribute('content'); + + this.api = axios.create({ + headers: { + Accept: 'application/json', + 'X-CSRF-Token': csrfToken, + 'X-Key-Inflection': 'camel' + }, + paramsSerializer: params => qs.stringify(params), + timeout: 5000 + }); + }; + + parseApiResponse = async (response, includeMeta) => { + const result = await deserialise(response.data); + + return includeMeta ? result : result.data; + }; + + parseApiError = async errorResponse => { + const result = await deserialise(errorResponse.response.data); + + const parsedErrors = {}; + + result.errors.forEach(error => { + if ( + _.has(error, 'source.pointer') && + error.source.pointer.startsWith('/data/attributes/') + ) { + const attr = _.last(error.source.pointer.split('/')); + const msg = error.detail; + + if (_.has(parsedErrors, attr)) { + parsedErrors[attr].push(msg); + } else { + parsedErrors[attr] = [msg]; + } + } + }); + + return parsedErrors; + }; + + resourceListParams = state => { + const defaultPageSize = 25; + const params = {}; + + if (!state) return params; + + if (state.filters) { + params.filter = {}; + + state.filters.forEach( + filter => (params.filter[filter.key] = filter.value) + ); + } + + if (state.pageNumber) { + params.page = { + number: state.pageNumber, + size: state.pageSize || defaultPageSize + }; + } + + if (state.searchQuery) { + params.search = state.searchQuery; + } + + if (state.sortBy) { + const match = /(.*)_((?:a|de)sc)/.exec(state.sortBy); + + if (match) { + const field = match[1]; + const descSignifier = match[2] === 'desc' ? '-' : ''; + + params.sort = `${descSignifier}${field}`; + } + } + + return params; + }; + + render() { + return ( + + ); + } + } + + return WithApi; +} + +export default withApi; diff --git a/lib/generators/disco_app/react/templates/app/webpack/javascripts/embedded/index.jsx b/lib/generators/disco_app/react/templates/app/webpack/javascripts/embedded/index.jsx new file mode 100644 index 00000000..4b390e3a --- /dev/null +++ b/lib/generators/disco_app/react/templates/app/webpack/javascripts/embedded/index.jsx @@ -0,0 +1,39 @@ +import bugsnag from 'bugsnag-js'; +import createPlugin from 'bugsnag-react'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import { BrowserRouter as Router } from 'react-router-dom'; +import { AppProvider } from '@shopify/polaris'; +import App from './components/App'; +import ScrollToTop from './components/Shared/ScrollToTop'; +import withApi from './components/withApi'; + +const app = document.getElementById('app'); + +const bugsnagClient = bugsnag({ + apiKey: app.dataset.bugsnagApiKey, + appVersion: app.dataset.version, + releaseStage: app.dataset.environment +}); + +const ErrorBoundary = bugsnagClient.use(createPlugin(React)); + +const AppWithApi = withApi(App); + +ReactDOM.render( + + + + + + + + + , + app +); diff --git a/lib/generators/disco_app/react/templates/app/webpack/javascripts/embedded/utils.js b/lib/generators/disco_app/react/templates/app/webpack/javascripts/embedded/utils.js new file mode 100644 index 00000000..7ba64922 --- /dev/null +++ b/lib/generators/disco_app/react/templates/app/webpack/javascripts/embedded/utils.js @@ -0,0 +1,19 @@ +import { DateTime } from 'luxon'; +import numeral from 'numeral'; + +export const numberToCurrency = amount => { + const format = '$0,0.00'; + const floatAmount = parseFloat(amount); + + if (floatAmount < 0) { + return `(${numeral(Math.abs(floatAmount)).format(format)})`; + } + + return numeral(floatAmount).format(format); +}; + +export const formatTime = time => { + if (!time) return 'N/A'; + + return DateTime.fromISO(time).toLocaleString(DateTime.DATETIME_SHORT); +}; diff --git a/lib/generators/disco_app/react/templates/app/webpack/packs/embedded.js b/lib/generators/disco_app/react/templates/app/webpack/packs/embedded.js new file mode 100644 index 00000000..6d03264f --- /dev/null +++ b/lib/generators/disco_app/react/templates/app/webpack/packs/embedded.js @@ -0,0 +1,2 @@ +import '../javascripts/embedded'; +import '../stylesheets/embedded.scss'; diff --git a/lib/generators/disco_app/react/templates/app/webpack/stylesheets/embedded.scss b/lib/generators/disco_app/react/templates/app/webpack/stylesheets/embedded.scss new file mode 100644 index 00000000..320921dd --- /dev/null +++ b/lib/generators/disco_app/react/templates/app/webpack/stylesheets/embedded.scss @@ -0,0 +1,2 @@ +@import '@shopify/polaris/styles.css'; +@import 'embedded/shared/index'; diff --git a/lib/generators/disco_app/react/templates/app/webpack/stylesheets/embedded/shared/banners.scss b/lib/generators/disco_app/react/templates/app/webpack/stylesheets/embedded/shared/banners.scss new file mode 100644 index 00000000..200c8f0e --- /dev/null +++ b/lib/generators/disco_app/react/templates/app/webpack/stylesheets/embedded/shared/banners.scss @@ -0,0 +1,7 @@ +.Polaris-Page__Content > .Polaris-Banner--withinPage { + margin-bottom: 2rem; +} + +.Polaris-DisplayText--sizeLarge + .Polaris-Banner--withinPage { + margin-top: 16px; +} diff --git a/lib/generators/disco_app/react/templates/app/webpack/stylesheets/embedded/shared/busy.scss b/lib/generators/disco_app/react/templates/app/webpack/stylesheets/embedded/shared/busy.scss new file mode 100644 index 00000000..5f1b768c --- /dev/null +++ b/lib/generators/disco_app/react/templates/app/webpack/stylesheets/embedded/shared/busy.scss @@ -0,0 +1,3 @@ +.Disco-ResourceListWrapper.busy .Polaris-ResourceList__ResourceListWrapper { + opacity: 0.5; +} diff --git a/lib/generators/disco_app/react/templates/app/webpack/stylesheets/embedded/shared/index.scss b/lib/generators/disco_app/react/templates/app/webpack/stylesheets/embedded/shared/index.scss new file mode 100644 index 00000000..5e0cef8d --- /dev/null +++ b/lib/generators/disco_app/react/templates/app/webpack/stylesheets/embedded/shared/index.scss @@ -0,0 +1,3 @@ +@import 'banners'; +@import 'busy'; +@import 'pagination'; diff --git a/lib/generators/disco_app/react/templates/app/webpack/stylesheets/embedded/shared/pagination.scss b/lib/generators/disco_app/react/templates/app/webpack/stylesheets/embedded/shared/pagination.scss new file mode 100644 index 00000000..9b506f50 --- /dev/null +++ b/lib/generators/disco_app/react/templates/app/webpack/stylesheets/embedded/shared/pagination.scss @@ -0,0 +1,5 @@ +.Disco-PaginationWrapper { + align-items: center; + display: flex; + justify-content: center; +} diff --git a/lib/generators/disco_app/react/templates/config/initializers/mime_types.rb b/lib/generators/disco_app/react/templates/config/initializers/mime_types.rb new file mode 100644 index 00000000..0f09b36b --- /dev/null +++ b/lib/generators/disco_app/react/templates/config/initializers/mime_types.rb @@ -0,0 +1,13 @@ +API_JSON = 'application/vnd.api+json'.freeze + +Mime::Type.register(API_JSON, :jsonapi) + +parsers = ActionDispatch::Request.parameter_parsers.merge( + Mime[:jsonapi].symbol => ->(body) { JSON.parse(body) } +) +ActionDispatch::Request.parameter_parsers = parsers + +ActionController::Renderers.add :jsonapi do |obj, _options| + self.content_type ||= Mime[:jsonapi] + self.response_body = obj.to_json +end diff --git a/lib/generators/disco_app/react/templates/config/initializers/omniauth.rb b/lib/generators/disco_app/react/templates/config/initializers/omniauth.rb new file mode 100644 index 00000000..230b57d8 --- /dev/null +++ b/lib/generators/disco_app/react/templates/config/initializers/omniauth.rb @@ -0,0 +1,27 @@ +module OmniAuth::Strategies + class ShopifyUser < Shopify + + def name + :shopify_user + end + + end +end + +SETUP_PROC = lambda do |env| + env['omniauth.strategy'].options[:per_user_permissions] = true + params = Rack::Utils.parse_query(env['QUERY_STRING']) + env['omniauth.strategy'].options[:client_options][:site] = "https://#{params['shop']}" +end + +Rails.application.config.middleware.use OmniAuth::Builder do + provider :shopify, + ShopifyApp.configuration.api_key, + ShopifyApp.configuration.secret, + scope: ShopifyApp.configuration.scope + provider :shopify_user, + ShopifyApp.configuration.api_key, + ShopifyApp.configuration.secret, + scope: ShopifyApp.configuration.scope, + setup: SETUP_PROC +end diff --git a/lib/generators/disco_app/react/templates/config/initializers/version.rb.tt b/lib/generators/disco_app/react/templates/config/initializers/version.rb.tt new file mode 100644 index 00000000..0d0c12a5 --- /dev/null +++ b/lib/generators/disco_app/react/templates/config/initializers/version.rb.tt @@ -0,0 +1,7 @@ +module <%= Rails.application.class.parent_name %> + class Application + + VERSION = File.read(Rails.root.join('VERSION')).chomp.freeze + + end +end diff --git a/lib/generators/disco_app/react/templates/config/webpack/staging.js b/lib/generators/disco_app/react/templates/config/webpack/staging.js new file mode 100644 index 00000000..93eb5804 --- /dev/null +++ b/lib/generators/disco_app/react/templates/config/webpack/staging.js @@ -0,0 +1,5 @@ +process.env.NODE_ENV = process.env.NODE_ENV || 'staging' + +const environment = require('./environment') + +module.exports = environment.toWebpackConfig() diff --git a/lib/generators/disco_app/react/templates/config/webpack/test.js b/lib/generators/disco_app/react/templates/config/webpack/test.js new file mode 100644 index 00000000..89d35d93 --- /dev/null +++ b/lib/generators/disco_app/react/templates/config/webpack/test.js @@ -0,0 +1,5 @@ +process.env.NODE_ENV = process.env.NODE_ENV || 'development'; + +const environment = require('./environment'); + +module.exports = environment.toWebpackConfig(); diff --git a/lib/generators/disco_app/react/templates/config/webpacker.yml b/lib/generators/disco_app/react/templates/config/webpacker.yml new file mode 100644 index 00000000..24b5a57b --- /dev/null +++ b/lib/generators/disco_app/react/templates/config/webpacker.yml @@ -0,0 +1,96 @@ +# Note: You must restart bin/webpack-dev-server for changes to take effect + +default: &default + source_path: app/webpack + source_entry_path: packs + public_root_path: public + public_output_path: packs + cache_path: tmp/cache/webpacker + check_yarn_integrity: false + webpack_compile_output: false + + # Additional paths webpack should lookup modules + # ['app/assets', 'engine/foo/app/assets'] + resolved_paths: [] + + # Reload manifest.json on all requests so we reload latest compiled packs + cache_manifest: false + + # Extract and emit a css file + extract_css: true + + static_assets_extensions: + - .jpg + - .jpeg + - .png + - .gif + - .tiff + - .ico + - .svg + - .eot + - .otf + - .ttf + - .woff + - .woff2 + + extensions: + - .jsx + - .mjs + - .js + - .sass + - .scss + - .css + - .module.sass + - .module.scss + - .module.css + - .png + - .svg + - .gif + - .jpeg + - .jpg + +development: + <<: *default + compile: true + + # Verifies that correct packages and versions are installed by inspecting package.json, yarn.lock, and node_modules + check_yarn_integrity: true + + # Reference: https://webpack.js.org/configuration/dev-server/ + dev_server: + https: false + host: localhost + port: 3035 + public: localhost:3035 + hmr: false + # Inline should be set to true if using HMR + inline: true + overlay: true + compress: true + disable_host_check: true + use_local_ip: false + quiet: false + headers: + 'Access-Control-Allow-Origin': '*' + watch_options: + ignored: '**/node_modules/**' + + +test: + <<: *default + compile: true + + # Compile test packs to a separate directory + public_output_path: packs-test + +production: + <<: *default + + # Production depends on precompilation of packs prior to booting for performance. + compile: false + + # Extract and emit a css file + extract_css: true + + # Cache manifest.json for performance + cache_manifest: true diff --git a/lib/generators/disco_app/react/templates/root/.eslintignore b/lib/generators/disco_app/react/templates/root/.eslintignore new file mode 100644 index 00000000..01fcec5a --- /dev/null +++ b/lib/generators/disco_app/react/templates/root/.eslintignore @@ -0,0 +1,5 @@ +app/assets/javascripts/* +config/webpack/* +node_modules/* +public/packs/* +vendor/ruby/* diff --git a/lib/generators/disco_app/react/templates/root/.eslintrc b/lib/generators/disco_app/react/templates/root/.eslintrc new file mode 100644 index 00000000..00c8d226 --- /dev/null +++ b/lib/generators/disco_app/react/templates/root/.eslintrc @@ -0,0 +1,69 @@ +{ + "env": { + "browser": true, + "es6": true, + "jest": true + }, + "extends": [ + "airbnb", + "plugin:prettier/recommended", + "plugin:import/errors", + "plugin:import/warnings", + "plugin:jest/recommended" + ], + "parser": "babel-eslint", + "parserOptions": { + "ecmaFeatures": { + "jsx": true, + "modules": true + }, + "ecmaVersion": 8, + "sourceType": "module" + }, + "plugins": [ + "jest" + ], + "root": true, + "rules": { + "import/first": "off", + "import/no-extraneous-dependencies": [ + "error", + { + "devDependencies": [ + "app/webpack/**/*.spec.*", + "spec/javascripts/setupTests.js" + ] + } + ], + "import/order": [ + "error", + { + "groups": [ + "builtin", + "external", + "parent", + "sibling", + "index" + ] + } + ], + "jsx-a11y/anchor-is-valid": "off", + "no-console": [ + "error", + { + "allow": [ + "error" + ] + } + ], + "no-return-assign": [ + "error", + "except-parens" + ], + "react/destructuring-assignment": "off", + "react/forbid-prop-types": "off", + "react/jsx-one-expression-per-line": "off", + "react/no-access-state-in-setstate": "off", + "react/no-danger": "off" + } +} diff --git a/lib/generators/disco_app/react/templates/root/.prettierrc b/lib/generators/disco_app/react/templates/root/.prettierrc new file mode 100644 index 00000000..544138be --- /dev/null +++ b/lib/generators/disco_app/react/templates/root/.prettierrc @@ -0,0 +1,3 @@ +{ + "singleQuote": true +} diff --git a/lib/generators/disco_app/react/templates/root/VERSION b/lib/generators/disco_app/react/templates/root/VERSION new file mode 100644 index 00000000..8acdd82b --- /dev/null +++ b/lib/generators/disco_app/react/templates/root/VERSION @@ -0,0 +1 @@ +0.0.1 diff --git a/lib/generators/disco_app/react/templates/root/babel.config.js b/lib/generators/disco_app/react/templates/root/babel.config.js new file mode 100644 index 00000000..9f0a8d36 --- /dev/null +++ b/lib/generators/disco_app/react/templates/root/babel.config.js @@ -0,0 +1,72 @@ +module.exports = api => { + const validEnv = ['development', 'test', 'production']; + const currentEnv = api.env(); + const isDevelopmentEnv = api.env('development'); + const isProductionEnv = api.env('production'); + const isTestEnv = api.env('test'); + + if (!validEnv.includes(currentEnv)) { + throw new Error( + `${'Please specify a valid `NODE_ENV` or ' + + '`BABEL_ENV` environment variables. Valid values are "development", ' + + '"test", and "production". Instead, received: '}${JSON.stringify( + currentEnv + )}.` + ); + } + + return { + presets: [ + isTestEnv && [ + '@babel/preset-env', + { + modules: 'commonjs', + targets: { + node: 'current' + } + } + ], + (isProductionEnv || isDevelopmentEnv) && [ + '@babel/preset-env', + { + forceAllTransforms: true, + useBuiltIns: 'entry', + modules: false, + exclude: ['transform-typeof-symbol'] + } + ], + '@babel/preset-react' + ].filter(Boolean), + plugins: [ + 'babel-plugin-macros', + '@babel/plugin-syntax-dynamic-import', + isTestEnv && 'babel-plugin-dynamic-import-node', + '@babel/plugin-transform-destructuring', + [ + '@babel/plugin-proposal-class-properties', + { + loose: true + } + ], + [ + '@babel/plugin-proposal-object-rest-spread', + { + useBuiltIns: true + } + ], + [ + '@babel/plugin-transform-runtime', + { + helpers: false, + regenerator: true + } + ], + [ + '@babel/plugin-transform-regenerator', + { + async: false + } + ] + ].filter(Boolean) + }; +}; diff --git a/lib/generators/disco_app/react/templates/root/package.json.tt b/lib/generators/disco_app/react/templates/root/package.json.tt new file mode 100644 index 00000000..994973e0 --- /dev/null +++ b/lib/generators/disco_app/react/templates/root/package.json.tt @@ -0,0 +1,84 @@ +{ + "name": "<%= Rails.application.class.parent_name.underscore.dasherize %>", + "private": true, + "scripts": { + "lint": "eslint app/webpack -c .eslintrc --ext js,jsx", + "test": "jest", + "test-watch": "jest --watch" + }, + "jest": { + "moduleDirectories": [ + "app/webpack/javascripts", + "node_modules" + ], + "roots": [ + "app/webpack/javascripts" + ], + "setupFilesAfterEnv": [ + "spec/javascripts/setupTests.js" + ], + "testPathIgnorePatterns": [ + "/config/", + "/node_modules/", + "/vendor/" + ], + "transform": { + "^.+\\.jsx?$": "babel-jest" + } + }, + "dependencies": { + "@babel/core": "^7.3.4", + "@babel/plugin-proposal-class-properties": "^7.3.4", + "@babel/plugin-proposal-object-rest-spread": "^7.3.4", + "@babel/plugin-syntax-dynamic-import": "^7.2.0", + "@babel/plugin-transform-destructuring": "^7.3.2", + "@babel/plugin-transform-regenerator": "^7.3.4", + "@babel/plugin-transform-runtime": "^7.3.4", + "@babel/preset-env": "^7.3.4", + "@babel/preset-react": "^7.0.0", + "@rails/webpacker": "^4.0.2", + "@shopify/app-bridge": "^1.2.0-0", + "@shopify/polaris": "^3.10.0", + "axios": "^0.18.0", + "babel-plugin-dynamic-import-node": "^2.2.0", + "babel-plugin-macros": "^2.5.0", + "bugsnag-js": "^4.7.3", + "bugsnag-react": "^1.1.1", + "kitsu-core": "^7.0.0", + "lodash": "^4.17.11", + "luxon": "^1.11.4", + "numeral": "^2.0.6", + "pluralize": "^7.0.0", + "postcss-flexbugs-fixes": "^4.1.0", + "postcss-import": "^12.0.1", + "postcss-preset-env": "^6.6.0", + "prop-types": "^15.7.2", + "qs": "^6.6.0", + "query-string": "^6.4.0", + "react": "^16.8.4", + "react-dom": "^16.6.3", + "react-router-dom": "^4.3.1", + "react-router-prop-types": "^1.0.4", + "regenerator": "^0.13.3", + "url-parse": "^1.4.4" + }, + "devDependencies": { + "babel-eslint": "^10.0.1", + "babel-jest": "^24.8.0", + "enzyme": "^3.10.0", + "enzyme-adapter-react-16": "^1.14.0", + "eslint": "^5.15.3", + "eslint-config-airbnb": "^17.1.0", + "eslint-config-prettier": "^4.1.0", + "eslint-plugin-import": "^2.16.0", + "eslint-plugin-jest": "^22.6.4", + "eslint-plugin-jsx-a11y": "^6.2.1", + "eslint-plugin-prettier": "^3.0.1", + "eslint-plugin-react": "^7.12.4", + "jest": "^24.8.0", + "jest-enzyme": "^7.0.2", + "prettier": "^1.16.4", + "react-test-renderer": "^16.8.6", + "webpack-dev-server": "^3.2.1" + } +} diff --git a/lib/generators/disco_app/react/templates/root/postcss.config.js b/lib/generators/disco_app/react/templates/root/postcss.config.js new file mode 100644 index 00000000..a4063b20 --- /dev/null +++ b/lib/generators/disco_app/react/templates/root/postcss.config.js @@ -0,0 +1,14 @@ +module.exports = { + plugins: { + 'postcss-import': {}, + 'postcss-flexbugs-fixes': {}, + 'postcss-preset-env': [ + { + autoprefixer: { + flexbox: 'no-2009' + }, + stage: 3 + } + ] + } +}; diff --git a/lib/generators/disco_app/templates/config/newrelic.yml b/lib/generators/disco_app/templates/config/newrelic.yml deleted file mode 100644 index f5eb2596..00000000 --- a/lib/generators/disco_app/templates/config/newrelic.yml +++ /dev/null @@ -1,29 +0,0 @@ -# This file configures the New Relic Agent. -# -# For full documentation of agent configuration options, please refer to -# https://docs.newrelic.com/docs/agents/ruby-agent/installation-configuration/ruby-agent-configuration - -common: &default_settings - license_key: <%= ENV['NEW_RELIC_LICENSE_KEY'] %> - app_name: <%= ENV['SHOPIFY_APP_NAME'] || 'Unknown App' %> - - # To disable the agent regardless of other settings, uncomment the following: - # agent_enabled: false - - # Logging level for log/newrelic_agent.log - log_level: info - -development: - <<: *default_settings - app_name: <%= ENV['SHOPIFY_APP_NAME'] || 'Unknown App' %> (Development) - developer_mode: true - -test: - <<: *default_settings - monitor_mode: false - -staging: - <<: *default_settings - -production: - <<: *default_settings diff --git a/lib/generators/disco_app/templates/root/.codeclimate.yml b/lib/generators/disco_app/templates/root/.codeclimate.yml deleted file mode 100644 index 183e3390..00000000 --- a/lib/generators/disco_app/templates/root/.codeclimate.yml +++ /dev/null @@ -1,7 +0,0 @@ -engines: - rubocop: - enabled: true - -ratings: - paths: - - "**.rb" diff --git a/lib/tasks/api.rake b/lib/tasks/api.rake index aeb1f2f3..e1f3365c 100644 --- a/lib/tasks/api.rake +++ b/lib/tasks/api.rake @@ -1,10 +1,8 @@ namespace :api do - desc 'Send all subscription information to the Disco API' task send_subscriptions: :environment do DiscoApp::Shop.find_each do |shop| DiscoApp::SendSubscriptionJob.perform_later(shop) end end - end diff --git a/lib/tasks/carrier_service.rake b/lib/tasks/carrier_service.rake index f06d39a1..1b4b23c7 100644 --- a/lib/tasks/carrier_service.rake +++ b/lib/tasks/carrier_service.rake @@ -1,10 +1,8 @@ namespace :carrier_service do - desc 'Synchronise carrier service across all installed shops.' task sync: :environment do DiscoApp::Shop.installed.has_active_shopify_plan.each do |shop| DiscoApp::SynchroniseCarrierServiceJob.perform_later(shop) end end - end diff --git a/lib/tasks/database.rake b/lib/tasks/database.rake index ecb767c1..281364ed 100644 --- a/lib/tasks/database.rake +++ b/lib/tasks/database.rake @@ -1,5 +1,5 @@ namespace :database do - desc "update postgres sequence numbers in case database has been migrated" + desc 'update postgres sequence numbers in case database has been migrated' task update_sequences: :environment do ActiveRecord::Base.connection.tables.each do |t| ActiveRecord::Base.connection.reset_pk_sequence!(t) diff --git a/lib/tasks/sessions.rake b/lib/tasks/sessions.rake index ce226508..42209c63 100644 --- a/lib/tasks/sessions.rake +++ b/lib/tasks/sessions.rake @@ -1,9 +1,7 @@ namespace :sessions do - desc 'Clean out any stale sessions.' task clean: [:environment, 'db:load_config'] do threshold = (ENV['SESSIONS_CLEAN_THRESHOLD_DAYS'] || 30).to_i.days.ago ActiveRecord::Base.connection.execute("DELETE FROM #{ActiveRecord::SessionStore::Session.table_name} WHERE updated_at < '#{threshold}'") end - end diff --git a/lib/tasks/shops.rake b/lib/tasks/shops.rake index 2d6e8e3c..93b492b6 100644 --- a/lib/tasks/shops.rake +++ b/lib/tasks/shops.rake @@ -1,10 +1,8 @@ namespace :shops do - desc 'Synchronise shop data across all installed shops.' task sync: :environment do DiscoApp::Shop.installed.has_active_shopify_plan.each do |shop| DiscoApp::ShopUpdateJob.perform_later(shop) end end - end diff --git a/lib/tasks/users.rake b/lib/tasks/users.rake index ce36bb23..b588c28c 100644 --- a/lib/tasks/users.rake +++ b/lib/tasks/users.rake @@ -1,10 +1,8 @@ namespace :users do - desc 'Synchronise user data accross all installed shops' task sync: :environment do DiscoApp::Shop.installed.has_active_shopify_plan.shopify_plus.each do |shop| DiscoApp::SynchroniseUsersJob.perform_later(shop) end end - end diff --git a/lib/tasks/webhooks.rake b/lib/tasks/webhooks.rake index 447a865e..168885f4 100644 --- a/lib/tasks/webhooks.rake +++ b/lib/tasks/webhooks.rake @@ -1,10 +1,8 @@ namespace :webhooks do - desc 'Synchronise webhooks across all installed shops.' task sync: :environment do DiscoApp::Shop.installed.has_active_shopify_plan.each do |shop| DiscoApp::SynchroniseWebhooksJob.perform_later(shop) end end - end diff --git a/test/clients/disco_app/api_client_test.rb b/test/clients/disco_app/api_client_test.rb index d0c6f4ca..7ffd8165 100644 --- a/test/clients/disco_app/api_client_test.rb +++ b/test/clients/disco_app/api_client_test.rb @@ -4,9 +4,9 @@ class ApiClientTest < ActiveSupport::TestCase def setup @shop = disco_app_shops(:widget_store) - stub_request(:post, "https://api.discolabs.com/v1/app_subscriptions.json"). - with(body: api_fixture('subscriptions/valid_request').to_json). - to_return(status: 200, body: api_fixture('subscriptions/valid_request').to_json) + stub_request(:post, 'https://api.discolabs.com/v1/app_subscriptions.json') + .with(body: api_fixture('subscriptions/valid_request').to_json) + .to_return(status: 200, body: api_fixture('subscriptions/valid_request').to_json) end def teardown diff --git a/test/controllers/disco_app/admin/shops_controller_test.rb b/test/controllers/disco_app/admin/shops_controller_test.rb index 51e4acfb..3cff6735 100644 --- a/test/controllers/disco_app/admin/shops_controller_test.rb +++ b/test/controllers/disco_app/admin/shops_controller_test.rb @@ -1,6 +1,7 @@ require 'test_helper' class DiscoApp::Admin::ShopsControllerTest < ActionController::TestCase + include ActiveJob::TestHelper def setup diff --git a/test/controllers/disco_app/charges_controller_test.rb b/test/controllers/disco_app/charges_controller_test.rb index 649e62ab..d9fc3abe 100644 --- a/test/controllers/disco_app/charges_controller_test.rb +++ b/test/controllers/disco_app/charges_controller_test.rb @@ -1,6 +1,7 @@ require 'test_helper' class DiscoApp::ChargesControllerTest < ActionController::TestCase + include ActiveJob::TestHelper include DiscoApp::Test::ShopifyAPI @@ -43,15 +44,13 @@ def teardown end test 'user with unpaid current subscription can create new charge and is redirected to confirmation url' do - res = { "recurring_application_charge": { "name": "Basic", - "price": "9.99", - "trial_days": 14, - "return_url": /^https:\/\/test\.example\.com\/subscriptions\/304261385\/charges\/53297050(1|2)\/activate$/, - "test": true - } } + res = { "recurring_application_charge": { "name": 'Basic', + "price": '9.99', + "trial_days": 14, + "return_url": %r{^https://test\.example\.com/subscriptions/304261385/charges/53297050(1|2)/activate$}, + "test": true } } stub_request(:post, "#{@shop.admin_url}/recurring_application_charges.json") - .with(body: res - ).to_return(status: 201, body:api_fixture("widget_store/charges/create_second_recurring_application_charge_response").to_json) + .with(body: res).to_return(status: 201, body: api_fixture('widget_store/charges/create_second_recurring_application_charge_response').to_json) @current_subscription.active_charge.destroy post :create, params: { subscription_id: @current_subscription } diff --git a/test/controllers/disco_app/install_controller_test.rb b/test/controllers/disco_app/install_controller_test.rb index a8125f73..59b196a0 100644 --- a/test/controllers/disco_app/install_controller_test.rb +++ b/test/controllers/disco_app/install_controller_test.rb @@ -1,6 +1,7 @@ require 'test_helper' class DiscoApp::InstallControllerTest < ActionController::TestCase + include ActiveJob::TestHelper def setup @@ -23,7 +24,7 @@ def teardown test 'logged-in and installed user is redirected to installing url for install/uninstalling actions' do @shop.installed! - [:install, :uninstalling].each do |action| + [:install, :uninstalling].each do |_action| get(:install) assert_redirected_to :installing end diff --git a/test/controllers/disco_app/subscriptions_controller_test.rb b/test/controllers/disco_app/subscriptions_controller_test.rb index 416132bb..36ce0eb9 100644 --- a/test/controllers/disco_app/subscriptions_controller_test.rb +++ b/test/controllers/disco_app/subscriptions_controller_test.rb @@ -1,6 +1,7 @@ require 'test_helper' class DiscoApp::SubscriptionsControllerTest < ActionController::TestCase + include ActiveJob::TestHelper def setup diff --git a/test/controllers/disco_app/webhooks_controller_test.rb b/test/controllers/disco_app/webhooks_controller_test.rb index 02f85e9c..8a56d457 100644 --- a/test/controllers/disco_app/webhooks_controller_test.rb +++ b/test/controllers/disco_app/webhooks_controller_test.rb @@ -1,6 +1,7 @@ require 'test_helper' class DiscoApp::WebhooksControllerTest < ActionController::TestCase + include ActiveJob::TestHelper def setup diff --git a/test/controllers/home_controller_test.rb b/test/controllers/home_controller_test.rb index 442665fe..0dcd1372 100644 --- a/test/controllers/home_controller_test.rb +++ b/test/controllers/home_controller_test.rb @@ -94,7 +94,7 @@ def teardown Timecop.freeze('2017-03-08 12:44:58 +1100') do hmac = 'eb49ba93a8daf8a11a04c66129faf98de1cd40f082b0ae78e79a2dfbbefb438d' get :index, params: { hmac: hmac, shop: 'widgets-dev.myshopify.com', timestamp: Time.now.to_i } - assert_response :success + assert_response :success end end diff --git a/test/disco_app_test.rb b/test/disco_app_test.rb index bb7e4898..daa54125 100644 --- a/test/disco_app_test.rb +++ b/test/disco_app_test.rb @@ -1,7 +1,9 @@ require 'test_helper' class DiscoAppTest < ActiveSupport::TestCase - test "truth" do + + test 'truth' do assert_kind_of Module, DiscoApp end + end diff --git a/test/dummy/Rakefile b/test/dummy/Rakefile index ba6b733d..f7a26dda 100644 --- a/test/dummy/Rakefile +++ b/test/dummy/Rakefile @@ -1,6 +1,6 @@ # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. -require File.expand_path('../config/application', __FILE__) +require File.expand_path('config/application', __dir__) Rails.application.load_tasks diff --git a/test/dummy/app/controllers/application_controller.rb b/test/dummy/app/controllers/application_controller.rb index c24cee2d..c57af07c 100644 --- a/test/dummy/app/controllers/application_controller.rb +++ b/test/dummy/app/controllers/application_controller.rb @@ -1,6 +1,8 @@ class ApplicationController < ActionController::Base + include ShopifyApp::LoginProtection # Prevent CSRF attacks by raising an exception. # For APIs, you may want to use :null_session instead. protect_from_forgery with: :exception + end diff --git a/test/dummy/app/controllers/carrier_request_controller.rb b/test/dummy/app/controllers/carrier_request_controller.rb index 568a4b6e..44bffefb 100644 --- a/test/dummy/app/controllers/carrier_request_controller.rb +++ b/test/dummy/app/controllers/carrier_request_controller.rb @@ -1,4 +1,5 @@ class CarrierRequestController < ActionController::Base + include DiscoApp::Concerns::CarrierRequestController def rates diff --git a/test/dummy/app/controllers/disco_app/admin/shops_controller.rb b/test/dummy/app/controllers/disco_app/admin/shops_controller.rb index b4e4aa5f..9069464b 100644 --- a/test/dummy/app/controllers/disco_app/admin/shops_controller.rb +++ b/test/dummy/app/controllers/disco_app/admin/shops_controller.rb @@ -1,4 +1,5 @@ class DiscoApp::Admin::ShopsController < DiscoApp::Admin::ApplicationController + include DiscoApp::Admin::Concerns::ShopsController def index diff --git a/test/dummy/app/controllers/home_controller.rb b/test/dummy/app/controllers/home_controller.rb index 1a20f94a..b8345c31 100644 --- a/test/dummy/app/controllers/home_controller.rb +++ b/test/dummy/app/controllers/home_controller.rb @@ -1,4 +1,5 @@ class HomeController < ApplicationController + include DiscoApp::Concerns::AuthenticatedController def index diff --git a/test/dummy/app/controllers/proxy_controller.rb b/test/dummy/app/controllers/proxy_controller.rb index 809b9dd6..124133fa 100644 --- a/test/dummy/app/controllers/proxy_controller.rb +++ b/test/dummy/app/controllers/proxy_controller.rb @@ -1,4 +1,5 @@ class ProxyController < ActionController::Base + include DiscoApp::Concerns::AppProxyController def index diff --git a/test/dummy/app/jobs/disco_app/app_installed_job.rb b/test/dummy/app/jobs/disco_app/app_installed_job.rb index 653dbb0b..0b415130 100644 --- a/test/dummy/app/jobs/disco_app/app_installed_job.rb +++ b/test/dummy/app/jobs/disco_app/app_installed_job.rb @@ -1,4 +1,5 @@ class DiscoApp::AppInstalledJob < DiscoApp::ShopJob + include DiscoApp::Concerns::AppInstalledJob DEVELOPMENT_PLAN_ID = 1 @@ -7,9 +8,7 @@ class DiscoApp::AppInstalledJob < DiscoApp::ShopJob # on their status. def default_plan @default_plan ||= begin - if @shop.plan_name === 'affiliate' - DiscoApp::Plan.find(DEVELOPMENT_PLAN_ID) - end + DiscoApp::Plan.find(DEVELOPMENT_PLAN_ID) if @shop.plan_name === 'affiliate' end end diff --git a/test/dummy/app/jobs/disco_app/app_uninstalled_job.rb b/test/dummy/app/jobs/disco_app/app_uninstalled_job.rb index bf303152..22f2917a 100644 --- a/test/dummy/app/jobs/disco_app/app_uninstalled_job.rb +++ b/test/dummy/app/jobs/disco_app/app_uninstalled_job.rb @@ -1,4 +1,5 @@ class DiscoApp::AppUninstalledJob < DiscoApp::ShopJob + include DiscoApp::Concerns::AppUninstalledJob # Extend the perform method to change the country name of the shop to diff --git a/test/dummy/app/models/application_record.rb b/test/dummy/app/models/application_record.rb index 10a4cba8..c6ae68f2 100644 --- a/test/dummy/app/models/application_record.rb +++ b/test/dummy/app/models/application_record.rb @@ -1,3 +1,5 @@ class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true + end diff --git a/test/dummy/app/models/cart.rb b/test/dummy/app/models/cart.rb index b3f0b98d..1d3409ea 100644 --- a/test/dummy/app/models/cart.rb +++ b/test/dummy/app/models/cart.rb @@ -1,13 +1,14 @@ class Cart < ApplicationRecord - include DiscoApp::Concerns::Synchronises - belongs_to :shop, class_name: 'DiscoApp::Shop' + include DiscoApp::Concerns::Synchronises SHOPIFY_API_CLASS = ShopifyAPI::Cart + belongs_to :shop, class_name: 'DiscoApp::Shop' + before_save :set_token - def self.synchronise_by(shop, data) + def self.synchronise_by(_shop, data) { token: data[:token] } end diff --git a/test/dummy/app/models/disco_app/shop.rb b/test/dummy/app/models/disco_app/shop.rb index b0c6d3ac..00ca5065 100644 --- a/test/dummy/app/models/disco_app/shop.rb +++ b/test/dummy/app/models/disco_app/shop.rb @@ -1,6 +1,7 @@ require 'active_utils' class DiscoApp::Shop < ApplicationRecord + include DiscoApp::Concerns::Shop has_one :js_configuration @@ -10,11 +11,9 @@ class DiscoApp::Shop < ApplicationRecord # Extend the Shop model to return the Shop's country as an ActiveUtils country. def country - begin - ActiveUtils::Country.find(data[:country_name]) - rescue ActiveUtils::InvalidCountryCodeError - nil - end + ActiveUtils::Country.find(data[:country_name]) + rescue ActiveUtils::InvalidCountryCodeError + nil end end diff --git a/test/dummy/app/models/js_configuration.rb b/test/dummy/app/models/js_configuration.rb index 4a32ba5c..ee8e5cb2 100644 --- a/test/dummy/app/models/js_configuration.rb +++ b/test/dummy/app/models/js_configuration.rb @@ -1,4 +1,5 @@ class JsConfiguration < ApplicationRecord + include DiscoApp::Concerns::RendersAssets belongs_to :shop, class_name: 'DiscoApp::Shop' diff --git a/test/dummy/app/models/product.rb b/test/dummy/app/models/product.rb index 93410195..19819638 100644 --- a/test/dummy/app/models/product.rb +++ b/test/dummy/app/models/product.rb @@ -1,9 +1,10 @@ class Product < ApplicationRecord + include DiscoApp::Concerns::Synchronises + include DiscoApp::Concerns::HasMetafields + SHOPIFY_API_CLASS = ShopifyAPI::Product belongs_to :shop, class_name: 'DiscoApp::Shop' - SHOPIFY_API_CLASS = ShopifyAPI::Product - end diff --git a/test/dummy/app/models/widget_configuration.rb b/test/dummy/app/models/widget_configuration.rb index 54a14f68..1afa966f 100644 --- a/test/dummy/app/models/widget_configuration.rb +++ b/test/dummy/app/models/widget_configuration.rb @@ -1,4 +1,5 @@ class WidgetConfiguration < ApplicationRecord + include DiscoApp::Concerns::RendersAssets belongs_to :shop, class_name: 'DiscoApp::Shop' diff --git a/test/dummy/bin/bundle b/test/dummy/bin/bundle index 66e9889e..f19acf5b 100755 --- a/test/dummy/bin/bundle +++ b/test/dummy/bin/bundle @@ -1,3 +1,3 @@ #!/usr/bin/env ruby -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) load Gem.bin_path('bundler', 'bundle') diff --git a/test/dummy/bin/rails b/test/dummy/bin/rails index 5191e692..07396602 100755 --- a/test/dummy/bin/rails +++ b/test/dummy/bin/rails @@ -1,4 +1,4 @@ #!/usr/bin/env ruby -APP_PATH = File.expand_path('../../config/application', __FILE__) +APP_PATH = File.expand_path('../config/application', __dir__) require_relative '../config/boot' require 'rails/commands' diff --git a/test/dummy/bin/setup b/test/dummy/bin/setup index acdb2c13..6942b154 100755 --- a/test/dummy/bin/setup +++ b/test/dummy/bin/setup @@ -2,15 +2,15 @@ require 'pathname' # path to your application root. -APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) +APP_ROOT = Pathname.new File.expand_path('..', __dir__) Dir.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 || bundle install" + puts '== Installing dependencies ==' + system 'gem install bundler --conservative' + system 'bundle check || bundle install' # puts "\n== Copying sample files ==" # unless File.exist?("config/database.yml") @@ -18,12 +18,12 @@ Dir.chdir APP_ROOT do # end puts "\n== Preparing database ==" - system "bin/rake db:setup" + system 'bin/rake db:setup' puts "\n== Removing old logs and tempfiles ==" - system "rm -f log/*" - system "rm -rf tmp/cache" + system 'rm -f log/*' + system 'rm -rf tmp/cache' puts "\n== Restarting application server ==" - system "touch tmp/restart.txt" + system 'touch tmp/restart.txt' end diff --git a/test/dummy/config/application.rb b/test/dummy/config/application.rb index 89da495c..ff785b6c 100644 --- a/test/dummy/config/application.rb +++ b/test/dummy/config/application.rb @@ -1,12 +1,13 @@ -require File.expand_path('../boot', __FILE__) +require File.expand_path('boot', __dir__) require 'rails/all' Bundler.require(*Rails.groups) -require "disco_app" +require 'disco_app' module Dummy class Application < Rails::Application + config.action_dispatch.default_headers['P3P'] = 'CP="Not used"' config.action_dispatch.default_headers.delete('X-Frame-Options') # Settings in config/environments/* take precedence over those specified here. @@ -30,6 +31,6 @@ class Application < Rails::Application # Explicitly prevent real charges being created by default config.x.shopify_charges_real = false + end end - diff --git a/test/dummy/config/boot.rb b/test/dummy/config/boot.rb index 6266cfc5..c9aef85d 100644 --- a/test/dummy/config/boot.rb +++ b/test/dummy/config/boot.rb @@ -1,5 +1,5 @@ # Set up gems listed in the Gemfile. -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__) +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../Gemfile', __dir__) require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) -$LOAD_PATH.unshift File.expand_path('../../../../lib', __FILE__) +$LOAD_PATH.unshift File.expand_path('../../../lib', __dir__) diff --git a/test/dummy/config/environment.rb b/test/dummy/config/environment.rb index ee8d90dc..0b8bdd82 100644 --- a/test/dummy/config/environment.rb +++ b/test/dummy/config/environment.rb @@ -1,5 +1,5 @@ # Load the Rails application. -require File.expand_path('../application', __FILE__) +require File.expand_path('application', __dir__) # Initialize the Rails application. Rails.application.initialize! diff --git a/test/dummy/config/initializers/disco_app.rb b/test/dummy/config/initializers/disco_app.rb index 92c874e9..160c11b9 100644 --- a/test/dummy/config/initializers/disco_app.rb +++ b/test/dummy/config/initializers/disco_app.rb @@ -6,7 +6,7 @@ # Set a list of webhook topics to listen for. # See https://help.shopify.com/api/reference/webhook. - config.webhook_topics = [:'orders/create', :'orders/paid', :'carts/create', :'carts/update'] + config.webhook_topics = %i[orders/create orders/paid carts/create carts/update] # Set the below if using an application proxy. config.app_proxy_prefix = ENV['SHOPIFY_APP_PROXY_PREFIX'] diff --git a/test/dummy/config/initializers/omniauth.rb b/test/dummy/config/initializers/omniauth.rb index 25e9016c..ce579b45 100644 --- a/test/dummy/config/initializers/omniauth.rb +++ b/test/dummy/config/initializers/omniauth.rb @@ -1,7 +1,6 @@ Rails.application.config.middleware.use OmniAuth::Builder do provider :shopify, - ShopifyApp.configuration.api_key, - ShopifyApp.configuration.secret, - - :scope => ShopifyApp.configuration.scope + ShopifyApp.configuration.api_key, + ShopifyApp.configuration.secret, + scope: ShopifyApp.configuration.scope end diff --git a/test/dummy/config/initializers/session_store.rb b/test/dummy/config/initializers/session_store.rb index 647e382c..74aa173a 100644 --- a/test/dummy/config/initializers/session_store.rb +++ b/test/dummy/config/initializers/session_store.rb @@ -1,2 +1,2 @@ # Use an ActiveRecord-based session store. -Rails.application.config.session_store :active_record_store, :key => '_disco_app_session' +Rails.application.config.session_store :active_record_store, key: '_disco_app_session' diff --git a/test/dummy/config/routes.rb b/test/dummy/config/routes.rb index c67eea1a..f2501136 100644 --- a/test/dummy/config/routes.rb +++ b/test/dummy/config/routes.rb @@ -1,5 +1,4 @@ Rails.application.routes.draw do - root to: 'home#index' get '/proxy', to: 'proxy#index' @@ -7,5 +6,4 @@ mount ShopifyApp::Engine, at: '/' mount DiscoApp::Engine, at: '/' - end diff --git a/test/dummy/db/migrate/20160307182229_create_products.rb b/test/dummy/db/migrate/20160307182229_create_products.rb index 9b92598f..670729ab 100644 --- a/test/dummy/db/migrate/20160307182229_create_products.rb +++ b/test/dummy/db/migrate/20160307182229_create_products.rb @@ -1,5 +1,6 @@ class CreateProducts < ActiveRecord::Migration[4.2] -def change + + def change create_table :products do |t| t.integer :shop_id, limit: 8 t.jsonb :data @@ -8,4 +9,5 @@ def change end add_foreign_key :products, :disco_app_shops, column: :shop_id end + end diff --git a/test/dummy/db/migrate/20160530160739_create_asset_models.rb b/test/dummy/db/migrate/20160530160739_create_asset_models.rb index 99dfb16a..7d5afbae 100644 --- a/test/dummy/db/migrate/20160530160739_create_asset_models.rb +++ b/test/dummy/db/migrate/20160530160739_create_asset_models.rb @@ -1,5 +1,6 @@ class CreateAssetModels < ActiveRecord::Migration[4.2] -def change + + def change create_table :js_configurations do |t| t.integer :shop_id, limit: 8 t.string :label, default: 'Default' @@ -16,4 +17,5 @@ def change add_foreign_key :js_configurations, :disco_app_shops, column: :shop_id add_foreign_key :widget_configurations, :disco_app_shops, column: :shop_id end + end diff --git a/test/dummy/db/migrate/20161105054746_create_carts.rb b/test/dummy/db/migrate/20161105054746_create_carts.rb index 5ae0cdd7..220784ae 100644 --- a/test/dummy/db/migrate/20161105054746_create_carts.rb +++ b/test/dummy/db/migrate/20161105054746_create_carts.rb @@ -1,6 +1,6 @@ class CreateCarts < ActiveRecord::Migration[4.2] -def change + def change create_table :carts do |t| t.integer :shop_id, limit: 8 t.string :token @@ -11,4 +11,5 @@ def change add_foreign_key :carts, :disco_app_shops, column: :shop_id add_index :carts, :token, unique: true end + end diff --git a/test/dummy/db/schema.rb b/test/dummy/db/schema.rb index e074d016..e6494a73 100644 --- a/test/dummy/db/schema.rb +++ b/test/dummy/db/schema.rb @@ -11,184 +11,183 @@ # It's strongly recommended that you check this file into your version control system. ActiveRecord::Schema.define(version: 2018_12_29_100327) do - # These are extensions that must be enabled in order to support this database - enable_extension "plpgsql" - - create_table "carts", id: :serial, force: :cascade do |t| - t.bigint "shop_id" - t.string "token" - t.jsonb "data" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.index ["token"], name: "index_carts_on_token", unique: true - end - - create_table "disco_app_app_settings", id: :serial, force: :cascade do |t| - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - end - - create_table "disco_app_application_charges", id: :serial, force: :cascade do |t| - t.bigint "shop_id" - t.bigint "subscription_id" - t.integer "status", default: 0 - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.bigint "shopify_id" - t.string "confirmation_url" - end - - create_table "disco_app_flow_actions", force: :cascade do |t| - t.bigint "shop_id" - t.string "action_id" - t.string "action_run_id" - t.jsonb "properties", default: {} - t.integer "status", default: 0 - t.datetime "processed_at" - t.jsonb "processing_errors", default: [] - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.index ["action_run_id"], name: "index_disco_app_flow_actions_on_action_run_id", unique: true - end - - create_table "disco_app_flow_triggers", force: :cascade do |t| - t.bigint "shop_id" - t.string "title" - t.string "resource_name" - t.string "resource_url" - t.jsonb "properties", default: {} - t.integer "status", default: 0 - t.datetime "processed_at" - t.jsonb "processing_errors", default: [] - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - end - - create_table "disco_app_plan_codes", id: :serial, force: :cascade do |t| - t.bigint "plan_id" - t.string "code" - t.integer "trial_period_days" - t.integer "amount" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.integer "status", default: 0 - end - - create_table "disco_app_plans", id: :serial, force: :cascade do |t| - t.integer "status", default: 0 - t.string "name" - t.integer "plan_type", default: 0 - t.integer "trial_period_days" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.integer "amount", default: 0 - t.string "currency", default: "USD" - t.integer "interval", default: 0 - t.integer "interval_count", default: 1 - end - - create_table "disco_app_recurring_application_charges", id: :serial, force: :cascade do |t| - t.bigint "shop_id" - t.bigint "subscription_id" - t.integer "status", default: 0 - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.bigint "shopify_id" - t.string "confirmation_url" - end - - create_table "disco_app_sessions", id: :serial, force: :cascade do |t| - t.string "session_id", null: false - t.text "data" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.integer "shop_id" - t.index ["session_id"], name: "index_disco_app_sessions_on_session_id", unique: true - t.index ["updated_at"], name: "index_disco_app_sessions_on_updated_at" - end - - create_table "disco_app_shops", id: :serial, force: :cascade do |t| - t.string "shopify_domain", null: false - t.string "shopify_token", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.integer "status", default: 0 - t.string "domain" - t.string "plan_name" - t.string "name" - t.jsonb "data", default: {} - t.index ["shopify_domain"], name: "index_disco_app_shops_on_shopify_domain", unique: true - end - - create_table "disco_app_sources", id: :serial, force: :cascade do |t| - t.string "source" - t.string "name" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.index ["source"], name: "index_disco_app_sources_on_source" - end - - create_table "disco_app_subscriptions", id: :serial, force: :cascade do |t| - t.integer "shop_id" - t.integer "plan_id" - t.integer "status" - t.integer "subscription_type" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.datetime "trial_start_at" - t.datetime "trial_end_at" - t.datetime "cancelled_at" - t.integer "amount", default: 0 - t.bigint "plan_code_id" - t.integer "trial_period_days" - t.bigint "source_id" - t.index ["plan_id"], name: "index_disco_app_subscriptions_on_plan_id" - t.index ["shop_id"], name: "index_disco_app_subscriptions_on_shop_id" - end - - create_table "disco_app_users", id: :serial, force: :cascade do |t| - t.bigint "shop_id" - t.string "first_name" - t.string "last_name" - t.string "email" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.index ["id", "shop_id"], name: "index_disco_app_users_on_id_and_shop_id", unique: true - end - - create_table "js_configurations", id: :serial, force: :cascade do |t| - t.bigint "shop_id" - t.string "label", default: "Default" - t.string "locale", default: "en" - end - - create_table "products", id: :serial, force: :cascade do |t| - t.bigint "shop_id" - t.jsonb "data" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - end - - create_table "widget_configurations", id: :serial, force: :cascade do |t| - t.bigint "shop_id" - t.string "label", default: "Default" - t.string "locale", default: "en" - t.string "background_color", default: "#FFFFFF" - end - - add_foreign_key "carts", "disco_app_shops", column: "shop_id" - add_foreign_key "disco_app_application_charges", "disco_app_shops", column: "shop_id" - add_foreign_key "disco_app_application_charges", "disco_app_subscriptions", column: "subscription_id" - add_foreign_key "disco_app_flow_actions", "disco_app_shops", column: "shop_id", on_delete: :cascade - add_foreign_key "disco_app_flow_triggers", "disco_app_shops", column: "shop_id", on_delete: :cascade - add_foreign_key "disco_app_plan_codes", "disco_app_plans", column: "plan_id" - add_foreign_key "disco_app_recurring_application_charges", "disco_app_shops", column: "shop_id" - add_foreign_key "disco_app_recurring_application_charges", "disco_app_subscriptions", column: "subscription_id" - add_foreign_key "disco_app_sessions", "disco_app_shops", column: "shop_id", on_delete: :cascade - add_foreign_key "disco_app_subscriptions", "disco_app_plan_codes", column: "plan_code_id" - add_foreign_key "disco_app_subscriptions", "disco_app_sources", column: "source_id" - add_foreign_key "js_configurations", "disco_app_shops", column: "shop_id" - add_foreign_key "products", "disco_app_shops", column: "shop_id" - add_foreign_key "widget_configurations", "disco_app_shops", column: "shop_id" + enable_extension 'plpgsql' + + create_table 'carts', id: :serial, force: :cascade do |t| + t.bigint 'shop_id' + t.string 'token' + t.jsonb 'data' + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.index ['token'], name: 'index_carts_on_token', unique: true + end + + create_table 'disco_app_app_settings', id: :serial, force: :cascade do |t| + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + end + + create_table 'disco_app_application_charges', id: :serial, force: :cascade do |t| + t.bigint 'shop_id' + t.bigint 'subscription_id' + t.integer 'status', default: 0 + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.bigint 'shopify_id' + t.string 'confirmation_url' + end + + create_table 'disco_app_flow_actions', force: :cascade do |t| + t.bigint 'shop_id' + t.string 'action_id' + t.string 'action_run_id' + t.jsonb 'properties', default: {} + t.integer 'status', default: 0 + t.datetime 'processed_at' + t.jsonb 'processing_errors', default: [] + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.index ['action_run_id'], name: 'index_disco_app_flow_actions_on_action_run_id', unique: true + end + + create_table 'disco_app_flow_triggers', force: :cascade do |t| + t.bigint 'shop_id' + t.string 'title' + t.string 'resource_name' + t.string 'resource_url' + t.jsonb 'properties', default: {} + t.integer 'status', default: 0 + t.datetime 'processed_at' + t.jsonb 'processing_errors', default: [] + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + end + + create_table 'disco_app_plan_codes', id: :serial, force: :cascade do |t| + t.bigint 'plan_id' + t.string 'code' + t.integer 'trial_period_days' + t.integer 'amount' + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.integer 'status', default: 0 + end + + create_table 'disco_app_plans', id: :serial, force: :cascade do |t| + t.integer 'status', default: 0 + t.string 'name' + t.integer 'plan_type', default: 0 + t.integer 'trial_period_days' + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.integer 'amount', default: 0 + t.string 'currency', default: 'USD' + t.integer 'interval', default: 0 + t.integer 'interval_count', default: 1 + end + + create_table 'disco_app_recurring_application_charges', id: :serial, force: :cascade do |t| + t.bigint 'shop_id' + t.bigint 'subscription_id' + t.integer 'status', default: 0 + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.bigint 'shopify_id' + t.string 'confirmation_url' + end + + create_table 'disco_app_sessions', id: :serial, force: :cascade do |t| + t.string 'session_id', null: false + t.text 'data' + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.integer 'shop_id' + t.index ['session_id'], name: 'index_disco_app_sessions_on_session_id', unique: true + t.index ['updated_at'], name: 'index_disco_app_sessions_on_updated_at' + end + + create_table 'disco_app_shops', id: :serial, force: :cascade do |t| + t.string 'shopify_domain', null: false + t.string 'shopify_token', null: false + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.integer 'status', default: 0 + t.string 'domain' + t.string 'plan_name' + t.string 'name' + t.jsonb 'data', default: {} + t.index ['shopify_domain'], name: 'index_disco_app_shops_on_shopify_domain', unique: true + end + + create_table 'disco_app_sources', id: :serial, force: :cascade do |t| + t.string 'source' + t.string 'name' + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.index ['source'], name: 'index_disco_app_sources_on_source' + end + + create_table 'disco_app_subscriptions', id: :serial, force: :cascade do |t| + t.integer 'shop_id' + t.integer 'plan_id' + t.integer 'status' + t.integer 'subscription_type' + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.datetime 'trial_start_at' + t.datetime 'trial_end_at' + t.datetime 'cancelled_at' + t.integer 'amount', default: 0 + t.bigint 'plan_code_id' + t.integer 'trial_period_days' + t.bigint 'source_id' + t.index ['plan_id'], name: 'index_disco_app_subscriptions_on_plan_id' + t.index ['shop_id'], name: 'index_disco_app_subscriptions_on_shop_id' + end + + create_table 'disco_app_users', id: :serial, force: :cascade do |t| + t.bigint 'shop_id' + t.string 'first_name' + t.string 'last_name' + t.string 'email' + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.index ['id', 'shop_id'], name: 'index_disco_app_users_on_id_and_shop_id', unique: true + end + + create_table 'js_configurations', id: :serial, force: :cascade do |t| + t.bigint 'shop_id' + t.string 'label', default: 'Default' + t.string 'locale', default: 'en' + end + + create_table 'products', id: :serial, force: :cascade do |t| + t.bigint 'shop_id' + t.jsonb 'data' + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + end + + create_table 'widget_configurations', id: :serial, force: :cascade do |t| + t.bigint 'shop_id' + t.string 'label', default: 'Default' + t.string 'locale', default: 'en' + t.string 'background_color', default: '#FFFFFF' + end + + add_foreign_key 'carts', 'disco_app_shops', column: 'shop_id' + add_foreign_key 'disco_app_application_charges', 'disco_app_shops', column: 'shop_id' + add_foreign_key 'disco_app_application_charges', 'disco_app_subscriptions', column: 'subscription_id' + add_foreign_key 'disco_app_flow_actions', 'disco_app_shops', column: 'shop_id', on_delete: :cascade + add_foreign_key 'disco_app_flow_triggers', 'disco_app_shops', column: 'shop_id', on_delete: :cascade + add_foreign_key 'disco_app_plan_codes', 'disco_app_plans', column: 'plan_id' + add_foreign_key 'disco_app_recurring_application_charges', 'disco_app_shops', column: 'shop_id' + add_foreign_key 'disco_app_recurring_application_charges', 'disco_app_subscriptions', column: 'subscription_id' + add_foreign_key 'disco_app_sessions', 'disco_app_shops', column: 'shop_id', on_delete: :cascade + add_foreign_key 'disco_app_subscriptions', 'disco_app_plan_codes', column: 'plan_code_id' + add_foreign_key 'disco_app_subscriptions', 'disco_app_sources', column: 'source_id' + add_foreign_key 'js_configurations', 'disco_app_shops', column: 'shop_id' + add_foreign_key 'products', 'disco_app_shops', column: 'shop_id' + add_foreign_key 'widget_configurations', 'disco_app_shops', column: 'shop_id' end diff --git a/test/integration/synchronises_test.rb b/test/integration/synchronises_test.rb index b02a4cf9..15082334 100644 --- a/test/integration/synchronises_test.rb +++ b/test/integration/synchronises_test.rb @@ -1,6 +1,7 @@ require 'test_helper' class SynchronisesTest < ActionDispatch::IntegrationTest + include ActiveJob::TestHelper fixtures :all @@ -57,7 +58,7 @@ def teardown test 'shopify api model still allows synchronisation' do assert_equal({}, @product.data) - shopify_product = ShopifyAPI::Product.new(ActiveSupport::JSON.decode(webhook_fixture('product_updated'))) + shopify_product = ShopifyAPI::Product.new(JSON.parse(webhook_fixture('product_updated'))) Product.synchronise(@shop, shopify_product) # Assert the product was updated locally, with the correct attributes. diff --git a/test/jobs/disco_app/app_installed_job_test.rb b/test/jobs/disco_app/app_installed_job_test.rb index 71b16018..bdd550b0 100644 --- a/test/jobs/disco_app/app_installed_job_test.rb +++ b/test/jobs/disco_app/app_installed_job_test.rb @@ -1,6 +1,7 @@ require 'test_helper' class DiscoApp::AppInstalledJobTest < ActionController::TestCase + include ActiveJob::TestHelper def setup @@ -11,7 +12,7 @@ def setup stub_request(:get, "#{@shop.admin_url}/shop.json").to_return(status: 200, body: api_fixture('widget_store/shop').to_json) stub_request(:get, "#{@shop.admin_url}/carrier_services.json").to_return(status: 200, body: api_fixture('widget_store/carrier_services').to_json) stub_request(:post, "#{@shop.admin_url}/carrier_services.json").to_return(status: 200) - stub_request(:post, "https://api.discolabs.com/v1/app_subscriptions.json").to_return(status: 200) + stub_request(:post, 'https://api.discolabs.com/v1/app_subscriptions.json').to_return(status: 200) end def teardown @@ -20,9 +21,11 @@ def teardown end test 'app installed job performs shop update job' do - # Assert the main install job can be enqueued and performed. - perform_enqueued_jobs do - DiscoApp::AppInstalledJob.perform_later(@shop) + with_suppressed_output do + # Assert the main install job can be enqueued and performed. + perform_enqueued_jobs do + DiscoApp::AppInstalledJob.perform_later(@shop) + end end # Assert the update shop job was performed. @@ -33,8 +36,10 @@ def teardown test 'app installed job automatically subscribes stores to the correct default plan' do @shop.current_subscription.destroy - perform_enqueued_jobs do - DiscoApp::AppInstalledJob.perform_later(@shop) + with_suppressed_output do + perform_enqueued_jobs do + DiscoApp::AppInstalledJob.perform_later(@shop) + end end # Assert the shop was subscribed to the development plan. @@ -44,8 +49,10 @@ def teardown test 'app installed job automatically subscribes stores to the correct default plan with a plan code and a source' do @shop.current_subscription.destroy - perform_enqueued_jobs do - DiscoApp::AppInstalledJob.perform_later(@shop, 'PODCAST', 'smp') + with_suppressed_output do + perform_enqueued_jobs do + DiscoApp::AppInstalledJob.perform_later(@shop, 'PODCAST', 'smp') + end end # Assert the shop was subscribed to the development plan. @@ -54,4 +61,16 @@ def teardown assert_equal 'smpodcast', @shop.current_subscription.source.name end + private + + # Prevents the output from the webhook synchronisation from + # printing to STDOUT and messing up the test output + def with_suppressed_output + original_stdout = $stdout.clone + $stdout.reopen(File.new('/dev/null', 'w')) + yield + ensure + $stdout.reopen(original_stdout) + end + end diff --git a/test/jobs/disco_app/app_uninstalled_job_test.rb b/test/jobs/disco_app/app_uninstalled_job_test.rb index 59f5abbe..997febf6 100644 --- a/test/jobs/disco_app/app_uninstalled_job_test.rb +++ b/test/jobs/disco_app/app_uninstalled_job_test.rb @@ -1,11 +1,12 @@ require 'test_helper' class DiscoApp::AppUninstalledJobTest < ActionController::TestCase + include ActiveJob::TestHelper def setup @shop = disco_app_shops(:widget_store) - stub_request(:post, "https://api.discolabs.com/v1/app_subscriptions.json").to_return(status: 200) + stub_request(:post, 'https://api.discolabs.com/v1/app_subscriptions.json').to_return(status: 200) perform_enqueued_jobs do DiscoApp::AppUninstalledJob.perform_later(@shop, {}) end @@ -25,6 +26,8 @@ def teardown test 'app uninstalled job can be extended using concerns' do assert_performed_jobs 2 @shop.reload - assert_equal 'Nowhere', @shop.data[:country_name] # Assert extended method called. + # Assert extended method called. + assert_equal 'Nowhere', @shop.data[:country_name] end + end diff --git a/test/jobs/disco_app/send_subscription_job_test.rb b/test/jobs/disco_app/send_subscription_job_test.rb index 482fc7e0..2a54f8c4 100644 --- a/test/jobs/disco_app/send_subscription_job_test.rb +++ b/test/jobs/disco_app/send_subscription_job_test.rb @@ -1,11 +1,12 @@ require 'test_helper' class DiscoApp::SendSubscriptionJobTest < ActionController::TestCase + include ActiveJob::TestHelper def setup @shop = disco_app_shops(:widget_store) - stub_request(:post, "https://api.discolabs.com/v1/app_subscriptions.json").to_return(status: 200) + stub_request(:post, 'https://api.discolabs.com/v1/app_subscriptions.json').to_return(status: 200) end def teardown @@ -18,7 +19,7 @@ def teardown perform_enqueued_jobs do DiscoApp::SendSubscriptionJob.perform_later(@shop) end - assert_requested(:post, "https://api.discolabs.com/v1/app_subscriptions.json", times: 1) + assert_requested(:post, 'https://api.discolabs.com/v1/app_subscriptions.json', times: 1) end end diff --git a/test/jobs/disco_app/synchronise_carrier_service_job_test.rb b/test/jobs/disco_app/synchronise_carrier_service_job_test.rb index 4924db54..0a82a0f2 100644 --- a/test/jobs/disco_app/synchronise_carrier_service_job_test.rb +++ b/test/jobs/disco_app/synchronise_carrier_service_job_test.rb @@ -1,6 +1,7 @@ require 'test_helper' class DiscoApp::SynchroniseCarrierServiceJobTest < ActionController::TestCase + include ActiveJob::TestHelper def setup diff --git a/test/jobs/disco_app/synchronise_users_job_test.rb b/test/jobs/disco_app/synchronise_users_job_test.rb index da3bc9e3..028c1a85 100644 --- a/test/jobs/disco_app/synchronise_users_job_test.rb +++ b/test/jobs/disco_app/synchronise_users_job_test.rb @@ -1,6 +1,7 @@ require 'test_helper' class DiscoApp::SynchroniseUsersJobTest < ActionController::TestCase + include ActiveJob::TestHelper def setup diff --git a/test/jobs/disco_app/synchronise_webhooks_job_test.rb b/test/jobs/disco_app/synchronise_webhooks_job_test.rb index d79d81e4..99fe9481 100644 --- a/test/jobs/disco_app/synchronise_webhooks_job_test.rb +++ b/test/jobs/disco_app/synchronise_webhooks_job_test.rb @@ -1,6 +1,7 @@ require 'test_helper' class DiscoApp::SynchroniseWebhooksJobTest < ActionController::TestCase + include ActiveJob::TestHelper def setup @@ -13,30 +14,46 @@ def teardown end test 'webhook synchronisation job creates webhooks for all expected topics' do - stub_request(:get, "#{@shop.admin_url}/webhooks.json").to_return(status: 200, body: api_fixture('widget_store/webhooks').to_json) - stub_request(:post, "#{@shop.admin_url}/webhooks.json").to_return(status: 200) + with_suppressed_output do + stub_request(:get, "#{@shop.admin_url}/webhooks.json").to_return(status: 200, body: api_fixture('widget_store/webhooks').to_json) + stub_request(:post, "#{@shop.admin_url}/webhooks.json").to_return(status: 200) - perform_enqueued_jobs do - DiscoApp::SynchroniseWebhooksJob.perform_later(@shop) - end + perform_enqueued_jobs do + DiscoApp::SynchroniseWebhooksJob.perform_later(@shop) + end - # Assert that all 4 expected webhook topics were POSTed to. - ['app/uninstalled', 'shop/update', 'orders/create', 'orders/paid'].each do |expected_webhook_topic| - assert_requested(:post, "#{@shop.admin_url}/webhooks.json", times: 1) { |request| request.body.include?(expected_webhook_topic) } + # Assert that all 4 expected webhook topics were POSTed to. + ['app/uninstalled', 'shop/update', 'orders/create', 'orders/paid'].each do |expected_webhook_topic| + assert_requested(:post, "#{@shop.admin_url}/webhooks.json", times: 1) { |request| request.body.include?(expected_webhook_topic) } + end end end test 'returns error messages for webhooks that cannot be registered' do VCR.use_cassette('webhook_failure') do - output = capture_io do - perform_enqueued_jobs do - DiscoApp::SynchroniseWebhooksJob.perform_later(@shop) + with_suppressed_output do + output = capture_io do + perform_enqueued_jobs do + DiscoApp::SynchroniseWebhooksJob.perform_later(@shop) + end end - end - assert output.first.include?('Invalid topic specified.') - assert output.first.include?('orders/create - not registered') + assert output.first.include?('Invalid topic specified.') + assert output.first.include?('orders/create - not registered') + end end end + private + + # Prevents the output from the webhook synchronisation from + # printing to STDOUT and messing up the test output + def with_suppressed_output + original_stdout = $stdout.clone + $stdout.reopen(File.new('/dev/null', 'w')) + yield + ensure + $stdout.reopen(original_stdout) + end + end diff --git a/test/models/disco_app/can_be_liquified_test.rb b/test/models/disco_app/can_be_liquified_test.rb index c8656155..6894be9e 100644 --- a/test/models/disco_app/can_be_liquified_test.rb +++ b/test/models/disco_app/can_be_liquified_test.rb @@ -3,6 +3,7 @@ class DiscoApp::CanBeLiquifiedTest < ActiveSupport::TestCase class Model + include ActiveModel::Model include DiscoApp::Concerns::CanBeLiquified @@ -17,6 +18,7 @@ def as_json def liquid_model_name 'model' end + end def setup @@ -48,7 +50,7 @@ def teardown # Return an asset fixture as a string. def liquid_fixture(path) - filename = File.join(File.dirname(File.dirname(File.dirname(__FILE__))), 'fixtures', 'liquid', "#{path}") + filename = File.join(File.dirname(File.dirname(File.dirname(__FILE__))), 'fixtures', 'liquid', path.to_s) File.read(filename).strip end diff --git a/test/models/disco_app/has_metafields_test.rb b/test/models/disco_app/has_metafields_test.rb index dac96758..1f83128d 100644 --- a/test/models/disco_app/has_metafields_test.rb +++ b/test/models/disco_app/has_metafields_test.rb @@ -1,6 +1,7 @@ require 'test_helper' class DiscoApp::HasMetafieldsTest < ActiveSupport::TestCase + include DiscoApp::Test::ShopifyAPI def setup @@ -14,27 +15,45 @@ def teardown end test 'can write metafields with a single namespace' do - stub_api_request(:put, "#{@shop.admin_url}/products/#{@product.id}.json", 'widget_store/products/write_metafields_single_namespace') - assert @shop.with_api_context { @product.write_metafields( - namespace1: { - key1: 'value1', - key2: 2 - } - ) } + stub_api_request( + :put, + "#{@shop.admin_url}/products/#{@product.id}.json", + 'widget_store/products/write_metafields_single_namespace' + ) + + assert( + @shop.with_api_context do + @product.write_metafields( + namespace1: { + key1: 'value1', + key2: 2 + } + ) + end + ) end test 'can write metafields with multiple namespaces' do - stub_api_request(:put, "#{@shop.admin_url}/products/#{@product.id}.json", 'widget_store/products/write_metafields_multiple_namespaces') - assert @shop.with_api_context { @product.write_metafields( - namespace1: { - n1key1: 'value1', - n1key2: 2 - }, - namespace2: { - n2key3: 'value3', - n2key4: 2 - }, - ) } + stub_api_request( + :put, + "#{@shop.admin_url}/products/#{@product.id}.json", + 'widget_store/products/write_metafields_multiple_namespaces' + ) + + assert( + @shop.with_api_context do + @product.write_metafields( + namespace1: { + n1key1: 'value1', + n1key2: 2 + }, + namespace2: { + n2key3: 'value3', + n2key4: 2 + } + ) + end + ) end end diff --git a/test/models/disco_app/renders_assets_test.rb b/test/models/disco_app/renders_assets_test.rb index aebe8bc5..4cddf9ee 100644 --- a/test/models/disco_app/renders_assets_test.rb +++ b/test/models/disco_app/renders_assets_test.rb @@ -1,6 +1,7 @@ require 'test_helper' class DiscoApp::RendersAssetsTest < ActiveSupport::TestCase + include ActiveJob::TestHelper include DiscoApp::Test::ShopifyAPI @@ -102,7 +103,7 @@ def teardown # Return an asset fixture as a string. def asset_fixture(path) - filename = File.join(File.dirname(File.dirname(File.dirname(__FILE__))), 'fixtures', 'assets', "#{path}") + filename = File.join(File.dirname(File.dirname(File.dirname(__FILE__))), 'fixtures', 'assets', path.to_s) File.read(filename) end diff --git a/test/models/disco_app/session_test.rb b/test/models/disco_app/session_test.rb index f3409be9..f1f1527f 100644 --- a/test/models/disco_app/session_test.rb +++ b/test/models/disco_app/session_test.rb @@ -7,8 +7,8 @@ def setup @session = DiscoApp::Session.create( session_id: 'a91bfc51fa79c9d09d43e2615d9345d4', data: { - :shopify => @shop.id, - :shopify_domain => @shop.shopify_domain + shopify: @shop.id, + shopify_domain: @shop.shopify_domain } ) end diff --git a/test/services/disco_app/charges_service_test.rb b/test/services/disco_app/charges_service_test.rb index 4b746f9a..c1248954 100644 --- a/test/services/disco_app/charges_service_test.rb +++ b/test/services/disco_app/charges_service_test.rb @@ -1,6 +1,7 @@ require 'test_helper' class DiscoApp::ChargesServiceTest < ActiveSupport::TestCase + include DiscoApp::Test::ShopifyAPI def setup @@ -30,15 +31,13 @@ def teardown end test 'creating a new charge for a recurring subscription is successful' do - res = { "recurring_application_charge": { "name": "Basic", - "price": "9.99", - "trial_days": 14, - "return_url": /^https:\/\/test\.example\.com\/subscriptions\/304261385\/charges\/53297050(1|2)\/activate$/, - "test": true - } } + res = { "recurring_application_charge": { "name": 'Basic', + "price": '9.99', + "trial_days": 14, + "return_url": %r{^https://test\.example\.com/subscriptions/304261385/charges/53297050(1|2)/activate$}, + "test": true } } stub_request(:post, "#{@shop.admin_url}/recurring_application_charges.json") - .with(body: res - ).to_return(status: 201, body:api_fixture("widget_store/charges/create_recurring_application_charge_response").to_json) + .with(body: res).to_return(status: 201, body: api_fixture('widget_store/charges/create_recurring_application_charge_response').to_json) new_charge = DiscoApp::ChargesService.create(@shop, @subscription) assert_equal 654381179, new_charge.shopify_id diff --git a/test/services/disco_app/flow/process_action_test.rb b/test/services/disco_app/flow/process_action_test.rb index ccb1db97..0bae5ab3 100644 --- a/test/services/disco_app/flow/process_action_test.rb +++ b/test/services/disco_app/flow/process_action_test.rb @@ -24,7 +24,7 @@ def setup action_run_id: 'bdb15e45-4f9d-4c80-88c8-7b43a24edaac-30892-cc8eb62a-14db-43fc-bc33-d6dea41ae623', properties: { 'customer_email' => 'name@example.com' } ) - @now = Time.parse('2018-12-29T00:00:00Z') + @now = Time.zone.parse('2018-12-29T00:00:00Z') Timecop.freeze(@now) end diff --git a/test/services/disco_app/flow/process_trigger_test.rb b/test/services/disco_app/flow/process_trigger_test.rb index 15b25c55..ae9f113e 100644 --- a/test/services/disco_app/flow/process_trigger_test.rb +++ b/test/services/disco_app/flow/process_trigger_test.rb @@ -14,7 +14,7 @@ def setup resource_url: 'https://example.com/test-resource-url', properties: { 'Customer email' => 'name@example.com' } ) - @now = Time.parse('2018-12-29T00:00:00Z') + @now = Time.zone.parse('2018-12-29T00:00:00Z') Timecop.freeze(@now) end diff --git a/test/services/disco_app/subscription_service_test.rb b/test/services/disco_app/subscription_service_test.rb index fca8641d..233f9063 100644 --- a/test/services/disco_app/subscription_service_test.rb +++ b/test/services/disco_app/subscription_service_test.rb @@ -1,6 +1,7 @@ require 'test_helper' class DiscoApp::SubscriptionServiceTest < ActiveSupport::TestCase + include ActiveJob::TestHelper def setup @@ -44,14 +45,14 @@ def teardown test 'new subscription for a plan with a trial period created correctly' do new_subscription = DiscoApp::SubscriptionService.subscribe(@shop, disco_app_plans(:premium)) assert new_subscription.trial? - assert_equal Time.now, new_subscription.trial_start_at + assert_equal Time.zone.now, new_subscription.trial_start_at assert_equal 28.days.from_now, new_subscription.trial_end_at end test 'new subscription for a plan with a plan code created correctly' do new_subscription = DiscoApp::SubscriptionService.subscribe(@shop, disco_app_plans(:premium), 'PODCAST') assert new_subscription.trial? - assert_equal Time.now, new_subscription.trial_start_at + assert_equal Time.zone.now, new_subscription.trial_start_at assert_equal 60.days.from_now, new_subscription.trial_end_at assert_equal 60, new_subscription.trial_period_days assert_equal 8999, new_subscription.amount diff --git a/test/support/test_file_fixtures.rb b/test/support/test_file_fixtures.rb index 3df86566..0994665d 100644 --- a/test/support/test_file_fixtures.rb +++ b/test/support/test_file_fixtures.rb @@ -10,13 +10,13 @@ def xml_fixture(path) # Return a JSON fixture as an indifferent hash. def json_fixture(path) filename = File.join(File.dirname(File.dirname(__FILE__)), 'fixtures', 'json', "#{path}.json") - HashWithIndifferentAccess.new(ActiveSupport::JSON.decode(File.read(filename))) + HashWithIndifferentAccess.new(JSON.parse(File.read(filename))) end # API fixtures are special-case JSON fixtures. def api_fixture(path) filename = File.join(File.dirname(File.dirname(__FILE__)), 'fixtures', 'api', "#{path}.json") - HashWithIndifferentAccess.new(ActiveSupport::JSON.decode(File.read(filename))) + HashWithIndifferentAccess.new(JSON.parse(File.read(filename))) end # Webhook fixtures are special-case JSON fixtures. diff --git a/test/support/test_shopify_api.rb b/test/support/test_shopify_api.rb index dda09221..b9c57fbc 100644 --- a/test/support/test_shopify_api.rb +++ b/test/support/test_shopify_api.rb @@ -5,7 +5,7 @@ def stub_api_request(method, endpoint, fixture_name) if method == :get stub_request(method, endpoint) .to_return(status: 200, body: api_fixture("#{fixture_name}_response").to_json) - elsif method == :post or method == :put + elsif (method == :post) || (method == :put) stub_request(method, endpoint) .with(body: api_fixture("#{fixture_name}_request").to_json) .to_return(status: 201, body: api_fixture("#{fixture_name}_response").to_json) diff --git a/test/test_helper.rb b/test/test_helper.rb index 627bef5f..34316375 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,5 +1,5 @@ # Prevent warnings from showing up during testing. -$VERBOSE=nil +$VERBOSE = nil # Configure Rails Environment ENV['RAILS_ENV'] = 'test' @@ -12,10 +12,10 @@ ENV['SHOPIFY_CHARGES_REAL'] = 'false' ENV['DISCO_API_URL'] = 'https://api.discolabs.com/v1/' -require File.expand_path("../../test/dummy/config/environment.rb", __FILE__) -ActiveRecord::Migrator.migrations_paths = [File.expand_path("../../test/dummy/db/migrate", __FILE__)] -ActiveRecord::Migrator.migrations_paths << File.expand_path('../../db/migrate', __FILE__) -require "rails/test_help" +require File.expand_path('../test/dummy/config/environment.rb', __dir__) +ActiveRecord::Migrator.migrations_paths = [File.expand_path('../test/dummy/db/migrate', __dir__)] +ActiveRecord::Migrator.migrations_paths << File.expand_path('../db/migrate', __dir__) +require 'rails/test_help' # Require our additional test support helpers. require 'support/test_file_fixtures' @@ -33,7 +33,7 @@ # Load fixtures from the engine if ActiveSupport::TestCase.respond_to?(:fixture_path=) - ActiveSupport::TestCase.fixture_path = File.expand_path("../fixtures", __FILE__) + ActiveSupport::TestCase.fixture_path = File.expand_path('fixtures', __dir__) ActiveSupport::TestCase.fixtures :all end @@ -48,7 +48,7 @@ # Minitest helpers to give a better formatted and more helpful output in Rubymine require 'minitest/reporters' require 'minitest/autorun' -MiniTest::Reporters.use! +MiniTest::Reporters.use! Minitest::Reporters::SpecReporter.new # Set up the base test class. class ActiveSupport::TestCase