Skip to content

Commit 9951bae

Browse files
nacchan99rakuda-san-desu
authored andcommitted
test: news:fetch タスクのエラーハンドリングテストを追加
- ネットワークエラーやRSS解析エラー時の挙動を確認 - 破損・不正なYAMLファイルのケースを追加 - ロガーをモック化してwarnログの出力を検証
1 parent b35ad64 commit 9951bae

File tree

1 file changed

+34
-186
lines changed

1 file changed

+34
-186
lines changed

spec/tasks/news_fetch_error_handling_spec.rb

Lines changed: 34 additions & 186 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,19 @@
77
before { Rails.application.load_tasks }
88
before { allow(Rails.env).to receive(:test?).and_return(true) }
99

10-
let(:yaml_path) { Rails.root.join('tmp', 'error_test_news.yml') }
11-
let(:fetch_task) { Rake::Task['news:fetch'] }
12-
let(:logger_mock) { instance_double(ActiveSupport::BroadcastLogger) }
10+
let(:yaml_path) { Rails.root.join('tmp', 'error_test_news.yml') }
11+
let(:fetch_task) { Rake::Task['news:fetch'] }
12+
let(:logger_mock) { instance_double(ActiveSupport::BroadcastLogger) }
13+
14+
around do |example|
15+
File.delete(yaml_path) if File.exist?(yaml_path)
16+
example.run
17+
File.delete(yaml_path) if File.exist?(yaml_path)
18+
end
1319

1420
before do
1521
ENV['NEWS_YAML_PATH'] = yaml_path.to_s
1622
fetch_task.reenable
17-
18-
# ロガーのモック設定
1923
allow(ActiveSupport::BroadcastLogger).to receive(:new).and_return(logger_mock)
2024
allow(logger_mock).to receive(:info)
2125
allow(logger_mock).to receive(:warn)
@@ -24,77 +28,40 @@
2428
after do
2529
ENV.delete('NEWS_YAML_PATH')
2630
ENV.delete('NEWS_RSS_PATH')
27-
File.delete(yaml_path) if File.exist?(yaml_path)
2831
end
2932

30-
describe 'ネットワークエラーのハンドリング' do
31-
context 'safe_open がネットワークエラーで例外を投げる場合' do
32-
before do
33-
ENV['NEWS_RSS_PATH'] = 'https://example.com/feed.rss'
34-
allow_any_instance_of(Object).to receive(:safe_open).and_raise(Net::OpenTimeout, '接続タイムアウト')
35-
end
36-
37-
it 'エラーをログに記録し、処理を継続する' do
38-
expect(logger_mock).to receive(:warn).with(/⚠️ Failed to fetch .+: 接続タイムアウト/)
39-
expect { fetch_task.invoke }.not_to raise_error
40-
41-
# 空の news.yml が作成される
42-
expect(File.exist?(yaml_path)).to be true
43-
yaml_content = YAML.safe_load(File.read(yaml_path), permitted_classes: [Time])
44-
expect(yaml_content['news']).to eq([])
45-
end
46-
end
47-
48-
context 'HTTPエラーレスポンスの場合' do
49-
before do
50-
ENV['NEWS_RSS_PATH'] = 'https://example.com/feed.rss'
51-
allow_any_instance_of(Object).to receive(:safe_open).and_raise(Net::HTTPServerException, '500 Internal Server Error')
52-
end
53-
54-
it 'エラーをログに記録し、処理を継続する' do
55-
expect(logger_mock).to receive(:warn).with(/⚠️ Failed to fetch .+: 500 Internal Server Error/)
56-
expect { fetch_task.invoke }.not_to raise_error
57-
end
58-
end
59-
60-
context '不正なURLの場合' do
33+
describe 'ネットワーク・RSSエラー時の挙動' do
34+
context 'ネットワークエラーの場合' do
6135
before do
62-
ENV['NEWS_RSS_PATH'] = 'https://example.com/feed.rss'
63-
allow_any_instance_of(Object).to receive(:safe_open).and_raise('不正なURLです: https://example.com/feed.rss')
36+
ENV['NEWS_RSS_PATH'] = 'https://invalid-url.example.com/rss'
37+
allow(self).to receive(:safe_open).and_raise(Net::OpenTimeout, '接続タイムアウト')
6438
end
6539

66-
it 'エラーをログに記録し、処理を継続する' do
67-
expect(logger_mock).to receive(:warn).with(/⚠️ Failed to fetch .+: 不正なURLです/)
40+
it 'warnログを出し、空のnews.ymlを生成する' do
41+
expect(logger_mock).to receive(:warn).with(/⚠️ Failed to fetch .+/)
6842
expect { fetch_task.invoke }.not_to raise_error
43+
yaml = YAML.safe_load(File.read(yaml_path), permitted_classes: [Time])
44+
expect(yaml['news']).to eq([])
6945
end
7046
end
71-
end
7247

