From 7e29ea1d0c0df1c2b2dba86659b21fa0d6d7303e Mon Sep 17 00:00:00 2001 From: nacchan Date: Thu, 24 Jul 2025 11:26:30 +0900 Subject: [PATCH 01/12] =?UTF-8?q?test:=20News=E3=83=A2=E3=83=87=E3=83=AB?= =?UTF-8?q?=E3=81=AE=E3=83=90=E3=83=AA=E3=83=87=E3=83=BC=E3=82=B7=E3=83=A7?= =?UTF-8?q?=E3=83=B3=E3=83=BB=E3=82=B9=E3=82=B3=E3=83=BC=E3=83=97=E3=81=AE?= =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/models/news_spec.rb | 64 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/spec/models/news_spec.rb b/spec/models/news_spec.rb index 0063aaf4..836ec814 100644 --- a/spec/models/news_spec.rb +++ b/spec/models/news_spec.rb @@ -1,5 +1,67 @@ require 'rails_helper' RSpec.describe News, type: :model do - pending "add some examples to (or delete) #{__FILE__}" + describe 'バリデーション' do + let(:news) { build(:news) } + + it '有効なファクトリーを持つ' do + expect(news).to be_valid + end + + describe 'title' do + it 'presence: true' do + news.title = nil + expect(news).not_to be_valid + expect(news.errors[:title]).not_to be_empty + end + end + + describe 'url' do + it 'presence: true' do + news.url = nil + expect(news).not_to be_valid + expect(news.errors[:url]).not_to be_empty + end + + it 'uniqueness: true' do + create(:news, url: 'https://example.com/test') + duplicate_news = build(:news, url: 'https://example.com/test') + expect(duplicate_news).not_to be_valid + expect(duplicate_news.errors[:url]).not_to be_empty + end + + it 'URL形式であること' do + news.url = 'invalid-url' + expect(news).not_to be_valid + expect(news.errors[:url]).not_to be_empty + end + + it 'HTTPSとHTTPを許可する' do + news.url = 'https://example.com' + expect(news).to be_valid + + news.url = 'http://example.com' + expect(news).to be_valid + end + end + + describe 'published_at' do + it 'presence: true' do + news.published_at = nil + expect(news).not_to be_valid + expect(news.errors[:published_at]).not_to be_empty + end + end + end + + describe 'スコープ' do + describe '.recent' do + it '公開日時の降順で並び替える' do + old_news = create(:news, published_at: 2.days.ago) + new_news = create(:news, published_at: 1.day.ago) + + expect(News.recent).to eq([new_news, old_news]) + end + end + end end From b0f43659eb9ac75ef8940a2df4eccf57fc973d85 Mon Sep 17 00:00:00 2001 From: nacchan Date: Thu, 24 Jul 2025 12:19:09 +0900 Subject: [PATCH 02/12] =?UTF-8?q?test:=20News=E3=83=A2=E3=83=87=E3=83=AB?= =?UTF-8?q?=E3=82=B9=E3=83=9A=E3=83=83=E3=82=AF=E3=81=AE=E8=AA=AC=E6=98=8E?= =?UTF-8?q?=E3=82=92=E6=97=A5=E6=9C=AC=E8=AA=9E=E3=81=A7=E7=B5=B1=E4=B8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/models/news_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/models/news_spec.rb b/spec/models/news_spec.rb index 836ec814..45a01f29 100644 --- a/spec/models/news_spec.rb +++ b/spec/models/news_spec.rb @@ -9,7 +9,7 @@ end describe 'title' do - it 'presence: true' do + it 'タイトルが空の場合は無効になる' do news.title = nil expect(news).not_to be_valid expect(news.errors[:title]).not_to be_empty @@ -17,13 +17,13 @@ end describe 'url' do - it 'presence: true' do + it 'URL が空の場合は無効になる' do news.url = nil expect(news).not_to be_valid expect(news.errors[:url]).not_to be_empty end - it 'uniqueness: true' do + it 'URL が重複している場合は無効になる' do create(:news, url: 'https://example.com/test') duplicate_news = build(:news, url: 'https://example.com/test') expect(duplicate_news).not_to be_valid @@ -46,7 +46,7 @@ end describe 'published_at' do - it 'presence: true' do + it '公開日時が空の場合は無効になる' do news.published_at = nil expect(news).not_to be_valid expect(news.errors[:published_at]).not_to be_empty From 052cfe2a9311ab3252f04874751d765206883ee1 Mon Sep 17 00:00:00 2001 From: nacchan Date: Fri, 25 Jul 2025 09:17:08 +0900 Subject: [PATCH 03/12] =?UTF-8?q?test:=20news=20Rake=E3=82=BF=E3=82=B9?= =?UTF-8?q?=E3=82=AF=E3=81=AERSpec=E3=82=92=E8=BF=BD=E5=8A=A0=20&=20fetch/?= =?UTF-8?q?import=E3=82=BF=E3=82=B9=E3=82=AF=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - news:fetchタスクでfileスキーム/ローカルパスをsafe_openで許可(テスト用RSSの読み込みに対応) - YAML書き込み・ログ出力のパスをENV['NEWS_YAML_PATH']で上書き可能なyaml_pathに統一 - ソート後に全アイテムへ1からの連番IDを再付与 - news:import_from_yamlタスクもENV['NEWS_YAML_PATH']に対応 --- lib/tasks/fetch_news.rake | 30 ++++++------- lib/tasks/import_news.rake | 5 ++- spec/tasks/news_rake_spec.rb | 86 ++++++++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 18 deletions(-) create mode 100644 spec/tasks/news_rake_spec.rb diff --git a/lib/tasks/fetch_news.rake b/lib/tasks/fetch_news.rake index 415d5df2..00de4927 100644 --- a/lib/tasks/fetch_news.rake +++ b/lib/tasks/fetch_news.rake @@ -7,8 +7,8 @@ require 'active_support/broadcast_logger' def safe_open(url) uri = URI.parse(url) + return File.read(url) if uri.scheme.nil? || uri.scheme == 'file' raise "不正なURLです: #{url}" unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS) - Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http| request = Net::HTTP::Get.new(uri) response = http.request(request) @@ -26,25 +26,19 @@ namespace :news do logger.info('==== START news:fetch ====') - # 既存の news.yml を読み込み - yaml_path = Rails.root.join('db', 'news.yml') + # YAML出力先を環境変数で上書きできるようにする + yaml_path = ENV['NEWS_YAML_PATH'] ? Pathname.new(ENV['NEWS_YAML_PATH']) : Rails.root.join('db', 'news.yml') + feed_urls = ENV['NEWS_RSS_PATH'] ? [ENV['NEWS_RSS_PATH']] : + (Rails.env.test? || Rails.env.staging? ? + [Rails.root.join('spec', 'fixtures', 'sample_news.rss').to_s] : + ['https://news.coderdojo.jp/feed/']) + existing_news = if File.exist?(yaml_path) YAML.safe_load(File.read(yaml_path), permitted_classes: [Time], aliases: true)['news'] || [] else [] end - # テスト/ステージング環境ではサンプルファイル、本番は実サイトのフィード - feed_urls = if Rails.env.test? || Rails.env.staging? - [Rails.root.join('spec', 'fixtures', 'sample_news.rss').to_s] - else - [ - 'https://news.coderdojo.jp/feed/' - # 必要に応じて他 Dojo の RSS もここに追加可能 - # 'https://coderdojotokyo.org/feed', - ] - end - # RSS 取得&パース new_items = feed_urls.flat_map do |url| logger.info("Fetching RSS → #{url}") @@ -107,7 +101,11 @@ namespace :news do Time.parse(item['published_at']) }.reverse - File.open('db/news.yml', 'w') do |f| + sorted_items.each_with_index do |item, index| + item['id'] = index + 1 + end + + File.open(yaml_path, 'w') do |f| formatted_items = sorted_items.map do |item| { 'id' => item['id'], @@ -120,7 +118,7 @@ namespace :news do f.write({ 'news' => formatted_items }.to_yaml) end - logger.info("✅ Wrote #{sorted_items.size} items to db/news.yml (#{truly_new_items_sorted.size} new, #{updated_items.size} updated)") + logger.info("✅ Wrote #{sorted_items.size} items to #{yaml_path} (#{truly_new_items_sorted.size} new, #{updated_items.size} updated)") logger.info('==== END news:fetch ====') end end diff --git a/lib/tasks/import_news.rake b/lib/tasks/import_news.rake index bebe7661..ba73110d 100644 --- a/lib/tasks/import_news.rake +++ b/lib/tasks/import_news.rake @@ -1,9 +1,10 @@ require 'yaml' namespace :news do - desc 'db/news.yml を読み込んで News テーブルを upsert する' + desc 'db/news.yml (またはENV指定のYAML)を読み込んで News テーブルを upsert する' task import_from_yaml: :environment do - yaml_path = Rails.root.join('db', 'news.yml') + # ENVで上書き可能にする(なければデフォルト db/news.yml) + yaml_path = ENV['NEWS_YAML_PATH'] ? Pathname.new(ENV['NEWS_YAML_PATH']) : Rails.root.join('db', 'news.yml') raw = YAML.safe_load(File.read(yaml_path), permitted_classes: [Time], aliases: true) # entries を計算 diff --git a/spec/tasks/news_rake_spec.rb b/spec/tasks/news_rake_spec.rb new file mode 100644 index 00000000..2bd58602 --- /dev/null +++ b/spec/tasks/news_rake_spec.rb @@ -0,0 +1,86 @@ +require 'rails_helper' +require 'rake' +require 'yaml' + +RSpec.describe 'news Rakeタスク', type: :task do + before { Rails.application.load_tasks } + before { allow(Rails.env).to receive(:test?).and_return(true) } + + # テスト用に tmp/news.yml を使う + let(:yaml_path) { Rails.root.join('tmp', 'news.yml') } + let(:fetch_task) { Rake::Task['news:fetch'] } + let(:import_task) { Rake::Task['news:import_from_yaml'] } + let(:yaml_content) { YAML.safe_load(File.read(yaml_path), permitted_classes: [Time]) } + + around do |example| + # テスト前後に一度だけ tmp/news.yml をクリア + File.delete(yaml_path) if File.exist?(yaml_path) + example.run + File.delete(yaml_path) if File.exist?(yaml_path) + end + + describe 'news:fetch タスク' do + before do + ENV['NEWS_YAML_PATH'] = yaml_path.to_s + ENV['NEWS_RSS_PATH'] = Rails.root.join('spec', 'fixtures', 'sample_news.rss').to_s + fetch_task.reenable + end + + after do + ENV.delete('NEWS_YAML_PATH') + ENV.delete('NEWS_RSS_PATH') + end + + it 'サンプルRSSからニュースを取得し YAML に書き込む' do + expect { fetch_task.invoke }.not_to raise_error + expect(File.exist?(yaml_path)).to be true + expect(yaml_content['news']).to be_an(Array) + expect(yaml_content['news'].size).to eq(3) + end + + it 'ID が 1 から連番で付与される' do + fetch_task.invoke + ids = yaml_content['news'].map { |item| item['id'] } + expect(ids).to eq([1, 2, 3]) + end + + it '公開日時で降順ソートされる' do + fetch_task.invoke + dates = yaml_content['news'].map { |item| Time.parse(item['published_at']) } + expect(dates).to eq(dates.sort.reverse) + end + end + + describe 'news:import_from_yaml タスク' do + let(:news_data) do + { + 'news' => [ + { 'id' => 1, 'url' => 'https://example.com/test1', 'title' => 'テスト記事1', 'published_at' => '2025-01-01T10:00:00Z' }, + { 'id' => 2, 'url' => 'https://example.com/test2', 'title' => 'テスト記事2', 'published_at' => '2025-01-02T10:00:00Z' } + ] + } + end + + before do + ENV['NEWS_YAML_PATH'] = yaml_path.to_s + File.write(yaml_path, news_data.to_yaml) + import_task.reenable + end + + after do + ENV.delete('NEWS_YAML_PATH') + end + + it 'YAML ファイルから News レコードを新規作成する' do + expect { import_task.invoke }.to change(News, :count).by(2) + expect(News.find_by(url: news_data['news'][0]['url']).title).to eq('テスト記事1') + expect(News.find_by(url: news_data['news'][1]['url']).title).to eq('テスト記事2') + end + + it '既存レコードがあれば属性を更新する' do + create(:news, url: news_data['news'][0]['url'], title: '古いタイトル') + expect { import_task.invoke }.to change(News, :count).by(1) + expect(News.find_by(url: news_data['news'][0]['url']).title).to eq('テスト記事1') + end + end +end From b35ad64d7d89cc9088b77c0a5101ea220e797d6e Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Fri, 25 Jul 2025 00:31:38 +0000 Subject: [PATCH 04/12] =?UTF-8?q?test:=20news:fetch=E3=82=BF=E3=82=B9?= =?UTF-8?q?=E3=82=AF=E3=81=AE=E3=82=A8=E3=83=A9=E3=83=BC=E3=83=8F=E3=83=B3?= =?UTF-8?q?=E3=83=89=E3=83=AA=E3=83=B3=E3=82=B0=E7=94=A8RSpec=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ネットワークエラー(接続タイムアウト、HTTPエラー、不正なURL)のテスト - 不正なRSS(無効なXML、空のフィード、必須フィールドの欠落)のテスト - 破損したYAMLファイル(構文エラー、不正な構造、許可されていないクラス)のテスト - 複数エラーの同時発生とエラーリカバリーのテストも含む Co-authored-by: nacchan99 --- spec/tasks/news_fetch_error_handling_spec.rb | 264 +++++++++++++++++++ 1 file changed, 264 insertions(+) create mode 100644 spec/tasks/news_fetch_error_handling_spec.rb diff --git a/spec/tasks/news_fetch_error_handling_spec.rb b/spec/tasks/news_fetch_error_handling_spec.rb new file mode 100644 index 00000000..574b6df2 --- /dev/null +++ b/spec/tasks/news_fetch_error_handling_spec.rb @@ -0,0 +1,264 @@ +require 'rails_helper' +require 'rake' +require 'yaml' +require 'net/http' + +RSpec.describe 'news:fetch エラーハンドリング', type: :task do + before { Rails.application.load_tasks } + before { allow(Rails.env).to receive(:test?).and_return(true) } + + let(:yaml_path) { Rails.root.join('tmp', 'error_test_news.yml') } + let(:fetch_task) { Rake::Task['news:fetch'] } + let(:logger_mock) { instance_double(ActiveSupport::BroadcastLogger) } + + before do + ENV['NEWS_YAML_PATH'] = yaml_path.to_s + fetch_task.reenable + + # ロガーのモック設定 + allow(ActiveSupport::BroadcastLogger).to receive(:new).and_return(logger_mock) + allow(logger_mock).to receive(:info) + allow(logger_mock).to receive(:warn) + end + + after do + ENV.delete('NEWS_YAML_PATH') + ENV.delete('NEWS_RSS_PATH') + File.delete(yaml_path) if File.exist?(yaml_path) + end + + describe 'ネットワークエラーのハンドリング' do + context 'safe_open がネットワークエラーで例外を投げる場合' do + before do + ENV['NEWS_RSS_PATH'] = 'https://example.com/feed.rss' + allow_any_instance_of(Object).to receive(:safe_open).and_raise(Net::OpenTimeout, '接続タイムアウト') + end + + it 'エラーをログに記録し、処理を継続する' do + expect(logger_mock).to receive(:warn).with(/⚠️ Failed to fetch .+: 接続タイムアウト/) + expect { fetch_task.invoke }.not_to raise_error + + # 空の news.yml が作成される + expect(File.exist?(yaml_path)).to be true + yaml_content = YAML.safe_load(File.read(yaml_path), permitted_classes: [Time]) + expect(yaml_content['news']).to eq([]) + end + end + + context 'HTTPエラーレスポンスの場合' do + before do + ENV['NEWS_RSS_PATH'] = 'https://example.com/feed.rss' + allow_any_instance_of(Object).to receive(:safe_open).and_raise(Net::HTTPServerException, '500 Internal Server Error') + end + + it 'エラーをログに記録し、処理を継続する' do + expect(logger_mock).to receive(:warn).with(/⚠️ Failed to fetch .+: 500 Internal Server Error/) + expect { fetch_task.invoke }.not_to raise_error + end + end + + context '不正なURLの場合' do + before do + ENV['NEWS_RSS_PATH'] = 'https://example.com/feed.rss' + allow_any_instance_of(Object).to receive(:safe_open).and_raise('不正なURLです: https://example.com/feed.rss') + end + + it 'エラーをログに記録し、処理を継続する' do + expect(logger_mock).to receive(:warn).with(/⚠️ Failed to fetch .+: 不正なURLです/) + expect { fetch_task.invoke }.not_to raise_error + end + end + end + + describe '不正なRSSのハンドリング' do + context 'RSS::Parser.parse が失敗する場合' do + before do + ENV['NEWS_RSS_PATH'] = 'https://example.com/feed.rss' + + # safe_open は成功するが、不正なXMLを返す + allow_any_instance_of(Object).to receive(:safe_open).and_return('not valid rss') + end + + it 'エラーをログに記録し、処理を継続する' do + expect(logger_mock).to receive(:warn).with(/⚠️ Failed to fetch .+: /) + expect { fetch_task.invoke }.not_to raise_error + + # 空の news.yml が作成される + expect(File.exist?(yaml_path)).to be true + yaml_content = YAML.safe_load(File.read(yaml_path), permitted_classes: [Time]) + expect(yaml_content['news']).to eq([]) + end + end + + context '空のRSSフィードの場合' do + before do + ENV['NEWS_RSS_PATH'] = 'https://example.com/feed.rss' + + # 有効だが空のRSSフィード + empty_rss = <<~RSS + + + + Empty Feed + Empty RSS Feed + https://example.com + + + RSS + + allow_any_instance_of(Object).to receive(:safe_open).and_return(empty_rss) + end + + it '空の配列として処理し、エラーにならない' do + expect { fetch_task.invoke }.not_to raise_error + + yaml_content = YAML.safe_load(File.read(yaml_path), permitted_classes: [Time]) + expect(yaml_content['news']).to eq([]) + end + end + + context 'RSSアイテムに必須フィールドが欠けている場合' do + before do + ENV['NEWS_RSS_PATH'] = 'https://example.com/feed.rss' + + # linkやpubDateが欠けているRSS + invalid_rss = <<~RSS + + + + Invalid Feed + Invalid RSS Feed + https://example.com + + タイトルのみの記事 + + + + + RSS + + allow_any_instance_of(Object).to receive(:safe_open).and_return(invalid_rss) + end + + it 'エラーをログに記録し、処理を継続する' do + expect(logger_mock).to receive(:warn).with(/⚠️ Failed to fetch .+/) + expect { fetch_task.invoke }.not_to raise_error + end + end + end + + describe '破損したYAMLファイルのハンドリング' do + context '既存のYAMLファイルが破損している場合' do + before do + ENV['NEWS_RSS_PATH'] = Rails.root.join('spec', 'fixtures', 'sample_news.rss').to_s + + # 破損したYAMLファイルを作成 + File.write(yaml_path, "invalid yaml content:\n - broken\n indentation:\n - here") + end + + it 'YAML読み込みエラーが発生し、タスクが失敗する' do + # YAML.safe_load のエラーは rescue されないため、タスク全体が失敗する + expect { fetch_task.invoke }.to raise_error(Psych::SyntaxError) + end + end + + context '既存のYAMLファイルが不正な構造の場合' do + before do + ENV['NEWS_RSS_PATH'] = Rails.root.join('spec', 'fixtures', 'sample_news.rss').to_s + + # 不正な構造のYAMLファイル(newsキーがない) + File.write(yaml_path, { 'invalid_key' => [{ 'id' => 1 }] }.to_yaml) + end + + it '空の配列として扱い、処理を継続する' do + expect { fetch_task.invoke }.not_to raise_error + + # 新しいデータで上書きされる + yaml_content = YAML.safe_load(File.read(yaml_path), permitted_classes: [Time]) + expect(yaml_content['news']).to be_an(Array) + expect(yaml_content['news'].size).to be > 0 + end + end + + context '許可されていないクラスを含むYAMLファイルの場合' do + before do + ENV['NEWS_RSS_PATH'] = Rails.root.join('spec', 'fixtures', 'sample_news.rss').to_s + + # DateTimeオブジェクトを含むYAML(Timeのみ許可されている) + yaml_content = { + 'news' => [ + { + 'id' => 1, + 'url' => 'https://example.com/test', + 'title' => 'テスト', + 'published_at' => DateTime.now + } + ] + } + + # 強制的にDateTimeオブジェクトを含むYAMLを作成 + File.write(yaml_path, yaml_content.to_yaml.gsub('!ruby/object:DateTime', '!ruby/object:DateTime')) + end + + it 'YAML読み込みエラーが発生し、タスクが失敗する' do + expect { fetch_task.invoke }.to raise_error(Psych::DisallowedClass) + end + end + end + + describe '複数のエラーが同時に発生する場合' do + context '複数のRSSフィードで異なるエラーが発生する場合' do + before do + # 複数のフィードURLを環境変数経由では設定できないため、 + # デフォルトの動作をオーバーライドする + allow(Rails.env).to receive(:test?).and_return(false) + allow(Rails.env).to receive(:staging?).and_return(false) + ENV.delete('NEWS_RSS_PATH') + + # 最初のフィードはネットワークエラー + allow_any_instance_of(Object).to receive(:safe_open) + .with('https://news.coderdojo.jp/feed/') + .and_raise(Net::OpenTimeout, 'タイムアウト') + end + + it '各エラーをログに記録し、処理を継続する' do + expect(logger_mock).to receive(:warn).with(/⚠️ Failed to fetch .+: タイムアウト/) + expect { fetch_task.invoke }.not_to raise_error + + # 空の news.yml が作成される + expect(File.exist?(yaml_path)).to be true + yaml_content = YAML.safe_load(File.read(yaml_path), permitted_classes: [Time]) + expect(yaml_content['news']).to eq([]) + end + end + end + + describe 'エラーリカバリー' do + context 'ネットワークエラー後に再実行した場合' do + before do + ENV['NEWS_RSS_PATH'] = Rails.root.join('spec', 'fixtures', 'sample_news.rss').to_s + end + + it '正常に処理される' do + # 最初はネットワークエラー + allow_any_instance_of(Object).to receive(:safe_open).and_raise(Net::OpenTimeout, 'タイムアウト') + expect { fetch_task.invoke }.not_to raise_error + + # エラー時は空のYAMLが作成される + yaml_content = YAML.safe_load(File.read(yaml_path), permitted_classes: [Time]) + expect(yaml_content['news']).to eq([]) + + # safe_openのモックを解除して正常動作に戻す + allow_any_instance_of(Object).to receive(:safe_open).and_call_original + + # タスクを再実行可能にする + fetch_task.reenable + + # 再実行すると正常に処理される + expect { fetch_task.invoke }.not_to raise_error + yaml_content = YAML.safe_load(File.read(yaml_path), permitted_classes: [Time]) + expect(yaml_content['news'].size).to be > 0 + end + end + end +end \ No newline at end of file From 9951baecf898cb74a5b76ff77a225d04613aa546 Mon Sep 17 00:00:00 2001 From: nacchan Date: Fri, 25 Jul 2025 11:04:15 +0900 Subject: [PATCH 05/12] =?UTF-8?q?test:=20news:fetch=20=E3=82=BF=E3=82=B9?= =?UTF-8?q?=E3=82=AF=E3=81=AE=E3=82=A8=E3=83=A9=E3=83=BC=E3=83=8F=E3=83=B3?= =?UTF-8?q?=E3=83=89=E3=83=AA=E3=83=B3=E3=82=B0=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ネットワークエラーやRSS解析エラー時の挙動を確認 - 破損・不正なYAMLファイルのケースを追加 - ロガーをモック化してwarnログの出力を検証 --- spec/tasks/news_fetch_error_handling_spec.rb | 220 +++---------------- 1 file changed, 34 insertions(+), 186 deletions(-) diff --git a/spec/tasks/news_fetch_error_handling_spec.rb b/spec/tasks/news_fetch_error_handling_spec.rb index 574b6df2..7bf71d55 100644 --- a/spec/tasks/news_fetch_error_handling_spec.rb +++ b/spec/tasks/news_fetch_error_handling_spec.rb @@ -7,15 +7,19 @@ before { Rails.application.load_tasks } before { allow(Rails.env).to receive(:test?).and_return(true) } - let(:yaml_path) { Rails.root.join('tmp', 'error_test_news.yml') } - let(:fetch_task) { Rake::Task['news:fetch'] } - let(:logger_mock) { instance_double(ActiveSupport::BroadcastLogger) } + let(:yaml_path) { Rails.root.join('tmp', 'error_test_news.yml') } + let(:fetch_task) { Rake::Task['news:fetch'] } + let(:logger_mock) { instance_double(ActiveSupport::BroadcastLogger) } + + around do |example| + File.delete(yaml_path) if File.exist?(yaml_path) + example.run + File.delete(yaml_path) if File.exist?(yaml_path) + end before do ENV['NEWS_YAML_PATH'] = yaml_path.to_s fetch_task.reenable - - # ロガーのモック設定 allow(ActiveSupport::BroadcastLogger).to receive(:new).and_return(logger_mock) allow(logger_mock).to receive(:info) allow(logger_mock).to receive(:warn) @@ -24,77 +28,40 @@ after do ENV.delete('NEWS_YAML_PATH') ENV.delete('NEWS_RSS_PATH') - File.delete(yaml_path) if File.exist?(yaml_path) end - describe 'ネットワークエラーのハンドリング' do - context 'safe_open がネットワークエラーで例外を投げる場合' do - before do - ENV['NEWS_RSS_PATH'] = 'https://example.com/feed.rss' - allow_any_instance_of(Object).to receive(:safe_open).and_raise(Net::OpenTimeout, '接続タイムアウト') - end - - it 'エラーをログに記録し、処理を継続する' do - expect(logger_mock).to receive(:warn).with(/⚠️ Failed to fetch .+: 接続タイムアウト/) - expect { fetch_task.invoke }.not_to raise_error - - # 空の news.yml が作成される - expect(File.exist?(yaml_path)).to be true - yaml_content = YAML.safe_load(File.read(yaml_path), permitted_classes: [Time]) - expect(yaml_content['news']).to eq([]) - end - end - - context 'HTTPエラーレスポンスの場合' do - before do - ENV['NEWS_RSS_PATH'] = 'https://example.com/feed.rss' - allow_any_instance_of(Object).to receive(:safe_open).and_raise(Net::HTTPServerException, '500 Internal Server Error') - end - - it 'エラーをログに記録し、処理を継続する' do - expect(logger_mock).to receive(:warn).with(/⚠️ Failed to fetch .+: 500 Internal Server Error/) - expect { fetch_task.invoke }.not_to raise_error - end - end - - context '不正なURLの場合' do + describe 'ネットワーク・RSSエラー時の挙動' do + context 'ネットワークエラーの場合' do before do - ENV['NEWS_RSS_PATH'] = 'https://example.com/feed.rss' - allow_any_instance_of(Object).to receive(:safe_open).and_raise('不正なURLです: https://example.com/feed.rss') + ENV['NEWS_RSS_PATH'] = 'https://invalid-url.example.com/rss' + allow(self).to receive(:safe_open).and_raise(Net::OpenTimeout, '接続タイムアウト') end - it 'エラーをログに記録し、処理を継続する' do - expect(logger_mock).to receive(:warn).with(/⚠️ Failed to fetch .+: 不正なURLです/) + it 'warnログを出し、空のnews.ymlを生成する' do + expect(logger_mock).to receive(:warn).with(/⚠️ Failed to fetch .+/) expect { fetch_task.invoke }.not_to raise_error + yaml = YAML.safe_load(File.read(yaml_path), permitted_classes: [Time]) + expect(yaml['news']).to eq([]) end end - end - describe '不正なRSSのハンドリング' do - context 'RSS::Parser.parse が失敗する場合' do + context 'RSS::Parser.parseが失敗する場合' do before do ENV['NEWS_RSS_PATH'] = 'https://example.com/feed.rss' - - # safe_open は成功するが、不正なXMLを返す - allow_any_instance_of(Object).to receive(:safe_open).and_return('not valid rss') + allow(self).to receive(:safe_open).and_return('not valid rss') end - it 'エラーをログに記録し、処理を継続する' do - expect(logger_mock).to receive(:warn).with(/⚠️ Failed to fetch .+: /) + it 'warnログを出し、空のnews.ymlを生成する' do + expect(logger_mock).to receive(:warn).with(/⚠️ Failed to fetch .+/) expect { fetch_task.invoke }.not_to raise_error - - # 空の news.yml が作成される - expect(File.exist?(yaml_path)).to be true - yaml_content = YAML.safe_load(File.read(yaml_path), permitted_classes: [Time]) - expect(yaml_content['news']).to eq([]) + yaml = YAML.safe_load(File.read(yaml_path), permitted_classes: [Time]) + expect(yaml['news']).to eq([]) end end context '空のRSSフィードの場合' do before do ENV['NEWS_RSS_PATH'] = 'https://example.com/feed.rss' - - # 有効だが空のRSSフィード empty_rss = <<~RSS @@ -105,160 +72,41 @@ RSS - - allow_any_instance_of(Object).to receive(:safe_open).and_return(empty_rss) + allow(self).to receive(:safe_open).and_return(empty_rss) end - it '空の配列として処理し、エラーにならない' do - expect { fetch_task.invoke }.not_to raise_error - - yaml_content = YAML.safe_load(File.read(yaml_path), permitted_classes: [Time]) - expect(yaml_content['news']).to eq([]) - end - end - - context 'RSSアイテムに必須フィールドが欠けている場合' do - before do - ENV['NEWS_RSS_PATH'] = 'https://example.com/feed.rss' - - # linkやpubDateが欠けているRSS - invalid_rss = <<~RSS - - - - Invalid Feed - Invalid RSS Feed - https://example.com - - タイトルのみの記事 - - - - - RSS - - allow_any_instance_of(Object).to receive(:safe_open).and_return(invalid_rss) - end - - it 'エラーをログに記録し、処理を継続する' do - expect(logger_mock).to receive(:warn).with(/⚠️ Failed to fetch .+/) + it '空配列でnews.ymlを生成する' do expect { fetch_task.invoke }.not_to raise_error + yaml = YAML.safe_load(File.read(yaml_path), permitted_classes: [Time]) + expect(yaml['news']).to eq([]) end end end describe '破損したYAMLファイルのハンドリング' do - context '既存のYAMLファイルが破損している場合' do + context '既存のYAMLが破損している場合' do before do ENV['NEWS_RSS_PATH'] = Rails.root.join('spec', 'fixtures', 'sample_news.rss').to_s - - # 破損したYAMLファイルを作成 File.write(yaml_path, "invalid yaml content:\n - broken\n indentation:\n - here") end it 'YAML読み込みエラーが発生し、タスクが失敗する' do - # YAML.safe_load のエラーは rescue されないため、タスク全体が失敗する expect { fetch_task.invoke }.to raise_error(Psych::SyntaxError) end end - context '既存のYAMLファイルが不正な構造の場合' do + context '既存のYAMLが不正な構造の場合' do before do ENV['NEWS_RSS_PATH'] = Rails.root.join('spec', 'fixtures', 'sample_news.rss').to_s - - # 不正な構造のYAMLファイル(newsキーがない) File.write(yaml_path, { 'invalid_key' => [{ 'id' => 1 }] }.to_yaml) end - it '空の配列として扱い、処理を継続する' do - expect { fetch_task.invoke }.not_to raise_error - - # 新しいデータで上書きされる - yaml_content = YAML.safe_load(File.read(yaml_path), permitted_classes: [Time]) - expect(yaml_content['news']).to be_an(Array) - expect(yaml_content['news'].size).to be > 0 - end - end - - context '許可されていないクラスを含むYAMLファイルの場合' do - before do - ENV['NEWS_RSS_PATH'] = Rails.root.join('spec', 'fixtures', 'sample_news.rss').to_s - - # DateTimeオブジェクトを含むYAML(Timeのみ許可されている) - yaml_content = { - 'news' => [ - { - 'id' => 1, - 'url' => 'https://example.com/test', - 'title' => 'テスト', - 'published_at' => DateTime.now - } - ] - } - - # 強制的にDateTimeオブジェクトを含むYAMLを作成 - File.write(yaml_path, yaml_content.to_yaml.gsub('!ruby/object:DateTime', '!ruby/object:DateTime')) - end - - it 'YAML読み込みエラーが発生し、タスクが失敗する' do - expect { fetch_task.invoke }.to raise_error(Psych::DisallowedClass) - end - end - end - - describe '複数のエラーが同時に発生する場合' do - context '複数のRSSフィードで異なるエラーが発生する場合' do - before do - # 複数のフィードURLを環境変数経由では設定できないため、 - # デフォルトの動作をオーバーライドする - allow(Rails.env).to receive(:test?).and_return(false) - allow(Rails.env).to receive(:staging?).and_return(false) - ENV.delete('NEWS_RSS_PATH') - - # 最初のフィードはネットワークエラー - allow_any_instance_of(Object).to receive(:safe_open) - .with('https://news.coderdojo.jp/feed/') - .and_raise(Net::OpenTimeout, 'タイムアウト') - end - - it '各エラーをログに記録し、処理を継続する' do - expect(logger_mock).to receive(:warn).with(/⚠️ Failed to fetch .+: タイムアウト/) - expect { fetch_task.invoke }.not_to raise_error - - # 空の news.yml が作成される - expect(File.exist?(yaml_path)).to be true - yaml_content = YAML.safe_load(File.read(yaml_path), permitted_classes: [Time]) - expect(yaml_content['news']).to eq([]) - end - end - end - - describe 'エラーリカバリー' do - context 'ネットワークエラー後に再実行した場合' do - before do - ENV['NEWS_RSS_PATH'] = Rails.root.join('spec', 'fixtures', 'sample_news.rss').to_s - end - - it '正常に処理される' do - # 最初はネットワークエラー - allow_any_instance_of(Object).to receive(:safe_open).and_raise(Net::OpenTimeout, 'タイムアウト') - expect { fetch_task.invoke }.not_to raise_error - - # エラー時は空のYAMLが作成される - yaml_content = YAML.safe_load(File.read(yaml_path), permitted_classes: [Time]) - expect(yaml_content['news']).to eq([]) - - # safe_openのモックを解除して正常動作に戻す - allow_any_instance_of(Object).to receive(:safe_open).and_call_original - - # タスクを再実行可能にする - fetch_task.reenable - - # 再実行すると正常に処理される + it '空配列として扱い、正常に上書きされる' do expect { fetch_task.invoke }.not_to raise_error - yaml_content = YAML.safe_load(File.read(yaml_path), permitted_classes: [Time]) - expect(yaml_content['news'].size).to be > 0 + yaml = YAML.safe_load(File.read(yaml_path), permitted_classes: [Time]) + expect(yaml['news']).to be_an(Array) + expect(yaml['news'].size).to be > 0 end end end -end \ No newline at end of file +end From 1850e1f20a65ac035d7c3d07f5503d804d0210e4 Mon Sep 17 00:00:00 2001 From: nacchan Date: Wed, 30 Jul 2025 12:12:03 +0900 Subject: [PATCH 06/12] =?UTF-8?q?=E6=96=B9=E9=87=9D=E5=A4=89=E6=9B=B4?= =?UTF-8?q?=E3=81=AB=E3=82=88=E3=82=8A=E4=B8=8D=E8=A6=81=E3=81=A8=E3=81=AA?= =?UTF-8?q?=E3=81=A3=E3=81=9Fnews=E3=82=BF=E3=82=B9=E3=82=AF=E9=96=A2?= =?UTF-8?q?=E9=80=A3=E3=81=AE=E3=83=86=E3=82=B9=E3=83=88=E3=82=92=E5=89=8A?= =?UTF-8?q?=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/tasks/news_fetch_error_handling_spec.rb | 112 ------------------- spec/tasks/news_rake_spec.rb | 86 -------------- 2 files changed, 198 deletions(-) delete mode 100644 spec/tasks/news_fetch_error_handling_spec.rb delete mode 100644 spec/tasks/news_rake_spec.rb diff --git a/spec/tasks/news_fetch_error_handling_spec.rb b/spec/tasks/news_fetch_error_handling_spec.rb deleted file mode 100644 index 7bf71d55..00000000 --- a/spec/tasks/news_fetch_error_handling_spec.rb +++ /dev/null @@ -1,112 +0,0 @@ -require 'rails_helper' -require 'rake' -require 'yaml' -require 'net/http' - -RSpec.describe 'news:fetch エラーハンドリング', type: :task do - before { Rails.application.load_tasks } - before { allow(Rails.env).to receive(:test?).and_return(true) } - - let(:yaml_path) { Rails.root.join('tmp', 'error_test_news.yml') } - let(:fetch_task) { Rake::Task['news:fetch'] } - let(:logger_mock) { instance_double(ActiveSupport::BroadcastLogger) } - - around do |example| - File.delete(yaml_path) if File.exist?(yaml_path) - example.run - File.delete(yaml_path) if File.exist?(yaml_path) - end - - before do - ENV['NEWS_YAML_PATH'] = yaml_path.to_s - fetch_task.reenable - allow(ActiveSupport::BroadcastLogger).to receive(:new).and_return(logger_mock) - allow(logger_mock).to receive(:info) - allow(logger_mock).to receive(:warn) - end - - after do - ENV.delete('NEWS_YAML_PATH') - ENV.delete('NEWS_RSS_PATH') - end - - describe 'ネットワーク・RSSエラー時の挙動' do - context 'ネットワークエラーの場合' do - before do - ENV['NEWS_RSS_PATH'] = 'https://invalid-url.example.com/rss' - allow(self).to receive(:safe_open).and_raise(Net::OpenTimeout, '接続タイムアウト') - end - - it 'warnログを出し、空のnews.ymlを生成する' do - expect(logger_mock).to receive(:warn).with(/⚠️ Failed to fetch .+/) - expect { fetch_task.invoke }.not_to raise_error - yaml = YAML.safe_load(File.read(yaml_path), permitted_classes: [Time]) - expect(yaml['news']).to eq([]) - end - end - - context 'RSS::Parser.parseが失敗する場合' do - before do - ENV['NEWS_RSS_PATH'] = 'https://example.com/feed.rss' - allow(self).to receive(:safe_open).and_return('not valid rss') - end - - it 'warnログを出し、空のnews.ymlを生成する' do - expect(logger_mock).to receive(:warn).with(/⚠️ Failed to fetch .+/) - expect { fetch_task.invoke }.not_to raise_error - yaml = YAML.safe_load(File.read(yaml_path), permitted_classes: [Time]) - expect(yaml['news']).to eq([]) - end - end - - context '空のRSSフィードの場合' do - before do - ENV['NEWS_RSS_PATH'] = 'https://example.com/feed.rss' - empty_rss = <<~RSS - - - - Empty Feed - Empty RSS Feed - https://example.com - - - RSS - allow(self).to receive(:safe_open).and_return(empty_rss) - end - - it '空配列でnews.ymlを生成する' do - expect { fetch_task.invoke }.not_to raise_error - yaml = YAML.safe_load(File.read(yaml_path), permitted_classes: [Time]) - expect(yaml['news']).to eq([]) - end - end - end - - describe '破損したYAMLファイルのハンドリング' do - context '既存のYAMLが破損している場合' do - before do - ENV['NEWS_RSS_PATH'] = Rails.root.join('spec', 'fixtures', 'sample_news.rss').to_s - File.write(yaml_path, "invalid yaml content:\n - broken\n indentation:\n - here") - end - - it 'YAML読み込みエラーが発生し、タスクが失敗する' do - expect { fetch_task.invoke }.to raise_error(Psych::SyntaxError) - end - end - - context '既存のYAMLが不正な構造の場合' do - before do - ENV['NEWS_RSS_PATH'] = Rails.root.join('spec', 'fixtures', 'sample_news.rss').to_s - File.write(yaml_path, { 'invalid_key' => [{ 'id' => 1 }] }.to_yaml) - end - - it '空配列として扱い、正常に上書きされる' do - expect { fetch_task.invoke }.not_to raise_error - yaml = YAML.safe_load(File.read(yaml_path), permitted_classes: [Time]) - expect(yaml['news']).to be_an(Array) - expect(yaml['news'].size).to be > 0 - end - end - end -end diff --git a/spec/tasks/news_rake_spec.rb b/spec/tasks/news_rake_spec.rb deleted file mode 100644 index 2bd58602..00000000 --- a/spec/tasks/news_rake_spec.rb +++ /dev/null @@ -1,86 +0,0 @@ -require 'rails_helper' -require 'rake' -require 'yaml' - -RSpec.describe 'news Rakeタスク', type: :task do - before { Rails.application.load_tasks } - before { allow(Rails.env).to receive(:test?).and_return(true) } - - # テスト用に tmp/news.yml を使う - let(:yaml_path) { Rails.root.join('tmp', 'news.yml') } - let(:fetch_task) { Rake::Task['news:fetch'] } - let(:import_task) { Rake::Task['news:import_from_yaml'] } - let(:yaml_content) { YAML.safe_load(File.read(yaml_path), permitted_classes: [Time]) } - - around do |example| - # テスト前後に一度だけ tmp/news.yml をクリア - File.delete(yaml_path) if File.exist?(yaml_path) - example.run - File.delete(yaml_path) if File.exist?(yaml_path) - end - - describe 'news:fetch タスク' do - before do - ENV['NEWS_YAML_PATH'] = yaml_path.to_s - ENV['NEWS_RSS_PATH'] = Rails.root.join('spec', 'fixtures', 'sample_news.rss').to_s - fetch_task.reenable - end - - after do - ENV.delete('NEWS_YAML_PATH') - ENV.delete('NEWS_RSS_PATH') - end - - it 'サンプルRSSからニュースを取得し YAML に書き込む' do - expect { fetch_task.invoke }.not_to raise_error - expect(File.exist?(yaml_path)).to be true - expect(yaml_content['news']).to be_an(Array) - expect(yaml_content['news'].size).to eq(3) - end - - it 'ID が 1 から連番で付与される' do - fetch_task.invoke - ids = yaml_content['news'].map { |item| item['id'] } - expect(ids).to eq([1, 2, 3]) - end - - it '公開日時で降順ソートされる' do - fetch_task.invoke - dates = yaml_content['news'].map { |item| Time.parse(item['published_at']) } - expect(dates).to eq(dates.sort.reverse) - end - end - - describe 'news:import_from_yaml タスク' do - let(:news_data) do - { - 'news' => [ - { 'id' => 1, 'url' => 'https://example.com/test1', 'title' => 'テスト記事1', 'published_at' => '2025-01-01T10:00:00Z' }, - { 'id' => 2, 'url' => 'https://example.com/test2', 'title' => 'テスト記事2', 'published_at' => '2025-01-02T10:00:00Z' } - ] - } - end - - before do - ENV['NEWS_YAML_PATH'] = yaml_path.to_s - File.write(yaml_path, news_data.to_yaml) - import_task.reenable - end - - after do - ENV.delete('NEWS_YAML_PATH') - end - - it 'YAML ファイルから News レコードを新規作成する' do - expect { import_task.invoke }.to change(News, :count).by(2) - expect(News.find_by(url: news_data['news'][0]['url']).title).to eq('テスト記事1') - expect(News.find_by(url: news_data['news'][1]['url']).title).to eq('テスト記事2') - end - - it '既存レコードがあれば属性を更新する' do - create(:news, url: news_data['news'][0]['url'], title: '古いタイトル') - expect { import_task.invoke }.to change(News, :count).by(1) - expect(News.find_by(url: news_data['news'][0]['url']).title).to eq('テスト記事1') - end - end -end From d4e01503909f1f9c680ac8d07e7fce22c1a08c0f Mon Sep 17 00:00:00 2001 From: nacchan Date: Mon, 4 Aug 2025 12:43:45 +0900 Subject: [PATCH 07/12] =?UTF-8?q?chore:=20=E4=BB=8A=E5=9B=9E=E3=81=AEPR?= =?UTF-8?q?=E3=81=A8=E9=96=A2=E4=BF=82=E3=81=AA=E3=81=84=E5=A4=89=E6=9B=B4?= =?UTF-8?q?=E3=82=92=E5=89=8A=E9=99=A4=EF=BC=88#1724=E3=81=AE=E9=87=8D?= =?UTF-8?q?=E8=A4=87=E5=88=86=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/tasks/fetch_news.rake | 30 ++++++++++++++++-------------- lib/tasks/import_news.rake | 5 ++--- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/lib/tasks/fetch_news.rake b/lib/tasks/fetch_news.rake index 00de4927..415d5df2 100644 --- a/lib/tasks/fetch_news.rake +++ b/lib/tasks/fetch_news.rake @@ -7,8 +7,8 @@ require 'active_support/broadcast_logger' def safe_open(url) uri = URI.parse(url) - return File.read(url) if uri.scheme.nil? || uri.scheme == 'file' raise "不正なURLです: #{url}" unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS) + Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http| request = Net::HTTP::Get.new(uri) response = http.request(request) @@ -26,19 +26,25 @@ namespace :news do logger.info('==== START news:fetch ====') - # YAML出力先を環境変数で上書きできるようにする - yaml_path = ENV['NEWS_YAML_PATH'] ? Pathname.new(ENV['NEWS_YAML_PATH']) : Rails.root.join('db', 'news.yml') - feed_urls = ENV['NEWS_RSS_PATH'] ? [ENV['NEWS_RSS_PATH']] : - (Rails.env.test? || Rails.env.staging? ? - [Rails.root.join('spec', 'fixtures', 'sample_news.rss').to_s] : - ['https://news.coderdojo.jp/feed/']) - + # 既存の news.yml を読み込み + yaml_path = Rails.root.join('db', 'news.yml') existing_news = if File.exist?(yaml_path) YAML.safe_load(File.read(yaml_path), permitted_classes: [Time], aliases: true)['news'] || [] else [] end + # テスト/ステージング環境ではサンプルファイル、本番は実サイトのフィード + feed_urls = if Rails.env.test? || Rails.env.staging? + [Rails.root.join('spec', 'fixtures', 'sample_news.rss').to_s] + else + [ + 'https://news.coderdojo.jp/feed/' + # 必要に応じて他 Dojo の RSS もここに追加可能 + # 'https://coderdojotokyo.org/feed', + ] + end + # RSS 取得&パース new_items = feed_urls.flat_map do |url| logger.info("Fetching RSS → #{url}") @@ -101,11 +107,7 @@ namespace :news do Time.parse(item['published_at']) }.reverse - sorted_items.each_with_index do |item, index| - item['id'] = index + 1 - end - - File.open(yaml_path, 'w') do |f| + File.open('db/news.yml', 'w') do |f| formatted_items = sorted_items.map do |item| { 'id' => item['id'], @@ -118,7 +120,7 @@ namespace :news do f.write({ 'news' => formatted_items }.to_yaml) end - logger.info("✅ Wrote #{sorted_items.size} items to #{yaml_path} (#{truly_new_items_sorted.size} new, #{updated_items.size} updated)") + logger.info("✅ Wrote #{sorted_items.size} items to db/news.yml (#{truly_new_items_sorted.size} new, #{updated_items.size} updated)") logger.info('==== END news:fetch ====') end end diff --git a/lib/tasks/import_news.rake b/lib/tasks/import_news.rake index ba73110d..bebe7661 100644 --- a/lib/tasks/import_news.rake +++ b/lib/tasks/import_news.rake @@ -1,10 +1,9 @@ require 'yaml' namespace :news do - desc 'db/news.yml (またはENV指定のYAML)を読み込んで News テーブルを upsert する' + desc 'db/news.yml を読み込んで News テーブルを upsert する' task import_from_yaml: :environment do - # ENVで上書き可能にする(なければデフォルト db/news.yml) - yaml_path = ENV['NEWS_YAML_PATH'] ? Pathname.new(ENV['NEWS_YAML_PATH']) : Rails.root.join('db', 'news.yml') + yaml_path = Rails.root.join('db', 'news.yml') raw = YAML.safe_load(File.read(yaml_path), permitted_classes: [Time], aliases: true) # entries を計算 From 7b0b652b45a16c2376d2c973fdc6748c088b70be Mon Sep 17 00:00:00 2001 From: nacchan Date: Mon, 4 Aug 2025 14:00:57 +0900 Subject: [PATCH 08/12] =?UTF-8?q?chore:=20=E4=B8=8D=E8=A6=81=E3=81=AA?= =?UTF-8?q?=E3=83=95=E3=82=A1=E3=82=AF=E3=83=88=E3=83=AA=E3=83=BC=E3=83=86?= =?UTF-8?q?=E3=82=B9=E3=83=88=E3=82=92=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/models/news_spec.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/spec/models/news_spec.rb b/spec/models/news_spec.rb index 45a01f29..8fb320fb 100644 --- a/spec/models/news_spec.rb +++ b/spec/models/news_spec.rb @@ -4,10 +4,6 @@ describe 'バリデーション' do let(:news) { build(:news) } - it '有効なファクトリーを持つ' do - expect(news).to be_valid - end - describe 'title' do it 'タイトルが空の場合は無効になる' do news.title = nil From 76202d8da820b9a1a4fed77dbb78dbe64e5e0f21 Mon Sep 17 00:00:00 2001 From: nacchan Date: Mon, 4 Aug 2025 14:13:31 +0900 Subject: [PATCH 09/12] =?UTF-8?q?test:=20title=E3=83=90=E3=83=AA=E3=83=87?= =?UTF-8?q?=E3=83=BC=E3=82=B7=E3=83=A7=E3=83=B3=E3=81=AE=E6=9C=89=E5=8A=B9?= =?UTF-8?q?=E3=82=B1=E3=83=BC=E3=82=B9=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/models/news_spec.rb | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/spec/models/news_spec.rb b/spec/models/news_spec.rb index 8fb320fb..6920884f 100644 --- a/spec/models/news_spec.rb +++ b/spec/models/news_spec.rb @@ -6,9 +6,14 @@ describe 'title' do it 'タイトルが空の場合は無効になる' do - news.title = nil - expect(news).not_to be_valid - expect(news.errors[:title]).not_to be_empty + news.title = nil + expect(news).not_to be_valid + expect(news.errors[:title]).not_to be_empty + end + + it 'タイトルが正しく設定されている場合は有効になる' do + news.title = '有効なタイトル' + expect(news).to be_valid end end From 270809103ebabb83b523586a159c18d98c0cba28 Mon Sep 17 00:00:00 2001 From: nacchan Date: Mon, 4 Aug 2025 14:27:21 +0900 Subject: [PATCH 10/12] =?UTF-8?q?test:=20published=5Fat=E3=83=90=E3=83=AA?= =?UTF-8?q?=E3=83=87=E3=83=BC=E3=82=B7=E3=83=A7=E3=83=B3=E3=81=AE=E6=9C=89?= =?UTF-8?q?=E5=8A=B9=E3=82=B1=E3=83=BC=E3=82=B9=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/models/news_spec.rb | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/spec/models/news_spec.rb b/spec/models/news_spec.rb index 6920884f..cd2bdfbf 100644 --- a/spec/models/news_spec.rb +++ b/spec/models/news_spec.rb @@ -48,9 +48,14 @@ describe 'published_at' do it '公開日時が空の場合は無効になる' do - news.published_at = nil - expect(news).not_to be_valid - expect(news.errors[:published_at]).not_to be_empty + news.published_at = nil + expect(news).not_to be_valid + expect(news.errors[:published_at]).not_to be_empty + end + + it '公開日時が正しく設定されている場合は有効になる' do + news.published_at = Time.current + expect(news).to be_valid end end end From 3cb6d0d680d9fcc96667576caa2ec9939877d4b7 Mon Sep 17 00:00:00 2001 From: nacchan Date: Mon, 4 Aug 2025 14:46:23 +0900 Subject: [PATCH 11/12] =?UTF-8?q?refactor:=20url=E3=83=90=E3=83=AA?= =?UTF-8?q?=E3=83=87=E3=83=BC=E3=82=B7=E3=83=A7=E3=83=B3=E3=83=86=E3=82=B9?= =?UTF-8?q?=E3=83=88=E3=82=92context=E3=81=A7=E6=95=B4=E7=90=86=E3=81=97?= =?UTF-8?q?=E3=80=81=E8=AA=AC=E6=98=8E=E6=96=87=E3=82=92=E6=94=B9=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/models/news_spec.rb | 46 +++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/spec/models/news_spec.rb b/spec/models/news_spec.rb index cd2bdfbf..13991387 100644 --- a/spec/models/news_spec.rb +++ b/spec/models/news_spec.rb @@ -18,31 +18,37 @@ end describe 'url' do - it 'URL が空の場合は無効になる' do - news.url = nil - expect(news).not_to be_valid - expect(news.errors[:url]).not_to be_empty - end + context '無効な場合' do + it 'URL が空の場合は無効になる' do + news.url = nil + expect(news).not_to be_valid + expect(news.errors[:url]).not_to be_empty + end - it 'URL が重複している場合は無効になる' do - create(:news, url: 'https://example.com/test') - duplicate_news = build(:news, url: 'https://example.com/test') - expect(duplicate_news).not_to be_valid - expect(duplicate_news.errors[:url]).not_to be_empty - end + it 'URL が重複している場合は無効になる' do + create(:news, url: 'https://example.com/test') + duplicate_news = build(:news, url: 'https://example.com/test') + expect(duplicate_news).not_to be_valid + expect(duplicate_news.errors[:url]).not_to be_empty + end - it 'URL形式であること' do - news.url = 'invalid-url' - expect(news).not_to be_valid - expect(news.errors[:url]).not_to be_empty + it 'URL形式でない場合は無効になる' do + news.url = 'invalid-url' + expect(news).not_to be_valid + expect(news.errors[:url]).not_to be_empty + end end - it 'HTTPSとHTTPを許可する' do - news.url = 'https://example.com' - expect(news).to be_valid + context '有効な場合' do + it 'HTTPSを許可する' do + news.url = 'https://example.com' + expect(news).to be_valid + end - news.url = 'http://example.com' - expect(news).to be_valid + it 'HTTPを許可する' do + news.url = 'http://example.com' + expect(news).to be_valid + end end end From 2305a9ddca1bbbd8b7ed442d271b3a2e79f32b97 Mon Sep 17 00:00:00 2001 From: nacchan Date: Mon, 4 Aug 2025 16:10:05 +0900 Subject: [PATCH 12/12] =?UTF-8?q?style:=20title=E3=83=BBpublished=5Fat?= =?UTF-8?q?=E3=82=BB=E3=82=AF=E3=82=B7=E3=83=A7=E3=83=B3=E3=81=AE=E3=82=A4?= =?UTF-8?q?=E3=83=B3=E3=83=87=E3=83=B3=E3=83=88=E3=82=92=E8=AA=BF=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/models/news_spec.rb | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/spec/models/news_spec.rb b/spec/models/news_spec.rb index 13991387..3beba335 100644 --- a/spec/models/news_spec.rb +++ b/spec/models/news_spec.rb @@ -6,14 +6,14 @@ describe 'title' do it 'タイトルが空の場合は無効になる' do - news.title = nil - expect(news).not_to be_valid - expect(news.errors[:title]).not_to be_empty + news.title = nil + expect(news).not_to be_valid + expect(news.errors[:title]).not_to be_empty end it 'タイトルが正しく設定されている場合は有効になる' do - news.title = '有効なタイトル' - expect(news).to be_valid + news.title = '有効なタイトル' + expect(news).to be_valid end end @@ -54,14 +54,14 @@ describe 'published_at' do it '公開日時が空の場合は無効になる' do - news.published_at = nil - expect(news).not_to be_valid - expect(news.errors[:published_at]).not_to be_empty + news.published_at = nil + expect(news).not_to be_valid + expect(news.errors[:published_at]).not_to be_empty end it '公開日時が正しく設定されている場合は有効になる' do - news.published_at = Time.current - expect(news).to be_valid + news.published_at = Time.current + expect(news).to be_valid end end end pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy