imap-backup 5.0.0 → 6.0.0.rc2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +8 -7
  3. data/bin/imap-backup +4 -0
  4. data/docs/development.md +10 -4
  5. data/imap-backup.gemspec +2 -7
  6. data/lib/cli_coverage.rb +18 -0
  7. data/lib/imap/backup/account/connection.rb +7 -11
  8. data/lib/imap/backup/account/folder.rb +0 -16
  9. data/lib/imap/backup/account.rb +31 -11
  10. data/lib/imap/backup/cli/folders.rb +3 -3
  11. data/lib/imap/backup/cli/migrate.rb +3 -3
  12. data/lib/imap/backup/cli/restore.rb +20 -4
  13. data/lib/imap/backup/cli/utils.rb +2 -2
  14. data/lib/imap/backup/cli.rb +6 -7
  15. data/lib/imap/backup/configuration.rb +1 -11
  16. data/lib/imap/backup/downloader.rb +13 -9
  17. data/lib/imap/backup/serializer/directory.rb +37 -0
  18. data/lib/imap/backup/serializer/imap.rb +120 -0
  19. data/lib/imap/backup/serializer/mbox.rb +23 -94
  20. data/lib/imap/backup/serializer/mbox_enumerator.rb +2 -0
  21. data/lib/imap/backup/serializer.rb +180 -3
  22. data/lib/imap/backup/setup/account.rb +52 -29
  23. data/lib/imap/backup/setup/helpers.rb +1 -1
  24. data/lib/imap/backup/thunderbird/mailbox_exporter.rb +1 -1
  25. data/lib/imap/backup/version.rb +2 -2
  26. data/lib/imap/backup.rb +0 -1
  27. data/spec/features/backup_spec.rb +22 -29
  28. data/spec/features/restore_spec.rb +8 -6
  29. data/spec/features/support/aruba.rb +12 -3
  30. data/spec/features/support/backup_directory.rb +0 -4
  31. data/spec/features/support/email_server.rb +0 -1
  32. data/spec/spec_helper.rb +4 -9
  33. data/spec/unit/imap/backup/account/connection_spec.rb +36 -8
  34. data/spec/unit/imap/backup/account/folder_spec.rb +18 -16
  35. data/spec/unit/imap/backup/account_spec.rb +246 -0
  36. data/spec/unit/imap/backup/cli/accounts_spec.rb +12 -1
  37. data/spec/unit/imap/backup/cli/backup_spec.rb +19 -0
  38. data/spec/unit/imap/backup/cli/folders_spec.rb +39 -0
  39. data/spec/unit/imap/backup/cli/local_spec.rb +26 -7
  40. data/spec/unit/imap/backup/cli/migrate_spec.rb +80 -0
  41. data/spec/unit/imap/backup/cli/restore_spec.rb +67 -0
  42. data/spec/unit/imap/backup/cli/setup_spec.rb +17 -0
  43. data/spec/unit/imap/backup/cli/utils_spec.rb +68 -5
  44. data/spec/unit/imap/backup/cli_spec.rb +93 -0
  45. data/spec/unit/imap/backup/client/apple_mail_spec.rb +9 -0
  46. data/spec/unit/imap/backup/configuration_spec.rb +2 -2
  47. data/spec/unit/imap/backup/downloader_spec.rb +60 -8
  48. data/spec/unit/imap/backup/logger_spec.rb +1 -1
  49. data/spec/unit/imap/backup/migrator_spec.rb +1 -1
  50. data/spec/unit/imap/backup/sanitizer_spec.rb +42 -0
  51. data/spec/unit/imap/backup/serializer/directory_spec.rb +37 -0
  52. data/spec/unit/imap/backup/serializer/imap_spec.rb +218 -0
  53. data/spec/unit/imap/backup/serializer/mbox_spec.rb +62 -183
  54. data/spec/unit/imap/backup/serializer_spec.rb +296 -0
  55. data/spec/unit/imap/backup/setup/account_spec.rb +120 -25
  56. data/spec/unit/imap/backup/setup/helpers_spec.rb +15 -0
  57. data/spec/unit/imap/backup/thunderbird/mailbox_exporter_spec.rb +116 -0
  58. data/spec/unit/imap/backup/uploader_spec.rb +1 -1
  59. data/spec/unit/retry_on_error_spec.rb +34 -0
  60. metadata +44 -37
  61. data/lib/imap/backup/serializer/mbox_store.rb +0 -217
  62. data/lib/thunderbird/install.rb +0 -16
  63. data/lib/thunderbird/local_folder.rb +0 -65
  64. data/lib/thunderbird/profile.rb +0 -30
  65. data/lib/thunderbird/profiles.rb +0 -71
  66. data/lib/thunderbird/subdirectory.rb +0 -93
  67. data/lib/thunderbird/subdirectory_placeholder.rb +0 -21
  68. data/lib/thunderbird.rb +0 -14
  69. data/spec/gather_rspec_coverage.rb +0 -1
  70. data/spec/unit/imap/backup/serializer/mbox_store_spec.rb +0 -329
@@ -1,28 +1,28 @@
1
1
  require "features/helper"
2
2
 
3
- RSpec.describe "backup", type: :feature, docker: true do
3
+ RSpec.describe "backup", type: :aruba, docker: true do
4
4
  include_context "imap-backup connection"
5
5
  include_context "message-fixtures"
6
6
 
7
+ let(:local_backup_path) { File.expand_path("~/backup") }
8
+ let(:backup_folders) { [{name: folder}] }
9
+ let(:folder) { "my-stuff" }
7
10
  let(:messages_as_mbox) do
8
11
  message_as_mbox_entry(msg1) + message_as_mbox_entry(msg2)
9
12
  end
10
- let(:folder) { "my-stuff" }
11
- let(:email1) { send_email folder, msg1 }
12
- let(:email2) { send_email folder, msg2 }
13
+
13
14
  let!(:pre) {}
14
15
  let!(:setup) do
15
16
  server_create_folder folder
16
- email1
17
- email2
18
- connection.run_backup
17
+ send_email folder, msg1
18
+ send_email folder, msg2
19
+ create_config(accounts: [account.to_h])
20
+
21
+ run_command_and_stop("imap-backup backup")
19
22
  end
20
23
 
21
24
  after do
22
- FileUtils.rm_rf local_backup_path
23
- delete_emails folder
24
25
  server_delete_folder folder
25
- connection.disconnect
26
26
  end
27
27
 
28
28
  it "downloads messages" do
@@ -51,17 +51,16 @@ RSpec.describe "backup", type: :feature, docker: true do
51
51
 
52
52
  context "when uid_validity does not match" do
53
53
  let(:new_name) { "NEWNAME" }
54
- let(:email3) { send_email folder, msg3 }
55
54
  let(:original_folder_uid_validity) { server_uid_validity(folder) }
56
55
  let!(:pre) do
57
56
  server_create_folder folder
58
- email3
57
+ send_email folder, msg3
59
58
  original_folder_uid_validity
60
59
  connection.run_backup
61
60
  connection.disconnect
62
61
  server_rename_folder folder, new_name
63
62
  end
64
- let(:renamed_folder) { "#{folder}.#{original_folder_uid_validity}" }
63
+ let(:renamed_folder) { "#{folder}-#{original_folder_uid_validity}" }
65
64
 
66
65
  after do
67
66
  server_delete_folder new_name
@@ -71,37 +70,31 @@ RSpec.describe "backup", type: :feature, docker: true do
71
70
  expect(mbox_content(renamed_folder)).to eq(message_as_mbox_entry(msg3))
72
71
  end
73
72
 
73
+ it "renames the old metadata file" do
74
+ expect(imap_parsed(renamed_folder)).to be_a Hash
75
+ end
76
+
74
77
  it "downloads messages" do
75
78
  expect(mbox_content(folder)).to eq(messages_as_mbox)
76
79
  end
77
80
 
81
+ it "creates a metadata file" do
82
+ expect(imap_parsed(folder)).to be_a Hash
83
+ end
84
+
78
85
  context "when a renamed local backup exists" do
79
86
  let!(:pre) do
80
87
  super()
88
+ create_directory local_backup_path
81
89
  File.write(imap_path(renamed_folder), "existing imap")
82
90
  File.write(mbox_path(renamed_folder), "existing mbox")
83
91
  end
84
92
 
85
93
  it "moves the old backup to a uniquely named directory" do
86
- renamed = "#{folder}.#{original_folder_uid_validity}.1"
94
+ renamed = "#{folder}-#{original_folder_uid_validity}-1"
87
95
  expect(mbox_content(renamed)).to eq(message_as_mbox_entry(msg3))
88
96
  end
89
97
  end
90
98
  end