73-
describe '不正なRSSのハンドリング' do
74-
context 'RSS::Parser.parse が失敗する場合' do
48+
context 'RSS::Parser.parseが失敗する場合' do
7549
before do
7650
ENV['NEWS_RSS_PATH'] = 'https://example.com/feed.rss'
77-
78-
# safe_open は成功するが、不正なXMLを返す
79-
allow_any_instance_of(Object).to receive(:safe_open).and_return('<invalid>not valid rss</invalid>')
51+
allow(self).to receive(:safe_open).and_return('<invalid>not valid rss</invalid>')
8052
end
8153

82-
it 'エラーをログに記録し、処理を継続する' do
83-
expect(logger_mock).to receive(:warn).with(/⚠️ Failed to fetch .+: /)
54+
it 'warnログを出し、空のnews.ymlを生成する' do
55+
expect(logger_mock).to receive(:warn).with(/⚠️ Failed to fetch .+/)
8456
expect { fetch_task.invoke }.not_to raise_error
85-
86-
# 空の news.yml が作成される
87-
expect(File.exist?(yaml_path)).to be true
88-
yaml_content = YAML.safe_load(File.read(yaml_path), permitted_classes: [Time])
89-
expect(yaml_content['news']).to eq([])
57+
yaml = YAML.safe_load(File.read(yaml_path), permitted_classes: [Time])
58+
expect(yaml['news']).to eq([])
9059
end
9160
end
9261

9362
context '空のRSSフィードの場合' do
9463
before do
9564
ENV['NEWS_RSS_PATH'] = 'https://example.com/feed.rss'
96-
97-
# 有効だが空のRSSフィード
9865
empty_rss = <<~RSS
9966
<?xml version="1.0" encoding="UTF-8"?>
10067
<rss version="2.0">
@@ -105,160 +72,41 @@
10572
</channel>
10673
</rss>
10774
RSS
108-
109-
allow_any_instance_of(Object).to receive(:safe_open).and_return(empty_rss)
75+
allow(self).to receive(:safe_open).and_return(empty_rss)
11076
end
11177

112-
it '空の配列として処理し、エラーにならない' do
113-
expect { fetch_task.invoke }.not_to raise_error
114-
115-
yaml_content = YAML.safe_load(File.read(yaml_path), permitted_classes: [Time])
116-
expect(yaml_content['news']).to eq([])
117-
end
118-
end
119-
120-
context 'RSSアイテムに必須フィールドが欠けている場合' do
121-
before do
122-
ENV['NEWS_RSS_PATH'] = 'https://example.com/feed.rss'
123-
124-
# linkやpubDateが欠けているRSS
125-
invalid_rss = <<~RSS
126-
<?xml version="1.0" encoding="UTF-8"?>
127-
<rss version="2.0">
128-
<channel>
129-
<title>Invalid Feed</title>
130-
<description>Invalid RSS Feed</description>
131-
<link>https://example.com</link>
132-
<item>
133-
<title>タイトルのみの記事</title>
134-
<!-- link と pubDate が欠けている -->
135-
</item>
136-
</channel>
137-
</rss>
138-
RSS
139-
140-
allow_any_instance_of(Object).to receive(:safe_open).and_return(invalid_rss)
141-
end
142-
143-
it 'エラーをログに記録し、処理を継続する' do
144-
expect(logger_mock).to receive(:warn).with(/⚠️ Failed to fetch .+/)
78+
it '空配列でnews.ymlを生成する' do
14579
expect { fetch_task.invoke }.not_to raise_error
80+
yaml = YAML.safe_load(File.read(yaml_path), permitted_classes: [Time])
81+
expect(yaml['news']).to eq([])
14682
end
14783
end
14884
end
14985

15086
describe '破損したYAMLファイルのハンドリング' do
151-
context '既存のYAMLファイルが破損している場合' do
87+
context '既存のYAMLが破損している場合' do
15288
before do
15389
ENV['NEWS_RSS_PATH'] = Rails.root.join('spec', 'fixtures', 'sample_news.rss').to_s
154-
155-
# 破損したYAMLファイルを作成
15690
File.write(yaml_path, "invalid yaml content:\n - broken\n indentation:\n - here")
15791
end
15892

15993
it 'YAML読み込みエラーが発生し、タスクが失敗する' do
160-
# YAML.safe_load のエラーは rescue されないため、タスク全体が失敗する
16194
expect { fetch_task.invoke }.to raise_error(Psych::SyntaxError)
16295
end
16396
end
16497

