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