91
-
92
- context "when an unversioned .imap file is found" do
93
- let!(:pre) do
94
- File.open(imap_path(folder), "w") { |f| f.write "old format imap" }
95
- File.open(mbox_path(folder), "w") { |f| f.write "old format emails" }
96
- end
97
-
98
- it "replaces the .imap file with a versioned JSON file" do
99
- expect(imap_metadata[:uids]).to eq(folder_uids)
100
- end
101
-
102
- it "does the download" do
103
- expect(mbox_content(folder)).to eq(messages_as_mbox)
104
- end
105
- end
106
99
  end
107
100
  end
@@ -1,9 +1,11 @@
1
1
  require "features/helper"
2
2
 
3
- RSpec.describe "restore", type: :feature, docker: true do
3
+ RSpec.describe "restore", type: :aruba, docker: true do
4
4
  include_context "imap-backup connection"
5
5
  include_context "message-fixtures"
6
6
 
7
+ let(:local_backup_path) { File.expand_path("~/backup") }
8
+ let(:folder) { "my-stuff" }
7
9
  let(:messages_as_mbox) do
8
10
  message_as_mbox_entry(msg1) + message_as_mbox_entry(msg2)
9
11
  end
@@ -12,19 +14,19 @@ RSpec.describe "restore", type: :feature, docker: true do
12
14
  end
13
15
  let(:message_uids) { [msg1[:uid], msg2[:uid]] }
14
16
  let(:existing_imap_content) { imap_data(uid_validity, message_uids).to_json }
15
- let(:folder) { "my-stuff" }
16
17
  let(:uid_validity) { 1234 }
17
18
 
18
19
  let!(:pre) {}
19
20
  let!(:setup) do
21
+ create_directory local_backup_path
20
22
  File.write(imap_path(folder), existing_imap_content)
21
23
  File.write(mbox_path(folder), messages_as_mbox)
22
- connection.restore
24
+ create_config(accounts: [account.to_h])
25
+
26
+ run_command_and_stop("imap-backup restore #{account.username}")
23
27
  end
24
28
  let(:cleanup) do
25
- FileUtils.rm_rf local_backup_path
26
29
  server_delete_folder folder
27
- connection.disconnect
28
30
  end
29
31
 
30
32
  after { cleanup }
@@ -92,7 +94,7 @@ RSpec.describe "restore", type: :feature, docker: true do
92
94
  end
93
95
 
94
96
  context "when the folder has content" do
95
- let(:new_folder) { "#{folder}.#{uid_validity}" }
97
+ let(:new_folder) { "#{folder}-#{uid_validity}" }
96
98
  let(:cleanup) do
97
99
  server_delete_folder new_folder
98
100
  super()
@@ -1,7 +1,11 @@
1
1
  require "aruba/rspec"
2
2
 
3
+ require_relative "backup_directory"
4
+ require "imap/backup/serializer/mbox"
5
+
3
6
  Aruba.configure do |config|
4
7
  config.home_directory = File.expand_path("./tmp/home")
8
+ config.allow_absolute_paths = true
5
9
  end
6
10
 
7
11
  module ConfigurationHelpers
@@ -33,10 +37,10 @@ module StoreHelpers
33
37
  account = config.accounts.find { |a| a.username == email }
34
38
  raise "Account not found" if !account
35
39
  FileUtils.mkdir_p account.local_path
36
- store = Imap::Backup::Serializer::MboxStore.new(account.local_path, folder)
37
- store.uid_validity = "42" if !store.uid_validity
40
+ serializer = Imap::Backup::Serializer.new(account.local_path, folder)
41
+ serializer.force_uid_validity("42") if !serializer.uid_validity
38
42
  serialized = to_serialized(from: from, subject: subject, body: body)
39
- store.add(uid, serialized)
43
+ serializer.append uid, serialized
40
44
  end
41
45
 
42
46
  def to_serialized(from:, subject:, body:)
@@ -58,11 +62,16 @@ end
58
62
  RSpec.configure do |config|
59
63
  config.include ConfigurationHelpers, type: :aruba
60
64
  config.include StoreHelpers, type: :aruba
65
+ config.include BackupDirectoryHelpers, type: :aruba
61
66
 
62
67
  config.before(:suite) do
63
68
  FileUtils.rm_rf "./tmp/home"
64
69
  end
65
70
 
71
+ config.before(:example, type: :aruba) do
72
+ set_environment_variable("COVERAGE", "aruba")
73
+ end
74
+
66
75
  config.after do
