neocities-red 1.1.2 → 1.2.0

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +29 -15
  3. data/.gitignore +4 -0
  4. data/CHANGELOG.md +95 -0
  5. data/Gemfile +1 -0
  6. data/Gemfile.lock +9 -6
  7. data/README.md +6 -0
  8. data/lib/neocities_red/cli.rb +156 -31
  9. data/lib/neocities_red/cli_display.rb +200 -5
  10. data/lib/neocities_red/client.rb +84 -77
  11. data/lib/neocities_red/errors.rb +20 -0
  12. data/lib/neocities_red/services/common/exclusions.rb +69 -0
  13. data/lib/neocities_red/services/common/pizza.rb +34 -21
  14. data/lib/neocities_red/services/common/worker_pool.rb +54 -0
  15. data/lib/neocities_red/services/file/folder_uploader.rb +39 -25
  16. data/lib/neocities_red/services/file/list.rb +60 -30
  17. data/lib/neocities_red/services/file/remover.rb +27 -12
  18. data/lib/neocities_red/services/file/uploader.rb +33 -13
  19. data/lib/neocities_red/services/site/differencer.rb +42 -12
  20. data/lib/neocities_red/services/site/exporter.rb +120 -7
  21. data/lib/neocities_red/services/site/informer.rb +29 -4
  22. data/lib/neocities_red/services/site/pusher.rb +74 -38
  23. data/lib/neocities_red/version.rb +2 -1
  24. data/lib/neocities_red.rb +32 -13
  25. data/neocities-red.gemspec +11 -6
  26. data/spec/neocities_red/cli_display_spec.rb +0 -6
  27. data/spec/neocities_red/cli_spec.rb +85 -0
  28. data/spec/neocities_red/client_spec.rb +7 -50
  29. data/spec/neocities_red/services/common/exclusions_spec.rb +47 -0
  30. data/spec/neocities_red/services/common/worker_pool_spec.rb +48 -0
  31. data/spec/neocities_red/services/file/folder_uploader_spec.rb +8 -6
  32. data/spec/neocities_red/services/file/list_spec.rb +13 -10
  33. data/spec/neocities_red/services/file/remover_spec.rb +15 -21
  34. data/spec/neocities_red/services/file/uploader_spec.rb +25 -20
  35. data/spec/neocities_red/services/site/differencer_spec.rb +32 -6
  36. data/spec/neocities_red/services/site/exporter_spec.rb +105 -38
  37. data/spec/neocities_red/services/site/informer_spec.rb +3 -3
  38. data/spec/neocities_red/services/site/pusher_spec.rb +1 -1
  39. data/spec/spec_helper.rb +1 -1
  40. metadata +11 -25
@@ -3,6 +3,7 @@
3
3
  require "spec_helper"
4
4
  require "json"
5
5
  require "fileutils"
6
+ require "time"
6
7
 
7
8
  RSpec.describe NeocitiesRed::Services::Site::Exporter do
8
9
  let(:client) { instance_double(NeocitiesRed::Client) }
@@ -11,29 +12,42 @@ RSpec.describe NeocitiesRed::Services::Site::Exporter do
11
12
  let(:tmp_root) { File.join(spec_root, "tmp", "site_exporter_spec") }
12
13
  let(:config_path) { File.join(tmp_root, "config.json") }
13
14
  let(:data) { { "LAST_PULL" => { "time" => nil, "loc" => nil } } }
15
+ let(:display) do
16
+ instance_double(
17
+ NeocitiesRed::CliDisplay,
18
+ display_pull_progress: nil,
19
+ display_pull_no_updates: nil,
20
+ display_pull_success: nil,
21
+ display_pull_failure: nil,
22
+ display_pull_stats: nil
23
+ )
24
+ end
25
+ let(:exporter) { described_class.new(client, sitename, data, config_path, display: display) }
26
+ let(:info_response) { { result: "success", info: { domain: nil } } }
27
+ let(:list_response) { { result: "success", files: [] } }
14
28
 
15
- before do
29
+ around do |example|
16
30
  FileUtils.rm_rf(tmp_root)
17
31
  FileUtils.mkdir_p(tmp_root)
18
32
  File.write(config_path, data.to_json)
19
- end
20
33
 
21
- after do
34
+ original_dir = Dir.pwd
35
+ Dir.chdir(tmp_root)
36
+ example.run
37
+ ensure
38
+ Dir.chdir(original_dir)
22
39
  FileUtils.rm_rf(tmp_root)
23
40
  end
24
41
 
25
- describe "#export" do
26
- context "when pull is successful" do
27
- let(:exporter) { described_class.new(client, sitename, data, config_path) }
28
-
29
- before do
30
- allow(client).to receive(:pull).and_return(nil)
31
- end
42
+ before do
43
+ allow(client).to receive_messages(info: info_response, list: list_response)
44
+ end
32
45
 
33
- it "pulls files from the site and updates the config file" do
46
+ describe "#export" do
47
+ context "when the site has no files" do
48
+ it "updates the config file with last pull data" do
34
49
  expect { exporter.export(quiet: true, last_pull_time: nil, last_pull_loc: nil) }.not_to raise_error
35
50
 
36
- expect(File.exist?(config_path)).to be(true)
37
51
  updated_data = JSON.parse(File.read(config_path))
38
52
  expect(updated_data["LAST_PULL"]).to have_key("time")
39
53
  expect(updated_data["LAST_PULL"]).to have_key("loc")
@@ -62,51 +76,104 @@ RSpec.describe NeocitiesRed::Services::Site::Exporter do
62
76
  expect(Whirly).not_to have_received(:stop)
63
77
  end
64
78
 
65
- context "with last_pull_time and last_pull_loc" do
66
- let(:last_pull_time) { "2024-01-01T00:00:00Z" }
67
- let(:last_pull_loc) { tmp_root }
79
+ it "displays pull stats" do
80
+ exporter.export(quiet: false, last_pull_time: nil, last_pull_loc: nil)
81
+
82
+ expect(display).to have_received(:display_pull_stats).with(0, anything)
83
+ end
84
+ end
85
+
86
+ context "when the site has files" do
87
+ let(:list_response) do
88
+ {
89
+ result: "success",
90
+ files: [
91
+ { path: "index.html", is_directory: false, updated_at: "2024-01-01T00:00:00Z" },
92
+ { path: "assets/", is_directory: true, updated_at: "2024-01-01T00:00:00Z" }
93
+ ]
94
+ }
95
+ end
96
+ let(:file_response) { instance_double(Faraday::Response, status: 200, body: "<h1>Hello</h1>") }
97
+
98
+ it "downloads files and writes them locally" do
99
+ allow(client).to receive(:download).with("https://test-site.neocities.org/index.html").and_return(file_response)
100
+
101
+ exporter.export(quiet: false, last_pull_time: nil, last_pull_loc: nil)
102
+
103
+ expect(File.read(File.join(tmp_root, "index.html"))).to eq("<h1>Hello</h1>")
104
+ expect(File.directory?(File.join(tmp_root, "assets"))).to be(true)
105
+ expect(display).to have_received(:display_pull_success)
106
+ end
107
+
108
+ it "uses the custom domain when present" do
109
+ allow(client).to receive(:info).and_return(result: "success", info: { domain: "example.com" })
110
+ allow(client).to receive(:download).with("https://example.com/index.html").and_return(file_response)
111
+
112
+ exporter.export(quiet: false, last_pull_time: nil, last_pull_loc: nil)
113
+
114
+ expect(client).to have_received(:download).with("https://example.com/index.html")
115
+ end
116
+
117
+ it "skips download when the file has not changed since last pull" do
118
+ allow(client).to receive(:download)
119
+ File.write(File.join(tmp_root, "index.html"), "existing content")
120
+
121
+ last_pull_time = Time.now.iso8601
122
+ exporter.export(quiet: false, last_pull_time: last_pull_time, last_pull_loc: Dir.pwd)
123
+
124
+ expect(display).to have_received(:display_pull_no_updates)
125
+ expect(client).not_to have_received(:download)
126
+ expect(File.read(File.join(tmp_root, "index.html"))).to eq("existing content")
127
+ end
128
+
129
+ it "reports failure when the download fails" do
130
+ allow(client).to receive(:download).with("https://test-site.neocities.org/index.html").and_return(
131
+ instance_double(Faraday::Response, status: 500, body: "")
132
+ )
68
133
 
69
- it "passes last_pull parameters to client.pull" do
70
- expect { exporter.export(quiet: true, last_pull_time: last_pull_time, last_pull_loc: last_pull_loc) }.not_to raise_error
134
+ exporter.export(quiet: false, last_pull_time: nil, last_pull_loc: nil)
71
135
 
72
- expect(client).to have_received(:pull).with(
73
- sitename, last_pull_time, last_pull_loc, quiet: true
74
- )
75
- end
136
+ expect(display).to have_received(:display_pull_failure)
137
+ expect(File.exist?(File.join(tmp_root, "index.html"))).to be(false)
76
138
  end
77
139
  end
78
140
 
