diff --git a/Rakefile b/Rakefile index 340100b2..0a711fb0 100644 --- a/Rakefile +++ b/Rakefile @@ -19,11 +19,13 @@ namespace :rdoc do intermediate = 'markdown_intermediate' file_to_doc = [ 'lib/shopify-cli/admin_api.rb', + 'lib/shopify-cli/context.rb', 'lib/shopify-cli/db.rb', 'lib/shopify-cli/git.rb', 'lib/shopify-cli/heroku.rb', 'lib/shopify-cli/partners_api.rb', 'lib/shopify-cli/process_supervision.rb', + 'lib/shopify-cli/project.rb', 'lib/shopify-cli/tunnel.rb', ] diff --git a/lib/docgen/index_template.md.erb b/lib/docgen/index_template.md.erb new file mode 100644 index 00000000..b5a6a5c9 --- /dev/null +++ b/lib/docgen/index_template.md.erb @@ -0,0 +1,5 @@ +The Shopify App CLI has a set of core APIs that simplify the process of extending its functionality. + +<%- classes.each do |method| -%> +- [<%= method.title %>](https://github.com/Shopify/shopify-app-cli/wiki/<%= method.filename %>) +<%- end -%> diff --git a/lib/docgen/markdown.rb b/lib/docgen/markdown.rb index af572145..83474435 100644 --- a/lib/docgen/markdown.rb +++ b/lib/docgen/markdown.rb @@ -8,7 +8,7 @@ class Markdown DocClass = Struct.new( :title, :kind, :comment, :class_methods, :instance_methods, :attributes, - :constants, :extended, :included, + :constants, :extended, :included, :filename, keyword_init: true, ) ClassMember = Struct.new(:title, :comment, :signature, :source_code, keyword_init: true) @@ -17,8 +17,6 @@ def initialize(store, options) @options = options @store = store @converter = ::RDoc::Markup::ToMarkdown.new - template_path = File.join(File.dirname(__FILE__), 'class_template.md.erb') - @renderer = ERB.new(File.read(template_path), nil, '-') end def generate @@ -28,20 +26,24 @@ def generate private def render(data) + class_template_path = File.join(File.dirname(__FILE__), 'class_template.md.erb') + class_renderer = ERB.new(File.read(class_template_path), nil, '-') data.each do |cls| - File.write( - "#{cls.kind}-#{cls.title}.md", - @renderer.result(cls.instance_eval { binding }), - ) + File.write("#{cls.filename}.md", class_renderer.result(cls.instance_eval { binding })) end + index_template_path = File.join(File.dirname(__FILE__), 'index_template.md.erb') + index_renderer = ERB.new(File.read(index_template_path), nil, '-') + File.write("Core-APIs.md", index_renderer.result(OpenStruct.new(classes: data).instance_eval { binding })) end def build_classes classes = @store.all_classes_and_modules.map do |klass| is_class = @store.all_modules.find { |m| m.full_name == klass.full_name }.nil? + kind = is_class ? :class : :module DocClass.new( + filename: "#{kind}-#{klass.full_name}", title: klass.full_name, - kind: is_class ? :class : :module, + kind: kind, comment: @converter.convert(klass.comment.parse), constants: build_members(klass.constants), class_methods: build_members(klass.method_list.select { |m| m.type == 'class' }), diff --git a/lib/project_types/node/commands/create.rb b/lib/project_types/node/commands/create.rb index 09d923ca..62440d0a 100644 --- a/lib/project_types/node/commands/create.rb +++ b/lib/project_types/node/commands/create.rb @@ -11,7 +11,9 @@ class Create < ShopifyCli::SubCommand NPM_REQUIRED_NOTICE = "node is required to create an app project. Download at https://www.npmjs.com/get-npm." options do |parser, flags| + # backwards compatibility allow 'title' for now parser.on('--title=TITLE') { |t| flags[:title] = t } + parser.on('--name=NAME') { |t| flags[:title] = t } parser.on('--organization_id=ID') { |url| flags[:organization_id] = url } parser.on('--shop_domain=MYSHOPIFYDOMAIN') { |url| flags[:shop_domain] = url } parser.on('--type=APPTYPE') { |url| flags[:type] = url } @@ -57,9 +59,9 @@ def self.help {{command:#{ShopifyCli::TOOL_NAME} create node}}: Creates an embedded nodejs app. Usage: {{command:#{ShopifyCli::TOOL_NAME} create node}} Options: - {{command:--title=TITLE}} App project title. Any string. - {{command:--app_url=APPURL}} App project URL. Must be valid URL. - {{command:--organization_id=ID}} App project Org ID. Must be existing org ID. + {{command:--name=NAME}} App name. Any string. + {{command:--app_url=APPURL}} App URL. Must be valid URL. + {{command:--organization_id=ID}} App Org ID. Must be existing org ID. {{command:--shop_domain=MYSHOPIFYDOMAIN }} Test store URL. Must be existing test store. HELP end @@ -91,12 +93,12 @@ def build(name) JsDeps.install(@ctx) begin - @ctx.rm_r(File.join(@ctx.root, '.git')) - @ctx.rm_r(File.join(@ctx.root, '.github')) - @ctx.rm(File.join(@ctx.root, 'server', 'handlers', 'client.js')) + @ctx.rm_r('.git') + @ctx.rm_r('.github') + @ctx.rm(File.join('server', 'handlers', 'client.js')) @ctx.rename( - File.join(@ctx.root, 'server', 'handlers', 'client.cli.js'), - File.join(@ctx.root, 'server', 'handlers', 'client.js') + File.join('server', 'handlers', 'client.cli.js'), + File.join('server', 'handlers', 'client.js') ) rescue Errno::ENOENT => e @ctx.debug(e) diff --git a/lib/project_types/node/commands/deploy.rb b/lib/project_types/node/commands/deploy.rb index 063adae2..c6699573 100644 --- a/lib/project_types/node/commands/deploy.rb +++ b/lib/project_types/node/commands/deploy.rb @@ -13,14 +13,14 @@ def call(*) def self.help <<~HELP Deploy the current Node project to a hosting service. Heroku ({{underline:https://www.heroku.com}}) is currently the only option, but more will be added in the future. - Usage: {{command:#{ShopifyCli::TOOL_NAME} deploy [heroku]}} + Usage: {{command:#{ShopifyCli::TOOL_NAME} deploy [ heroku ]}} HELP end def self.extended_help <<~HELP - Subcommands: - * heroku: Deploys the current Rails project to Heroku. + {{bold:Subcommands:}} + {{cyan:heroku}}: Deploys the current Node project to Heroku. Usage: {{command:#{ShopifyCli::TOOL_NAME} deploy heroku}} HELP end diff --git a/lib/project_types/node/commands/deploy/heroku.rb b/lib/project_types/node/commands/deploy/heroku.rb index 08bac577..947c4714 100644 --- a/lib/project_types/node/commands/deploy/heroku.rb +++ b/lib/project_types/node/commands/deploy/heroku.rb @@ -14,7 +14,6 @@ def self.help def call(_args, _name) spin_group = CLI::UI::SpinGroup.new - git_service = ShopifyCli::Git.new(@ctx) heroku_service = ShopifyCli::Heroku.new(@ctx) spin_group.add('Downloading Heroku CLI…') do |spinner| @@ -28,14 +27,13 @@ def call(_args, _name) spinner.update_title('Installed Heroku CLI') end spin_group.add('Checking git repo…') do |spinner| - git_service.init + ShopifyCli::Git.init(@ctx) spinner.update_title('Git repo initialized') end spin_group.wait if (account = heroku_service.whoami) - spin_group.add("Authenticated with Heroku as `#{account}`") { true } - spin_group.wait + @ctx.puts("{{v}} Authenticated with Heroku as `#{account}`") else CLI::UI::Frame.open("Authenticating with Heroku…", success_text: '{{v}} Authenticated with Heroku') do heroku_service.authenticate @@ -43,8 +41,7 @@ def call(_args, _name) end if (app_name = heroku_service.app) - spin_group.add("Heroku app `#{app_name}` selected") { true } - spin_group.wait + @ctx.puts("{{v}} Heroku app `#{app_name}` selected") else app_type = CLI::UI::Prompt.ask('No existing Heroku app found. What would you like to do?') do |handler| handler.option('Create a new Heroku app') { :new } @@ -66,11 +63,10 @@ def call(_args, _name) end end - branches = git_service.branches + branches = ShopifyCli::Git.branches(@ctx) if branches.length == 1 branch_to_deploy = branches[0] - spin_group.add("Git branch `#{branch_to_deploy}` selected for deploy") { true } - spin_group.wait + @ctx.puts("{{v}} Git branch `#{branch_to_deploy}` selected for deploy") else branch_to_deploy = CLI::UI::Prompt.ask('What branch would you like to deploy?') do |handler| branches.each do |branch| diff --git a/lib/project_types/node/commands/generate.rb b/lib/project_types/node/commands/generate.rb index 9c152b81..d5062cee 100644 --- a/lib/project_types/node/commands/generate.rb +++ b/lib/project_types/node/commands/generate.rb @@ -14,20 +14,26 @@ def call(*) def self.help <<~HELP - Generate code in your app project. Supports generating new pages, new billing API calls, or new webhooks. - Usage: {{command:#{ShopifyCli::TOOL_NAME} generate [ page | billing | webhook ]}} + Generate code in your Node project. Supports generating new billing API calls, new pages, or new webhooks. + Usage: {{command:#{ShopifyCli::TOOL_NAME} generate [ billing | page | webhook ]}} HELP end def self.extended_help - <<~HELP - {{bold:Subcommands:}} - {{cyan:webhook}}: Generate and register a new webhook that listens for the specified Shopify store event. - Usage: {{command:#{ShopifyCli::TOOL_NAME} generate webhook [type]}} + extended_help = "{{bold:Subcommands:}}\n" + subcommand_registry.resolved_commands.sort.each do |name, klass| + extended_help += " {{cyan:#{name}}}: " + + if (subcmd_help = klass.help) + extended_help += subcmd_help.gsub("\n ", "\n ") + end + extended_help += "\n" + end + extended_help += <<~EXAMPLES {{bold:Examples:}} {{cyan:#{ShopifyCli::TOOL_NAME} generate webhook PRODUCTS_CREATE}} Generate and register a new webhook that will be called every time a new product is created on your store. - HELP + EXAMPLES end def self.run_generate(script, name, ctx) diff --git a/lib/project_types/node/commands/generate/billing.rb b/lib/project_types/node/commands/generate/billing.rb index dccc2ddc..2d75ab5a 100644 --- a/lib/project_types/node/commands/generate/billing.rb +++ b/lib/project_types/node/commands/generate/billing.rb @@ -33,7 +33,7 @@ def call(args, _name) def self.help <<~HELP Enable charging for your app. This command generates the necessary code to call Shopify’s billing API. - Usage: {{command:#{ShopifyCli::TOOL_NAME} generate billing recurring-billing | one-time-billing}} + Usage: {{command:#{ShopifyCli::TOOL_NAME} generate billing [ one-time-billing | recurring-billing ]}} HELP end end diff --git a/lib/project_types/node/commands/populate.rb b/lib/project_types/node/commands/populate.rb index fbeb92d8..b5035b11 100644 --- a/lib/project_types/node/commands/populate.rb +++ b/lib/project_types/node/commands/populate.rb @@ -3,9 +3,9 @@ module Node module Commands class Populate < ShopifyCli::Command - subcommand :Product, 'products', Project.project_filepath('commands/populate/product') subcommand :Customer, 'customers', Project.project_filepath('commands/populate/customer') subcommand :DraftOrder, 'draftorders', Project.project_filepath('commands/populate/draft_order') + subcommand :Product, 'products', Project.project_filepath('commands/populate/product') def call(_args, _name) @ctx.puts(self.class.help) @@ -13,8 +13,8 @@ def call(_args, _name) def self.help <<~HELP - Populate your Shopify development store with example products, customers, or orders. - Usage: {{command:#{ShopifyCli::TOOL_NAME} populate [ products | customers | draftorders ]}} + Populate your Shopify development store with example customers, orders, or products. + Usage: {{command:#{ShopifyCli::TOOL_NAME} populate [ customers | draftorders | products ]}} HELP end @@ -22,15 +22,15 @@ def self.extended_help <<~HELP {{bold:Subcommands:}} - {{cyan:products [options]}}: Add dummy products to the specified development store. - Usage: {{command:#{ShopifyCli::TOOL_NAME} populate products}} - {{cyan:customers [options]}}: Add dummy customers to the specified development store. Usage: {{command:#{ShopifyCli::TOOL_NAME} populate customers}} {{cyan:draftorders [options]}}: Add dummy orders to the specified development store. Usage: {{command:#{ShopifyCli::TOOL_NAME} populate draftorders}} + {{cyan:products [options]}}: Add dummy products to the specified development store. + Usage: {{command:#{ShopifyCli::TOOL_NAME} populate products}} + {{bold:Options:}} {{cyan:--count [integer]}}: The number of dummy items to populate. Defaults to 5. diff --git a/lib/project_types/node/commands/serve.rb b/lib/project_types/node/commands/serve.rb index 65c3b3ed..b5a1e3bf 100644 --- a/lib/project_types/node/commands/serve.rb +++ b/lib/project_types/node/commands/serve.rb @@ -13,6 +13,7 @@ class Serve < ShopifyCli::Command def call(*) project = ShopifyCli::Project.current url = options.flags[:host] || ShopifyCli::Tunnel.start(@ctx) + @ctx.abort("{{red:HOST must be a HTTPS url.}}") if url.match(/^https/i).nil? project.env.update(@ctx, :host, url) ShopifyCli::Tasks::UpdateDashboardURLS.call( @ctx, @@ -42,7 +43,7 @@ def self.help def self.extended_help <<~HELP {{bold:Options:}} - {{cyan:--host=HOST}}: Must be HTTPS url. Bypass running tunnel and use custom host. + {{cyan:--host=HOST}}: Bypass running tunnel and use custom host. HOST must be HTTPS url. HELP end end diff --git a/lib/project_types/node/commands/tunnel.rb b/lib/project_types/node/commands/tunnel.rb index 13fde517..b240060e 100644 --- a/lib/project_types/node/commands/tunnel.rb +++ b/lib/project_types/node/commands/tunnel.rb @@ -12,7 +12,12 @@ def call(args, _name) case subcommand when 'auth' token = args.shift - ShopifyCli::Tunnel.auth(@ctx, token) + if token.nil? + @ctx.puts("{{x}} {{red:auth requires a token argument}}\n\n") + @ctx.puts("#{self.class.help}\n#{self.class.extended_help}") + else + ShopifyCli::Tunnel.auth(@ctx, token) + end when 'start' ShopifyCli::Tunnel.start(@ctx) when 'stop' @@ -33,7 +38,7 @@ def self.extended_help <<~HELP {{bold:Subcommands:}} - {{cyan:auth}}: Writes an ngrok auth token to ~/.ngrok2/ngrok.yml to allow connecting with an ngrok account. Visit https://dashboard.ngrok.com/signup to sign up. + {{cyan:auth}}: Writes an ngrok auth token to ~/.ngrok2/ngrok.yml to connect with an ngrok account. Visit https://dashboard.ngrok.com/signup to sign up. Usage: {{command:#{ShopifyCli::TOOL_NAME} tunnel auth }} {{cyan:start}}: Starts an ngrok tunnel, will print the URL for an existing tunnel if already running. diff --git a/lib/project_types/rails/commands/create.rb b/lib/project_types/rails/commands/create.rb index 25de0580..6142ce17 100644 --- a/lib/project_types/rails/commands/create.rb +++ b/lib/project_types/rails/commands/create.rb @@ -17,7 +17,9 @@ class Base < ActiveResource::Base MSG options do |parser, flags| + # backwards compatibility allow 'title' for now parser.on('--title=TITLE') { |t| flags[:title] = t } + parser.on('--name=NAME') { |t| flags[:title] = t } parser.on('--organization_id=ID') { |url| flags[:organization_id] = url } parser.on('--shop_domain=MYSHOPIFYDOMAIN') { |url| flags[:shop_domain] = url } parser.on('--type=APPTYPE') { |url| flags[:type] = url } @@ -65,9 +67,9 @@ def self.help {{command:#{ShopifyCli::TOOL_NAME} create rails}}: Creates a ruby on rails app. Usage: {{command:#{ShopifyCli::TOOL_NAME} create rails}} Options: - {{command:--title=TITLE}} App project title. Any string. - {{command:--app_url=APPURL}} App project URL. Must be valid URL. - {{command:--organization_id=ID}} App project Org ID. Must be existing org ID. + {{command:--name=NAME}} App name. Any string. + {{command:--app_url=APPURL}} App URL. Must be valid URL. + {{command:--organization_id=ID}} App Org ID. Must be existing org ID. {{command:--shop_domain=MYSHOPIFYDOMAIN }} Test store URL. Must be existing test store. HELP end diff --git a/lib/project_types/rails/commands/deploy.rb b/lib/project_types/rails/commands/deploy.rb index b673d286..ee22d154 100644 --- a/lib/project_types/rails/commands/deploy.rb +++ b/lib/project_types/rails/commands/deploy.rb @@ -13,14 +13,14 @@ def call(*) def self.help <<~HELP Deploy the current Rails project to a hosting service. Heroku ({{underline:https://www.heroku.com}}) is currently the only option, but more will be added in the future. - Usage: {{command:#{ShopifyCli::TOOL_NAME} deploy [heroku]}} + Usage: {{command:#{ShopifyCli::TOOL_NAME} deploy [ heroku ]}} HELP end def self.extended_help <<~HELP - Subcommands: - * heroku: Deploys the current Rails project to Heroku. + {{bold:Subcommands:}} + {{cyan:heroku}}: Deploys the current Rails project to Heroku. Usage: {{command:#{ShopifyCli::TOOL_NAME} deploy heroku}} HELP end diff --git a/lib/project_types/rails/commands/deploy/heroku.rb b/lib/project_types/rails/commands/deploy/heroku.rb index 92f8993f..97828fbd 100644 --- a/lib/project_types/rails/commands/deploy/heroku.rb +++ b/lib/project_types/rails/commands/deploy/heroku.rb @@ -14,7 +14,6 @@ def self.help def call(_args, _name) spin_group = CLI::UI::SpinGroup.new - git_service = ShopifyCli::Git.new(@ctx) heroku_service = ShopifyCli::Heroku.new(@ctx) spin_group.add('Downloading Heroku CLI…') do |spinner| @@ -28,14 +27,13 @@ def call(_args, _name) spinner.update_title('Installed Heroku CLI') end spin_group.add('Checking git repo…') do |spinner| - git_service.init + ShopifyCli::Git.init(@ctx) spinner.update_title('Git repo initialized') end spin_group.wait if (account = heroku_service.whoami) - spin_group.add("Authenticated with Heroku as `#{account}`") { true } - spin_group.wait + @ctx.puts("{{v}} Authenticated with Heroku as `#{account}`") else CLI::UI::Frame.open("Authenticating with Heroku…", success_text: '{{v}} Authenticated with Heroku') do heroku_service.authenticate @@ -43,8 +41,7 @@ def call(_args, _name) end if (app_name = heroku_service.app) - spin_group.add("Heroku app `#{app_name}` selected") { true } - spin_group.wait + @ctx.puts("{{v}} Heroku app `#{app_name}` selected") else app_type = CLI::UI::Prompt.ask('No existing Heroku app found. What would you like to do?') do |handler| handler.option('Create a new Heroku app') { :new } @@ -66,11 +63,10 @@ def call(_args, _name) end end - branches = git_service.branches + branches = ShopifyCli::Git.branches(@ctx) if branches.length == 1 branch_to_deploy = branches[0] - spin_group.add("Git branch `#{branch_to_deploy}` selected for deploy") { true } - spin_group.wait + @ctx.puts("{{v}} Git branch `#{branch_to_deploy}` selected for deploy") else branch_to_deploy = CLI::UI::Prompt.ask('What branch would you like to deploy?') do |handler| branches.each do |branch| diff --git a/lib/project_types/rails/commands/generate.rb b/lib/project_types/rails/commands/generate.rb index 000aa744..0033723c 100644 --- a/lib/project_types/rails/commands/generate.rb +++ b/lib/project_types/rails/commands/generate.rb @@ -12,20 +12,26 @@ def call(*) def self.help <<~HELP - Generate code in your app project. Currently supports generating new webhooks. - Usage: {{command:#{ShopifyCli::TOOL_NAME} generate webhook}} + Generate code in your Rails project. Currently supports generating new webhooks. + Usage: {{command:#{ShopifyCli::TOOL_NAME} generate [ webhook ]}} HELP end def self.extended_help - <<~HELP - {{bold:Subcommands:}} - {{cyan:webhook}}: Generate and register a new webhook that listens for the specified Shopify store event. - Usage: {{command:#{ShopifyCli::TOOL_NAME} generate webhook [type]}} + extended_help = "{{bold:Subcommands:}}\n" + subcommand_registry.resolved_commands.sort.each do |name, klass| + extended_help += " {{cyan:#{name}}}: " + + if (subcmd_help = klass.help) + extended_help += subcmd_help.gsub("\n ", "\n ") + end + extended_help += "\n" + end + extended_help += <<~EXAMPLES {{bold:Examples:}} {{cyan:#{ShopifyCli::TOOL_NAME} generate webhook PRODUCTS_CREATE}} Generate and register a new webhook that will be called every time a new product is created on your store. - HELP + EXAMPLES end def self.run_generate(script, name, ctx) diff --git a/lib/project_types/rails/commands/populate.rb b/lib/project_types/rails/commands/populate.rb index caf78a4f..8a9d6b27 100644 --- a/lib/project_types/rails/commands/populate.rb +++ b/lib/project_types/rails/commands/populate.rb @@ -13,8 +13,8 @@ def call(_args, _name) def self.help <<~HELP - Populate your Shopify development store with example products, customers, or orders. - Usage: {{command:#{ShopifyCli::TOOL_NAME} populate [ products | customers | draftorders ]}} + Populate your Shopify development store with example customers, orders, or products. + Usage: {{command:#{ShopifyCli::TOOL_NAME} populate [ customers | draftorders | products ]}} HELP end @@ -22,15 +22,15 @@ def self.extended_help <<~HELP {{bold:Subcommands:}} - {{cyan:products [options]}}: Add dummy products to the specified development store. - Usage: {{command:#{ShopifyCli::TOOL_NAME} populate products}} - {{cyan:customers [options]}}: Add dummy customers to the specified development store. Usage: {{command:#{ShopifyCli::TOOL_NAME} populate customers}} {{cyan:draftorders [options]}}: Add dummy orders to the specified development store. Usage: {{command:#{ShopifyCli::TOOL_NAME} populate draftorders}} + {{cyan:products [options]}}: Add dummy products to the specified development store. + Usage: {{command:#{ShopifyCli::TOOL_NAME} populate products}} + {{bold:Options:}} {{cyan:--count [integer]}}: The number of dummy items to populate. Defaults to 5. diff --git a/lib/project_types/rails/commands/serve.rb b/lib/project_types/rails/commands/serve.rb index 4fff23c4..67406b69 100644 --- a/lib/project_types/rails/commands/serve.rb +++ b/lib/project_types/rails/commands/serve.rb @@ -13,6 +13,7 @@ class Serve < ShopifyCli::Command def call(*) project = ShopifyCli::Project.current url = options.flags[:host] || ShopifyCli::Tunnel.start(@ctx) + @ctx.abort("{{red:HOST must be a HTTPS url.}}") if url.match(/^https/i).nil? project.env.update(@ctx, :host, url) ShopifyCli::Tasks::UpdateDashboardURLS.call( @ctx, @@ -44,7 +45,7 @@ def self.help def self.extended_help <<~HELP {{bold:Options:}} - {{cyan:--host=HOST}}: Must be HTTPS url. Bypass running tunnel and use custom host. + {{cyan:--host=HOST}}: Bypass running tunnel and use custom host. HOST must be HTTPS url. HELP end end diff --git a/lib/project_types/rails/commands/tunnel.rb b/lib/project_types/rails/commands/tunnel.rb index 4857a8cb..440e4f1e 100644 --- a/lib/project_types/rails/commands/tunnel.rb +++ b/lib/project_types/rails/commands/tunnel.rb @@ -12,7 +12,12 @@ def call(args, _name) case subcommand when 'auth' token = args.shift - ShopifyCli::Tunnel.auth(@ctx, token) + if token.nil? + @ctx.puts("{{x}} {{red:auth requires a token argument}}\n\n") + @ctx.puts("#{self.class.help}\n#{self.class.extended_help}") + else + ShopifyCli::Tunnel.auth(@ctx, token) + end when 'start' ShopifyCli::Tunnel.start(@ctx) when 'stop' @@ -33,7 +38,7 @@ def self.extended_help <<~HELP {{bold:Subcommands:}} - {{cyan:auth}}: Writes an ngrok auth token to ~/.ngrok2/ngrok.yml to allow connecting with an ngrok account. Visit https://dashboard.ngrok.com/signup to sign up. + {{cyan:auth}}: Writes an ngrok auth token to ~/.ngrok2/ngrok.yml to connect with an ngrok account. Visit https://dashboard.ngrok.com/signup to sign up. Usage: {{command:#{ShopifyCli::TOOL_NAME} tunnel auth }} {{cyan:start}}: Starts an ngrok tunnel, will print the URL for an existing tunnel if already running. diff --git a/lib/shopify-cli/admin_api.rb b/lib/shopify-cli/admin_api.rb index 2993068d..74e21ab9 100644 --- a/lib/shopify-cli/admin_api.rb +++ b/lib/shopify-cli/admin_api.rb @@ -1,52 +1,99 @@ require 'shopify_cli' module ShopifyCli + ## + # ShopifyCli::AdminAPI wraps our graphql functionality with authentication so that + # these concerns are taken care of. + # class AdminAPI < API autoload :PopulateResourceCommand, 'shopify-cli/admin_api/populate_resource_command' autoload :Schema, 'shopify-cli/admin_api/schema' class << self - def query(ctx, body, api_version: nil, shop: nil, **variables) - @shop = shop - authenticated_req(ctx) do - api_client(ctx, api_version).query(body, variables: variables) + ## + # issues a graphql query or mutation to the Shopify Admin API. It loads a graphql + # query from a file so that you do not need to use large unwieldy query strings. + # + # #### Parameters + # - `ctx`: running context from your command + # - `query_name`: name of the query you want to use, loaded from the `lib/graphql` directory. + # - `api_version`: an api version string to specify version. If no version is supplied then unstable will be used + # - `shop`: shop domain string for which shop that you are calling the admin + # API on. If not supplied, then it will be fetched from the `.env` file + # - `**variable`: a hash of variables to be supplied to the query ro mutation + # + # #### Raises + # + # * http 404 will raise a ShopifyCli::API::APIRequestNotFoundError + # * http 400..499 will raise a ShopifyCli::API::APIRequestClientError + # * http 500..599 will raise a ShopifyCli::API::APIRequestServerError + # * All other codes will raise ShopifyCli::API::APIRequestUnexpectedError + # + # #### Returns + # + # * `resp` - graphql response data hash. This can be a different shape for every query. + # + # #### Example + # + # ShopifyCli::AdminAPI.query(@ctx, 'all_organizations') + # + def query(ctx, query_name, api_version: nil, shop: nil, **variables) + shop ||= Project.current.env.shop + authenticated_req(ctx, shop) do + api_client(ctx, api_version, shop).query(query_name, variables: variables) end end private - def authenticated_req(ctx) + def authenticated_req(ctx, shop) yield rescue API::APIRequestUnauthorizedError - Tasks::AuthenticateShopify.call(ctx, shop: @shop) + authenticate(ctx, shop) retry end - def api_client(ctx, api_version) + def authenticate(ctx, shop) + env = Project.current.env + ShopifyCli::OAuth.new( + ctx: ctx, + service: 'admin', + client_id: env.api_key, + secret: env.secret, + scopes: env.scopes, + token_path: "/access_token", + options: { 'grant_options[]' => 'per user' }, + ).authenticate("https://#{shop}/admin/oauth") + end + + def api_client(ctx, api_version, shop) new( ctx: ctx, auth_header: 'X-Shopify-Access-Token', - token: Resources::Tokens.admin(ctx), - url: "#{endpoint}/#{fetch_api_version(ctx, api_version)}/graphql.json", + token: admin_access_token(ctx, shop), + url: "https://#{shop}/admin/api/#{fetch_api_version(ctx, api_version, shop)}/graphql.json", ) end - def fetch_api_version(ctx, api_version) + def admin_access_token(ctx, shop) + ShopifyCli::DB.get(:admin_access_token) do + authenticate(ctx, shop) + ShopifyCli::DB.get(:admin_access_token) + end + end + + def fetch_api_version(ctx, api_version, shop) return api_version unless api_version.nil? client = new( ctx: ctx, auth_header: 'X-Shopify-Access-Token', - token: Resources::Tokens.admin(ctx), - url: "#{endpoint}/unstable/graphql.json", + token: admin_access_token(ctx, shop), + url: "https://#{shop}/admin/api/unstable/graphql.json", ) versions = client.query('api_versions')['data']['publicApiVersions'] latest = versions.find { |version| version['displayName'].include?('Latest') } latest['handle'] end - - def endpoint - "https://#{@shop || Project.current.env.shop}/admin/api" - end end end end diff --git a/lib/shopify-cli/api.rb b/lib/shopify-cli/api.rb index 5f05068f..4847ccce 100644 --- a/lib/shopify-cli/api.rb +++ b/lib/shopify-cli/api.rb @@ -82,7 +82,7 @@ def current_sha def default_headers { - 'User-Agent' => "Shopify App CLI #{ShopifyCli::VERSION} #{current_sha} | #{ctx.uname(flag: 'v')}", + 'User-Agent' => "Shopify App CLI #{ShopifyCli::VERSION} #{current_sha} | #{ctx.uname}", }.merge(auth_headers(token)) end diff --git a/lib/shopify-cli/commands/connect.rb b/lib/shopify-cli/commands/connect.rb index e1fa0fb7..67300b02 100644 --- a/lib/shopify-cli/commands/connect.rb +++ b/lib/shopify-cli/commands/connect.rb @@ -42,7 +42,7 @@ def get_app(apps) def get_shop(shops, id) if shops.count == 1 - shops.first + shop = shops.first["shopDomain"] elsif shops.count == 0 @ctx.puts('No development stores available.') @ctx.puts("Visit {{underline:https://partners.shopify.com/#{id}/stores}} to create one") @@ -65,17 +65,10 @@ def write_env(app, shop) def self.help <<~HELP - Connect a Shopify-App-Cli project. Restores the ENV file + Connect a Shopify App CLI project. Restores the ENV file. Usage: {{command:#{ShopifyCli::TOOL_NAME} connect}} HELP end - - def self.extended_help - <<~HELP - Connect a Shopify-App-Cli project. Restores the Env file - Usage: {{command:#{ShopifyCli::TOOL_NAME} connect}} - HELP - end end end end diff --git a/lib/shopify-cli/commands/help.rb b/lib/shopify-cli/commands/help.rb index c417c11d..495fa215 100644 --- a/lib/shopify-cli/commands/help.rb +++ b/lib/shopify-cli/commands/help.rb @@ -20,11 +20,14 @@ def call(args, _name) end end - # a line break before output aids scanning/readability - @ctx.puts("") - @ctx.puts('{{bold:Available commands}}') - @ctx.puts('Use {{command:shopify help [command]}} to display detailed information about a specific command.') - @ctx.puts("") + preamble = <<~MESSAGE + CLI to help build Shopify apps faster. + + Use {{command:#{ShopifyCli::TOOL_NAME} help }} to display detailed information about a specific command. + + {{bold:Available commands}} + MESSAGE + @ctx.puts(preamble) visible_commands = ShopifyCli::Commands::Registry .resolved_commands @@ -33,11 +36,7 @@ def call(args, _name) visible_commands.each do |name, klass| next if name == 'help' - @ctx.puts("{{command:#{ShopifyCli::TOOL_NAME} #{name}}}") - if (help = klass.help) - @ctx.puts(help) - end - @ctx.puts("") + @ctx.puts("{{command:#{name}}}: #{klass.help}\n") end end diff --git a/lib/shopify-cli/context.rb b/lib/shopify-cli/context.rb index d9e8f794..80702589 100644 --- a/lib/shopify-cli/context.rb +++ b/lib/shopify-cli/context.rb @@ -1,37 +1,153 @@ # frozen_string_literal: true require 'shopify_cli' +require 'fileutils' module ShopifyCli + ## + # Context captures a lot about the current running command. It captures the + # environment, output, system and file operations. It is useful to have the + # context especially in tests so that you have a single access point to these + # resoures. + # class Context - autoload :FileSystem, 'shopify-cli/context/file_system' - autoload :Output, 'shopify-cli/context/output' - autoload :System, 'shopify-cli/context/system' + # is the directory root that the current command is running in. If you want to + # simulate a `cd` for the file operations, you can change this variable. + attr_accessor :root + # is an accessor for environment variables. These variables are also added to + # any command run by the context. + attr_accessor :env - include SmartProperties - include FileSystem - include Output - include System + def initialize(root: Dir.pwd, env: ($original_env || ENV).clone) # :nodoc: + self.root = root + self.env = env + end + + # will return which operating system that the cli is running on [:mac, :linux] + def os + host = uname + return :mac if /darwin/.match(host) + return :linux if /linux/.match(host) + end - property :root, default: lambda { Dir.pwd }, converts: :to_s - property :env, default: lambda { ($original_env || ENV).clone } + # will return true if the cli is running on an apple computer. + def mac? + os == :mac + end + + # will return true if the cli is running on a linux distro + def linux? + os == :linux + end + # will return true if the cli is being run from an installation, and not a + # development instance. See `#development?` for checking for development environment. + # + def system? + ShopifyCli::INSTALL_DIR == ShopifyCli::ROOT + end + + # will return true if the cli is running on your development instance. + # + def development? + !system? && !testing? + end + + # will return true while tests are running, either locally or on CI + def testing? + ci? || ENV['TEST'] + end + + ## + # will return true if the cli is being tested on CI + def ci? + ENV['CI'] + end + + # get a environment variable value by name. + # + # #### Parameters + # * `name` - the name of the environment variable that you want to fetch + # + # #### Returns + # * `value` - will return the value, or nil if the variable does not exist + # def getenv(name) v = @env[name] v == '' ? nil : v end + # set a environment variable value by name. + # + # #### Parameters + # * `key` - the name of the environment variable that you want to set + # * `value` - the value of the variable + # def setenv(key, value) @env[key] = value end - def app_metadata - @app_metadata ||= {} + # will write/overwrite a file with the provided contents, relative to the context root + # + # #### Parameters + # * `fname` - filename of the file that you are writing, relative to root. + # * `content` - the body contents of the file that you are writing + # + # #### Example + # + # @ctx.write('new.txt', 'hello world') + # + def write(fname, content) + File.write(File.join(root, fname), content) + end + + # will rename a file from one place to another, all relative to the command root + # + # #### Parameters + # * `from` - the path of the original file + # * `to` - the destination path + # + def rename(from, to) + File.rename(File.join(root, from), File.join(root, to)) end - def app_metadata=(hash) - @app_metadata = app_metadata.merge(hash) + # will remove a plain file from the FS, the filepath is relative to the command + # root. + # + # #### Parameters + # * `fname` - the file path relative to the context root to remove from the FS + # + def rm(fname) + FileUtils.rm(File.join(root, fname)) end + # will remove a directory from the FS, the filepath is relative to the command + # root. + # + # #### Parameters + # * `fname` - the file path to a directory, relative to the context root to remove from the FS + # + def rm_r(fname) + FileUtils.rm_r(File.join(root, fname)) + end + + # will create a directory, recursively if it does not exist. So if you create + # a directory `foo/bar/dun`, this will also create the directories `foo` and + # `foo/bar` if they do not exist. + # + # #### Parameters + # * `path` - file path of the directory that you want to create + # + def mkdir_p(path) + FileUtils.mkdir_p(path) + end + + # will open a url in a browser if that functionality is available, otherwise + # it will simply output to the console a link for the user to either copy/paste + # or click on. + # + # #### Parameters + # * `uri` - a http URI to open in a browser + # def open_url!(uri) return system("open '#{uri}'") if mac? help = <<~OPEN @@ -39,5 +155,166 @@ def open_url!(uri) OPEN puts(help) end + + # will output a message, prefixed by a yellow star, indicating that task + # started. + # + # #### Parameters + # * `text` - a string message to output + # + def print_task(text) + puts "{{yellow:*}} #{text}" + end + + # a wrapper around Kernel.puts to allow for easy formatting + # + # #### Parameters + # * `text` - a string message to output + # + def puts(*args) + Kernel.puts(CLI::UI.fmt(*args)) + end + + # outputs a message, prefixed by a checkmark indicating that something completed + # + # #### Parameters + # * `text` - a string message to output + # + def done(text) + puts("{{v}} #{text}") + end + + # aborts the current running command and outputs an error message, prefixed + # by a red x + # + # #### Parameters + # * `text` - a string message to output + # + def abort(text) + raise ShopifyCli::Abort, "{{x}} #{text}" + end + + # outputs a message, prefixed by a red `DEBUG` tag. This will only output to + # the console if you have `DEBUG=1` set in your shell environment. + # + # #### Parameters + # * `text` - a string message to output + # + def debug(text) + puts("{{red:DEBUG}} #{text}") if getenv('DEBUG') + end + + # will grab the host info of the computer running the cli. This indicates the + # computer architecture and operating system + def uname + @uname ||= RbConfig::CONFIG["host"] + end + + # Execute a command in the user's environment + # Outputs result of the command without capturing it + # + # #### Parameters + # - `*args`: A splat of arguments evaluated as a command. (e.g. `'rm', folder` is equivalent to `rm #{folder}`) + # - `**kwargs`: additional keyword arguments to pass to Process.spawn + # + # #### Returns + # - `status`: boolean success status of the command execution + # + # #### Usage + # + # stat = @ctx.system('ls', 'a_folder') + # + def system(*args, **kwargs) + CLI::Kit::System.system(*args, env: @env, **kwargs) + end + + # Execute a command in the user's environment + # This is meant to be largely equivalent to backticks, only with the env passed in. + # Captures the results of the command without output to the console + # + # #### Parameters + # - `*args`: A splat of arguments evaluated as a command. (e.g. `'rm', folder` is equivalent to `rm #{folder}`) + # - `**kwargs`: additional arguments to pass to Open3.capture2 + # + # #### Returns + # - `output`: output (STDOUT) of the command execution + # - `status`: boolean success status of the command execution + # + # #### Usage + # + # out, stat = @ctx.capture2('ls', 'a_folder') + # + def capture2(*args, **kwargs) + CLI::Kit::System.capture2(*args, env: @env, **kwargs) + end + + # Execute a command in the user's environment + # This is meant to be largely equivalent to backticks, only with the env passed in. + # Captures the results of the command without output to the console + # + # #### Parameters + # - `*args`: A splat of arguments evaluated as a command. (e.g. `'rm', folder` is equivalent to `rm #{folder}`) + # - `**kwargs`: additional arguments to pass to Open3.capture2e + # + # #### Returns + # - `output`: output (STDOUT merged with STDERR) of the command execution + # - `status`: boolean success status of the command execution + # + # #### Usage + # + # out_and_err, stat = @ctx.capture2e('ls', 'a_folder') + # + def capture2e(*args, **kwargs) + CLI::Kit::System.capture2e(*args, env: @env, **kwargs) + end + + # Execute a command in the user's environment + # This is meant to be largely equivalent to backticks, only with the env passed in. + # Captures the results of the command without output to the console + # + # #### Parameters + # - `*args`: A splat of arguments evaluated as a command. (e.g. `'rm', folder` is equivalent to `rm #{folder}`) + # - `**kwargs`: additional arguments to pass to Open3.capture3 + # + # #### Returns + # - `output`: STDOUT of the command execution + # - `error`: STDERR of the command execution + # - `status`: boolean success status of the command execution + # + # #### Usage + # + # out, err, stat = @ctx.capture3('ls', 'a_folder') + # + def capture3(*args, **kwargs) + CLI::Kit::System.capture3(*args, env: @env, **kwargs) + end + + # captures the info signal (ctrl-T) and provide a handler to it. + # + # #### Example + # + # @ctx.on_siginfo do + # @ctx.open_url!("http://google.com") + # end + # + def on_siginfo + fork do + begin + r, w = IO.pipe + @signal = false + trap('SIGINFO') do + @signal = true + w.write(0) + end + while r.read(1) + next unless @signal + @signal = false + yield + end + rescue Interrupt + exit(0) + end + end + end end end diff --git a/lib/shopify-cli/context/file_system.rb b/lib/shopify-cli/context/file_system.rb deleted file mode 100644 index 4dc0729d..00000000 --- a/lib/shopify-cli/context/file_system.rb +++ /dev/null @@ -1,27 +0,0 @@ -require 'fileutils' - -module ShopifyCli - class Context - module FileSystem - def write(fname, content) - File.write(File.join(root, fname), content) - end - - def rename(*args) - File.rename(*args) - end - - def rm(*args) - FileUtils.rm(*args) - end - - def rm_r(*args) - FileUtils.rm_r(*args) - end - - def mkdir_p(*args) - FileUtils.mkdir_p(*args) - end - end - end -end diff --git a/lib/shopify-cli/context/output.rb b/lib/shopify-cli/context/output.rb deleted file mode 100644 index 76b22f54..00000000 --- a/lib/shopify-cli/context/output.rb +++ /dev/null @@ -1,35 +0,0 @@ -module ShopifyCli - class Context - module Output - def print_task(text) - puts "{{yellow:*}} #{text}" - end - - def puts(*args) - Kernel.puts(CLI::UI.fmt(*args)) - end - - def done(string) - puts("{{v}} #{string}") - end - - def abort(string) - raise ShopifyCli::Abort, "{{x}} #{string}" - end - - def debug(string) - puts("{{red:DEBUG}} #{string}") if getenv('DEBUG') - end - - def page(output) - if output.split("\n").size > CLI::UI::Terminal.height - IO.popen(getenv('PAGER') || 'less', "w") do |pipe| - pipe.puts CLI::UI.fmt(output) - end - else - puts(output) - end - end - end - end -end diff --git a/lib/shopify-cli/context/system.rb b/lib/shopify-cli/context/system.rb index 7fcec371..8f6cbae0 100644 --- a/lib/shopify-cli/context/system.rb +++ b/lib/shopify-cli/context/system.rb @@ -11,16 +11,17 @@ module System } def os - return :mac if mac? - return :linux if linux? + host = uname + return :mac if /darwin/.match(host) + return :linux if /linux/.match(host) end def mac? - /Darwin/.match(uname) + os == :mac end def linux? - /Linux/.match(uname) + os == :linux end def system? @@ -39,8 +40,8 @@ def ci? ENV['CI'] end - def uname(flag: 'a') - @uname ||= capture2("uname -#{flag}")[0].strip + def uname + @uname ||= RbConfig::CONFIG["host"] end def spawn(*args, **kwargs) diff --git a/lib/shopify-cli/core/monorail.rb b/lib/shopify-cli/core/monorail.rb index b960acda..62bc302f 100644 --- a/lib/shopify-cli/core/monorail.rb +++ b/lib/shopify-cli/core/monorail.rb @@ -73,7 +73,7 @@ def prompt_for_consent def monorail_payload(args:, duration:, result:) { cli_sha: ShopifyCli::Git.sha(dir: ShopifyCli::ROOT), - uname: uname, + uname: RbConfig::CONFIG["host"], args: args, timestamp: Time.now.utc.iso8601(MICROSECOND_PRECISION), duration: duration, @@ -81,10 +81,6 @@ def monorail_payload(args:, duration:, result:) } end - def uname - @uname ||= %x{uname -a} - end - def ruby_version @ruby_version ||= RUBY_VERSION end diff --git a/lib/shopify-cli/git.rb b/lib/shopify-cli/git.rb index 8b65aad9..9b49f30d 100644 --- a/lib/shopify-cli/git.rb +++ b/lib/shopify-cli/git.rb @@ -1,39 +1,113 @@ module ShopifyCli + ## + # ShopifyCli::Git wraps git functionality to make it easier to integrate will + # git. class Git - GIT_SHA_PATTERN = %r{^[a-f0-9]{40}$} - PROJECT_EXISTS = "Project directory already exists. Please create a project with a new name." + class << self + ## + # will return the current sha of the cli repo + # + # #### Parameters + # + # * `dir` - the directory of the git repo. This defaults to the cli repo + # * `ctx` - the current running context of your command + # + # #### Returns + # + # * `sha_string` - string of the sha of the most recent commit to the repo + # + # #### Example + # + # ShopifyCli::Git.sha + # + def sha(dir: Dir.pwd, ctx: Context.new) + rev_parse('HEAD', dir: dir, ctx: ctx) + end - def initialize(ctx) - @ctx = ctx - end + ## + # will make calls to git to clone a new repo into a supplied destination, + # it will also output progress of the cloning process. + # + # #### Parameters + # + # * `repository` - a git url for git to clone the repo from + # * `dest` - a filepath to where the repo should be cloned to + # * `ctx` - the current running context of your command, defaults to a new context. + # + # #### Returns + # + # * `sha_string` - string of the sha of the most recent commit to the repo + # + # #### Example + # + # ShopifyCli::Git.clone('git@github.com:shopify/test.git', 'test-app') + # + def clone(repository, dest, ctx: Context.new) + if Dir.exist?(dest) + ctx.abort("Project directory already exists. Please create a project with a new name.") + else + CLI::UI::Frame.open("Cloning into #{dest}...") do + clone_progress('clone', '--single-branch', repository, dest, ctx: ctx) + end + ctx.done("Cloned into #{dest}") + end + end - def branches - output, status = @ctx.capture2e('git', 'branch', '--list', '--format=%(refname:short)') - @ctx.abort("Could not find any git branches") unless status.success? + ## + # will fetch the repos list of branches. + # + # #### Parameters + # + # * `ctx` - the current running context of your command, defaults to a new context. + # + # #### Returns + # + # * `branches` - [String] an array of strings that are branch names + # + # #### Example + # + # branches = ShopifyCli::Git.branches(@ctx) + # + def branches(ctx) + output, status = ctx.capture2e('git', 'branch', '--list', '--format=%(refname:short)') + ctx.abort("Could not find any git branches") unless status.success? - branches = if output == '' - ['master'] - else - output.split("\n") + branches = if output == '' + ['master'] + else + output.split("\n") + end + + branches end - branches - end + ## + # will initialize a new repo in the current directory. This will output + # if it was successful or not. + # + # #### Parameters + # + # * `ctx` - the current running context of your command, defaults to a new context. + # + # #### Example + # + # ShopifyCli::Git.init(@ctx) + # + def init(ctx) + output, status = ctx.capture2e('git', 'status') - def init - output, status = @ctx.capture2e('git', 'status') + unless status.success? + msg = "Git repo is not initiated. Please run `git init` and make at least one commit." + ctx.abort(msg) + end - unless status.success? - msg = "Git repo is not initiated. Please run `git init` and make at least one commit." - @ctx.abort(msg) + if output.include?('No commits yet') + ctx.abort("No git commits have been made. Please make at least one commit.") + end end - if output.include?('No commits yet') - @ctx.abort("No git commits have been made. Please make at least one commit.") - end - end + private - class << self def exec(*args, dir: Dir.pwd, default: nil, ctx: Context.new) args = %w(git) + args out, _, stat = ctx.capture3(*args, chdir: dir) @@ -41,23 +115,6 @@ def exec(*args, dir: Dir.pwd, default: nil, ctx: Context.new) out.chomp end - def sha(dir: Dir.pwd, ctx: Context.new) - rev_parse('HEAD', dir: dir, ctx: ctx) - end - - def clone(repository, dest, ctx: Context.new) - if Dir.exist?(dest) - ctx.abort(PROJECT_EXISTS) - else - CLI::UI::Frame.open("Cloning into #{dest}...") do - clone_progress('clone', '--single-branch', repository, dest, ctx: ctx) - end - ctx.done("Cloned app in #{dest}") - end - end - - private - def rev_parse(*args, dir: nil, ctx: Context.new) exec('rev-parse', *args, dir: dir, ctx: ctx) end diff --git a/lib/shopify-cli/heroku.rb b/lib/shopify-cli/heroku.rb index da7198b7..2db59a80 100644 --- a/lib/shopify-cli/heroku.rb +++ b/lib/shopify-cli/heroku.rb @@ -26,12 +26,6 @@ def create_new_app output, status = @ctx.capture2e(heroku_command, 'create') @ctx.abort('Heroku app could not be created') unless status.success? @ctx.puts(output) - - new_remote = output.split("\n").last.split("|").last.strip - result = @ctx.system('git', 'remote', 'add', 'heroku', new_remote) - - msg = "Heroku app created, but couldn’t be set as a git remote" - @ctx.abort(msg) unless result.success? end def deploy(branch_to_deploy) diff --git a/lib/shopify-cli/partners_api.rb b/lib/shopify-cli/partners_api.rb index 971fec5a..b3fa3176 100644 --- a/lib/shopify-cli/partners_api.rb +++ b/lib/shopify-cli/partners_api.rb @@ -1,39 +1,52 @@ require 'shopify_cli' module ShopifyCli + ## + # ShopifyCli::PartnersAPI provides easy access to the partners dashboard CLI + # schema. + # class PartnersAPI < API autoload :Organizations, 'shopify-cli/partners_api/organizations' - ENV_VAR = 'SHOPIFY_APP_CLI_LOCAL_PARTNERS' - AUTH_PROD_URI = 'https://accounts.shopify.com' - AUTH_DEV_URI = 'https://identity.myshopify.io' - PROD_URI = 'https://partners.shopify.com' - DEV_URI = 'https://partners.myshopify.io/' - PROD_ID = '271e16d403dfa18082ffb3d197bd2b5f4479c3fc32736d69296829cbb28d41a6' - DEV_ID = 'df89d73339ac3c6c5f0a98d9ca93260763e384d51d6038da129889c308973978' - PROD_CLI_ID = 'fbdb2649-e327-4907-8f67-908d24cfd7e3' - DEV_CLI_ID = 'e5380e02-312a-7408-5718-e07017e9cf52' + # Defines the environment variable that this API looks for to operate on local + # services. If you set this environment variable in your shell then the partners + # api will operation on your local instance + # + # #### Example + # + # SHOPIFY_APP_CLI_LOCAL_PARTNERS=1 shopify create + # + LOCAL_DEBUG = 'SHOPIFY_APP_CLI_LOCAL_PARTNERS' class << self - def id - ENV[ENV_VAR].nil? ? PROD_ID : DEV_ID - end - - def cli_id - ENV[ENV_VAR].nil? ? PROD_CLI_ID : DEV_CLI_ID - end - - def auth_endpoint - ENV[ENV_VAR].nil? ? AUTH_PROD_URI : AUTH_DEV_URI - end - - def endpoint - ENV[ENV_VAR].nil? ? PROD_URI : DEV_URI - end - - def query(ctx, body, **variables) + ## + # issues a graphql query or mutation to the Shopify Partners Dashboard CLI Schema. + # It loads a graphql query from a file so that you do not need to use large + # unwieldy query strings. It also handles authentication for you as well. + # + # #### Parameters + # - `ctx`: running context from your command + # - `query_name`: name of the query you want to use, loaded from the `lib/graphql` directory. + # - `**variable`: a hash of variables to be supplied to the query ro mutation + # + # #### Raises + # + # * http 404 will raise a ShopifyCli::API::APIRequestNotFoundError + # * http 400..499 will raise a ShopifyCli::API::APIRequestClientError + # * http 500..599 will raise a ShopifyCli::API::APIRequestServerError + # * All other codes will raise ShopifyCli::API::APIRequestUnexpectedError + # + # #### Returns + # + # * `resp` - graphql response data hash. This can be a different shape for every query. + # + # #### Example + # + # ShopifyCli::PartnersAPI.query(@ctx, 'all_organizations') + # + def query(ctx, query_name, **variables) authenticated_req(ctx) do - api_client(ctx).query(body, variables: variables) + api_client(ctx).query(query_name, variables: variables) end end @@ -42,7 +55,7 @@ def query(ctx, body, **variables) def authenticated_req(ctx) yield rescue API::APIRequestUnauthorizedError - Tasks::AuthenticateIdentity.call(ctx) + authenticate(ctx) retry rescue API::APIRequestNotFoundError ctx.puts("{{x}} error: Your account was not found. Please sign up at https://partners.shopify.com/signup") @@ -54,10 +67,47 @@ def authenticated_req(ctx) def api_client(ctx) new( ctx: ctx, - token: Resources::Tokens.identity(ctx), + token: access_token(ctx), url: "#{endpoint}/api/cli/graphql", ) end + + def access_token(ctx) + ShopifyCli::DB.get(:identity_exchange_token) do + authenticate(ctx) + ShopifyCli::DB.get(:identity_exchange_token) + end + end + + def authenticate(ctx) + OAuth.new( + ctx: ctx, + service: 'identity', + client_id: cli_id, + scopes: 'openid https://api.shopify.com/auth/partners.app.cli.access', + request_exchange: partners_id, + ).authenticate("#{auth_endpoint}/oauth") + end + + def partners_id + return '271e16d403dfa18082ffb3d197bd2b5f4479c3fc32736d69296829cbb28d41a6' if ENV[LOCAL_DEBUG].nil? + 'df89d73339ac3c6c5f0a98d9ca93260763e384d51d6038da129889c308973978' + end + + def cli_id + return 'fbdb2649-e327-4907-8f67-908d24cfd7e3' if ENV[LOCAL_DEBUG].nil? + 'e5380e02-312a-7408-5718-e07017e9cf52' + end + + def auth_endpoint + return 'https://accounts.shopify.com' if ENV[LOCAL_DEBUG].nil? + 'https://identity.myshopify.io' + end + + def endpoint + return 'https://partners.shopify.com' if ENV[LOCAL_DEBUG].nil? + 'https://partners.myshopify.io/' + end end def auth_headers(token) diff --git a/lib/shopify-cli/process_supervision.rb b/lib/shopify-cli/process_supervision.rb index 3f1e4ec7..ddd8aee5 100644 --- a/lib/shopify-cli/process_supervision.rb +++ b/lib/shopify-cli/process_supervision.rb @@ -1,13 +1,38 @@ require 'fileutils' module ShopifyCli + ## + # ProcessSupervision wraps a running process spawned by `exec` and keeps track + # if its `pid` and keeps a log file for it as well class ProcessSupervision - DebriefableError = Class.new(StandardError) + # is the directory where the pid and logfile are kept RUN_DIR = File.join(ShopifyCli::TEMP_DIR, 'sv') - attr_reader :identifier, :pid, :time, :pid_path, :log_path + # a string or a symbol to identify this process by + attr_reader :identifier + # process ID for the running process + attr_reader :pid + # starttime of the process + attr_reader :time + # filepath to the pidfile for this process + attr_reader :pid_path + # filepath to the logfile for this process + attr_reader :log_path class << self + ## + # Will find and create a new instance of ProcessSupervision for a running process + # if it is currently running. It will return nil if the process is not running. + # + # #### Parameters + # + # * `identifier` - a string or a symbol that a process was started with + # + # #### Returns + # + # * `process` - ProcessSupervision instance if the process is running this + # will be nil if the process is not running. + # def for_ident(identifier) pid, time = File.read(File.join(RUN_DIR, "#{identifier}.pid")).split(':') new(identifier, pid: Integer(pid), time: time) @@ -15,6 +40,20 @@ def for_ident(identifier) nil end + ## + # will fork and spawn a new process that is separate from the current process. + # This process will keep running beyond the command running so be careful! + # + # #### Parameters + # + # * `identifier` - a string or symbol to identify the new process by. + # * `args` - a command to run, either a string or array of strings + # + # #### Returns + # + # * `process` - ProcessSupervision instance if the process is running, this + # will be nil if the process did not start. + # def start(identifier, args) return for_ident(identifier) if running?(identifier) fork do @@ -30,12 +69,34 @@ def start(identifier, args) for_ident(identifier) end + ## + # will attempt to shutdown a running process + # + # #### Parameters + # + # * `identifier` - a string or symbol to identify the new process by. + # + # #### Returns + # + # * `stopped` - [true, false] + # def stop(identifier) process = for_ident(identifier) return false unless process process.stop end + ## + # will help identify if your process is still running in the background. + # + # #### Parameters + # + # * `identifier` - a string or symbol to identify the new process by. + # + # #### Returns + # + # * `running` - [true, false] + # def running?(identifier) process = for_ident(identifier) return false unless process @@ -43,7 +104,7 @@ def running?(identifier) end end - def initialize(identifier, pid:, time: Time.now.strftime('%s')) + def initialize(identifier, pid:, time: Time.now.strftime('%s')) # :nodoc: @identifier = identifier @pid = pid @time = time @@ -51,6 +112,13 @@ def initialize(identifier, pid:, time: Time.now.strftime('%s')) @log_path = File.join(RUN_DIR, "#{identifier}.log") end + ## + # will attempt to shutdown a running process + # + # #### Returns + # + # * `stopped` - [true, false] + # def stop unlink kill_proc @@ -59,10 +127,27 @@ def stop false end + ## + # will help identify if your process is still running in the background. + # + # #### Returns + # + # * `alive` - [true, false] + # def alive? stat(pid) end + ## + # persists the pidfile + # + def write + FileUtils.mkdir_p(File.dirname(pid_path)) + File.write(pid_path, "#{pid}:#{time}") + end + + private + def unlink File.unlink(pid_path) File.unlink(log_path) @@ -71,13 +156,6 @@ def unlink nil end - def write - FileUtils.mkdir_p(File.dirname(pid_path)) - File.write(pid_path, "#{pid}:#{time}") - end - - private - def kill_proc kill(-pid) # process group rescue Errno::ESRCH diff --git a/lib/shopify-cli/project.rb b/lib/shopify-cli/project.rb index b0d6ce34..5c991851 100644 --- a/lib/shopify-cli/project.rb +++ b/lib/shopify-cli/project.rb @@ -2,38 +2,75 @@ require 'shopify_cli' module ShopifyCli + ## + # ShopifyCli::Project captures the current project that the user is working on. + # This class can be used to fetch and save project environment as well as the + # project config `.shopify-cli.yml`. + # class Project include SmartProperties + NOT_IN_PROJECT = <<~MESSAGE + {{x}} You are not in a Shopify app project + {{yellow:{{*}}}}{{reset: Run}}{{cyan: shopify create}}{{reset: to create your app}} + MESSAGE + private_constant :NOT_IN_PROJECT class << self + ## + # will get an instance of the project that the user is currently operating + # on. This is used for access to project resources. + # + # #### Returns + # + # * `project` - a Project instance if the user is currently in the project. + # + # #### Raises + # + # * `ShopifyCli::Abort` - If the cli is not currently in a project directory + # then this will be raised with a message implying that the user is not in + # a project directory. + # + # #### Example + # + # project = ShopifyCli::Project.current + # def current at(Dir.pwd) end + ## + # will fetch the project type of the current project. This is mostly used + # for internal project type loading, you should not normally need this. + # + # #### Returns + # + # * `type` - a string of the name of the app identifier. i.e. [rails, node] + # This will be nil if the user is not in a current project. + # + # #### Example + # + # type = ShopifyCli::Project.current_app_type + # def current_project_type proj_dir = directory(Dir.pwd) return if proj_dir.nil? - current.app_type_id + current.config['app_type'].to_sym end - # Returns the directory of the project you are current in - # Traverses up directory hierarchy until it finds a `.shopify-cli.yml`, then returns the directory is it in + ## + # writes out the `.shopify-cli.yml` file. You should use this when creating + # a project type so that the rest of your project type commands will load + # in this project, in the future. # - # #### Example Usage - # `directory`, e.g. `~/src/Shopify/dev` + # #### Parameters + # + # * `ctx` - the current running context of your command + # * `identifier` - a string or symbol of your app type name + # + # #### Example + # + # type = ShopifyCli::Project.current_app_type # - def directory(dir) - @dir ||= Hash.new { |h, k| h[k] = __directory(k) } - @dir[dir] - end - - def message - <<~MESSAGE - {{x}} You are not in a Shopify app project - {{yellow:{{*}}}}{{reset: Run}}{{cyan: shopify create}}{{reset: to create your app}} - MESSAGE - end - def write(ctx, identifier) require 'yaml' # takes 20ms, so deferred as late as possible. content = { @@ -44,10 +81,15 @@ def write(ctx, identifier) private + def directory(dir) + @dir ||= Hash.new { |h, k| h[k] = __directory(k) } + @dir[dir] + end + def at(dir) proj_dir = directory(dir) unless proj_dir - raise(ShopifyCli::Abort, "{{x}} #{message}") + raise(ShopifyCli::Abort, NOT_IN_PROJECT) end @at ||= Hash.new { |h, k| h[k] = new(directory: k) } @at[proj_dir] @@ -63,16 +105,39 @@ def __directory(curr) end end - property :directory - - def app_type_id - config['app_type'].to_sym - end + property :directory # :nodoc: + ## + # will read, parse and return the envfile for the project + # + # #### Returns + # + # * `env` - An instance of a ShopifyCli::Resources::EnvFile + # + # #### Example + # + # ShopifyCli::Project.current.env + # def env @env ||= Resources::EnvFile.read(directory) end + ## + # will read, parse and return the .shopify-cli.yml for the project + # + # #### Returns + # + # * `config` - A hash of configuration + # + # #### Raises + # + # * `ShopifyCli::Abort` - If the yml is invalid or poorly formatted + # * `ShopifyCli::Abort` - If the yml file does not exist + # + # #### Example + # + # ShopifyCli::Project.current.config + # def config @config ||= begin config = load_yaml_file('.shopify-cli.yml') diff --git a/lib/shopify-cli/resources.rb b/lib/shopify-cli/resources.rb index f1a98928..7b22855f 100644 --- a/lib/shopify-cli/resources.rb +++ b/lib/shopify-cli/resources.rb @@ -1,6 +1,5 @@ module ShopifyCli module Resources autoload :EnvFile, 'shopify-cli/resources/env_file' - autoload :Tokens, 'shopify-cli/resources/tokens' end end diff --git a/lib/shopify-cli/resources/tokens.rb b/lib/shopify-cli/resources/tokens.rb deleted file mode 100644 index c72a3eb1..00000000 --- a/lib/shopify-cli/resources/tokens.rb +++ /dev/null @@ -1,21 +0,0 @@ -module ShopifyCli - module Resources - class Tokens - class << self - def admin(ctx) - ShopifyCli::DB.get(:admin_access_token) do - ShopifyCli::Tasks::AuthenticateShopify.call(ctx) - ShopifyCli::DB.get(:admin_access_token) - end - end - - def identity(ctx) - ShopifyCli::DB.get(:identity_exchange_token) do - ShopifyCli::Tasks::AuthenticateIdentity.call(ctx) - ShopifyCli::DB.get(:identity_exchange_token) - end - end - end - end - end -end diff --git a/lib/shopify-cli/tasks.rb b/lib/shopify-cli/tasks.rb index e240fb4f..7121a4f3 100644 --- a/lib/shopify-cli/tasks.rb +++ b/lib/shopify-cli/tasks.rb @@ -24,8 +24,6 @@ def self.register(task, name, path) end register :CreateApiClient, :create_api_client, 'shopify-cli/tasks/create_api_client' - register :AuthenticateIdentity, :authenticate_identity, 'shopify-cli/tasks/authenticate_identity' - register :AuthenticateShopify, :authenticate_shopify, 'shopify-cli/tasks/authenticate_shopify' register :EnsureEnv, :ensure_env, 'shopify-cli/tasks/ensure_env' register :EnsureLoopbackURL, :ensure_loopback_url, 'shopify-cli/tasks/ensure_loopback_url' register :EnsureTestShop, :ensure_test_shop, 'shopify-cli/tasks/ensure_test_shop' diff --git a/lib/shopify-cli/tasks/authenticate_identity.rb b/lib/shopify-cli/tasks/authenticate_identity.rb deleted file mode 100644 index 220e88b6..00000000 --- a/lib/shopify-cli/tasks/authenticate_identity.rb +++ /dev/null @@ -1,19 +0,0 @@ -require 'shopify_cli' - -module ShopifyCli - module Tasks - class AuthenticateIdentity < ShopifyCli::Task - SCOPES = 'openid https://api.shopify.com/auth/partners.app.cli.access' - - def call(ctx) - OAuth.new( - ctx: ctx, - service: 'identity', - client_id: ShopifyCli::PartnersAPI.cli_id, - scopes: SCOPES, - request_exchange: ShopifyCli::PartnersAPI.id, - ).authenticate("#{ShopifyCli::PartnersAPI.auth_endpoint}/oauth") - end - end - end -end diff --git a/lib/shopify-cli/tasks/authenticate_shopify.rb b/lib/shopify-cli/tasks/authenticate_shopify.rb deleted file mode 100644 index 985059fc..00000000 --- a/lib/shopify-cli/tasks/authenticate_shopify.rb +++ /dev/null @@ -1,22 +0,0 @@ -require 'shopify_cli' - -module ShopifyCli - module Tasks - class AuthenticateShopify < ShopifyCli::Task - def call(ctx, shop: nil) - Tasks::EnsureEnv.call(ctx) - env = Resources::EnvFile.read - shop ||= env.shop - OAuth.new( - ctx: ctx, - service: 'admin', - client_id: env.api_key, - secret: env.secret, - scopes: env.scopes, - token_path: "/access_token", - options: { 'grant_options[]' => 'per user' }, - ).authenticate("https://#{shop}/admin/oauth") - end - end - end -end diff --git a/lib/shopify-cli/tunnel.rb b/lib/shopify-cli/tunnel.rb index 1c460008..e59c6618 100644 --- a/lib/shopify-cli/tunnel.rb +++ b/lib/shopify-cli/tunnel.rb @@ -4,6 +4,10 @@ require 'forwardable' module ShopifyCli + ## + # Wraps around ngrok functionality to allow you to spawn a ngrok proccess in the + # background and stop the process when you need to. It also allows control over + # the ngrok process between application runs. class Tunnel extend SingleForwardable @@ -12,13 +16,21 @@ class Tunnel class FetchUrlError < RuntimeError; end class NgrokError < RuntimeError; end - PORT = 8081 + PORT = 8081 # port that ngrok will bind to + # mapping for supported operating systems for where to download ngrok from. DOWNLOAD_URLS = { mac: 'https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-darwin-amd64.zip', linux: 'https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-amd64.zip', } - TIMEOUT = 10 + ## + # will find and stop a running tunnel process. It will also output if the + # operation was successful or not + # + # #### Paramters + # + # * `ctx` - running context from your command + # def stop(ctx) if ShopifyCli::ProcessSupervision.running?(:ngrok) if ShopifyCli::ProcessSupervision.stop(:ngrok) @@ -31,6 +43,18 @@ def stop(ctx) end end + ## + # start will start a running ngrok process running in the background. It will + # also output the success of this operation + # + # #### Paramters + # + # * `ctx` - running context from your command + # + # #### Returns + # + # * `url` - the url that the tunnel is now bound to and available to the public + # def start(ctx) install(ctx) process = ShopifyCli::ProcessSupervision.start(:ngrok, ngrok_command) @@ -40,10 +64,18 @@ def start(ctx) else ctx.puts("{{v}} ngrok tunnel running at {{underline:#{log.url}}}") end - ctx.app_metadata = { host: log.url } log.url end + ## + # will add the users authentication token to our version of ngrok to unlock the + # extended ngrok features + # + # #### Paramters + # + # * `ctx` - running context from your command + # * `token` - authentication token provided by ngrok for extended features + # def auth(ctx, token) install(ctx) ctx.system(File.join(ShopifyCli::ROOT, 'ngrok'), 'authtoken', token) @@ -76,7 +108,9 @@ def ngrok_command "exec #{File.join(ShopifyCli::ROOT, 'ngrok')} http -log=stdout -log-level=debug #{PORT}" end - class LogParser + class LogParser # :nodoc: + TIMEOUT = 10 + attr_reader :url, :account def initialize(log_path) diff --git a/test/minitest_ext.rb b/test/minitest_ext.rb index 8aa4c8d2..3d022f39 100644 --- a/test/minitest_ext.rb +++ b/test/minitest_ext.rb @@ -16,7 +16,8 @@ def setup end def run_cmd(cmd) - stub_monorail_log_invocation + stub_prompt_for_cli_updates + stub_monorail_log_git_sha ShopifyCli::Core::EntryPoint.call(cmd.split(' '), @context) end @@ -43,8 +44,12 @@ def to_s # :nodoc: private - def stub_monorail_log_invocation + def stub_monorail_log_git_sha ShopifyCli::Git.stubs(:sha).returns("bb6f42193239a248f054e5019e469bc75f3adf1b") end + + def stub_prompt_for_cli_updates + ShopifyCli::Config.stubs(:get_section).with("autoupdate").returns(stub("key?" => true)) + end end end diff --git a/test/project_types/node/commands/create_test.rb b/test/project_types/node/commands/create_test.rb index 7c04b9c9..2bd8d703 100644 --- a/test/project_types/node/commands/create_test.rb +++ b/test/project_types/node/commands/create_test.rb @@ -63,7 +63,7 @@ def test_can_create_new_app FileUtils.touch('test-app/server/handlers/client.js') FileUtils.touch('test-app/server/handlers/client.cli.js') - @context.stubs(:uname).with(flag: 'v').returns('Mac') + @context.stubs(:uname).returns('Mac') @context.expects(:capture2e).with('npm', '-v').returns(['1', mock(success?: true)]) @context.expects(:capture2e).with('node', '-v').returns(['8.0.0', mock(success?: true)]) @context.expects(:capture2).with('npm config get @shopify:registry').returns( @@ -113,7 +113,7 @@ def expect_command(command, chdir: @context.root) def perform_command run_cmd("create node \ - --title=test-app \ + --name=test-app \ --type=public \ --organization_id=42 \ --shop_domain=testshop.myshopify.com") diff --git a/test/project_types/node/commands/deploy/heroku_test.rb b/test/project_types/node/commands/deploy/heroku_test.rb index aa06b202..be21c482 100644 --- a/test/project_types/node/commands/deploy/heroku_test.rb +++ b/test/project_types/node/commands/deploy/heroku_test.rb @@ -101,8 +101,8 @@ def test_call_raises_if_git_is_inited_but_there_are_no_commits def test_call_uses_existing_heroku_auth_if_available expects_heroku_whoami(status: true) - CLI::UI::SpinGroup.any_instance.expects(:add).with( - 'Authenticated with Heroku as `username`' + @context.expects(:puts).with( + '{{v}} Authenticated with Heroku as `username`' ) run_cmd('deploy heroku') @@ -127,8 +127,8 @@ def test_call_raises_if_heroku_auth_fails def test_call_uses_existing_heroku_app_if_available expects_git_remote_get_url_heroku(status: true, remote: 'heroku') - CLI::UI::SpinGroup.any_instance.expects(:add).with( - 'Heroku app `app-name` selected' + @context.expects(:puts).with( + '{{v}} Heroku app `app-name` selected' ) run_cmd('deploy heroku') @@ -169,7 +169,6 @@ def test_call_raises_if_choosing_existing_heroku_app_fails def test_call_lets_you_create_new_heroku_app expects_git_remote_get_url_heroku(status: false, remote: 'heroku') expects_heroku_create(status: true) - expects_git_remote_add_heroku(status: true) CLI::UI::Prompt.expects(:ask) .with('No existing Heroku app found. What would you like to do?') @@ -181,21 +180,6 @@ def test_call_lets_you_create_new_heroku_app def test_call_raises_if_creating_new_heroku_app_fails expects_git_remote_get_url_heroku(status: false, remote: 'heroku') expects_heroku_create(status: false) - expects_git_remote_add_heroku(status: nil) - - CLI::UI::Prompt.expects(:ask) - .with('No existing Heroku app found. What would you like to do?') - .returns(:new) - - assert_raises ShopifyCli::Abort do - run_cmd('deploy heroku') - end - end - - def test_call_raises_if_setting_remote_heroku_fails - expects_git_remote_get_url_heroku(status: false, remote: 'heroku') - expects_heroku_create(status: true) - expects_git_remote_add_heroku(status: false) CLI::UI::Prompt.expects(:ask) .with('No existing Heroku app found. What would you like to do?') @@ -209,8 +193,8 @@ def test_call_raises_if_setting_remote_heroku_fails def test_call_doesnt_prompt_if_only_one_branch_exists expects_git_branch(status: true, multiple: false) - CLI::UI::SpinGroup.any_instance.expects(:add).with( - 'Git branch `master` selected for deploy' + @context.expects(:puts).with( + '{{v}} Git branch `master` selected for deploy' ) run_cmd('deploy heroku') diff --git a/test/project_types/node/commands/populate/customer_test.rb b/test/project_types/node/commands/populate/customer_test.rb index 5d077b0d..bb320a5d 100644 --- a/test/project_types/node/commands/populate/customer_test.rb +++ b/test/project_types/node/commands/populate/customer_test.rb @@ -9,7 +9,6 @@ class CustomerTest < MiniTest::Test def setup super ShopifyCli::ProjectType.load_type(:node) - ShopifyCli::Resources::Tokens.stubs(:admin).returns('myaccesstoken') end def test_populate_calls_api_with_mutation diff --git a/test/project_types/node/commands/populate/draft_order_test.rb b/test/project_types/node/commands/populate/draft_order_test.rb index f84be0b4..f68be259 100644 --- a/test/project_types/node/commands/populate/draft_order_test.rb +++ b/test/project_types/node/commands/populate/draft_order_test.rb @@ -10,7 +10,6 @@ class DraftOrderTest < MiniTest::Test def setup super ShopifyCli::ProjectType.load_type(:node) - ShopifyCli::Resources::Tokens.stubs(:admin).returns('myaccesstoken') end def test_populate_calls_api_with_mutation diff --git a/test/project_types/node/commands/populate/product_test.rb b/test/project_types/node/commands/populate/product_test.rb index 38826b50..912f3026 100644 --- a/test/project_types/node/commands/populate/product_test.rb +++ b/test/project_types/node/commands/populate/product_test.rb @@ -9,7 +9,6 @@ class ProductTest < MiniTest::Test def setup super ShopifyCli::ProjectType.load_type(:node) - ShopifyCli::Resources::Tokens.stubs(:admin).returns('myaccesstoken') end def test_populate_calls_api_with_mutation diff --git a/test/project_types/node/commands/populate_test.rb b/test/project_types/node/commands/populate_test.rb index bf006435..30e4eb84 100644 --- a/test/project_types/node/commands/populate_test.rb +++ b/test/project_types/node/commands/populate_test.rb @@ -5,7 +5,6 @@ module Commands class PopulateTest < MiniTest::Test def setup super - ShopifyCli::Resources::Tokens.stubs(:admin).returns('myaccesstoken') ShopifyCli::Tasks::EnsureEnv.stubs(:call) project_context('app_types', 'node') ShopifyCli::ProjectType.load_type(:node) diff --git a/test/project_types/node/commands/serve_test.rb b/test/project_types/node/commands/serve_test.rb index 658b17e3..f6ee3117 100644 --- a/test/project_types/node/commands/serve_test.rb +++ b/test/project_types/node/commands/serve_test.rb @@ -14,7 +14,7 @@ def setup end def test_server_command - ShopifyCli::Tunnel.stubs(:start) + ShopifyCli::Tunnel.stubs(:start).returns('https://example.com') ShopifyCli::Tasks::UpdateDashboardURLS.expects(:call) ShopifyCli::Resources::EnvFile.any_instance.expects(:update) @context.expects(:system).with( @@ -31,6 +31,27 @@ def test_server_command run_cmd('serve') end + def test_server_command_with_invalid_host_url + ShopifyCli::Tunnel.stubs(:start).returns('garbage://example.com') + ShopifyCli::Tasks::UpdateDashboardURLS.expects(:call).never + ShopifyCli::Resources::EnvFile.any_instance.expects(:update).never + @context.expects(:system).with( + 'npm run dev', + env: { + "SHOPIFY_API_KEY" => "mykey", + "SHOPIFY_API_SECRET" => "mysecretkey", + "SHOP" => "my-test-shop.myshopify.com", + "SCOPES" => "read_products", + "HOST" => "garbage://example.com", + "PORT" => "8081", + } + ).never + + assert_raises ShopifyCli::Abort do + run_cmd('serve') + end + end + def test_open_while_run ShopifyCli::Context.any_instance.stubs(:on_siginfo).yields ShopifyCli::Tunnel.stubs(:start).returns('https://example.com') diff --git a/test/project_types/node/commands/tunnel_test.rb b/test/project_types/node/commands/tunnel_test.rb index f798ea5b..9595f951 100644 --- a/test/project_types/node/commands/tunnel_test.rb +++ b/test/project_types/node/commands/tunnel_test.rb @@ -10,6 +10,11 @@ def setup def test_auth ShopifyCli::Tunnel.any_instance.expects(:auth) + run_cmd('tunnel auth adfhauf98q7rtqhfkajf') + end + + def test_auth_no_token + ShopifyCli::Tunnel.any_instance.expects(:auth).never run_cmd('tunnel auth') end diff --git a/test/project_types/rails/commands/create_test.rb b/test/project_types/rails/commands/create_test.rb index e5e51bfe..9ad7157b 100644 --- a/test/project_types/rails/commands/create_test.rb +++ b/test/project_types/rails/commands/create_test.rb @@ -96,7 +96,7 @@ def expect_command(command, chdir: @context.root) def perform_command run_cmd("create rails \ --type=public \ - --title=test-app \ + --name=test-app \ --organization_id=42 \ --shop_domain=testshop.myshopify.com") end diff --git a/test/project_types/rails/commands/deploy/heroku_test.rb b/test/project_types/rails/commands/deploy/heroku_test.rb index 7579b35f..a418b75a 100644 --- a/test/project_types/rails/commands/deploy/heroku_test.rb +++ b/test/project_types/rails/commands/deploy/heroku_test.rb @@ -101,8 +101,8 @@ def test_call_raises_if_git_is_inited_but_there_are_no_commits def test_call_uses_existing_heroku_auth_if_available expects_heroku_whoami(status: true) - CLI::UI::SpinGroup.any_instance.expects(:add).with( - 'Authenticated with Heroku as `username`' + @context.expects(:puts).with( + "{{v}} Authenticated with Heroku as `username`" ) run_cmd('deploy heroku') @@ -127,8 +127,8 @@ def test_call_raises_if_heroku_auth_fails def test_call_uses_existing_heroku_app_if_available expects_git_remote_get_url_heroku(status: true, remote: 'heroku') - CLI::UI::SpinGroup.any_instance.expects(:add).with( - 'Heroku app `app-name` selected' + @context.expects(:puts).with( + '{{v}} Heroku app `app-name` selected' ) run_cmd('deploy heroku') @@ -169,7 +169,6 @@ def test_call_raises_if_choosing_existing_heroku_app_fails def test_call_lets_you_create_new_heroku_app expects_git_remote_get_url_heroku(status: false, remote: 'heroku') expects_heroku_create(status: true) - expects_git_remote_add_heroku(status: true) CLI::UI::Prompt.expects(:ask) .with('No existing Heroku app found. What would you like to do?') @@ -181,21 +180,6 @@ def test_call_lets_you_create_new_heroku_app def test_call_raises_if_creating_new_heroku_app_fails expects_git_remote_get_url_heroku(status: false, remote: 'heroku') expects_heroku_create(status: false) - expects_git_remote_add_heroku(status: nil) - - CLI::UI::Prompt.expects(:ask) - .with('No existing Heroku app found. What would you like to do?') - .returns(:new) - - assert_raises ShopifyCli::Abort do - run_cmd('deploy heroku') - end - end - - def test_call_raises_if_setting_remote_heroku_fails - expects_git_remote_get_url_heroku(status: false, remote: 'heroku') - expects_heroku_create(status: true) - expects_git_remote_add_heroku(status: false) CLI::UI::Prompt.expects(:ask) .with('No existing Heroku app found. What would you like to do?') @@ -209,8 +193,8 @@ def test_call_raises_if_setting_remote_heroku_fails def test_call_doesnt_prompt_if_only_one_branch_exists expects_git_branch(status: true, multiple: false) - CLI::UI::SpinGroup.any_instance.expects(:add).with( - 'Git branch `master` selected for deploy' + @context.expects(:puts).with( + '{{v}} Git branch `master` selected for deploy' ) run_cmd('deploy heroku') diff --git a/test/project_types/rails/commands/populate/customer_test.rb b/test/project_types/rails/commands/populate/customer_test.rb index 643c6e5e..9f7d4e74 100644 --- a/test/project_types/rails/commands/populate/customer_test.rb +++ b/test/project_types/rails/commands/populate/customer_test.rb @@ -9,7 +9,6 @@ class CustomerTest < MiniTest::Test def setup super ShopifyCli::ProjectType.load_type(:rails) - ShopifyCli::Resources::Tokens.stubs(:admin).returns('myaccesstoken') end def test_populate_calls_api_with_mutation diff --git a/test/project_types/rails/commands/populate/draft_order_test.rb b/test/project_types/rails/commands/populate/draft_order_test.rb index 3088e4bd..5e0d7830 100644 --- a/test/project_types/rails/commands/populate/draft_order_test.rb +++ b/test/project_types/rails/commands/populate/draft_order_test.rb @@ -10,7 +10,6 @@ class DraftOrderTest < MiniTest::Test def setup super ShopifyCli::ProjectType.load_type(:rails) - ShopifyCli::Resources::Tokens.stubs(:admin).returns('myaccesstoken') end def test_populate_calls_api_with_mutation diff --git a/test/project_types/rails/commands/populate/product_test.rb b/test/project_types/rails/commands/populate/product_test.rb index 65f2e6c1..a4073017 100644 --- a/test/project_types/rails/commands/populate/product_test.rb +++ b/test/project_types/rails/commands/populate/product_test.rb @@ -9,7 +9,6 @@ class ProductTest < MiniTest::Test def setup super ShopifyCli::ProjectType.load_type(:rails) - ShopifyCli::Resources::Tokens.stubs(:admin).returns('myaccesstoken') end def test_populate_calls_api_with_mutation diff --git a/test/project_types/rails/commands/populate_test.rb b/test/project_types/rails/commands/populate_test.rb index 4cad0b98..3e9c498d 100644 --- a/test/project_types/rails/commands/populate_test.rb +++ b/test/project_types/rails/commands/populate_test.rb @@ -5,7 +5,6 @@ module Commands class PopulateTest < MiniTest::Test def setup super - ShopifyCli::Resources::Tokens.stubs(:admin).returns('myaccesstoken') ShopifyCli::Tasks::EnsureEnv.stubs(:call) project_context('app_types', 'rails') ShopifyCli::ProjectType.load_type(:rails) diff --git a/test/project_types/rails/commands/serve_test.rb b/test/project_types/rails/commands/serve_test.rb index d37a51af..d26773b1 100644 --- a/test/project_types/rails/commands/serve_test.rb +++ b/test/project_types/rails/commands/serve_test.rb @@ -14,7 +14,7 @@ def setup end def test_server_command - ShopifyCli::Tunnel.stubs(:start) + ShopifyCli::Tunnel.stubs(:start).returns('https://example.com') ShopifyCli::Tasks::UpdateDashboardURLS.expects(:call) ShopifyCli::Resources::EnvFile.any_instance.expects(:update) @context.expects(:system).with( @@ -30,6 +30,26 @@ def test_server_command run_cmd('serve') end + def test_server_command_with_invalid_host_url + ShopifyCli::Tunnel.stubs(:start).returns('garbage://example.com') + ShopifyCli::Tasks::UpdateDashboardURLS.expects(:call).never + ShopifyCli::Resources::EnvFile.any_instance.expects(:update).never + @context.expects(:system).with( + 'bin/rails server', + env: { + "SHOPIFY_API_KEY" => "api_key", + "SHOPIFY_API_SECRET" => "secret", + "SHOP" => "my-test-shop.myshopify.com", + "SCOPES" => "write_products,write_customers,write_orders", + 'PORT' => '8081', + } + ).never + + assert_raises ShopifyCli::Abort do + run_cmd('serve') + end + end + def test_open_while_run ShopifyCli::Context.any_instance.stubs(:on_siginfo).yields ShopifyCli::Tunnel.stubs(:start).returns('https://example.com') diff --git a/test/project_types/rails/commands/tunnel_test.rb b/test/project_types/rails/commands/tunnel_test.rb index 603e9d32..314e6d7b 100644 --- a/test/project_types/rails/commands/tunnel_test.rb +++ b/test/project_types/rails/commands/tunnel_test.rb @@ -10,6 +10,11 @@ def setup def test_auth ShopifyCli::Tunnel.any_instance.expects(:auth) + run_cmd('tunnel auth adfhauf98q7rtqhfkajf') + end + + def test_auth_no_token + ShopifyCli::Tunnel.any_instance.expects(:auth).never run_cmd('tunnel auth') end diff --git a/test/shopify-cli/admin_api/populate_resource_command_test.rb b/test/shopify-cli/admin_api/populate_resource_command_test.rb index 2770eb5a..21b05599 100644 --- a/test/shopify-cli/admin_api/populate_resource_command_test.rb +++ b/test/shopify-cli/admin_api/populate_resource_command_test.rb @@ -9,7 +9,6 @@ class PopulateResourceCommandTest < MiniTest::Test def setup super @resource = Rails::Commands::Populate::Product.new(@context) - ShopifyCli::Resources::Tokens.stubs(:admin).returns('myaccesstoken') ShopifyCli::AdminAPI.stubs(:new).returns(Object.new) end diff --git a/test/shopify-cli/admin_api_test.rb b/test/shopify-cli/admin_api_test.rb index 20cefc11..dac1ffda 100644 --- a/test/shopify-cli/admin_api_test.rb +++ b/test/shopify-cli/admin_api_test.rb @@ -16,7 +16,7 @@ def test_latest_api_version .with('api_versions') .returns(JSON.parse(File.read(File.join(FIXTURE_DIR, 'api/versions.json')))) - Resources::Tokens.expects(:admin).returns('token123').twice + ShopifyCli::DB.expects(:get).with(:admin_access_token).returns('token123').twice api_stub = Object.new AdminAPI.expects(:new).with( ctx: @context, @@ -29,7 +29,7 @@ def test_latest_api_version end def test_query_calls_admin_api - Resources::Tokens.expects(:admin).returns('token123') + ShopifyCli::DB.expects(:get).with(:admin_access_token).returns('token123') api_stub = Object.new AdminAPI.expects(:new).with( ctx: @context, @@ -42,7 +42,7 @@ def test_query_calls_admin_api end def test_query_can_reauth - Resources::Tokens.expects(:admin).returns('token123').twice + ShopifyCli::DB.expects(:get).with(:admin_access_token).returns('token123').twice api_stub = Object.new AdminAPI.expects(:new).with( ctx: @context, @@ -52,12 +52,28 @@ def test_query_can_reauth ).returns(api_stub).twice api_stub.expects(:query).with('query', variables: {}).returns('response') api_stub.expects(:query).raises(API::APIRequestUnauthorizedError) - Tasks::AuthenticateShopify.expects(:call) + + @oauth_client = Object.new + ShopifyCli::OAuth + .expects(:new) + .with( + ctx: @context, + service: 'admin', + client_id: 'apikey', + secret: 'secret', + scopes: nil, + token_path: "/access_token", + options: { 'grant_options[]' => 'per user' }, + ).returns(@oauth_client) + @oauth_client + .expects(:authenticate) + .with("https://my-test-shop.myshopify.com/admin/oauth") + AdminAPI.query(@context, 'query', api_version: '2019-04') end def test_query_calls_admin_api_with_different_shop - Resources::Tokens.expects(:admin).returns('token123') + ShopifyCli::DB.expects(:get).with(:admin_access_token).returns('token123') api_stub = Object.new AdminAPI.expects(:new).with( ctx: @context, diff --git a/test/shopify-cli/api_test.rb b/test/shopify-cli/api_test.rb index 9edfa84f..01e6d437 100644 --- a/test/shopify-cli/api_test.rb +++ b/test/shopify-cli/api_test.rb @@ -20,7 +20,7 @@ def setup url: "https://my-test-shop.myshopify.com/admin/api/2019-04/graphql.json", ) Git.stubs(:sha).returns('abcde') - @context.stubs(:uname).with(flag: 'v').returns('Mac') + @context.stubs(:uname).returns('Mac') end def test_mutation_makes_request_to_shopify diff --git a/test/shopify-cli/context_test.rb b/test/shopify-cli/context_test.rb index 9d30eed6..6a30ef97 100644 --- a/test/shopify-cli/context_test.rb +++ b/test/shopify-cli/context_test.rb @@ -17,18 +17,14 @@ def test_write_writes_to_file_in_project end def test_mac_matches - @ctx.expects(:capture2).with('uname -a').returns( - ['Darwin hostname.local 18.6.0 Darwin Kernel Version 18.6.0', nil] - ) + @ctx.expects(:uname).returns('x86_64-apple-darwin19.3.0').times(3) assert(@ctx.mac?) assert_equal(:mac, @ctx.os) refute(@ctx.linux?) end def test_linux_matches - @ctx.expects(:capture2).with('uname -a').returns( - ['Linux hostname 4.15.0-50-generic #54-Ubuntu SMP', nil] - ) + @ctx.expects(:uname).returns('x86_64-pc-linux-gnu').times(3) assert(@ctx.linux?) assert_equal(:linux, @ctx.os) refute(@ctx.mac?) diff --git a/test/shopify-cli/git_test.rb b/test/shopify-cli/git_test.rb index a632de4f..2f8bf5d4 100644 --- a/test/shopify-cli/git_test.rb +++ b/test/shopify-cli/git_test.rb @@ -18,9 +18,7 @@ def test_branches_returns_master_if_no_branches_exist .with('git', 'branch', '--list', '--format=%(refname:short)') .returns(['', @status_mock[:true]]) - git_service = ShopifyCli::Git.new(@context) - - assert_equal(['master'], git_service.branches) + assert_equal(['master'], ShopifyCli::Git.branches(@context)) end def test_branches_returns_list_of_branches_if_multiple_exist @@ -28,44 +26,35 @@ def test_branches_returns_list_of_branches_if_multiple_exist .with('git', 'branch', '--list', '--format=%(refname:short)') .returns(["master\nsecond_branch\n", @status_mock[:true]]) - git_service = ShopifyCli::Git.new(@context) - - assert_equal(['master', 'second_branch'], git_service.branches) + assert_equal(['master', 'second_branch'], ShopifyCli::Git.branches(@context)) end def test_branches_raises_if_finding_branches_fails @context.stubs(:capture2e) .with('git', 'branch', '--list', '--format=%(refname:short)') .returns(['', @status_mock[:false]]) - git_service = ShopifyCli::Git.new(@context) assert_raises ShopifyCli::Abort do - git_service.branches + ShopifyCli::Git.branches(@context) end end def test_init_initializes_successfully stub_git_init(status: true, commits: true) - git_service = ShopifyCli::Git.new(@context) - - assert_nil(git_service.init) + assert_nil(ShopifyCli::Git.init(@context)) end def test_init_raises_if_git_isnt_inited stub_git_init(status: false, commits: false) - git_service = ShopifyCli::Git.new(@context) - assert_raises ShopifyCli::Abort do - git_service.init + ShopifyCli::Git.init(@context) end end def test_init_raises_if_git_is_inited_but_there_are_no_commits stub_git_init(status: true, commits: false) - git_service = ShopifyCli::Git.new(@context) - assert_raises ShopifyCli::Abort do - git_service.init + ShopifyCli::Git.init(@context) end end @@ -86,7 +75,7 @@ def test_head_sha end def test_clones_git_repo - ShopifyCli::Context.any_instance.expects(:system).with( + @context.expects(:system).with( 'git', 'clone', '--single-branch', @@ -95,13 +84,13 @@ def test_clones_git_repo '--progress' ).returns(mock(success?: true)) capture_io do - ShopifyCli::Git.clone('git@github.com:shopify/test.git', 'test-app') + ShopifyCli::Git.clone('git@github.com:shopify/test.git', 'test-app', ctx: @context) end end def test_clone_failure assert_raises(ShopifyCli::Abort) do - ShopifyCli::Context.any_instance.expects(:system).with( + @context.expects(:system).with( 'git', 'clone', '--single-branch', @@ -110,7 +99,7 @@ def test_clone_failure '--progress' ).returns(mock(success?: false)) capture_io do - ShopifyCli::Git.clone('git@github.com:shopify/test.git', 'test-app') + ShopifyCli::Git.clone('git@github.com:shopify/test.git', 'test-app', ctx: @context) end end end @@ -127,7 +116,7 @@ def in_repo end def empty_commit - ShopifyCli::Git.exec('commit', '-m', 'commit', '--allow-empty') + Context.new.capture3('git', 'commit', '-m', 'commit', '--allow-empty') end def stub_git_init(status:, commits:) diff --git a/test/shopify-cli/heroku_test.rb b/test/shopify-cli/heroku_test.rb index 2e60057b..d1144e46 100644 --- a/test/shopify-cli/heroku_test.rb +++ b/test/shopify-cli/heroku_test.rb @@ -54,7 +54,6 @@ def test_authenticate_raises_if_heroku_auth_fails def test_create_new_app_using_full_path_heroku_to_create_new_heroku_app expects_heroku_create(status: true, full_path: true) - expects_git_remote_add_heroku(status: true) heroku_service = ShopifyCli::Heroku.new(@context) @@ -63,7 +62,6 @@ def test_create_new_app_using_full_path_heroku_to_create_new_heroku_app def test_create_new_app_using_non_full_path_heroku_to_create_new_heroku_app expects_heroku_create(status: true, full_path: false) - expects_git_remote_add_heroku(status: true) heroku_service = ShopifyCli::Heroku.new(@context) @@ -72,18 +70,6 @@ def test_create_new_app_using_non_full_path_heroku_to_create_new_heroku_app def test_create_new_app_raises_if_creating_new_heroku_app_fails expects_heroku_create(status: false) - expects_git_remote_add_heroku(status: nil) - - heroku_service = ShopifyCli::Heroku.new(@context) - - assert_raises ShopifyCli::Abort do - heroku_service.create_new_app - end - end - - def test_create_new_app_raises_if_setting_remote_heroku_fails - expects_heroku_create(status: true) - expects_git_remote_add_heroku(status: false) heroku_service = ShopifyCli::Heroku.new(@context) diff --git a/test/shopify-cli/partners_api_test.rb b/test/shopify-cli/partners_api_test.rb index 3aa2b073..634727d4 100644 --- a/test/shopify-cli/partners_api_test.rb +++ b/test/shopify-cli/partners_api_test.rb @@ -4,69 +4,54 @@ module ShopifyCli class PartnersAPITest < MiniTest::Test include TestHelpers::Project - def setup - super - Resources::Tokens.stubs(:identity).returns('token123') - end - - def test_id - ENV['SHOPIFY_APP_CLI_LOCAL_PARTNERS'] = '1' - assert_equal PartnersAPI::DEV_ID, PartnersAPI.id - ENV.delete('SHOPIFY_APP_CLI_LOCAL_PARTNERS') - assert_equal PartnersAPI::PROD_ID, PartnersAPI.id - end - - def test_cli_id - ENV['SHOPIFY_APP_CLI_LOCAL_PARTNERS'] = '1' - assert_equal PartnersAPI::DEV_CLI_ID, PartnersAPI.cli_id - ENV.delete('SHOPIFY_APP_CLI_LOCAL_PARTNERS') - assert_equal PartnersAPI::PROD_CLI_ID, PartnersAPI.cli_id - end - - def test_auth_endpoint - ENV['SHOPIFY_APP_CLI_LOCAL_PARTNERS'] = '1' - assert_equal PartnersAPI::AUTH_DEV_URI, PartnersAPI.auth_endpoint - ENV.delete('SHOPIFY_APP_CLI_LOCAL_PARTNERS') - assert_equal PartnersAPI::AUTH_PROD_URI, PartnersAPI.auth_endpoint - end - - def test_endpoint - ENV['SHOPIFY_APP_CLI_LOCAL_PARTNERS'] = '1' - assert_equal PartnersAPI::DEV_URI, PartnersAPI.endpoint - ENV.delete('SHOPIFY_APP_CLI_LOCAL_PARTNERS') - assert_equal PartnersAPI::PROD_URI, PartnersAPI.endpoint - end - def test_query_calls_partners_api + ShopifyCli::DB.expects(:get).with(:identity_exchange_token).returns('token123') api_stub = Object.new PartnersAPI.expects(:new).with( ctx: @context, token: 'token123', - url: "#{PartnersAPI.endpoint}/api/cli/graphql", + url: "https://partners.shopify.com/api/cli/graphql", ).returns(api_stub) api_stub.expects(:query).with('query', variables: {}).returns('response') assert_equal 'response', PartnersAPI.query(@context, 'query') end def test_query_can_reauth + ShopifyCli::DB.expects(:get).with(:identity_exchange_token).returns('token123').twice + api_stub = Object.new PartnersAPI.expects(:new).with( ctx: @context, token: 'token123', - url: "#{PartnersAPI.endpoint}/api/cli/graphql", + url: "https://partners.shopify.com/api/cli/graphql", ).returns(api_stub).twice api_stub.expects(:query).with('query', variables: {}).returns('response') api_stub.expects(:query).raises(API::APIRequestUnauthorizedError) - Tasks::AuthenticateIdentity.expects(:call) + + @oauth_client = Object.new + ShopifyCli::OAuth + .expects(:new) + .with( + ctx: @context, + service: 'identity', + client_id: 'fbdb2649-e327-4907-8f67-908d24cfd7e3', + scopes: 'openid https://api.shopify.com/auth/partners.app.cli.access', + request_exchange: '271e16d403dfa18082ffb3d197bd2b5f4479c3fc32736d69296829cbb28d41a6', + ).returns(@oauth_client) + @oauth_client + .expects(:authenticate) + .with("https://accounts.shopify.com/oauth") + PartnersAPI.query(@context, 'query') end def test_query_fails_gracefully_without_partners_account + ShopifyCli::DB.expects(:get).with(:identity_exchange_token).returns('token123') api_stub = Object.new PartnersAPI.expects(:new).with( ctx: @context, token: 'token123', - url: "#{PartnersAPI.endpoint}/api/cli/graphql", + url: "https://partners.shopify.com/api/cli/graphql", ).returns(api_stub) api_stub.expects(:query).raises(API::APIRequestNotFoundError) @context.expects(:puts).with( @@ -78,11 +63,12 @@ def test_query_fails_gracefully_without_partners_account end def test_query + ShopifyCli::DB.expects(:get).with(:identity_exchange_token).returns('token123') api_stub = Object.new PartnersAPI.expects(:new).with( ctx: @context, token: 'token123', - url: "#{PartnersAPI.endpoint}/api/cli/graphql", + url: "https://partners.shopify.com/api/cli/graphql", ).returns(api_stub) api_stub.expects(:query).with('query', variables: {}).returns('response') assert_equal 'response', PartnersAPI.query(@context, 'query') diff --git a/test/shopify-cli/tunnel_test.rb b/test/shopify-cli/tunnel_test.rb index b9eba252..d5bcbba0 100644 --- a/test/shopify-cli/tunnel_test.rb +++ b/test/shopify-cli/tunnel_test.rb @@ -17,7 +17,7 @@ def test_start_running_returns_url .with(:ngrok).returns(:true) with_log do ShopifyCli::Tunnel.start(@context) - assert_equal 'https://example.ngrok.io', @context.app_metadata[:host] + assert_equal 'https://example.ngrok.io', ShopifyCli::Tunnel.start(@context) end end @@ -32,7 +32,6 @@ def test_start_not_running_starts_ngrok "{{v}} ngrok tunnel running at {{underline:https://example.ngrok.io}}" ) assert_equal 'https://example.ngrok.io', ShopifyCli::Tunnel.start(@context) - assert_equal 'https://example.ngrok.io', @context.app_metadata[:host] end end @@ -83,7 +82,7 @@ def with_log(fixture = 'ngrok') process.write File.write(process.log_path, File.read(log_path)) yield - process.unlink + process.stop end end end diff --git a/test/task/authenticate_identity_test.rb b/test/task/authenticate_identity_test.rb deleted file mode 100644 index 61989dad..00000000 --- a/test/task/authenticate_identity_test.rb +++ /dev/null @@ -1,26 +0,0 @@ -require 'test_helper' - -module ShopifyCli - module Tasks - class AuthenticateIdentityTest < MiniTest::Test - include TestHelpers::Constants - - def test_negotiate_oauth_and_store_token - @oauth_client = Object.new - ShopifyCli::OAuth - .expects(:new) - .with( - ctx: @context, - service: 'identity', - client_id: PartnersAPI.cli_id, - scopes: AuthenticateIdentity::SCOPES, - request_exchange: PartnersAPI.id, - ).returns(@oauth_client) - @oauth_client - .expects(:authenticate) - .with("https://accounts.shopify.com/oauth") - AuthenticateIdentity.call(@context) - end - end - end -end diff --git a/test/task/authenticate_shopify_test.rb b/test/task/authenticate_shopify_test.rb deleted file mode 100644 index b0f1d5a5..00000000 --- a/test/task/authenticate_shopify_test.rb +++ /dev/null @@ -1,45 +0,0 @@ -require 'test_helper' - -module ShopifyCli - module Tasks - class AuthenticateShopifyTest < MiniTest::Test - def test_negotiate_oauth_and_store_token - @oauth_client = Object.new - ShopifyCli::OAuth - .expects(:new) - .with( - ctx: @context, - service: 'admin', - client_id: 'apikey', - secret: 'secret', - scopes: nil, - token_path: "/access_token", - options: { 'grant_options[]' => 'per user' }, - ).returns(@oauth_client) - @oauth_client - .expects(:authenticate) - .with("https://my-test-shop.myshopify.com/admin/oauth") - AuthenticateShopify.new.call(@context) - end - - def test_authenticates_against_specified_shop - @oauth_client = Object.new - ShopifyCli::OAuth - .expects(:new) - .with( - ctx: @context, - service: 'admin', - client_id: 'apikey', - secret: 'secret', - scopes: nil, - token_path: "/access_token", - options: { 'grant_options[]' => 'per user' }, - ).returns(@oauth_client) - @oauth_client - .expects(:authenticate) - .with("https://other-test-shop.myshopify.com/admin/oauth") - AuthenticateShopify.new.call(@context, shop: 'other-test-shop.myshopify.com') - end - end - end -end diff --git a/test/test_helpers/heroku.rb b/test/test_helpers/heroku.rb index b57acf77..d0e08c6f 100644 --- a/test/test_helpers/heroku.rb +++ b/test/test_helpers/heroku.rb @@ -43,6 +43,9 @@ def stub_successful_heroku_flow(full_path: false) @context.stubs(:system) .with('git', 'push', '-u', 'heroku', 'master:master') .returns(status_mock[:true]) + + # user outputs + @context.stubs(:puts) end def expects_tar_heroku(status:) @@ -105,18 +108,6 @@ def expects_git_remote_get_url_heroku(status:, remote:) .returns([output, status_mock[:"#{status}"]]) end - def expects_git_remote_add_heroku(status:) - if status.nil? - @context.expects(:system) - .with('git', 'remote', 'add', 'heroku', heroku_remote) - .never - else - @context.expects(:system) - .with('git', 'remote', 'add', 'heroku', heroku_remote) - .returns(status_mock[:"#{status}"]) - end - end - def expects_git_push_heroku(status:, branch:) @context.expects(:system) .with('git', 'push', '-u', 'heroku', branch)