snow_sync 3.1.2 → 3.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 7d72e4b145e73ab7e226651173c037d5e104aa06
4
- data.tar.gz: c94c90bda93ccd9680bb0e4bc641030fe1ae75fb
2
+ SHA256:
3
+ metadata.gz: 006d22f37ee45ef3de904baba95ba0ee0e6a80dc25259741bbab58e9e34db4ee
4
+ data.tar.gz: 0e31d3394fc906562060141146dc15032c8701d7da5ec76495b9cc7d17e68bd6
5
5
  SHA512:
6
- metadata.gz: 07568e10689e39c77d50aac5dc8dd9a73f9f4fda63ebb8e02bd930ff231c16f0eafad56d8240ffcb816e6fa58a37c93f8aece453278ae0b56e0995970d46fa5f
7
- data.tar.gz: d536814539b196f09019e80eeb2c004fbe10db7ebab8cf6566ede6cd93c045462ea9b8e129b0ed5bcc39de5d69e182be330b156380eddd4eb881ee05720bd98b
6
+ metadata.gz: f45e339f65b62bc897d697b1bb46f442cbb6a48f5ab6728701d7d1415efa91223725db708237b7637e8c34c8ad756b18d309a10a6a2a78ffeb3cc4c452637fe6
7
+ data.tar.gz: 4e3701f3d65594d6d284b99e86f8694d9565e87546570440ba89842163c1a1493aecdbbbc10e9ee755369f5e6a2ce66c351947b584b3d8bca5b8d9c13c640a20
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # SnowSync
2
2
 