79
- context "when pull raises an error" do
80
- let(:exporter) { described_class.new(client, sitename, data, config_path) }
141
+ context "when the API returns an error for info" do
142
+ let(:info_response) { { result: "error", message: "Site not found" } }
81
143
 
82
- it "stops Whirly and returns the error" do
83
- allow(Whirly).to receive(:start)
84
- allow(Whirly).to receive(:stop)
85
- allow(client).to receive(:pull).and_raise(StandardError.new("network error"))
144
+ it "raises APIError" do
145
+ expect do
146
+ exporter.export(quiet: true, last_pull_time: nil, last_pull_loc: nil)
147
+ end.to raise_error(NeocitiesRed::APIError, /Site not found/)
148
+ end
149
+ end
86
150
 
87
- result = exporter.export(quiet: true, last_pull_time: nil, last_pull_loc: nil)
151
+ context "when the API returns an error for list" do
152
+ let(:list_response) { { result: "error", message: "List failed" } }
88
153
 
89
- expect(result).to be_a(StandardError)
90
- expect(result.message).to eq("network error")
91
- expect(Whirly).to have_received(:stop)
154
+ it "raises APIError" do
155
+ expect do
156
+ exporter.export(quiet: true, last_pull_time: nil, last_pull_loc: nil)
157
+ end.to raise_error(NeocitiesRed::APIError, /List failed/)
92
158
  end
159
+ end
93
160
 
94
- it "does not raise and returns an error object" do
161
+ context "when fetching raises an error" do
162
+ it "propagates the error and stops Whirly" do
163
+ allow(client).to receive(:info).and_raise(StandardError.new("network error"))
95
164
  allow(Whirly).to receive(:start)
96
165
  allow(Whirly).to receive(:stop)
97
- allow(client).to receive(:pull).and_raise(StandardError.new("network error"))
98
166
 
99
- result = nil
100
- expect { result = exporter.export(quiet: true, last_pull_time: nil, last_pull_loc: nil) }.not_to raise_error
101
- expect(result).to be_a(StandardError)
167
+ expect do
168
+ exporter.export(quiet: true, last_pull_time: nil, last_pull_loc: nil)
169
+ end.to raise_error(StandardError, "network error")
170
+ expect(Whirly).to have_received(:stop)
102
171
  end
103
172
  end
104
173
  end
105
174
 
106
175
  describe "#initialize" do
107
176
  it "stores client, sitename, data, and app_config_path" do
108
- exporter = described_class.new(client, sitename, data, config_path)
109
-
110
177
  expect(exporter.client).to eq(client)
111
178
  expect(exporter.sitename).to eq(sitename)
112
179
  expect(exporter.data).to eq(data)
@@ -54,10 +54,10 @@ RSpec.describe NeocitiesRed::Services::Site::Informer do
54
54
  allow(client).to receive(:info).and_return(error_response)
55
55
  end
56
56
 
57
- it "raises ClientError" do
57
+ it "raises APIError with the message" do
58
58
  expect { profile_info.stats }.to raise_error(
59
- NeocitiesRed::Services::ClientError,
60
- /site_not_found/
59
+ NeocitiesRed::APIError,
60
+ /could not find site nonexistent/
61
61
  )
62
62
  end
63
63
  end
@@ -47,7 +47,7 @@ RSpec.describe NeocitiesRed::Services::Site::Pusher do
47
47
  end
48
48
 
49
49
  before do
50
- allow(NeocitiesRed::Services::File::Uploader).to receive(:new) do |_client, filepath, remote_path|
50
+ allow(NeocitiesRed::Services::File::Uploader).to receive(:new) do |_client, filepath, remote_path, _display|
51
51
  upload_calls << [filepath.to_s, remote_path.to_s]
52
52
  uploader_instance
53
53
  end
data/spec/spec_helper.rb CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "neocities_red"
4
4
  require "fileutils"
5
- require "webmock"
5
+ require "webmock/rspec"
6
6
 
7
7
  RSpec.configure do |config|
8
8
  # Enable flags like --only-failures and --next-failure
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: neocities-red
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.2
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kyle Drake
@@ -84,9 +84,6 @@ dependencies:
84
84
  name: pastel
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
- - - "~>"
88
- - !ruby/object:Gem::Version
89
- version: '0.8'
90
87
  - - '='
91
88
  - !ruby/object:Gem::Version
92
89
  version: 0.8.0
@@ -94,9 +91,6 @@ dependencies:
94
91
  prerelease: false
95
92
  version_requirements: !ruby/object:Gem::Requirement
96
93
  requirements:
97
- - - "~>"
98
- - !ruby/object:Gem::Version
99
- version: '0.8'
100
94
  - - '='
