Skip to content

Commit b35ad64

Browse files
claude[bot]nacchan99
authored andcommitted
test: news:fetchタスクのエラーハンドリング用RSpecを追加
- ネットワークエラー(接続タイムアウト、HTTPエラー、不正なURL)のテスト - 不正なRSS(無効なXML、空のフィード、必須フィールドの欠落)のテスト - 破損したYAMLファイル(構文エラー、不正な構造、許可されていないクラス)のテスト - 複数エラーの同時発生とエラーリカバリーのテストも含む Co-authored-by: nacchan99 <[email protected]>
1 parent 052cfe2 commit b35ad64

File tree

1 file changed

+264
-0
lines changed

1 file changed

+264
-0
lines changed
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
require 'rails_helper'
2+
require 'rake'
3+
require 'yaml'
4+
require 'net/http'
5+
6+
RSpec.describe 'news:fetch エラーハンドリング', type: :task do
7+
before { Rails.application.load_tasks }
8+
before { allow(Rails.env).to receive(:test?).and_return(true) }
9+
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+
before do
15+
ENV['NEWS_YAML_PATH'] = yaml_path.to_s
16+
fetch_task.reenable
17+
18+
# ロガーのモック設定
19+
allow(ActiveSupport::BroadcastLogger).to receive(:new).and_return(logger_mock)
20+
allow(logger_mock).to receive(:info)
21+
allow(logger_mock).to receive(:warn)
22+
end
23+
24+
after do
25+
ENV.delete('NEWS_YAML_PATH')
26+
ENV.delete('NEWS_RSS_PATH')
27+
File.delete(yaml_path) if File.exist?(yaml_path)
28+
end
29+
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
61+
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')
64+
end
65+
66+
it 'エラーをログに記録し、処理を継続する' do
67+
expect(logger_mock).to receive(:warn).with(/⚠️ Failed to fetch .+: 不正なURLです/)
68+
expect { fetch_task.invoke }.not_to raise_error
69+
end
70+
end
71+
end
72+
73+
describe '不正なRSSのハンドリング' do
74+
context 'RSS::Parser.parse が失敗する場合' do
75+
before do
76+
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>')
80+
end
81+
82+
it 'エラーをログに記録し、処理を継続する' do
83+
expect(logger_mock).to receive(:warn).with(/⚠️ Failed to fetch .+: /)
84+
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([])
90+
end
91+
end
92+
93+
context '空のRSSフィードの場合' do
94+
before do
95+
ENV['NEWS_RSS_PATH'] = 'https://example.com/feed.rss'
96+
97+
# 有効だが空のRSSフィード
98+
empty_rss = <<~RSS
99+
<?xml version="1.0" encoding="UTF-8"?>
100+
<rss version="2.0">
101+
<channel>
102+
<title>Empty Feed</title>
103+
<description>Empty RSS Feed</description>
104+
<link>https://example.com</link>
105+
</channel>
106+
</rss>
107+
RSS
108+
109+
allow_any_instance_of(Object).to receive(:safe_open).and_return(empty_rss)
110+
end
111+
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 .+/)
145+
expect { fetch_task.invoke }.not_to raise_error
146+
end
147+
end
148+
end
149+
150+
describe '破損したYAMLファイルのハンドリング' do
151+
context '既存のYAMLファイルが破損している場合' do
152+
before do
153+
ENV['NEWS_RSS_PATH'] = Rails.root.join('spec', 'fixtures', 'sample_news.rss').to_s
154+
155+
# 破損したYAMLファイルを作成
156+
File.write(yaml_path, "invalid yaml content:\n - broken\n indentation:\n - here")
157+
end
158+
159+
it 'YAML読み込みエラーが発生し、タスクが失敗する' do
160+
# YAML.safe_load のエラーは rescue されないため、タスク全体が失敗する
161+
expect { fetch_task.invoke }.to raise_error(Psych::SyntaxError)
162+
end
163+
end
164+
165+
context '既存のYAMLファイルが不正な構造の場合' do
166+
before do
167+
ENV['NEWS_RSS_PATH'] = Rails.root.join('spec', 'fixtures', 'sample_news.rss').to_s
168+
169+
# 不正な構造のYAMLファイル(newsキーがない)
170+
File.write(yaml_path, { 'invalid_key' => [{ 'id' => 1 }] }.to_yaml)
171+
end
172+
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+
# 再実行すると正常に処理される
258+
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
261+
end
262+
end
263+
end
264+
end

0 commit comments

Comments
 (0)