gistribute 0.2 → 0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/blocking-issues.yml +14 -0
- data/.github/workflows/lint.yml +17 -0
- data/.github/workflows/test.yml +21 -0
- data/.gitignore +21 -0
- data/.rubocop.yml +94 -0
- data/Gemfile +4 -0
- data/README.md +32 -23
- data/Rakefile +2 -0
- data/VERSION +1 -1
- data/bin/gistribute +3 -83
- data/gistribute.gemspec +14 -7
- data/lib/cli/auth.rb +64 -0
- data/lib/cli/install.rb +86 -0
- data/lib/cli/upload.rb +54 -0
- data/lib/cli.rb +117 -0
- data/lib/gistribute.rb +33 -0
- data/spec/.rubocop.yml +46 -0
- data/spec/gistribute/cli_auth_spec.rb +133 -0
- data/spec/gistribute/cli_install_spec.rb +193 -0
- data/spec/gistribute/cli_spec.rb +31 -0
- data/spec/gistribute/cli_upload_spec.rb +161 -0
- data/spec/gistribute_spec.rb +15 -27
- data/spec/spec_helper.rb +40 -0
- data/spec/support/cli.rb +27 -0
- data/spec/support/constants.rb +4 -0
- metadata +123 -6
data/lib/cli.rb
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "cli/auth"
|
4
|
+
require "cli/install"
|
5
|
+
require "cli/upload"
|
6
|
+
|
7
|
+
module Gistribute
|
8
|
+
class CLI
|
9
|
+
def initialize
|
10
|
+
@options = OptimistXL.options do
|
11
|
+
version "gistribute #{File.read(File.expand_path('../VERSION', __dir__)).strip}"
|
12
|
+
|
13
|
+
banner <<~BANNER
|
14
|
+
#{version}
|
15
|
+
|
16
|
+
Usage:
|
17
|
+
gistribute SUBCOMMAND [OPTION]... INPUT
|
18
|
+
|
19
|
+
Try `gistribute SUBCOMMAND -h` for more info. Available subcommands:
|
20
|
+
* login: log into your GitHub account
|
21
|
+
* logout: log out of your GitHub account
|
22
|
+
* install: download and install files from a gistribution
|
23
|
+
* upload: upload a new gistribution
|
24
|
+
|
25
|
+
Options:
|
26
|
+
BANNER
|
27
|
+
|
28
|
+
opt :version, "display version number"
|
29
|
+
opt :help, "display a help message"
|
30
|
+
|
31
|
+
# Sub-commands can't access the version from this scope for whatever reason
|
32
|
+
v = version
|
33
|
+
|
34
|
+
subcmd :login, "log into your GitHub account"
|
35
|
+
subcmd :logout, "log out of your GitHub account"
|
36
|
+
|
37
|
+
subcmd :install, "install from a gistribution" do
|
38
|
+
banner <<~BANNER
|
39
|
+
#{v}
|
40
|
+
|
41
|
+
Usage:
|
42
|
+
gistribute install [OPTION]... URL_OR_ID
|
43
|
+
|
44
|
+
Options:
|
45
|
+
BANNER
|
46
|
+
|
47
|
+
opt :yes, "install files without prompting"
|
48
|
+
opt :force, "overwrite existing files without prompting"
|
49
|
+
end
|
50
|
+
|
51
|
+
subcmd :upload, "upload a gistribution" do
|
52
|
+
banner <<~BANNER
|
53
|
+
#{v}
|
54
|
+
|
55
|
+
Usage:
|
56
|
+
gistribute upload [OPTION]... FILE...
|
57
|
+
gistribute upload [OPTION]... DIRECTORY
|
58
|
+
|
59
|
+
Options:
|
60
|
+
BANNER
|
61
|
+
|
62
|
+
opt :description, "description for the Gist", type: :string
|
63
|
+
opt :private, "use a private Gist"
|
64
|
+
opt :yes, "upload files without prompting"
|
65
|
+
end
|
66
|
+
|
67
|
+
educate_on_error
|
68
|
+
end
|
69
|
+
|
70
|
+
@subcommand, @global_options, @subcommand_options =
|
71
|
+
@options.subcommand, @options.global_options, @options.subcommand_options
|
72
|
+
|
73
|
+
authenticate unless @subcommand == "logout"
|
74
|
+
|
75
|
+
case @subcommand
|
76
|
+
when "install"
|
77
|
+
@gist_input = ARGV.first
|
78
|
+
when "upload"
|
79
|
+
@files = ARGV.dup
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def run
|
84
|
+
case @subcommand
|
85
|
+
when "login"
|
86
|
+
# Do nothing, #authenticate is run from the constructor
|
87
|
+
when "logout"
|
88
|
+
FileUtils.rm_rf CONFIG_FILE
|
89
|
+
puts "Logged out.".green
|
90
|
+
else
|
91
|
+
if ARGV.empty?
|
92
|
+
OptimistXL.educate
|
93
|
+
end
|
94
|
+
|
95
|
+
eval @subcommand
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def confirm?(prompt)
|
100
|
+
print prompt
|
101
|
+
input = $stdin.gets.strip.downcase
|
102
|
+
|
103
|
+
input.start_with?("y") || input.empty?
|
104
|
+
end
|
105
|
+
|
106
|
+
def get_input(prompt)
|
107
|
+
print prompt
|
108
|
+
$stdin.gets.strip
|
109
|
+
end
|
110
|
+
|
111
|
+
# Prints an error message and exits the program.
|
112
|
+
def panic!(message)
|
113
|
+
$stderr.puts "#{'Error'.red}: #{message}"
|
114
|
+
exit 1
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
data/lib/gistribute.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "colorize"
|
4
|
+
require "fileutils"
|
5
|
+
require "json"
|
6
|
+
require "launchy"
|
7
|
+
require "octokit"
|
8
|
+
require "optimist_xl"
|
9
|
+
|
10
|
+
require "cli"
|
11
|
+
|
12
|
+
CLIENT_ID = "3f37dc8255e5ab891c3d"
|
13
|
+
CONFIG_FILE = "#{Dir.home}/.config/gistribute".freeze
|
14
|
+
|
15
|
+
module Gistribute
|
16
|
+
class << self
|
17
|
+
# The user may enter either the full URL or just the ID, this function
|
18
|
+
# will parse it out of the input.
|
19
|
+
def parse_id(str)
|
20
|
+
str[%r{(^|/)([[:xdigit:]]+)}, 2]
|
21
|
+
end
|
22
|
+
|
23
|
+
# Encodes a file path for use in the Gist filename
|
24
|
+
def encode(filename)
|
25
|
+
filename.sub(/^#{Dir.home}/, "~").gsub("/", "|")
|
26
|
+
end
|
27
|
+
|
28
|
+
# Decodes the filename from the Gist into a usable path
|
29
|
+
def decode(filename)
|
30
|
+
filename.gsub(/[~|]/, "|" => "/", "~" => Dir.home)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/spec/.rubocop.yml
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
inherit_from: ../.rubocop.yml
|
2
|
+
|
3
|
+
RSpec/Focus:
|
4
|
+
Severity: warning
|
5
|
+
|
6
|
+
Style/SignalException:
|
7
|
+
Enabled: false
|
8
|
+
|
9
|
+
# Lots of spacing for alignment in the tests
|
10
|
+
Layout/SpaceInsideArrayLiteralBrackets:
|
11
|
+
Enabled: false
|
12
|
+
|
13
|
+
Lint/AmbiguousBlockAssociation:
|
14
|
+
Enabled: false
|
15
|
+
|
16
|
+
################################################################################
|
17
|
+
|
18
|
+
RSpec/ContextWording:
|
19
|
+
Enabled: false
|
20
|
+
|
21
|
+
RSpec/EmptyExampleGroup:
|
22
|
+
Enabled: false
|
23
|
+
|
24
|
+
RSpec/ExampleLength:
|
25
|
+
Enabled: false
|
26
|
+
|
27
|
+
RSpec/InstanceVariable:
|
28
|
+
Enabled: false
|
29
|
+
|
30
|
+
RSpec/MultipleExpectations:
|
31
|
+
Enabled: false
|
32
|
+
|
33
|
+
RSpec/NestedGroups:
|
34
|
+
Enabled: false
|
35
|
+
|
36
|
+
RSpec/NoExpectationExample:
|
37
|
+
Enabled: false
|
38
|
+
|
39
|
+
RSpec/ScatteredLet:
|
40
|
+
Enabled: false
|
41
|
+
|
42
|
+
RSpec/SortMetadata:
|
43
|
+
Enabled: false
|
44
|
+
|
45
|
+
RSpec/VerifiedDoubles:
|
46
|
+
Enabled: false
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
DEVICE_RES = {
|
6
|
+
"device_code" => "testdevicecode",
|
7
|
+
"expires_in" => "899",
|
8
|
+
"interval" => "1",
|
9
|
+
"user_code" => "1337-6969",
|
10
|
+
"verification_uri" => "https://github.com/login/device"
|
11
|
+
}.freeze
|
12
|
+
|
13
|
+
PENDING_RES = {
|
14
|
+
"error" => "authorization_pending",
|
15
|
+
"error_description" => "The authorization request is still pending.",
|
16
|
+
"error_uri" => "https://docs.github.com/developers/apps/authorizing-oauth-apps#error-codes-for-the-device-flow"
|
17
|
+
}.freeze
|
18
|
+
|
19
|
+
EXPIRED_RES = {
|
20
|
+
"error" => "expired_token",
|
21
|
+
"error_description" => "The `device code` has expired.",
|
22
|
+
"error_uri" => "https://docs.github.com/developers/apps/authorizing-oauth-apps#error-codes-for-the-device-flow"
|
23
|
+
}.freeze
|
24
|
+
|
25
|
+
SUCCESS_RES = {
|
26
|
+
"access_token" => OAUTH_TOKEN,
|
27
|
+
"scope" => "gist",
|
28
|
+
"token_type" => "bearer"
|
29
|
+
}.freeze
|
30
|
+
|
31
|
+
def mock_oauth_response(res)
|
32
|
+
allow(Net::HTTP).to receive(:post_form)
|
33
|
+
.with(URI("https://github.com/login/oauth/access_token"), anything)
|
34
|
+
.and_return(:res2)
|
35
|
+
allow(URI).to receive(:decode_www_form)
|
36
|
+
.with(:res2)
|
37
|
+
.and_return(res)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Hack to get the `:resX` symbols to fall through into #decode_www_form
|
41
|
+
class Symbol
|
42
|
+
def body
|
43
|
+
itself
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe Gistribute::CLI do
|
48
|
+
before do
|
49
|
+
allow(Launchy).to receive(:open)
|
50
|
+
allow(File).to receive(:write)
|
51
|
+
suppress_stdout
|
52
|
+
end
|
53
|
+
|
54
|
+
let(:http_ok) { Net::HTTPOK.new("1.1", "200", "OK") }
|
55
|
+
|
56
|
+
describe "#authenticate" do
|
57
|
+
context "when there is no access key saved" do
|
58
|
+
before do
|
59
|
+
allow(File).to receive(:exist?).and_return(false)
|
60
|
+
|
61
|
+
allow(Net::HTTP).to receive(:post_form)
|
62
|
+
.with(URI("https://github.com/login/device/code"), anything)
|
63
|
+
.and_return(:res1)
|
64
|
+
allow(URI).to receive(:decode_www_form)
|
65
|
+
.with(:res1)
|
66
|
+
.and_return(DEVICE_RES.to_a)
|
67
|
+
end
|
68
|
+
|
69
|
+
describe "the initial output" do
|
70
|
+
before do
|
71
|
+
mock_oauth_response SUCCESS_RES
|
72
|
+
run "login"
|
73
|
+
end
|
74
|
+
|
75
|
+
it "prints the user code" do
|
76
|
+
expect($stdout).to have_received(:write)
|
77
|
+
.with(a_string_matching DEVICE_RES["user_code"])
|
78
|
+
end
|
79
|
+
|
80
|
+
it "prints the verification URI" do
|
81
|
+
expect($stdout).to have_received(:write)
|
82
|
+
.with(a_string_matching DEVICE_RES["verification_uri"])
|
83
|
+
end
|
84
|
+
|
85
|
+
it "opens the verification URI in a browser" do
|
86
|
+
expect(Launchy).to have_received(:open).with(DEVICE_RES["verification_uri"])
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
describe "when the response is a success" do
|
91
|
+
before do
|
92
|
+
mock_oauth_response SUCCESS_RES
|
93
|
+
allow(Octokit::Client).to receive(:new).and_call_original
|
94
|
+
run "login"
|
95
|
+
end
|
96
|
+
|
97
|
+
it "writes the token to the config file" do
|
98
|
+
expect(File).to have_received(:write).with(CONFIG_FILE, OAUTH_TOKEN)
|
99
|
+
end
|
100
|
+
|
101
|
+
it "logs in with Octokit" do
|
102
|
+
expect(Octokit::Client).to have_received(:new)
|
103
|
+
.with(a_hash_including access_token: OAUTH_TOKEN)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
context "when the response is timed out" do
|
108
|
+
before do
|
109
|
+
mock_oauth_response EXPIRED_RES
|
110
|
+
%i[puts print].each { |p| allow($stderr).to receive p }
|
111
|
+
run "login", fail_on_exit: false
|
112
|
+
end
|
113
|
+
|
114
|
+
let(:error) { "#{'Error'.red}: Token expired! Please try again." }
|
115
|
+
|
116
|
+
it "displays an error" do
|
117
|
+
expect($stderr).to have_received(:puts).with(error)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
describe "the `logout` subcommand" do
|
124
|
+
before do
|
125
|
+
allow(FileUtils).to receive(:rm_rf)
|
126
|
+
run "logout"
|
127
|
+
end
|
128
|
+
|
129
|
+
it "deletes the auth token" do
|
130
|
+
expect(FileUtils).to have_received(:rm_rf).with(CONFIG_FILE)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,193 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
NEW_DIR_FILENAME = "nested/test.file"
|
6
|
+
MULTI_FILENAMES = ["file1", "dir/file2"].freeze
|
7
|
+
|
8
|
+
SINGLE_FILE_CONTENTS = "Line 1\nLine 2\n"
|
9
|
+
|
10
|
+
# Test Gists
|
11
|
+
PUB_SINGLE_FILE_ID = "4346763"
|
12
|
+
SEC_SINGLE_FILE_ID = "5fa3c6bab88036e95d62cadf15128ec3"
|
13
|
+
NO_TITLE_ID = "5865a130f9cf40acd9f0e85d15e601a7"
|
14
|
+
NO_PIPE_SPACING_ID = "5677679b0db25054521753e5d59bed3d"
|
15
|
+
NON_EXISTENT_DIR_ID = "8c86bf9cda921ebe7ad1bf0c46afb108"
|
16
|
+
MULTI_FILE_ID = "8d4a2a4c8fe0b1427fed39c939857a40"
|
17
|
+
CWD_ID = "3c7006e629fcc54262ef02a5b2204735"
|
18
|
+
HOME_ID = "acb6caa80886101a68c5c85e4c100ddb"
|
19
|
+
|
20
|
+
def test_single_file(id, path)
|
21
|
+
before { run "install", id }
|
22
|
+
|
23
|
+
# TEMP already gets `rm -rf`ed in the `cli_spec.rb` #after
|
24
|
+
unless path == TEMP
|
25
|
+
after { FileUtils.rm "#{path}/#{FILENAME}" }
|
26
|
+
end
|
27
|
+
|
28
|
+
let(:file_contents) { File.read "#{path}/#{FILENAME}" }
|
29
|
+
|
30
|
+
it "downloads the file into #{path}" do
|
31
|
+
expect(file_contents).to eq SINGLE_FILE_CONTENTS
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe Gistribute::CLI do
|
36
|
+
before do
|
37
|
+
FileUtils.rm_rf TEMP
|
38
|
+
FileUtils.mkdir_p TEMP
|
39
|
+
suppress_stdout
|
40
|
+
end
|
41
|
+
|
42
|
+
after { FileUtils.rm_rf TEMP }
|
43
|
+
|
44
|
+
describe "#install" do
|
45
|
+
context "when user inputs `y` at the installation prompt" do
|
46
|
+
before { simulate_user_input "y\n" }
|
47
|
+
|
48
|
+
{
|
49
|
+
"public single file": PUB_SINGLE_FILE_ID,
|
50
|
+
"secret single file": SEC_SINGLE_FILE_ID,
|
51
|
+
"no title": NO_TITLE_ID,
|
52
|
+
"no || spacing": NO_PIPE_SPACING_ID
|
53
|
+
}.each do |description, id|
|
54
|
+
context "when run with a #{description} Gist" do
|
55
|
+
test_single_file id, TEMP
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
context "when given a directory that doesn't exist" do
|
60
|
+
before { run "install", NON_EXISTENT_DIR_ID }
|
61
|
+
|
62
|
+
let(:file_contents) { File.read "#{TEMP}/#{NEW_DIR_FILENAME}" }
|
63
|
+
|
64
|
+
it "creates the directory" do
|
65
|
+
expect(file_contents).to eq SINGLE_FILE_CONTENTS
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
context "when run with a multi-file Gist" do
|
70
|
+
before { run "install", MULTI_FILE_ID }
|
71
|
+
|
72
|
+
let(:file1_contents) { File.read "#{TEMP}/#{MULTI_FILENAMES[0]}" }
|
73
|
+
let(:file2_contents) { File.read "#{TEMP}/#{MULTI_FILENAMES[1]}" }
|
74
|
+
|
75
|
+
it "downloads the files into the correct locations" do
|
76
|
+
[file1_contents, file2_contents].each_with_index do |result, i|
|
77
|
+
file_number = i + 1
|
78
|
+
expect(result).to eq "F#{file_number}L1\nF#{file_number}L2\n"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
context "when given a file for the current working directory" do
|
84
|
+
test_single_file CWD_ID, Dir.pwd
|
85
|
+
end
|
86
|
+
|
87
|
+
context "when given a file for the home directory" do
|
88
|
+
test_single_file HOME_ID, Dir.home
|
89
|
+
end
|
90
|
+
|
91
|
+
context "when given a bad ID (404)" do
|
92
|
+
before do
|
93
|
+
%i[puts print].each { |p| allow($stderr).to receive p }
|
94
|
+
run "install", "bad", fail_on_exit: false
|
95
|
+
end
|
96
|
+
|
97
|
+
it "prints the error to STDERR" do
|
98
|
+
expect($stderr).to have_received(:print).with <<~EOS.chop.red
|
99
|
+
\rThere was an error downloading the requested Gist.
|
100
|
+
The error is as follows:
|
101
|
+
EOS
|
102
|
+
|
103
|
+
expect($stderr).to have_received(:puts).with(" 404 Not Found")
|
104
|
+
expect($stderr).to have_received(:print).with("The ID that was queried is: ".red)
|
105
|
+
expect($stderr).to have_received(:puts).with("bad")
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
context "when user inputs nothing at the installation prompt" do
|
111
|
+
before do
|
112
|
+
simulate_user_input "\n"
|
113
|
+
run "install", PUB_SINGLE_FILE_ID
|
114
|
+
end
|
115
|
+
|
116
|
+
it "saves the file" do
|
117
|
+
expect(File).to exist("#{TEMP}/#{FILENAME}")
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
%w[n m].each do |ch|
|
122
|
+
context "when user inputs `#{ch}` at the installation prompt" do
|
123
|
+
before do
|
124
|
+
simulate_user_input "#{ch}\n"
|
125
|
+
run "install", PUB_SINGLE_FILE_ID
|
126
|
+
end
|
127
|
+
|
128
|
+
it "doesn't save the file" do
|
129
|
+
expect(File).not_to exist("#{TEMP}/#{FILENAME}")
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
context "when a file would be overwritten" do
|
135
|
+
let(:orig_content) { "original" }
|
136
|
+
|
137
|
+
before do
|
138
|
+
FileUtils.mkdir_p "#{TEMP}/dir"
|
139
|
+
MULTI_FILENAMES.each do |filename|
|
140
|
+
File.write("#{TEMP}/#{filename}", orig_content)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
let(:file1_contents) { File.read "#{TEMP}/#{MULTI_FILENAMES[0]}" }
|
145
|
+
let(:file2_contents) { File.read "#{TEMP}/#{MULTI_FILENAMES[1]}" }
|
146
|
+
|
147
|
+
context "when the user inputs `y` at the file overwrite prompts" do
|
148
|
+
before do
|
149
|
+
simulate_user_input "y\n", "y\n", "y\n"
|
150
|
+
run "install", MULTI_FILE_ID
|
151
|
+
end
|
152
|
+
|
153
|
+
it "downloads the files into the correct locations" do
|
154
|
+
expect([file1_contents, file2_contents]).not_to include orig_content
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
context "when the user inputs `n` at the file overwrite prompts" do
|
159
|
+
before do
|
160
|
+
simulate_user_input "y\n", "n\n", "n\n"
|
161
|
+
run "install", MULTI_FILE_ID
|
162
|
+
end
|
163
|
+
|
164
|
+
it "doesn't download the files" do
|
165
|
+
expect([file1_contents, file2_contents]).to all eq orig_content
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
context "with the `--force` flag" do
|
170
|
+
before do
|
171
|
+
simulate_user_input "y\n"
|
172
|
+
run "install", "--force", MULTI_FILE_ID
|
173
|
+
end
|
174
|
+
|
175
|
+
it "overwrites the files without prompting" do
|
176
|
+
[file1_contents, file2_contents].each do |result|
|
177
|
+
expect(result).not_to eq orig_content
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
context "when ran with the --yes flag" do
|
184
|
+
before { run "install", "--yes", PUB_SINGLE_FILE_ID }
|
185
|
+
|
186
|
+
let(:file_contents) { File.read "#{TEMP}/#{FILENAME}" }
|
187
|
+
|
188
|
+
it "downloads the file without prompting" do
|
189
|
+
expect(file_contents).to eq SINGLE_FILE_CONTENTS
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
describe Gistribute::CLI do
|
6
|
+
before { suppress_stdout }
|
7
|
+
|
8
|
+
%w[install upload].each do |subcmd|
|
9
|
+
describe "the `#{subcmd}` subcommand" do
|
10
|
+
context "when no argument is provided" do
|
11
|
+
it "shows the help screen" do
|
12
|
+
allow(OptimistXL).to receive(:educate).and_call_original
|
13
|
+
run subcmd, fail_on_exit: false
|
14
|
+
|
15
|
+
expect(OptimistXL).to have_received :educate
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
context "when run with the `--version` flag" do
|
22
|
+
let :version do
|
23
|
+
File.read(File.expand_path("../../VERSION", __dir__)).strip
|
24
|
+
end
|
25
|
+
|
26
|
+
it "outputs the version number" do
|
27
|
+
expect { run "--version", fail_on_exit: false }
|
28
|
+
.to output(a_string_matching(version)).to_stdout
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|