165-
context '既存のYAMLファイルが不正な構造の場合' do
98+
context '既存のYAMLが不正な構造の場合' do
16699
before do
167100
ENV['NEWS_RSS_PATH'] = Rails.root.join('spec', 'fixtures', 'sample_news.rss').to_s
168-
169-
# 不正な構造のYAMLファイル(newsキーがない)
170101
File.write(yaml_path, { 'invalid_key' => [{ 'id' => 1 }] }.to_yaml)
171102
end
172103

173-
it '空の配列として扱い、処理を継続する' do
174-
expect { fetch_task.invoke }.not_to raise_error
175-
176-
# 新しいデータで上書きされる
177-
yaml_content = YAML.safe_load(File.read(yaml_path), permitted_classes: [Time])
178-
expect(yaml_content['news']).to be_an(Array)
179-
expect(yaml_content['news'].size).to be > 0
180-
end
181-
end
182-
183-
context '許可されていないクラスを含むYAMLファイルの場合' do
184-
before do
185-
ENV['NEWS_RSS_PATH'] = Rails.root.join('spec', 'fixtures', 'sample_news.rss').to_s
186-
187-
# DateTimeオブジェクトを含むYAML(Timeのみ許可されている)
188-
yaml_content = {
189-
'news' => [
190-
{
191-
'id' => 1,
192-
'url' => 'https://example.com/test',
193-
'title' => 'テスト',
194-
'published_at' => DateTime.now
195-
}
196-
]
197-
}
198-
199-
# 強制的にDateTimeオブジェクトを含むYAMLを作成
200-
File.write(yaml_path, yaml_content.to_yaml.gsub('!ruby/object:DateTime', '!ruby/object:DateTime'))
201-
end
202-
203-
it 'YAML読み込みエラーが発生し、タスクが失敗する' do
204-
expect { fetch_task.invoke }.to raise_error(Psych::DisallowedClass)
205-
end
206-
end
207-
end
208-
209-
describe '複数のエラーが同時に発生する場合' do
210-
context '複数のRSSフィードで異なるエラーが発生する場合' do
211-
before do
212-
# 複数のフィードURLを環境変数経由では設定できないため、
213-
# デフォルトの動作をオーバーライドする
214-
allow(Rails.env).to receive(:test?).and_return(false)
215-
allow(Rails.env).to receive(:staging?).and_return(false)
216-
ENV.delete('NEWS_RSS_PATH')
217-
218-
# 最初のフィードはネットワークエラー
219-
allow_any_instance_of(Object).to receive(:safe_open)
220-
.with('https://news.coderdojo.jp/feed/')
221-
.and_raise(Net::OpenTimeout, 'タイムアウト')
222-
end
223-
224-
it '各エラーをログに記録し、処理を継続する' do
225-
expect(logger_mock).to receive(:warn).with(/⚠️ Failed to fetch .+: タイムアウト/)
226-
expect { fetch_task.invoke }.not_to raise_error
227-
228-
# 空の news.yml が作成される
229-
expect(File.exist?(yaml_path)).to be true
230-
yaml_content = YAML.safe_load(File.read(yaml_path), permitted_classes: [Time])
231-
expect(yaml_content['news']).to eq([])
232-
end
233-
end
234-
end
235-
236-
describe 'エラーリカバリー' do
237-
context 'ネットワークエラー後に再実行した場合' do
238-
before do
239-
ENV['NEWS_RSS_PATH'] = Rails.root.join('spec', 'fixtures', 'sample_news.rss').to_s
240-
end
241-
242-
it '正常に処理される' do
243-
# 最初はネットワークエラー
244-
allow_any_instance_of(Object).to receive(:safe_open).and_raise(Net::OpenTimeout, 'タイムアウト')
245-
expect { fetch_task.invoke }.not_to raise_error
246-
247-
# エラー時は空のYAMLが作成される
248-
yaml_content = YAML.safe_load(File.read(yaml_path), permitted_classes: [Time])
249-
expect(yaml_content['news']).to eq([])
250-
251-
# safe_openのモックを解除して正常動作に戻す
252-
allow_any_instance_of(Object).to receive(:safe_open).and_call_original
253-
254-
# タスクを再実行可能にする
255-
fetch_task.reenable
256-
257-
# 再実行すると正常に処理される
104+
it '空配列として扱い、正常に上書きされる' do
258105
expect { fetch_task.invoke }.not_to raise_error
259-
yaml_content = YAML.safe_load(File.read(yaml_path), permitted_classes: [Time])
260-
expect(yaml_content['news'].size).to be > 0
106+
yaml = YAML.safe_load(File.read(yaml_path), permitted_classes: [Time])
107+
expect(yaml['news']).to be_an(Array)
108+
expect(yaml['news'].size).to be > 0
261109
end
262110
end
263111
end
264-
end
112+
end

0 commit comments

Comments
 (0)