67
76
  FileUtils.rm_rf "./tmp/home"
68
77
  end
@@ -41,7 +41,3 @@ module BackupDirectoryHelpers
41
41
  JSON.parse(imap_content(name), symbolize_names: true)
42
42
  end
43
43
  end
44
-
45
- RSpec.configure do |config|
46
- config.include BackupDirectoryHelpers, type: :feature
47
- end
@@ -87,7 +87,6 @@ module EmailServerHelpers
87
87
  imap.delete folder
88
88
  imap.disconnect
89
89
  rescue StandardError => e
90
- puts e.to_s
91
90
  ensure
92
91
  @imap = nil
93
92
  end
data/spec/spec_helper.rb CHANGED
@@ -1,18 +1,13 @@
1
- require "codeclimate-test-reporter"
2
1
  require "rspec"
3
2
 
4
- CodeClimate::TestReporter.start
3
+ $LOAD_PATH << File.expand_path("../lib", __dir__)
5
4
 
6
- spec_path = File.dirname(__FILE__)
7
- $LOAD_PATH << File.expand_path("../lib", spec_path)
8
-
9
- support_glob = File.join(spec_path, "support", "**", "*.rb")
5
+ support_glob = File.join(__dir__, "support", "**", "*.rb")
10
6
  Dir[support_glob].sort.each { |f| require f }
11
7
 
12
8
  require "simplecov"
13
- SimpleCov.start do
14
- add_filter "/spec/"
15
- end
9
+
10
+ SimpleCov.command_name "RSpec tests"
16
11
 
17
12
  require "imap/backup"
18
13
  require "imap/backup/cli"
@@ -24,21 +24,24 @@ describe Imap::Backup::Account::Connection do
24
24
  let(:account) do
25
25
  instance_double(
26
26
  Imap::Backup::Account,
27
- username: USERNAME,
27
+ username: username,
28
28
  password: PASSWORD,
29
29
  local_path: LOCAL_PATH,
30
30
  folders: config_folders,
31
+ multi_fetch_size: multi_fetch_size,
31
32
  server: server,
32
33
  connection_options: nil
33
34
  )
34
35
  end
36
+ let(:username) { USERNAME }
35
37
  let(:config_folders) { [FOLDER_CONFIG] }
38
+ let(:multi_fetch_size) { 1 }
36
39
  let(:root_info) do
37
40
  instance_double(Net::IMAP::MailboxList, name: ROOT_NAME)
38
41
  end
39
42
  let(:serializer) do