101
95
  - !ruby/object:Gem::Version
102
96
  version: 0.8.0
@@ -107,9 +101,6 @@ dependencies:
107
101
  - - "~>"
108
102
  - !ruby/object:Gem::Version
109
103
  version: '13'
110
- - - ">="
111
- - !ruby/object:Gem::Version
112
- version: 13.3.0
113
104
  type: :runtime
114
105
  prerelease: false
115
106
  version_requirements: !ruby/object:Gem::Requirement
@@ -117,9 +108,6 @@ dependencies:
117
108
  - - "~>"
118
109
  - !ruby/object:Gem::Version
119
110
  version: '13'
120
- - - ">="
121
- - !ruby/object:Gem::Version
122
- version: 13.3.0
123
111
  - !ruby/object:Gem::Dependency
124
112
  name: thor
125
113
  requirement: !ruby/object:Gem::Requirement
@@ -144,9 +132,6 @@ dependencies:
144
132
  name: tty-prompt
145
133
  requirement: !ruby/object:Gem::Requirement
146
134
  requirements:
147
- - - "~>"
148
- - !ruby/object:Gem::Version
149
- version: '0.23'
150
135
  - - '='
151
136
  - !ruby/object:Gem::Version
152
137
  version: 0.23.1
@@ -154,9 +139,6 @@ dependencies:
154
139
  prerelease: false
155
140
  version_requirements: !ruby/object:Gem::Requirement
156
141
  requirements:
157
- - - "~>"
158
- - !ruby/object:Gem::Version
159
- version: '0.23'
160
142
  - - '='
161
143
  - !ruby/object:Gem::Version
162
144
  version: 0.23.1
@@ -164,9 +146,6 @@ dependencies:
164
146
  name: tty-table
165
147
  requirement: !ruby/object:Gem::Requirement
166
148
  requirements:
167
- - - "~>"
168
- - !ruby/object:Gem::Version
169
- version: '0.12'
170
149
  - - '='
171
150
  - !ruby/object:Gem::Version
172
151
  version: 0.12.0
@@ -174,9 +153,6 @@ dependencies:
174
153
  prerelease: false
175
154
  version_requirements: !ruby/object:Gem::Requirement
176
155
  requirements:
177
- - - "~>"
178
- - !ruby/object:Gem::Version
179
- version: '0.12'
180
156
  - - '='
181
157
  - !ruby/object:Gem::Version
182
158
  version: 0.12.0
@@ -213,6 +189,7 @@ files:
213
189
  - ".gitignore"
214
190
  - ".rspec"
215
191
  - ".rubocop.yml"
192
+ - CHANGELOG.md
216
193
  - CODE_OF_CONDUCT.md
217
194
  - CONTRIBUTING.md
218
195
  - Gemfile
@@ -226,7 +203,10 @@ files:
226
203
  - lib/neocities_red/cli.rb
227
204
  - lib/neocities_red/cli_display.rb
228
205
  - lib/neocities_red/client.rb
206
+ - lib/neocities_red/errors.rb
207
+ - lib/neocities_red/services/common/exclusions.rb
229
208
  - lib/neocities_red/services/common/pizza.rb
209
+ - lib/neocities_red/services/common/worker_pool.rb
230
210
  - lib/neocities_red/services/file/folder_uploader.rb
231
211
  - lib/neocities_red/services/file/list.rb
232
212
  - lib/neocities_red/services/file/remover.rb
@@ -240,7 +220,9 @@ files:
240
220
  - spec/neocities_red/cli_display_spec.rb
241
221
  - spec/neocities_red/cli_spec.rb
242
222
  - spec/neocities_red/client_spec.rb
223
+ - spec/neocities_red/services/common/exclusions_spec.rb
243
224
  - spec/neocities_red/services/common/pizza_spec.rb
225
+ - spec/neocities_red/services/common/worker_pool_spec.rb
244
226
  - spec/neocities_red/services/file/folder_uploader_spec.rb
245
227
  - spec/neocities_red/services/file/list_spec.rb
246
228
  - spec/neocities_red/services/file/remover_spec.rb
@@ -255,6 +237,10 @@ licenses:
255
237
  - MIT
256
238
  metadata:
257
239
  rubygems_mfa_required: 'true'
240
+ homepage_uri: https://github.com/o-200/neocities-red
241
+ source_code_uri: https://github.com/o-200/neocities-red
242
+ changelog_uri: https://github.com/o-200/neocities-red/blob/main/CHANGELOG.md
243
+ documentation_uri: https://github.com/o-200/neocities-red#readme
258
244
  rdoc_options: []
259
245
  require_paths:
260
246
  - lib