imap-backup 5.0.0 → 6.0.0.rc2

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,296 @@
1
+ module Imap::Backup
2
+ describe Serializer do
3
+ subject { described_class.new("path", "folder/sub") }
4
+
5
+ let(:directory) { instance_double(Serializer::Directory, ensure_exists: nil) }
6
+ let(:imap) do
7
+ instance_double(
8
+ Serializer::Imap,
9
+ exist?: true,
10
+ rename: nil,
11
+ uid_validity: existing_uid_validity,
12
+ "uid_validity=": nil
13
+ )
14
+ end
15
+ let(:mbox) do
16
+ instance_double(
17
+ Serializer::Mbox,
18
+ append: nil,
19
+ exist?: false,
20
+ length: 1,
21
+ pathname: "aaa",
22
+ rename: nil,
23
+ rewind: nil
24
+ )
25
+ end
26
+ let(:folder_path) { File.expand_path(File.join("path", "folder/sub")) }
27
+ let(:existing_uid_validity) { nil }
28
+ let(:enumerator) { instance_double(Serializer::MboxEnumerator) }
29
+
30
+ before do
31
+ allow(Serializer::Directory).to receive(:new) { directory }
32
+ allow(Serializer::Imap).to receive(:new).with(folder_path) { imap }
33
+ allow(Serializer::Mbox).to receive(:new) { mbox }
34
+ allow(Serializer::MboxEnumerator).to receive(:new) { enumerator }
35
+ end
36
+
37
+ describe "#apply_uid_validity" do
38
+ let(:imap_test) { instance_double(Serializer::Imap, exist?: imap_test_exists) }
39
+ let(:imap_test_exists) { false }
40
+ let(:test_folder_path) do
41
+ File.expand_path(File.join("path", "folder/sub-#{existing_uid_validity}"))
42
+ end
43
+ let(:result) { subject.apply_uid_validity("new") }
44
+
45
+ before do
46
+ allow(Serializer::Imap).to receive(:new).with(test_folder_path) { imap_test }
47
+ end
48
+
49
+ context "when there is no existing uid_validity" do
50
+ it "sets the metadata file's uid_validity" do
51
+ result
52
+
53
+ expect(imap).to have_received(:"uid_validity=").with("new")
54
+ end
55
+ end
56
+
57
+ context "when the new value is the same as the old value" do
58
+ let(:existing_uid_validity) { "new" }
59
+
60
+ it "does nothing" do
61
+ result
62
+
63
+ expect(imap).to_not have_received(:"uid_validity=")
64
+ end
65
+ end
66
+
67
+ context "when the new value is different from the old value" do
68
+ let(:existing_uid_validity) { "existing" }
69
+
70
+ it "renames the existing mailbox" do
71
+ result
72
+
73
+ expect(mbox).to have_received(:rename).with(test_folder_path)
74
+ end
75
+
76
+ it "renames the existing metadata file" do
77
+ result
78
+
79
+ expect(imap).to have_received(:rename).with(test_folder_path)
80
+ end
81
+
82
+ it "returns the new name for the old folder" do
83
+ expect(result).to eq("folder/sub-existing")
84
+ end
85
+
86
+ context "when the default rename is not possible" do
87
+ let(:imap_test_exists) { true }
88
+ let(:imap_test1) { instance_double(Serializer::Imap, exist?: false) }
89
+ let(:test_folder_path1) do
90
+ File.expand_path(File.join("path", "folder/sub-#{existing_uid_validity}-1"))
91
+ end
92
+
93
+ before do
94
+ allow(Serializer::Imap).to receive(:new).with(test_folder_path1) { imap_test1 }
95
+ end
96
+
97
+ it "renames the mailbox, appending a numeral" do
98
+ result
99
+
100
+ expect(mbox).to have_received(:rename).with(test_folder_path1)
101
+ end
102
+
103
+ it "renames the metadata file, appending a numeral" do
104
+ result
105
+
106
+ expect(imap).to have_received(:rename).with(test_folder_path1)
107
+ end
108
+ end
109
+ end
110
+ end
111
+
112
+ describe "#force_uid_validity" do
113
+ it "sets the metadata file's uid_validity" do
114
+ subject.force_uid_validity("new")
115
+
116
+ expect(imap).to have_received(:"uid_validity=").with("new")
117
+ end
118
+ end
119
+
120
+ describe "#append" do
121
+ let(:existing_uid_validity) { "42" }
122
+ let(:mboxrd_message) do
123
+ instance_double(Email::Mboxrd::Message, to_serialized: "serialized")
124
+ end
125
+ let(:uid_found) { false }
126
+ let(:command) { subject.append(99, "Hi") }
127
+
128
+ before do
129
+ allow(imap).to receive(:include?) { uid_found }
130
+ allow(imap).to receive(:append)
131
+ allow(Email::Mboxrd::Message).to receive(:new) { mboxrd_message }
132
+ end
133
+
134
+ it "appends the message to the mailbox" do
135
+ command
136
+
137
+ expect(mbox).to have_received(:append).with("serialized")
138
+ end
139
+
140
+ it "appends the UID to the metadata" do
141
+ command
142
+
143
+ expect(imap).to have_received(:append).with(99)
144
+ end
145
+
146
+ context "when appending to the mailbox causes an error" do
147
+ before do
148
+ allow(mbox).to receive(:append).and_throw(RuntimeError, "Boom")
149
+ end
150
+
151
+ it "does not fail" do
152
+ command
153
+ end
154
+
155
+ it "leaves the metadata file unchanged" do
156
+ command
157
+
158
+ expect(imap).to_not have_received(:append)
159
+ end
160
+ end
161
+
162
+ context "when appending to the metadata file causes an error" do
163
+ before do
164
+ allow(imap).to receive(:append).and_throw(RuntimeError, "Boom")
165
+ end
166
+
167
+ it "does not fail" do
168
+ command
169
+ end
170
+
171
+ it "reset the mailbox to the previous position" do
172
+ command
173
+
174
+ expect(mbox).to have_received(:rewind)
175
+ end
176
+ end
177
+
178
+ context "when the metadata uid_validity has not been set" do
179
+ let(:existing_uid_validity) { nil }
180
+
181
+ it "fails" do
182
+ expect { command }.to raise_error(RuntimeError, /without uid_validity/)
183
+ end
184
+ end
185
+
186
+ context "when the message has already been backed up" do
187
+ let(:uid_found) { true }
188
+
189
+ it "doesn't append to the mailbox file" do
190
+ command
191
+
192
+ expect(mbox).to_not have_received(:append)
193
+ end
194
+
195
+ it "doesn't append to the metadata file" do
196
+ command
197
+
198
+ expect(imap).to_not have_received(:append)
199
+ end
200
+ end
201
+ end
202
+
203
+ describe "#load" do
204
+ let(:uid) { 999 }
205
+ let(:imap_index) { 0 }
206
+ let(:result) { subject.load(uid) }
207
+
208
+ before do
209
+ allow(imap).to receive(:index).with(999) { imap_index }
210
+ allow(enumerator).to receive(:each) { ["message"].enum_for(:each) }
211
+ end
212
+
213
+ it "returns an Email::Mboxrd::Message" do
214
+ expect(result).to be_a(Email::Mboxrd::Message)
215
+ end
216
+
217
+ it "returns the message" do
218
+ expect(result.supplied_body).to eq("message")
219
+ end
220
+
221
+ context "when the message is not found" do
222
+ let(:imap_index) { nil }
223
+
224
+ it "returns nil" do
225
+ expect(result).to be nil
226
+ end
227
+ end
228
+
229
+ context "when the supplied UID is a string" do
230
+ let(:uid) { "999" }
231
+
232
+ it "works" do
233
+ expect(result).to be_a(Email::Mboxrd::Message)
234
+ end
235
+ end
236
+ end
237
+
238
+ describe "#load_nth" do
239
+ let(:imap_index) { 0 }
240
+ let(:result) { subject.load_nth(imap_index) }
241
+
242
+ before do
243
+ allow(enumerator).to receive(:each) { ["message"].enum_for(:each) }
244
+ end
245
+
246
+ it "returns an Email::Mboxrd::Message" do
247
+ expect(result).to be_a(Email::Mboxrd::Message)
248
+ end
249
+
250
+ it "returns the message" do
251
+ expect(result.supplied_body).to eq("message")
252
+ end
253
+
254
+ context "when the message is not found" do
255
+ let(:imap_index) { 1 }
256
+
257
+ it "returns nil" do
258
+ expect(result).to be nil
259
+ end
260
+ end
261
+ end
262
+
263
+ describe "#each_message" do
264
+ let(:good_uid) { 999 }
265
+
266
+ before do
267
+ allow(imap).to receive(:index) { nil }
268
+ allow(imap).to receive(:index).with(good_uid) { 0 }
269
+ allow(enumerator).to receive(:each) { ["message"].enum_for(:each) }
270
+ end
271
+
272
+ it "yields matching UIDs" do
273
+ expect { |b| subject.each_message([good_uid], &b) }.
274
+ to yield_successive_args([good_uid, anything])
275
+ end
276
+
277
+ it "yields matching messages" do
278
+ messages = subject.each_message([good_uid]).map { |_uid, message| message }
279
+ expect(messages[0].supplied_body).to eq("message")
280
+ end
281
+
282
+ context "with UIDs that are not present" do
283
+ it "skips them" do
284
+ expect { |b| subject.each_message([good_uid, 1234], &b) }.
285
+ to yield_successive_args([good_uid, anything])
286
+ end
287
+ end
288
+
289
+ context "when called without a block" do
290
+ it "returns an Enumerator" do
291
+ expect(subject.each_message([])).to be_a(Enumerator)
292
+ end
293
+ end
294
+ end
295
+ end
296
+ end
@@ -11,10 +11,11 @@ describe Imap::Backup::Setup::Account do
11
11
  Imap::Backup::Account,