3
- [![Gem Version](https://img.shields.io/badge/gem-v3.1.2-brightgreen.svg)](https://rubygems.org/gems/snow_sync) [![Dependency Status](https://img.shields.io/badge/dependencies-up--to--date-blue.svg)](https://rubygems.org/gems/snow_sync) [![Downloads](https://img.shields.io/badge/downloads-5k%2B-lightgrey.svg)](https://rubygems.org/gems/snow_sync)
3
+ [![Gem Version](https://img.shields.io/badge/gem-v3.1.5-brightgreen.svg)](https://rubygems.org/gems/snow_sync) [![Dependency Status](https://img.shields.io/badge/dependencies-up--to--date-blue.svg)](https://rubygems.org/gems/snow_sync) [![Downloads](https://img.shields.io/badge/downloads-25k%2B-lightgrey.svg)](https://rubygems.org/gems/snow_sync)
4
4
 
5
5
  SnowSync is a file sync utility tool and API which provides a bridge for off platform ServiceNow development using an IDE or text editor locally.
6
6
 
@@ -9,15 +9,15 @@ SnowSync syncronizes configured fields (scripts) for a ServiceNow instance local
9
9
  ## Installation
10
10
 
11
11
  ```bash
12
- mkdir snow_sync
12
+ mkdir snow-sync
13
13
  ```
14
14
 
15
15
  ```bash
16
- cd snow_sync
16
+ cd snow-sync
17
17
  ```
18
18
 
19
19
  ```ruby
20
- gem install --install-dir <path-to-the-snow_sync-dir> snow_sync
20
+ gem install --install-dir <path-to-the-snow-sync-dir> snow_sync
21
21
  ```
22
22
 
23
23
  ## Setup & Usage
@@ -30,19 +30,27 @@ gem install bundler
30
30
  cd <path-to-the-snow_sync-dir>/gems/snow_sync-<version>
31
31
  ```
32
32
 
33
+ OSX users run the following command:
34
+
35
+ ```bash
36
+ brew install terminal-notifier
37
+ ```
38
+
33
39
  Create a Gemfile and add the following Gem dependencies:
34
40
 
35
41
  ```ruby
36
- source 'https://rubygems.org'
37
- gem 'facets', '~> 3.1.0'
38
- gem 'guard', '~> 2.14.0'
39
- gem 'guard-yield', '~> 0.1.0'
40
- gem 'json', '>= 1.8.3', '~> 1.8.0'
41
- gem 'libnotify', '~> 0.9.1'
42
- gem 'rake', '~> 10.0.0'
43
- gem 'rest-client', '~> 2.0.0'
44
- gem 'rspec', '~> 3.5.0'
45
- gem 'thor', '0.19.1'
42
+ source "https://rubygems.org"
43
+ gem "facets", "~> 3.1.0"
44
+ gem "guard", "~> 2.14.0"
45
+ gem "guard-yield", "~> 0.1.0"
46
+ gem "json", "~> 2.6.1"
47
+ gem "libnotify", "~> 0.9.2"
48
+ gem "rake", "~> 13.0.6"
49
+ gem "rest-client", "~> 2.0.0"
50
+ gem "rspec", "~> 3.10.0"
51
+ gem "rspec-core", "~> 3.10.1"
52
+ gem "terminal-notifier-guard", "~> 1.7"
53
+ gem "thor", "0.19.1"
46
54
  ```
47
55
 
48
56
  ```bash
@@ -54,12 +62,12 @@ cd /lib/snow_sync
54
62
  ```
55
63
 
56
64
  * Setup the configurations in the configs.yml
57
- * Now supports multi-table map configurations
58
- * Configuration path is the current working directory
65
+ * Supports multi-table map configurations
66
+ * YAML configuration path is the current working directory
59
67
  * Append /api/now/table/ to the base_url
60
68
 
61
69
  ```bash
62
- guard -i
70
+ bundle exec guard -i
63
71
  ```
64
72
 
65
73
  **Note:** if the sync directory is deleted after a successful sync, reset the credential configs in the configs.yml so they can be re-encrypted on the next sync
@@ -68,17 +76,22 @@ guard -i
68
76
  ## Running the Tests
69
77
 
70
78
  ```bash
71
- cd <path-to-the-snow_sync-dir>/gems/snow_sync-<version>
79
+ cd <path-to-the-snow-sync-dir>/gems/snow_sync-<version>
72
80
  ```
73
81
 
74
- * Integration tests use a test record in the instance (e.g. a script include)
75
- * Unit tests are all stubbed out
82
+ * Integration tests use a test record in the test instance
76
83
  * Setup the test configurations in the test_configs.yml
77
- * Configuration path is the current working directory
84
+ * YAML configuration path is the current working directory
78
85
  * Append /api/now/table/ to the base_url
79
86
 
80
87
  ```ruby
81
- rspec spec/sync_util_spec.rb
88
+ bundle exec rspec spec/sync_util_spec.rb
89
+ ```
90
+
91
+ * Unit tests are pure, so they're not externally dependent
92
+
93
+ ```ruby
94
+ bundle exec rspec spec/sync_util_mock_spec.rb
82
95
  ```
83
96
 
84
97
  **Note:** if the sync directory is deleted after a successful sync, reset the credential configs in the test_configs.yml so they can be re-encrypted on the next sync
@@ -16,18 +16,19 @@ yield_options = {
16
16
  logger.info "Guard::Yield - Done!"
17
17
  end,
18
18
 
19
- run_on_modifications: proc do |log, _, files|
20
- log.info "!!: #{files * ','}"
19
+ run_on_modifications: proc do |logger, _, files|
21
20
  @util.push_modifications(files)
22
- @util.notify(files, log)
21
+ logger.info "!!: #{files}"
22
+ uname = `uname`.chomp.downcase
23
+ @util.notify(files, uname, logger)
23
24
  end,
24
25
 
25
- run_on_additions: proc do |log, _, files|
26
- log.info "++: #{files * ','}"
26
+ run_on_additions: proc do |logger, _, files|
27
+ logger.info "++: #{files}"
27
28
  end,
28
29
 
29
- run_on_removals: proc do |log, _, files|
30
- log.warn "xx: #{files * ','}"
30
+ run_on_removals: proc do |logger, _, files|
31
+ logger.warn "xx: #{files}"
31
32
  end,
32
33
 
33
34
  }
@@ -6,6 +6,7 @@ base_url:
6
6
  creds:
7
7
  user:
8
8
  pass:
9
+ encrypted:
9
10
 
10
11
  table_map:
11
12
  script_include:
@@ -75,8 +75,7 @@ module SnowSync
75
75
  # Encrypts config credentials based on previous sync
76
76
 
77
77
  def encrypt_credentials
78
- previous_sync = File.directory?("sync")
79
- if !previous_sync
78
+ unless @configs["creds"]["encrypted"]
80
79
  configs_path = @configs["conf_path"] + @cf
81
80
  configs = YAML::load_file(configs_path)
82
81
  # local configuration changes
@@ -84,10 +83,12 @@ module SnowSync
84
83
  passb64 = Base64.strict_encode64(@configs["creds"]["pass"])
85
84
  configs["creds"]["user"] = userb64
86
85
  configs["creds"]["pass"] = passb64
86
+ configs["creds"]["encrypted"] = true
87
87
  File.open(configs_path, 'w') { |f| YAML::dump(configs, f) }
88
88
  # object state configuration changes
89
89
  @configs["creds"]["user"] = userb64
90
90
  @configs["creds"]["pass"] = passb64
91
+ @configs["creds"]["encrypted"] = true
91
92
  end
92
93
  end
93
94
 
@@ -155,9 +156,8 @@ module SnowSync
155
156
  # Merges JS file changes with the encapsulated table response value
156
157
  # @param [String] type Config table label
157
158
  # @param [String] file JS file by name
158
- # @param [Hash] table_hash Configured servicenow table hash
159
159
 
160
- def merge_update(type, file, table_hash)
160
+ def merge_update(type, file)
161
161
  FileUtils.cd("sync" + "/" + type)
162
162
  script_body = File.open(file).read
163
163
  @configs["table_map"][type]["mod"] = script_body
@@ -182,7 +182,7 @@ module SnowSync
182
182
  type = path[1]
183
183
  file = path[2]
184
184
  table_hash = table_lookup(type, file)
185
- merge_update(type, file, table_hash)
185
+ merge_update(type, file)
186
186
  begin
187
187
  user = Base64.strict_decode64(@configs["creds"]["user"])
188
188
  pass = Base64.strict_decode64(@configs["creds"]["pass"])
@@ -201,22 +201,22 @@ module SnowSync
201
201
 
202
202
  # Dispatches osx & linux platform notification when file updates are pushed
203
203
  # @param [Array] update File updated
204
- # @param [Object] log Log Object
204
+ # @param [String] uname Operating system name
205
+ # @param [Object] logger Logger object
205
206
 
206
- def notify(update, log)
207
- if `uname` =~ /Darwin/
207
+ def notify(update, uname, logger)
208
+ if uname =~ /darwin/
208
209
  TerminalNotifier::Guard.success(
209
- "File: #{update * ','}",
210
+ "File: #{update}",
210
211
  :title => "ServiceNow Script Update"
211
212
  )
212
- log.info "->: osx notification dispatched"
213
- elsif `uname` =~ /Linux/
213
+ logger.info "->: osx notification dispatched"
214
+ elsif uname =~ /linux/
214
215
  Libnotify.show(
215
216
  :summary => "ServiceNow Script Update",
216
- :body => "File: #{update * ','}",
217
- :timeout => 1.5
217
+ :body => "File: #{update}"
218
218
  )
219
- log.info "->: linux notification dispatched"
219
+ logger.info "->: linux notification dispatched"
220
220
  end
221
221
  end
222
222
 
@@ -1,3 +1,3 @@
1
1
  module SnowSync
2
- VERSION = "3.1.2"
2
+ VERSION = "3.1.6"
3
3
  end
@@ -0,0 +1,30 @@
1
+ describe "Dockerfile" do
2
+
3
+ def run_os_check(command)
4
+ `#{command}`.downcase
5
+ end
6
+
7
+ it "installs the right version of Ubuntu" do
8
+ os = "ubuntu"
9
+ version = "20.04"
10
+ installation = run_os_check('lsb_release -a')
11
+ expect(installation).to include(os)
12
+ expect(installation).to include(version)
13
+ end
14
+
15
+ it "installs vim" do
16
+ installation = run_os_check('vim --version')
17
+ expect(installation).to include('vim - vi')
18
+ end
19
+
20
+ it "installs git" do
21
+ installation = run_os_check('git version')
22
+ expect(installation).to include('git version')
23
+ end
24
+
25
+ it "installs ruby" do
26
+ installation = run_os_check('ruby -v')
27
+ expect(installation).to include('ruby')
28
+ end
29
+
30
+ end
data/spec/spec_helper.rb CHANGED
@@ -5,8 +5,8 @@ RSpec.configure do |config|
5
5
  require "json"
6
6
  require_relative "../lib/snow_sync/sync_util.rb"
7
7
 
8
- @util = SnowSync::SyncUtil.new(opts = "test")
9
- @util.encrypt_credentials
8
+ config.color
9
+ config.formatter = :documentation
10
10
 
11
11
  config.expect_with :rspec do |expectations|
12
12
  expectations.include_chain_clauses_in_custom_matcher_descriptions = true
@@ -0,0 +1,549 @@
1
+ require "spec_helper"
2
+
3
+ describe "#initialize" do
4
+
5
+ before do
6
+ allow(YAML).to receive(:load_file)
7
+ allow(Logger).to receive(:new)
8
+ end
9
+
10
+ it "should construct an instance" do
11
+ SnowSync::SyncUtil.new(opts = "test")
12
+ expect(YAML).to have_received(:load_file).with("test_configs.yml")
13
+ expect(Logger).to have_received(:new).with(STDERR)
14
+ end
15
+
16
+ end
17
+
18
+ describe "#create_directory" do
19
+
20
+ let :util do
21
+ SnowSync::SyncUtil.new(opts = "test")
22
+ end
23
+
24
+ let :logger do
25
+ instance_double(Logger)
26
+ end
27
+
28
+ let :dir_name do
29
+ "sync"
30
+ end
31
+
32
+ before do
33
+ allow(File).to receive(:directory?).and_return(false)
34
+ allow(FileUtils).to receive(:mkdir_p)
35
+ allow(Logger).to receive(:new).and_return(logger)
36
+ allow(logger).to receive(:info)
37
+ end
38
+
39
+ it "should create a directory" do
40
+ util.create_directory(dir_name)
41
+ expect(File).to have_received(:directory?).with(dir_name)
42
+ expect(FileUtils).to have_received(:mkdir_p).with(dir_name)
43
+ expect(logger).to have_received(:info).with("++: #{dir_name}")
44
+ end
45
+
46
+ end
47
+
48
+ describe "#create_subdirectory" do
49
+
50
+ let :util do
51
+ SnowSync::SyncUtil.new(opts = "test")
52
+ end
53
+
54
+ let :path do
55
+ proc { FileUtils.cd("sync") }
56
+ end
57
+
58
+ let :sub_dir_name do
59
+ "sub-dir-of-sync"
60
+ end
61
+
62
+ before do
63
+ allow_any_instance_of(SnowSync::SyncUtil).to receive(:create_directory)
64
+ .and_return("++: #{sub_dir_name}")
65
+ end
66
+
67
+ it "should create a subdirectory" do
68
+ expect(util.create_subdirectory(sub_dir_name, &path)).to eq("++: #{sub_dir_name}")
69
+ end
70
+
71
+ end
72
+
73
+ describe "#create_file" do
74
+
75
+ let :util do
76
+ SnowSync::SyncUtil.new(opts = "test")
77
+ end
78
+
79
+ let :logger do
80
+ instance_double(Logger)
81
+ end
82
+
83
+ let :file_name do
84
+ "test_file"
85
+ end
86
+
87
+ let :json do
88
+ { "test-json": "testing" }
89
+ end
90
+
91
+ let :path do
92
+ proc { }
93
+ end
94
+
95
+ before do
96
+ allow(File).to receive(:open).and_call_original
97
+ allow(Logger).to receive(:new).and_return(logger)
98
+ allow(logger).to receive(:info)
99
+ end
100
+
101
+ after do
102
+ test_file = `ls`.split("\n").pop
103
+ FileUtils.rm_rf(test_file)
104
+ end
105
+
106
+ it "should create a js file" do
107
+ util.create_file(file_name, json, &path)
108
+ expect(File).to have_received(:open).with(file_name + ".js", "w")
109
+ expect(logger).to have_received(:info).with("->: #{file_name}" + ".js")
110
+ expect(`ls`.split("\n").pop).to eq("test_file.js")
111
+ expect(File.open("#{file_name}.js").read).to eq("{:\"test-json\"=>\"testing\"}")
112
+ end
113
+
114
+ end
115
+
116
+ describe "#check_required_configs" do
117
+
118
+ let :util do
119
+ SnowSync::SyncUtil.new(opts = "test")
120
+ end
121
+
122
+ it "should raise an exception when there is no config path" do
123
+ util.configs["conf_path"] = nil
124
+ expect{util.check_required_configs}.to raise_error(
125
+ /Check the configuration path, base url, credentials or table to sync/)
126
+ end
127
+
128
+ it "should raise an exception when there is no base url" do
129
+ util.configs["base_url"] = nil
130
+ expect{util.check_required_configs}.to raise_error(
131
+ /Check the configuration path, base url, credentials or table to sync/)
132
+ end
133
+
134
+ it "should raise an exception when there is no username" do
135
+ util.configs["creds"]["user"] = nil
136
+ expect{util.check_required_configs}.to raise_error(
137
+ /Check the configuration path, base url, credentials or table to sync/)
138
+ end
139
+
140
+ it "should raise an exception when there is no password" do
141
+ util.configs["creds"]["pass"] = nil
142
+ expect{util.check_required_configs}.to raise_error(
143
+ /Check the configuration path, base url, credentials or table to sync/)
144
+ end
145
+
146
+ it "should raise an exception when there are no tables mapped" do
147
+ tables = util.configs["table_map"].keys
148
+ tables.each do |table|
149
+ util.configs["table_map"][table]["table"] = nil
150
+ expect{util.check_required_configs}.to raise_error(
151
+ /Check the configuration path, base url, credentials or table to sync/)
152
+ end
153
+ end
154
+
155
+ end
156
+
157
+ describe "#encrypt_credentials" do
158
+
159
+ let :util do
160
+ SnowSync::SyncUtil.new(opts = "test")
161
+ end
162
+
163
+ before do
164
+ FileUtils.touch("spec/test_configs.yml")
165
+ local_configs = {
166
+ "conf_path" => "spec/",
167
+ "base_url" => nil,
168
+ "creds" => {
169
+ "user" => "test-name",
170
+ "pass" => "test-password",
171
+ "encrypted" => false
172
+ }
173
+ }
174
+ allow(YAML).to receive(:load_file).and_return(local_configs)
175
+ allow(Base64).to receive(:strict_encode64).and_call_original
176
+ allow(File).to receive(:open).and_call_original
177
+ end
178
+
179
+ after do
180
+ FileUtils.rm_rf("spec/test_configs.yml")
181
+ end
182
+
183
+ it "should encrypt credentials" do
184
+ util.configs["conf_path"] = "spec/"
185
+ util.configs["creds"]["user"] = "test-name"
186
+ util.configs["creds"]["pass"] = "test-password"
187
+ util.encrypt_credentials
188
+ expect(YAML).to have_received(:load_file).with("spec/test_configs.yml")
189
+ allow(Base64).to receive(:strict_encode64).twice
190
+ expect(File).to have_received(:open).with("spec/test_configs.yml", "w")
191
+ # configs object state updates
192
+ expect(util.configs["creds"]["user"]).to eq("dGVzdC1uYW1l")
193
+ expect(util.configs["creds"]["pass"]).to eq("dGVzdC1wYXNzd29yZA==")
194
+ expect(util.configs["creds"]["encrypted"]).to eq(true)
195
+ # test configs file updates
196
+ encrypted_content = YAML::load_file("spec/test_configs.yml")
197
+ expect(encrypted_content["creds"]["user"]).to eq("dGVzdC1uYW1l")
198
+ expect(encrypted_content["creds"]["pass"]).to eq("dGVzdC1wYXNzd29yZA==")
199
+ expect(encrypted_content["creds"]["encrypted"]).to eq(true)
200
+ end
201
+ end
202
+
203
+ describe "#run_setup_and_sync" do
204
+
205
+ let :util do
206
+ SnowSync::SyncUtil.new(opts = "test")
207
+ end
208
+
209
+ let :logger do
210
+ instance_double(Logger)
211
+ end
212
+
213
+ before do
214
+ allow(Base64).to receive(:strict_decode64).and_call_original
215
+ allow(Base64).to receive(:strict_encode64).and_call_original
216
+ allow(RestClient).to receive(:get).and_return("{\"result\":[{ \"status\": \"200\" }]}")
217
+ allow(FileUtils).to receive(:cd)
218
+ end
219
+
220
+ it "should run setup & sync - sync directory present path" do
221
+ # allow not included in 'before' setup to set return bool
222
+ allow(File).to receive(:directory?).and_return(true)
223
+ expect_any_instance_of(SnowSync::SyncUtil).to receive(:create_subdirectory)
224
+ expect_any_instance_of(SnowSync::SyncUtil).to receive(:create_file)
225
+ util.configs = { "conf_path" => nil,
226
+ "base_url" => "https://test.com/api/now/table/",
227
+ "creds" => {
228
+ "user" => "dGVzdC1uYW1l",
229
+ "pass" => "dGVzdC1wYXNzd29yZA=="
230
+ },
231
+ "table_map" => {
232
+ "script_include" => {
233
+ "name" => "TestClass",
234
+ "table" => "sys_script_include",
235
+ "sys_id" => "xxxxxx-sysid-xxxxxx",
236
+ "field" => "test-field"
237
+ }
238
+ }
239
+ }
240
+ util.run_setup_and_sync
241
+ expect(File).to have_received(:directory?).with("sync")
242
+ expect(Base64).to have_received(:strict_decode64).with("dGVzdC1uYW1l")
243
+ expect(Base64).to have_received(:strict_decode64).with("dGVzdC1wYXNzd29yZA==")
244
+ expect(Base64).to have_received(:strict_encode64).with("test-name:test-password")
245
+ expect(RestClient).to have_received(:get).with(
246
+ "https://test.com/api/now/table/sys_script_include?sysparm_query=sys_id%3D" +
247
+ "xxxxxx-sysid-xxxxxx%5Ename%3DTestClass",
248
+ {:authorization => "Basic " + "dGVzdC1uYW1lOnRlc3QtcGFzc3dvcmQ=",
249
+ :accept => "application/json"})
250
+ expect(FileUtils).to have_received(:cd).with("../..")
251
+ end
252
+
253
+ it "should run setup & sync - sync directory not present path" do
254
+ # allow not included in 'before' setup to set return bool
255
+ allow(File).to receive(:directory?).and_return(false)
256
+ expect_any_instance_of(SnowSync::SyncUtil).to receive(:create_directory)
257
+ expect_any_instance_of(SnowSync::SyncUtil).to receive(:create_subdirectory)
258
+ expect_any_instance_of(SnowSync::SyncUtil).to receive(:create_file)
259
+ util.configs = { "conf_path" => nil,
260
+ "base_url" => "https://test.com/api/now/table/",
261
+ "creds" => {
262
+ "user" => "dGVzdC1uYW1l",
263
+ "pass" => "dGVzdC1wYXNzd29yZA=="
264
+ },
265
+ "table_map" => {
266
+ "script_include" => {
267
+ "name" => "TestClass",
268
+ "table" => "sys_script_include",
269
+ "sys_id" => "xxxxxx-sysid-xxxxxx",
270
+ "field" => "test-field"
271
+ }
272
+ }
273
+ }
274
+ util.run_setup_and_sync
275
+ expect(File).to have_received(:directory?).with("sync")
276
+ expect(Base64).to have_received(:strict_decode64).with("dGVzdC1uYW1l")
277
+ expect(Base64).to have_received(:strict_decode64).with("dGVzdC1wYXNzd29yZA==")
278
+ expect(Base64).to have_received(:strict_encode64).with("test-name:test-password")
279
+ expect(RestClient).to have_received(:get).with(
280
+ "https://test.com/api/now/table/sys_script_include?sysparm_query=sys_id%3D" +
281
+ "xxxxxx-sysid-xxxxxx%5Ename%3DTestClass",
282
+ {:authorization => "Basic " + "dGVzdC1uYW1lOnRlc3QtcGFzc3dvcmQ=",
283
+ :accept => "application/json"})
284
+ expect(FileUtils).to have_received(:cd).with("../..")
285
+ end
286
+
287
+ it "should handle an exception" do
288
+ # allow not included in 'before' setup to set return bool
289
+ allow(File).to receive(:directory?).and_return(true)
290
+ expect_any_instance_of(SnowSync::SyncUtil).to receive(:create_subdirectory)
291
+ allow(RestClient).to receive(:get).and_raise(RestClient::ExceptionWithResponse)
292
+ allow(Logger).to receive(:new).and_return(logger)
293
+ allow(logger).to receive(:error)
294
+ util.configs = { "conf_path" => nil,
295
+ "base_url" => "https://test.com/api/now/table/",
296
+ "creds" => {
297
+ "user" => "dGVzdC1uYW1l",
298
+ "pass" => "dGVzdC1wYXNzd29yZA=="
299
+ },
300
+ "table_map" => {
301
+ "script_include" => {
302
+ "name" => "TestClass",
303
+ "table" => "sys_script_include",
304
+ "sys_id" => "xxxxxx-sysid-xxxxxx",
305
+ "field" => "test-field"
306
+ }
307
+ }
308
+ }
309
+ util.run_setup_and_sync
310
+ expect(logger).to have_received(:error)
311
+ .with("ERROR: RestClient::ExceptionWithResponse").once
312
+ end
313
+
314
+ end
315
+
316
+ describe "#classify" do
317
+
318
+ let :util do
319
+ SnowSync::SyncUtil.new(opts = "test")
320
+ end
321
+
322
+ it "should convert snake_case string to CamelCase" do
323
+ expect(util.classify("test_class.js")).to eq("TestClass")
324
+ end
325
+
326
+ end
327
+
328
+ describe "#table_lookup" do
329
+
330
+ let :util do
331
+ SnowSync::SyncUtil.new(opts = "test")
332
+ end
333
+
334
+ it "should return configured SN table" do
335
+ util.configs = { "conf_path" => nil,
336
+ "base_url" => "https://test.com/api/now/table/",
337
+ "creds" => {
338
+ "user" => "dGVzdC1uYW1l",
339
+ "pass" => "dGVzdC1wYXNzd29yZA=="
340
+ },
341
+ "table_map" => {
342
+ "script_include" => {
343
+ "name" => "TestClass",
344
+ "table" => "sys_script_include",
345
+ "sys_id" => "xxxxxx-sysid-xxxxxx",
346
+ "field" => "test-field"
347
+ }
348
+ }
349
+ }
350
+ table_map = util.table_lookup("script_include", "test_class.js")
351
+ expect(table_map == { "name" => "TestClass",
352
+ "table" => "sys_script_include",
353
+ "sys_id" => "xxxxxx-sysid-xxxxxx",
354
+ "field" => "test-field"
355
+ }).to be(true)
356
+ end
357
+
358
+ end
359
+
360
+ describe "#merge_update" do
361
+
362
+ let! :util do
363
+ SnowSync::SyncUtil.new(opts = "test")
364
+ end
365
+
366
+ before do
367
+ allow(FileUtils).to receive(:cd)
368
+ allow(File).to receive(:open).and_return(IO)
369
+ allow(IO).to receive(:read).and_return("var x = 10;")
370
+ end
371
+
372
+ it "should merge a script change" do
373
+ util.configs = { "table_map" => {
374
+ "script_include" => {
375
+ "name" => "TestClass",
376
+ "table" => "sys_script_include",
377
+ "sys_id" => "xxxxxx-sysid-xxxxxx",
378
+ "field" => "test-field"
379
+ }
380
+ }
381
+ }
382
+ util.merge_update("script_include", "test_class.js")
383
+ expect(File).to have_received(:open).with("test_class.js")
384
+ expect(IO).to have_received(:read)
385
+ expect(FileUtils).to have_received(:cd).twice
386
+ expect(util.configs == { "table_map" => {
387
+ "script_include" => {
388
+ "name" => "TestClass",
389
+ "table" => "sys_script_include",
390
+ "sys_id" => "xxxxxx-sysid-xxxxxx",
391
+ "field" => "test-field",
392
+ "mod" => "var x = 10;"
393
+ }
394
+ }}).to be(true)
395
+ end
396
+
397
+ end
398
+
399
+ describe "#start_sync" do
400
+
401
+ let :util do
402
+ SnowSync::SyncUtil.new(opts = "test")
403
+ end
404
+
405
+ before do
406
+ allow_any_instance_of(SnowSync::SyncUtil).to receive(:check_required_configs)
407
+ allow_any_instance_of(SnowSync::SyncUtil).to receive(:encrypt_credentials)
408
+ allow_any_instance_of(SnowSync::SyncUtil).to receive(:run_setup_and_sync)
409
+ .and_return("sync-and-setup-complete!")
410
+ end
411
+
412
+ it "should create a subdirectory" do
413
+ expect(util.start_sync).to eq("sync-and-setup-complete!")
414
+ end
415
+
416
+ end
417
+
418
+ describe "#push_modifications" do
419
+
420
+ let :util do
421
+ SnowSync::SyncUtil.new(opts = "test")
422
+ end
423
+
424
+ let :logger do
425
+ instance_double(Logger)
426
+ end
427
+
428
+ before do
429
+ allow_any_instance_of(SnowSync::SyncUtil).to receive(:table_lookup)
430
+ .and_return({
431
+ "name" => "TestClass",
432
+ "table" => "sys_script_include",
433
+ "sys_id" => "xxxxxx-sysid-xxxxxx",
434
+ "field" => "test-field",
435
+ "mod" => "var x = 10;"
436
+ })
437
+ allow_any_instance_of(SnowSync::SyncUtil).to receive(:merge_update)
438
+ allow(Base64).to receive(:strict_decode64).and_call_original
439
+ allow(Base64).to receive(:strict_encode64).and_call_original
440
+ allow(RestClient).to receive(:patch).and_return("{\"result\":[{ \"status\": \"201\" }]}")
441
+ end
442
+
443
+ it "should push modifications to the instance" do
444
+ util.configs = { "conf_path" => nil,
445
+ "base_url" => "https://test.com/api/now/table/",
446
+ "creds" => {
447
+ "user" => "dGVzdC1uYW1l",
448
+ "pass" => "dGVzdC1wYXNzd29yZA=="
449
+ },
450
+ "table_map" => {
451
+ "script_include" => {
452
+ "name" => "TestClass",
453
+ "table" => "sys_script_include",
454
+ "sys_id" => "xxxxxx-sysid-xxxxxx",
455
+ "field" => "test-field",
456
+ "mod" => "var x = 10;"
457
+ }
458
+ }
459
+ }
460
+ util.push_modifications(["sync/script_include/test_class.js"])
461
+ expect(Base64).to have_received(:strict_decode64).with("dGVzdC1uYW1l")
462
+ expect(Base64).to have_received(:strict_decode64).with("dGVzdC1wYXNzd29yZA==")
463
+ expect(Base64).to have_received(:strict_encode64).with("test-name:test-password")
464
+ expect(RestClient).to have_received(:patch).with(
465
+ "https://test.com/api/now/table/sys_script_include/xxxxxx-sysid-xxxxxx",
466
+ "{\"test-field\":\"var x = 10;\"}",
467
+ {:authorization => "Basic " + "dGVzdC1uYW1lOnRlc3QtcGFzc3dvcmQ=",
468
+ :content_type => "application/json", :accept => "application/json"})
469
+ end
470
+
471
+ it "should handle an exception" do
472
+ allow(RestClient).to receive(:patch).and_raise(RestClient::ExceptionWithResponse)
473
+ allow(Logger).to receive(:new).and_return(logger)
474
+ allow(logger).to receive(:error)
475
+ util.configs = { "conf_path" => nil,
476
+ "base_url" => "https://test.com/api/now/table/",
477
+ "creds" => {
478
+ "user" => "dGVzdC1uYW1l",
479
+ "pass" => "dGVzdC1wYXNzd29yZA=="
480
+ },
481
+ "table_map" => {
482
+ "script_include" => {
483
+ "name" => "TestClass",
484
+ "table" => "sys_script_include",
485
+ "sys_id" => "xxxxxx-sysid-xxxxxx",
486
+ "field" => "test-field",
487
+ "mod" => "var x = 10;"
488
+ }
489
+ }
490
+ }
491
+ util.push_modifications(["sync/script_include/test_class.js"])
492
+ expect(logger).to have_received(:error)
493
+ .with("ERROR: RestClient::ExceptionWithResponse").once
494
+ end
495
+
496
+ end
497
+
498
+ describe "#notify" do
499
+
500
+ let :util do
501
+ SnowSync::SyncUtil.new(opts = "test")
502
+ end
503
+
504
+ let :logger do
505
+ instance_double(Logger)
506
+ end
507
+
508
+ let :update do
509
+ "test_class"
510
+ end
511
+
512
+ let :macosx? do
513
+ `uname`.chomp.downcase == "darwin"
514
+ end
515
+
516
+ let :linux? do
517
+ `uname`.chomp.downcase == "linux"
518
+ end
519
+
520
+ before do
521
+ allow(TerminalNotifier::Guard).to receive(:success) if macosx?
522
+ allow(Libnotify).to receive(:show) if linux?
523
+ allow(Logger).to receive(:new).and_return(logger)
524
+ allow(logger).to receive(:info)
525
+ end
526
+
527
+ condition = `uname`.chomp.downcase == "darwin"
528
+ context "when true", if: condition do
529
+ it "should send notification - macosx path" do
530
+ uname = "darwin"
531
+ util.notify(update, uname, util.logger)
532
+ expect(TerminalNotifier::Guard).to have_received(:success)
533
+ .with("File: #{update}", :title => "ServiceNow Script Update")
534
+ expect(logger).to have_received(:info).with("->: osx notification dispatched")
535
+ end
536
+ end
537
+
538
+ condition = `uname`.chomp.downcase == "linux"
539
+ context "when true", if: condition do
540
+ it "should send notification - linux path" do
541
+ uname = "linux"
542
+ util.notify(update, uname, util.logger)
543
+ expect(Libnotify).to have_received(:show)
544
+ .with(:summary => "ServiceNow Script Update", :body => "File: #{update}")
545
+ expect(logger).to have_received(:info).with("->: linux notification dispatched")
546
+ end
547
+ end
548
+
549
+ end
@@ -1,9 +1,8 @@
1
1
  require "spec_helper"
2
2
 
3
- ## --> unit tests
4
3
  describe "utility object" do
5
4
 
6
- let! :util do
5
+ let :util do
7
6
  SnowSync::SyncUtil.new(opts = "test")
8
7
  end
9
8
 
@@ -21,22 +20,22 @@ describe "utility object" do
21
20
 
22
21
  end
23
22
 
24
- describe "create_directory" do
23
+ describe "#create_directory" do
25
24
 
26
- let! :util do
25
+ let :util do
27
26
  SnowSync::SyncUtil.new(opts = "test")
28
27
  end
29
28
 
30
- let! :created_time do
31
- util.create_directory("sync")
32
- return File.ctime("sync")
33
- end
34
-
35
29
  it "should create a directory" do
30
+ util.create_directory("sync")
36
31
  dir = `ls`.split("\n")
37
32
  expect(dir.include?("sync")).to eq true
38
33
  end
39
34
 
35
+ let :created_time do
36
+ File.ctime("sync")
37
+ end
38
+
40
39
  it "should not create directory" do
41
40
  util.create_directory("sync")
42
41
  check_created_time = File.ctime("sync")
@@ -45,15 +44,22 @@ describe "create_directory" do
45
44
 
46
45
  end
47
46
 
48
- describe "create_file" do
47
+ describe "#create_file" do
49
48
 
50
- let! :util do
49
+ let :util do
51
50
  SnowSync::SyncUtil.new(opts = "test")
52
51
  end
53
52
 
54
- it "should create a file" do
55
- FileUtils.mkdir_p("sync")
53
+ before do
56
54
  FileUtils.mkdir_p("sync/test_sub_dir")
55
+ end
56
+
57
+ after do
58
+ FileUtils.cd("../..")
59
+ FileUtils.rm_rf("sync")
60
+ end
61
+
62
+ it "should create a file" do
57
63
  json = { "property" => "value" }
58
64
  name = "TestClass".snakecase
59
65
  path = proc do
@@ -61,13 +67,11 @@ describe "create_file" do
61
67
  end
62
68
  util.create_file(name, json, &path)
63
69
  expect(File.exists?("test_class.js")).to eq true
64
- FileUtils.cd("../..")
65
- FileUtils.rm_rf("sync")
66
70
  end
67
71
 
68
72
  end
69
73
 
70
- describe "check_required_configs" do
74
+ describe "#check_required_configs" do
71
75
 
72
76
  let :util do
73
77
  SnowSync::SyncUtil.new(opts = "test")
@@ -75,35 +79,40 @@ describe "check_required_configs" do
75
79
 
76
80
  it "should raise an exception when there is no config path" do
77
81
  util.configs["conf_path"] = nil
78
- expect{util.check_required_configs}.to raise_error(/Check the configuration path, base url, credentials or table to sync/)
82
+ expect{util.check_required_configs}.to raise_error(
83
+ /Check the configuration path, base url, credentials or table to sync/)
79
84
  end
80
85
 
81
86
  it "should raise an exception when there is no base url" do
82
87
  util.configs["base_url"] = nil
83
- expect{util.check_required_configs}.to raise_error(/Check the configuration path, base url, credentials or table to sync/)
88
+ expect{util.check_required_configs}.to raise_error(
89
+ /Check the configuration path, base url, credentials or table to sync/)
84
90
  end
85
91
 
86
92
  it "should raise an exception when there is no username" do
87
93
  util.configs["creds"]["user"] = nil
88
- expect{util.check_required_configs}.to raise_error(/Check the configuration path, base url, credentials or table to sync/)
94
+ expect{util.check_required_configs}.to raise_error(
95
+ /Check the configuration path, base url, credentials or table to sync/)
89
96
  end
90
97
 
91
98
  it "should raise an exception when there is no password" do
92
99
  util.configs["creds"]["pass"] = nil
93
- expect{util.check_required_configs}.to raise_error(/Check the configuration path, base url, credentials or table to sync/)
100
+ expect{util.check_required_configs}.to raise_error(
101
+ /Check the configuration path, base url, credentials or table to sync/)
94
102
  end
95
103
 
96
104
  it "should raise an exception when there are no tables mapped" do
97
105
  tables = util.configs["table_map"].keys
98
106
  tables.each do |table|
99
107
  util.configs["table_map"][table]["table"] = nil
100
- expect{util.check_required_configs}.to raise_error(/Check the configuration path, base url, credentials or table to sync/)
108
+ expect{util.check_required_configs}.to raise_error(
109
+ /Check the configuration path, base url, credentials or table to sync/)
101
110
  end
102
111
  end
103
112
 
104
113
  end
105
114
 
106
- describe "classify" do
115
+ describe "#classify" do
107
116
 
108
117
  let :util do
109
118
  SnowSync::SyncUtil.new(opts = "test")
@@ -118,7 +127,7 @@ describe "classify" do
118
127
 
119
128
  end
120
129
 
121
- describe "table_lookup" do
130
+ describe "#table_lookup" do
122
131
 
123
132
  let :util do
124
133
  SnowSync::SyncUtil.new(opts = "test")
@@ -132,15 +141,21 @@ describe "table_lookup" do
132
141
 
133
142
  end
134
143
 
135
- describe "merge_update" do
144
+ describe "#merge_update" do
136
145
 
137
- let! :util do
146
+ let :util do
138
147
  SnowSync::SyncUtil.new(opts = "test")
139
148
  end
140
149
 
150
+ before do
151
+ FileUtils.mkdir_p("sync/script_include")
152
+ end
153
+
154
+ after do
155
+ FileUtils.rm_rf("sync")
156
+ end
157
+
141
158
  it "should merge script with the configs object" do
142
- FileUtils.mkdir_p("sync")
143
- FileUtils.mkdir_p("sync/script_include")
144
159
  json_resp = "var test = 'test'; \n" +
145
160
  "var testing = function(arg) { \n\tgs.print(arg) \n}; \n" +
146
161
  "testing('test');"
@@ -154,26 +169,30 @@ describe "merge_update" do
154
169
  path = file.split("/")
155
170
  type = path[1]
156
171
  file = path[2]
157
- table_map = util.table_lookup(type, file)
158
- util.merge_update(type, file, table_map)
172
+ util.merge_update(type, file)
159
173
  expect(util.configs["table_map"]["script_include"]["mod"] != nil).to eq true
160
- FileUtils.rm_rf("sync")
161
174
  end
162
175
 
163
176
  end
164
177
 
165
- ## --> integration tests
166
- describe "setup_sync_directories" do
178
+ describe "#run_setup_and_sync" do
167
179
 
168
- let! :util do
180
+ let :util do
169
181
  SnowSync::SyncUtil.new(opts = "test")
170
182
  end
171
183
 
172
- it "should setup and synchronize field from the SN instance" do
184
+ before do
185
+ util.encrypt_credentials
186
+ end
187
+
188
+ after do
189
+ FileUtils.rm_rf("sync")
190
+ end
191
+
192
+ it "should setup file locally and sync code from the instance" do
173
193
  util.run_setup_and_sync
174
194
  file = File.open("sync/script_include/test_class.js")
175
195
  expect(file.is_a?(Object)).to eq true
176
- FileUtils.rm_rf("sync")
177
196
  end
178
197
 
179
198
  end
@@ -184,23 +203,32 @@ describe "push_modifications - single table configuration" do
184
203
  SnowSync::SyncUtil.new(opts = "test")
185
204
  end
186
205
 
206
+ before do
207
+ util.encrypt_credentials
208
+ end
209
+
210
+ after do
211
+ FileUtils.rm_rf("sync")
212
+ end
213
+
187
214
  it "should push modifications to a configured instance" do
188
215
  util.run_setup_and_sync
189
216
  file = File.open("sync/script_include/test_class.js", "r+")
190
217
  lines = file.readlines
191
218
  file.close
192
- lines[0] = "// test comment -\n"
219
+ lines[0] = "// test comment - single push \n"
193
220
  newfile = File.new("sync/script_include/test_class.js", "w")
194
221
  lines.each do |line|
195
222
  newfile.write(line)
196
223
  end
197
224
  newfile.close
198
225
  util.push_modifications(["sync/script_include/test_class.js"])
226
+ # resync confirms mods were pushed to the instance
199
227
  util.run_setup_and_sync
200
228
  file = File.open("sync/script_include/test_class.js", "r+")
201
229
  lines = file.readlines
202
230
  file.close
203
- expect(lines[0]).to eq "// test comment -\n"
231
+ expect(lines[0]).to eq "// test comment - single push \n"
204
232
  end
205
233
 
206
234
  end
@@ -211,7 +239,15 @@ describe "push_modifications - mutli-table configuration" do
211
239
  SnowSync::SyncUtil.new(opts = "test")
212
240
  end
213
241
 
214
- it "should sync, update, queue, push, re-sync mods for a configured instance" do
242
+ before do
243
+ util.encrypt_credentials
244
+ end
245
+
246
+ after do
247
+ FileUtils.rm_rf("sync")
248
+ end
249
+
250
+ it "should push modifications to a configured instance" do
215
251
  def do_edit(file, edit)
216
252
  file = File.open(file, "r+")
217
253
  lines = file.readlines
@@ -231,14 +267,18 @@ describe "push_modifications - mutli-table configuration" do
231
267
  end
232
268
  util.run_setup_and_sync
233
269
  # sys_script_include
234
- do_edit("sync/script_include/test_class.js", "// test comment -\n")
270
+ do_edit(
271
+ "sync/script_include/test_class.js", "// test comment - multi push 1\n")
235
272
  # sys_ui_action
236
- do_edit("sync/ui_action/test.js", "// test comment -\n")
273
+ do_edit(
274
+ "sync/ui_action/test_action.js", "// test comment - multi push 2\n")
237
275
  # queued mods, push in sequence
238
- util.push_modifications(["sync/script_include/test_class.js", "sync/ui_action/test.js"])
276
+ util.push_modifications(
277
+ ["sync/script_include/test_class.js", "sync/ui_action/test_action.js"])
278
+ # resync confirms mods were pushed to the instance
239
279
  util.run_setup_and_sync
240
- run_check("sync/script_include/test_class.js", "// test comment -\n")
241
- run_check("sync/ui_action/test.js", "// test comment -\n")
280
+ run_check("sync/script_include/test_class.js", "// test comment - multi push 1\n")
281
+ run_check("sync/ui_action/test_action.js", "// test comment - multi push 2\n")
242
282
  end
243
283
 
244
284
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: snow_sync
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.1.2
4
+ version: 3.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Wallace
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-06-03 00:00:00.000000000 Z
11
+ date: 2022-01-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '1.13'
19
+ version: 2.3.3
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '1.13'
26
+ version: 2.3.3
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: facets
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -72,20 +72,14 @@ dependencies:
72
72
  requirements:
73
73
  - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: 1.8.0
76
- - - ">="
77
- - !ruby/object:Gem::Version
78
- version: 1.8.3
75
+ version: 2.6.1
79
76
  type: :development
80
77
  prerelease: false
81
78
  version_requirements: !ruby/object:Gem::Requirement
82
79
  requirements:
83
80
  - - "~>"
84
81
  - !ruby/object:Gem::Version
85
- version: 1.8.0
86
- - - ">="
87
- - !ruby/object:Gem::Version
88
- version: 1.8.3
82
+ version: 2.6.1
89
83
  - !ruby/object:Gem::Dependency
90
84
  name: libnotify
91
85
  requirement: !ruby/object:Gem::Requirement
@@ -106,14 +100,14 @@ dependencies:
106
100
  requirements:
107
101
  - - "~>"
108
102
  - !ruby/object:Gem::Version
109
- version: 10.0.0
103
+ version: 13.0.6
110
104
  type: :development
111
105
  prerelease: false
112
106
  version_requirements: !ruby/object:Gem::Requirement
113
107
  requirements:
114
108
  - - "~>"
115
109
  - !ruby/object:Gem::Version
116
- version: 10.0.0
110
+ version: 13.0.6
117
111
  - !ruby/object:Gem::Dependency
118
112
  name: rest-client
119
113
  requirement: !ruby/object:Gem::Requirement
@@ -134,14 +128,28 @@ dependencies:
134
128
  requirements:
135
129
  - - "~>"
136
130
  - !ruby/object:Gem::Version
137
- version: 3.5.0
131
+ version: 3.10.0
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: 3.10.0
139
+ - !ruby/object:Gem::Dependency
140
+ name: rspec-core
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: 3.10.1
138
146
  type: :development
139
147
  prerelease: false
140
148
  version_requirements: !ruby/object:Gem::Requirement
141
149
  requirements:
142
150
  - - "~>"
143
151
  - !ruby/object:Gem::Version
144
- version: 3.5.0
152
+ version: 3.10.1
145
153
  - !ruby/object:Gem::Dependency
146
154
  name: terminal-notifier-guard
147
155
  requirement: !ruby/object:Gem::Requirement
@@ -187,7 +195,9 @@ files:
187
195
  - lib/snow_sync/configs.yml
188
196
  - lib/snow_sync/sync_util.rb
189
197
  - lib/snow_sync/version.rb
198
+ - spec/docker_install_spec.rb
190
199
  - spec/spec_helper.rb
200
+ - spec/sync_util_mock_spec.rb
191
201
  - spec/sync_util_spec.rb
192
202
  homepage: https://rubygems.org/gems/snow_sync
193
203
  licenses:
@@ -208,8 +218,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
208
218
  - !ruby/object:Gem::Version
209
219
  version: '0'
210
220
  requirements: []
211
- rubyforge_project:
212
- rubygems_version: 2.6.8
221
+ rubygems_version: 3.3.3
213
222
  signing_key:
214
223
  specification_version: 4
215
224
  summary: SnowSync is a file sync utility tool and API which provides a bridge for