40
43
  instance_double(
41
- Imap::Backup::Serializer::Mbox,
44
+ Imap::Backup::Serializer,
42
45
  folder: serialized_folder,
43
46
  force_uid_validity: nil,
44
47
  apply_uid_validity: new_uid_validity,
@@ -87,6 +90,23 @@ describe Imap::Backup::Account::Connection do
87
90
  end
88
91
  end
89
92
 
93
+ context "when the provider is Apple" do
94
+ let(:username) { "user@mac.com" }
95
+ let(:apple_client) do
96
+ instance_double(
97
+ Imap::Backup::Client::AppleMail, login: nil
98
+ )
99
+ end
100
+
101
+ before do
102
+ allow(Imap::Backup::Client::AppleMail).to receive(:new) { apple_client }
103
+ end
104
+
105
+ it "returns the Apple client" do
106
+ expect(result).to eq(apple_client)
107
+ end
108
+ end
109
+
90
110
  context "when run" do
91
111
  before { subject.client }
92
112
 
@@ -116,7 +136,7 @@ describe Imap::Backup::Account::Connection do
116
136
 
117
137
  before do
118
138
  allow(Imap::Backup::Account::Folder).to receive(:new) { folder }
119
- allow(Imap::Backup::Serializer::Mbox).to receive(:new) { serializer }
139
+ allow(Imap::Backup::Serializer).to receive(:new) { serializer }
120
140
  end
121
141
 
122
142
  it "creates the path" do
@@ -150,16 +170,24 @@ describe Imap::Backup::Account::Connection do
150
170
  let(:exists) { true }
151
171
  let(:uid_validity) { 123 }
152
172
  let(:downloader) { instance_double(Imap::Backup::Downloader, run: nil) }
173
+ let(:multi_fetch_size) { 10 }
153
174
 
154
175
  before do
155
176
  allow(Imap::Backup::Downloader).
156
177
  to receive(:new).with(folder, serializer, anything) { downloader }
157
178
  allow(Imap::Backup::Account::Folder).to receive(:new).
158
179
  with(subject, BACKUP_FOLDER) { folder }
159
- allow(Imap::Backup::Serializer::Mbox).to receive(:new).
180
+ allow(Imap::Backup::Serializer).to receive(:new).
160
181
  with(LOCAL_PATH, IMAP_FOLDER) { serializer }
161
182
  end
162
183
 
184
+ it "passes the multi_fetch_size" do
185
+ subject.run_backup
186
+
187
+ expect(Imap::Backup::Downloader).to have_received(:new).
188
+ with(anything, anything, {multi_fetch_size: 10})
189
+ end
190
+
163
191
  context "with supplied config_folders" do
164
192
  it "runs the downloader" do
165
193
  expect(downloader).to receive(:run)
@@ -184,7 +212,7 @@ describe Imap::Backup::Account::Connection do
184
212
  before do
185
213
  allow(Imap::Backup::Account::Folder).to receive(:new).
186
214
  with(subject, ROOT_NAME) { folder }
187
- allow(Imap::Backup::Serializer::Mbox).to receive(:new).
215
+ allow(Imap::Backup::Serializer).to receive(:new).
188
216
  with(LOCAL_PATH, ROOT_NAME) { serializer }
189
217
  end
190
218
 
@@ -273,18 +301,18 @@ describe Imap::Backup::Account::Connection do
273
301
  end
274
302
  let(:updated_serializer) do
275
303
  instance_double(
276
- Imap::Backup::Serializer::Mbox, force_uid_validity: nil
304
+ Imap::Backup::Serializer, force_uid_validity: nil
277
305
  )
278
306
  end
279
307
 
280
308
  before do
281
309
  allow(Imap::Backup::Account::Folder).to receive(:new).
282
310
  with(subject, FOLDER_NAME) { folder }
283
- allow(Imap::Backup::Serializer::Mbox).to receive(:new).
311
+ allow(Imap::Backup::Serializer).to receive(:new).
284
312
  with(anything, FOLDER_NAME) { serializer }
285
313
  allow(Imap::Backup::Account::Folder).to receive(:new).
286
314
  with(subject, "new name") { updated_folder }
287
- allow(Imap::Backup::Serializer::Mbox).to receive(:new).
315
+ allow(Imap::Backup::Serializer).to receive(:new).
288
316
  with(anything, "new name") { updated_serializer }
289
317
  allow(Imap::Backup::Uploader).to receive(:new).
290
318
  with(folder, serializer) { uploader }
@@ -64,26 +64,36 @@ describe Imap::Backup::Account::Folder do
64
64
  expect(subject.uids).to eq([])
65
65
  end
66
66
  end
67
+
68
+ context "when the UID search fails" do
69
+ before do
70
+ allow(client).to receive(:uid_search).and_raise(NoMethodError)
71
+ end
72
+
73
+ it "returns an empty array" do
74
+ expect(subject.uids).to eq([])
75
+ end
76
+ end
67
77
  end
68
78
 
69
- describe "#fetch" do
79
+ describe "#fetch_multi" do
70
80
  let(:message_body) { instance_double(String, force_encoding: nil) }
71
- let(:attributes) { {"BODY[]" => message_body, "other" => "xxx"} }
81
+ let(:attributes) { {"UID" => "uid", "BODY[]" => message_body, "other" => "xxx"} }
72
82
  let(:fetch_data_item) do
73
83
  instance_double(Net::IMAP::FetchData, attr: attributes)
74
84
  end
75
85
 
76
86
  before { allow(client).to receive(:uid_fetch) { [fetch_data_item] } }
77
87
 
78
- it "returns the message" do
79
- expect(subject.fetch(123)).to eq(message_body)
88
+ it "returns the uid and message" do
89
+ expect(subject.fetch_multi([123])).to eq([{uid: "uid", body: message_body}])
80
90
  end
81
91
 
82
92
  context "when the server responds with nothing" do
83
93
  before { allow(client).to receive(:uid_fetch) { nil } }
84
94
 
85
95
  it "is nil" do
86
- expect(subject.fetch(123)).to be_nil
96
+ expect(subject.fetch_multi([123])).to be_nil
87
97
  end
88
98
  end
89
99
 
@@ -94,15 +104,7 @@ describe Imap::Backup::Account::Folder do
94
104
  end
95
105
 
96
106
  it "is nil" do
97
- expect(subject.fetch(123)).to be_nil
98
- end
99
- end
100
-
101
- context "when the response doesn't include 'BODY[]'" do
102
- let(:attributes) { {} }
103
-
104
- it "is nil" do
105
- expect(subject.fetch(123)).to be_nil
107
+ expect(subject.fetch_multi([123])).to be_nil
106
108
  end
107
109
  end
108
110
 
@@ -113,13 +115,13 @@ describe Imap::Backup::Account::Folder do
113
115
  end
114
116
 
115
117
  it "retries" do
116
- subject.fetch(123)
118
+ subject.fetch_multi([123])
117
119
 
118
120
  expect(client).to have_received(:uid_fetch).twice
119
121
  end
120
122
 
121
123
  it "succeeds" do
122
- subject.fetch(123)
124
+ subject.fetch_multi([123])
123
125
  end
124
126
  end
125
127
  end
@@ -0,0 +1,246 @@
1
+ module Imap::Backup
2
+ describe Account do
3
+ subject { described_class.new(options) }
4
+
5
+ let(:options) { {username: "user", password: "pwd"} }
6
+
7
+ describe "#changes" do
8
+ it "lists changes" do
9
+ subject.username = "new"
10
+
11
+ expect(subject.changes).to eq(username: {from: "user", to: "new"})
12
+ end
13
+
14
+ context "when more than one change is made" do
15
+ it "retains the last one" do
16
+ subject.username = "first"
17
+ subject.username = "second"
18
+
19
+ expect(subject.changes).to eq(username: {from: "user", to: "second"})
20
+ end
21
+ end
22
+
23
+ context "when a value is reset to its original value" do
24
+ it "removes the change" do
25
+ subject.username = "changed"
26
+ subject.username = "user"
27
+
28
+ expect(subject.changes).to eq({})
29
+ end
30
+ end
31
+ end
32
+
33
+ describe "#connection" do
34
+ it "returns a Connection for the Account" do
35
+ result = subject.connection
36
+
37
+ expect(result).to be_a(Account::Connection)
38
+ expect(result.account).to be subject
39
+ end
40
+ end
41
+
42
+ describe "#valid?" do
43
+ context "with username and password" do
44
+ it "is true" do
45
+ expect(subject.valid?).to be true
46
+ end
47
+ end
48
+
49
+ context "without a username" do
50
+ let(:options) { {password: "pwd"} }
51
+
52
+ it "is false" do
53
+ expect(subject.valid?).to be false
54
+ end
55
+ end
56
+
57
+ context "without a password" do
58
+ let(:options) { {username: "user"} }
59
+
60
+ it "is false" do
61
+ expect(subject.valid?).to be false
62
+ end
63
+ end
64
+ end
65
+
66
+ describe "#modified?" do
67
+ context "with changes" do
68
+ it "is true" do
69
+ subject.username = "new"
70
+
71
+ expect(subject.modified?).to be true
72
+ end
73
+ end
74
+
75
+ context "without changes" do
76
+ it "is false" do
77
+ expect(subject.modified?).to be false
78
+ end
79
+ end
80
+ end
81
+
82
+ describe "#clear_changes" do
83
+ it "clears changes" do
84
+ subject.username = "new"
85
+ subject.clear_changes
86
+
87
+ expect(subject.changes).to eq({})
88
+ end
89
+ end
90
+
91
+ describe "#mark_for_deletion" do
92
+ it "sets marked_for_deletion" do
93
+ subject.mark_for_deletion
94
+
95
+ expect(subject.marked_for_deletion?).to be true
96
+ end
97
+ end
98
+
99
+ describe "#marked_for_deletion?" do
100
+ it "defaults to false" do
101
+ expect(subject.marked_for_deletion?).to be false
102
+ end
103
+ end
104
+
105
+ describe "#to_h" do
106
+ it "returns a Hash representation" do
107
+ expect(subject.to_h).to eq({username: "user", password: "pwd"})
108
+ end
109
+
110
+ context "when local_path is set" do
111
+ let(:options) { {username: "user", password: "pwd", local_path: "local_path"} }
112
+
113
+ it "includes local_path" do
114
+ expect(subject.to_h).to include({local_path: "local_path"})
115
+ end
116
+ end
117
+
118
+ context "when folders is set" do
119
+ let(:options) { {username: "user", password: "pwd", folders: ["folder"]} }
120
+
121
+ it "includes folders" do
122
+ expect(subject.to_h).to include({folders: ["folder"]})
123
+ end
124
+ end
125
+
126
+ context "when multi_fetch_size is set" do
127
+ let(:options) { {username: "user", password: "pwd", multi_fetch_size: "3"} }
128
+
129
+ it "includes multi_fetch_size" do
130
+ expect(subject.to_h).to include({multi_fetch_size: 3})
131
+ end
132
+ end
133
+
134
+ context "when server is set" do
135
+ let(:options) { {username: "user", password: "pwd", server: "server"} }
136
+
137
+ it "includes server" do
138
+ expect(subject.to_h).to include({server: "server"})
139
+ end
140
+ end
141
+
142
+ context "when connection_options is set" do
143
+ let(:options) { {username: "user", password: "pwd", connection_options: "foo"} }
144
+
145
+ it "includes connection_options" do
146
+ expect(subject.to_h).to include({connection_options: "foo"})
147
+ end
148
+ end
149
+ end
150
+
151
+ describe "#multi_fetch_size" do
152
+ let(:options) { {username: "user", password: "pwd", multi_fetch_size: multi_fetch_size} }
153
+ let(:multi_fetch_size) { 10 }
154
+
155
+ it "returns the initialized value" do
156
+ expect(subject.multi_fetch_size).to eq(10)
157
+ end
158
+
159
+ context "when the initialized value is a string representation of a positive number" do
160
+ let(:multi_fetch_size) { "10" }
161
+
162
+ it "returns one" do
163
+ expect(subject.multi_fetch_size).to eq(10)
164
+ end
165
+ end
166
+
167
+ context "when the initialized value is not a number" do
168
+ let(:multi_fetch_size) { "ciao" }
169
+
170
+ it "returns one" do
171
+ expect(subject.multi_fetch_size).to eq(1)
172
+ end
173
+ end
174
+
175
+ context "when the initialized value is not a positive number" do
176
+ let(:multi_fetch_size) { "-99" }
177
+
178
+ it "returns one" do
179
+ expect(subject.multi_fetch_size).to eq(1)
180
+ end
181
+ end
182
+ end
183
+
184
+ [
185
+ [:username, "username", "username"],
186
+ [:password, "password", "password"],
187
+ [:local_path, "local_path", "local_path"],
188
+ [:multi_fetch_size, "2", 2],
189
+ [:server, "server", "server"],
190
+ [:folders, ["folder"], ["folder"]],
191
+ [:connection_options, '{"some": "option"}', {"some" => "option"}]
192
+ ].each do |attribute, value, expected|
193
+ describe "##{attribute}=" do
194
+ let(:options) { {} }
195
+
196
+ before { subject.send(:"#{attribute}=", value) }
197
+
198
+ it "sets the #{attribute}" do
199
+ expect(subject.send(attribute)).to eq(expected)
200
+ end
201
+
202
+ it "adds a change" do
203
+ expect(subject.changes).to eq(attribute => {from: nil, to: expected})
204
+ end
205
+
206
+ if attribute == :folders
207
+ context "when the supplied value is not an Array" do
208
+ it "fails" do
209
+ expect do
210
+ subject.folders = "aaa"
211
+ end.to raise_error(RuntimeError, /must be an Array/)
212
+ end
213
+ end
214
+ end
215
+
216
+ if attribute == :multi_fetch_size
217
+ context "when the supplied value is not a number" do
218
+ before { subject.multi_fetch_size = "ciao" }
219
+
220
+ it "sets multi_fetch_size to one" do
221
+ expect(subject.multi_fetch_size).to eq(1)
222
+ end
223
+ end
224
+
225
+ context "when the supplied value is not a positive number" do
226
+ before { subject.multi_fetch_size = "-1" }
227
+
228
+ it "sets multi_fetch_size to one" do
229
+ expect(subject.multi_fetch_size).to eq(1)
230
+ end
231
+ end
232
+ end
233
+
234
+ if attribute == :connection_options
235
+ context "when the supplied value is not valid JSON" do
236
+ it "fails" do
237
+ expect do
238
+ subject.connection_options = "NOT JSON"
239
+ end.to raise_error(JSON::ParserError)
240
+ end
241
+ end
242
+ end
243
+ end
244
+ end
245
+ end
246
+ end