12
12
  username: existing_email,
13
13
  password: existing_password,
14
- server: current_server,
15
- connection_options: nil,
16
14
  local_path: "/backup/path",
17
15
  folders: [{name: "my_folder"}],
16
+ multi_fetch_size: multi_fetch_size,
17
+ server: current_server,
18
+ connection_options: connection_options,
18
19
  modified?: false
19
20
  )
20
21
  end
@@ -28,10 +29,12 @@ describe Imap::Backup::Setup::Account do
28
29
  let(:accounts) { [account, account1] }
29
30
  let(:existing_email) { "user@example.com" }
30
31
  let(:new_email) { "foo@example.com" }
31
- let(:current_server) { "imap.example.com" }
32
32
  let(:existing_password) { "password" }
33
33
  let(:other_email) { "other@example.com" }
34
34
  let(:other_existing_path) { "/other/existing/path" }
35
+ let(:multi_fetch_size) { 1 }
36
+ let(:current_server) { "imap.example.com" }
37
+ let(:connection_options) { nil }
35
38
 
36
39
  let(:highline) { HIGHLINE }
37
40
  let(:config) { CONFIG }
@@ -99,9 +102,11 @@ describe Imap::Backup::Setup::Account do
99
102
  [
100
103
  "modify email",
101
104
  "modify password",
102
- "modify server",
103
105
  "modify backup path",
104
106
  "choose backup folders",
107
+ "modify multi-fetch size (number of emails to fetch at a time)",
108
+ "modify server",
109
+ "modify connection options",
105
110
  "test connection",
106
111
  "delete",
107
112
  "(q) return to main menu",
@@ -136,7 +141,23 @@ describe Imap::Backup::Setup::Account do
136
141
  before { subject.run }
137
142
 
138
143
  it "indicates that a password is not set" do
139
- expect(menu.header).to include("password (unset)")
144
+ expect(menu.header).to match(/^password\s+\(unset\)/)
145
+ end
146
+ end
147
+
148
+ context "with multi_fetch_size" do
149
+ let(:multi_fetch_size) { 4 }
150
+
151
+ it "shows the size" do
152
+ expect(menu.header).to match(/^multi-fetch\s+4/)
153
+ end
154
+ end
155
+
156
+ context "with connection_options" do
157
+ let(:connection_options) { {some: "option"} }
158
+
159
+ it "shows the options" do
160
+ expect(menu.header).to match(/^connection options\s+'{"some":"option"}'/)
140
161
  end
141
162
  end
142
163
  end
@@ -240,23 +261,6 @@ describe Imap::Backup::Setup::Account do
240
261
  end
241
262
  end
242
263
 
243
- describe "choosing 'modify server'" do
244
- let(:server) { "server" }
245
-
246
- before do
247
- allow(account).to receive(:"server=")
248
- allow(highline).to receive(:ask).with("server: ") { server }
249
-
250
- subject.run
251
-
252
- menu.choices["modify server"].call
253
- end
254
-
255
- it "updates the server" do
256
- expect(account).to have_received(:"server=").with(server)
257
- end
258
- end
259
-
260
264
  describe "choosing 'modify backup path'" do
261
265
  let(:new_backup_path) { "/new/path" }
262
266
 
@@ -311,6 +315,97 @@ describe Imap::Backup::Setup::Account do
311
315
  end
312
316
  end
313
317
 
318
+ describe "choosing 'modify multi-fetch size'" do
319
+ let(:supplied) { "10" }
320
+
321
+ before do
322
+ allow(account).to receive(:multi_fetch_size=)
323
+ allow(highline).to receive(:ask).with("size: ") { supplied }
324
+
325
+ subject.run
326
+ menu.choices[
327
+ "modify multi-fetch size (number of emails to fetch at a time)"
328
+ ].call
329
+ end
330
+
331
+ it "sets the multi-fetch size" do
332
+ expect(account).to have_received(:multi_fetch_size=).with(10)
333
+ end
334
+
335
+ context "when the supplied value is not a number" do
336
+ let(:supplied) { "wrong!" }
337
+
338
+ it "does nothing" do
339
+ expect(account).to_not have_received(:multi_fetch_size=)
340
+ end
341
+ end
342
+
343
+ context "when the supplied value is not a positive number" do
344
+ let(:supplied) { "0" }
345
+
346
+ it "does nothing" do
347
+ expect(account).to_not have_received(:multi_fetch_size=)
348
+ end
349
+ end
350
+ end
351
+
352
+ describe "choosing 'modify server'" do
353
+ let(:server) { "server" }
354
+
355
+ before do
356
+ allow(account).to receive(:"server=")
357
+ allow(highline).to receive(:ask).with("server: ") { server }
358
+
359
+ subject.run
360
+
361
+ menu.choices["modify server"].call
362
+ end
363
+
364
+ it "updates the server" do
365
+ expect(account).to have_received(:"server=").with(server)
366
+ end
367
+ end
368
+
369
+ describe "choosing 'modify connection options'" do
370
+ context "when the JSON is well formed" do
371
+ let(:json) { "{}" }
372
+
373
+ before do
374
+ allow(highline).to receive(:ask).with("connections options (as JSON): ") { json }
375
+ allow(account).to receive(:"connection_options=")
376
+
377
+ subject.run
378
+
379
+ menu.choices["modify connection options"].call
380
+ end
381
+
382
+ it "updates the connection options" do
383
+ expect(account).to have_received(:"connection_options=").with(json)
384
+ end
385
+ end
386
+
387
+ context "when the JSON is malformed" do
388
+ before do
389
+ allow(highline).to receive(:ask).with("connections options (as JSON): ") { "xx" }
390
+ allow(account).to receive(:"connection_options=").and_raise(JSON::ParserError)
391
+ allow(highline).to receive(:ask).with("Press a key ")
392
+
393
+ subject.run
394
+
395
+ menu.choices["modify connection options"].call
396
+ end
397
+
398
+ it "does not fail" do
399
+ subject.run
400
+ end
401
+
402
+ it "reports the problem" do
403
+ expect(Kernel).to have_received(:puts).
404
+ with(/Malformed/)
405
+ end
406
+ end
407
+ end
408
+
314
409
  describe "choosing 'test connection'" do
315
410
  let(:connection_tester) do
316
411
  instance_double(
@@ -336,7 +431,7 @@ describe Imap::Backup::Setup::Account do
336
431
  let(:confirmed) { true }
337
432
 
338
433
  before do
339
- allow(account).to receive(:mark_for_deletion!)
434
+ allow(account).to receive(:mark_for_deletion)
340
435
  allow(highline).to receive(:agree) { confirmed }
341
436
  subject.run
342
437
  catch :done do
@@ -350,7 +445,7 @@ describe Imap::Backup::Setup::Account do
350
445
 
351
446
  context "when the user confirms deletion" do
352
447
  it "flags the account to be deleted" do
353
- expect(account).to have_received(:mark_for_deletion!)
448
+ expect(account).to have_received(:mark_for_deletion)
354
449
  end
355
450
  end
356
451
 
@@ -358,7 +453,7 @@ describe Imap::Backup::Setup::Account do
358
453
  let(:confirmed) { false }
359
454
 
360
455
  it "doesn't flag the account to be deleted" do
361
- expect(account).to_not have_received(:mark_for_deletion!)
456
+ expect(account).to_not have_received(:mark_for_deletion)
362
457
  end
363
458
  end
364
459
  end
@@ -0,0 +1,15 @@
1
+ module Imap::Backup
2
+ RSpec.describe Setup::Helpers do
3
+ describe "#title_prefix" do
4
+ it "is a string" do
5
+ expect(subject.title_prefix).to eq("imap-backup –")
6
+ end
7
+ end
8
+
9
+ describe "#version" do
10
+ it "is a version string" do
11
+ expect(subject.version).to match(/\A\d+\.\d+\.\d+(\.\w+)?\z/)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,116 @@
1
+ module Imap::Backup
2
+ describe Thunderbird::MailboxExporter do
3
+ subject { described_class.new("email", serializer, "profile", **args) }
4
+
5
+ let(:args) { {} }
6
+ let(:serializer) do
7
+ instance_double(
8
+ Serializer,
9
+ folder: "folder",
10
+ mbox_pathname: "mbox_pathname"
11
+ )
12
+ end
13
+ let(:local_folder) do
14
+ instance_double(
15
+ Thunderbird::LocalFolder,
16
+ exists?: local_folder_exists,
17
+ full_path: "full_path",
18
+ msf_exists?: msf_exists,
19
+ msf_path: "msf_path",
20
+ path: "path",
21
+ set_up: set_up_result
22
+ )
23
+ end
24
+ let(:local_folder_exists) { false }
25
+ let(:msf_exists) { false }
26
+ let(:set_up_result) { true }
27
+ let(:file) { instance_double(File, write: nil) }
28
+ let(:enumerator) { instance_double(Serializer::MboxEnumerator) }
29
+
30
+ before do
31
+ allow(File).to receive(:open).with("full_path", "w").and_yield(file)
32
+ allow(File).to receive(:unlink)
33
+ allow(Thunderbird::LocalFolder).to receive(:new) { local_folder }
34
+ allow(Serializer::MboxEnumerator).to receive(:new) { enumerator }
35
+ allow(enumerator).to receive(:each) { ["message"].enum_for(:each) }
36
+ allow(Email::Mboxrd::Message).to receive(:clean_serialized) { "cleaned" }
37
+ allow(Kernel).to receive(:puts)
38
+ end
39
+
40
+ describe "#run" do
41
+ let!(:result) { subject.run }
42
+
43
+ context "when the destination folder cannot be set up" do
44
+ let(:set_up_result) { false }
45
+
46
+ it "doesn't copy the mailbox" do
47
+ expect(file).to_not have_received(:write)
48
+ end
49
+
50
+ it "returns false" do
51
+ expect(result).to be false
52
+ end
53
+ end
54
+
55
+ context "when the .msf file exists" do
56
+ let(:msf_exists) { true }
57
+
58
+ context "when 'force' is set" do
59
+ let(:args) { {force: true} }
60
+
61
+ it "deletes the file" do
62
+ expect(File).to have_received(:unlink).with("msf_path")
63
+ end
64
+ end
65
+
66
+ context "when 'force' isn't set" do
67
+ it "doesn't copy the mailbox" do
68
+ expect(file).to_not have_received(:write)
69
+ end
70
+
71
+ it "returns false" do
72
+ expect(result).to be false
73
+ end
74
+ end
75
+ end
76
+
77
+ context "when the destination mailbox exists" do
78
+ let(:local_folder_exists) { true }
79
+
80
+ context "when 'force' is set" do
81
+ let(:args) { {force: true} }
82
+
83
+ it "writes the message" do
84
+ expect(file).to have_received(:write)
85
+ end
86
+
87
+ it "returns true" do
88
+ expect(result).to be true
89
+ end
90
+ end
91
+
92
+ context "when 'force' isn't set" do
93
+ it "doesn't copy the mailbox" do
94
+ expect(file).to_not have_received(:write)
95
+ end
96
+
97
+ it "returns false" do
98
+ expect(result).to be false
99
+ end
100
+ end
101
+ end
102
+
103
+ it "adds a 'From' line" do
104
+ expect(file).to have_received(:write).with(/From - \w+ \w+ \d+ \d+:\d+:\d+/)
105
+ end
106
+
107
+ it "writes the cleaned message" do
108
+ expect(file).to have_received(:write).with(/cleaned/)
109
+ end
110
+
111
+ it "returns true" do
112
+ expect(result).to be true
113
+ end
114
+ end
115
+ end
116
+ end
@@ -8,7 +8,7 @@ describe Imap::Backup::Uploader do
8
8
  end
9
9
  let(:serializer) do
10
10
  instance_double(
11
- Imap::Backup::Serializer::Mbox,
11
+ Imap::Backup::Serializer,
12
12
  uids: [1, 2],
13
13
  update_uid: nil
14
14
  )
@@ -0,0 +1,34 @@
1
+ class Retrier
2
+ class FooError < StandardError; end
3
+
4
+ include RetryOnError
5
+
6
+ def do_stuff(errors:, limit:)
7
+ calls = 0
8
+
9
+ retry_on_error(errors: errors, limit: limit) do
10
+ calls += 1
11
+ raise FooError if calls < 3
12
+
13
+ 42
14
+ end
15
+ end
16
+ end
17
+
18
+ RSpec.describe RetryOnError do
19
+ describe "#retry_on_error" do
20
+ subject { Retrier.new }
21
+
22
+ it "retries" do
23
+ expect(subject.do_stuff(errors: [Retrier::FooError], limit: 3)).to eq(42)
24
+ end
25
+
26
+ context "when the block fails more than the limit" do
27
+ it "fails" do
28
+ expect do
29
+ subject.do_stuff(errors: [Retrier::FooError], limit: 1)
30
+ end.to raise_error(Retrier::FooError)
31
+ end
32
+ end
33
+ end
34
+ end