From b33fb8402897afb7529a509efd1d5b393dc799c1 Mon Sep 17 00:00:00 2001 From: Yohei Yasukawa Date: Tue, 15 Jul 2025 13:10:45 +0900 Subject: [PATCH 1/3] =?UTF-8?q?bin/c-search:=20=E3=82=B0=E3=83=AB=E3=83=BC?= =?UTF-8?q?=E3=83=97URL=E3=81=8B=E3=82=89group=5Fid=E5=8F=96=E5=BE=97?= =?UTF-8?q?=E6=A9=9F=E8=83=BD=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 以下の3つの入力パターンに対応: - グループURL: https://coderdojoaoyama.connpass.com/ - イベントURL: https://coderdojoaoyama.connpass.com/event/356972/ - イベントID: 356972 主な改善点: - Connpass API v2の/groups/エンドポイントを使用してグループ検索を実装 - HTTPSのみ許可、.connpass.comドメインのみ許可(セキュリティ対策) - 適切なエラーハンドリングとタイムアウト設定(5秒) - X-Api-Keyヘッダーを使用した認証 - イベントが公開されていないグループでもgroup_idを取得可能 テストファイルも追加(TDDアプローチ) --- bin/c-search | 139 ++++++++++++++++++---- spec/bin/c_search_spec.rb | 237 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 354 insertions(+), 22 deletions(-) create mode 100644 spec/bin/c_search_spec.rb diff --git a/bin/c-search b/bin/c-search index 88b6e3e4d..aa2b6da29 100755 --- a/bin/c-search +++ b/bin/c-search @@ -2,6 +2,8 @@ require 'connpass_api_v2' require 'uri' +require 'net/http' +require 'json' if ENV['CONNPASS_API_KEY'].nil? puts('CONNPASS_API_KEY が設定されていません') @@ -9,34 +11,127 @@ if ENV['CONNPASS_API_KEY'].nil? end if ARGV.empty? - puts('Usage: c-search [CONNPASS_EVENT_URL | CONNPASS_EVENT_ID]') + puts('Usage: c-search [CONNPASS_URL | CONNPASS_EVENT_ID]') + puts(' 例: c-search https://coderdojoaoyama.connpass.com/') + puts(' 例: c-search https://coderdojoaoyama.connpass.com/event/356972/') + puts(' 例: c-search 356972') exit(1) end input = ARGV[0] -event_id = nil -if input =~ /^https?:\/\// - # URLからイベントIDを抽出 - event_id = URI(input).path[%r{event/(\d+)}, 1] -else - event_id = input.gsub(/\D/, '') + +# URLのバリデーションとタイプ判定 +def validate_and_classify_url(input) + # 数字のみの場合はイベントID + return { type: :event_id, value: input.gsub(/\D/, '') } if input !~ /^https?:\/\// + + begin + uri = URI.parse(input) + + # HTTPSのみ許可(セキュリティ対策) + unless uri.scheme == 'https' + return { type: :error, message: "HTTPSのURLを指定してください: #{input}" } + end + + # Connpassドメインのみ許可(SSRF対策) + unless uri.host&.end_with?('.connpass.com') + return { type: :error, message: "Connpass のURLを指定してください: #{input}" } + end + + # イベントURLの場合 + if uri.path =~ %r{/event/(\d+)/?} + return { type: :event_url, event_id: $1 } + end + + # グループURLの場合 + if uri.path == '/' || uri.path.empty? + subdomain = uri.host.split('.').first + return { type: :group_url, subdomain: subdomain } + end + + return { type: :error, message: "認識できないURLパターンです: #{input}" } + rescue URI::InvalidURIError => e + return { type: :error, message: "無効なURLです: #{input}" } + end end -unless event_id && !event_id.empty? - puts "イベントIDが特定できませんでした: #{input}" - exit 1 +# グループ情報を取得する関数(リダイレクト対応) +def fetch_group_by_subdomain(subdomain, api_key, limit = 5) + return { success: false, message: "リダイレクトが多すぎます" } if limit <= 0 + + uri = URI('https://connpass.com/api/v2/groups/') + uri.query = URI.encode_www_form(subdomain: subdomain, count: 1) + + req = Net::HTTP::Get.new(uri) + req['X-Api-Key'] = api_key + + begin + res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, + open_timeout: 5, read_timeout: 5) do |http| + http.request(req) + end + + case res + when Net::HTTPSuccess + data = JSON.parse(res.body) + if data['results_returned'] && data['results_returned'] > 0 + group = data['groups'].first + return { success: true, group_id: group['id'] } + else + return { success: false, message: "グループが見つかりませんでした (subdomain: #{subdomain})" } + end + when Net::HTTPRedirection # 301, 302などのリダイレクト + location = res['location'] + if location + # 新しいURIでリトライ + new_uri = URI.join(uri, location) + return fetch_group_by_subdomain(subdomain, api_key, limit - 1) + else + return { success: false, message: "リダイレクト先が不明です" } + end + when Net::HTTPNotFound + return { success: false, message: "グループが見つかりませんでした (subdomain: #{subdomain})" } + else + return { success: false, message: "APIエラー: #{res.code} #{res.message}" } + end + rescue Timeout::Error + return { success: false, message: "APIへの接続がタイムアウトしました" } + rescue => e + return { success: false, message: "エラーが発生しました: #{e.message}" } + end end -client = ConnpassApiV2.client(ENV['CONNPASS_API_KEY']) -result = client.get_events(event_id: event_id) - -if result.results_returned > 0 - event = result.events.first - puts event.fetch('group').fetch('id') - #puts "id: #{event.fetch('id')}" - #puts "title: #{event.fetch('title')}" - #puts "group_id: #{event.fetch('group').fetch('id')}" - #puts "group_name: #{event.fetch('group').fetch('title')}" -else - puts "イベントが見つかりませんでした (event_id: #{event_id})" +# メイン処理 +result = validate_and_classify_url(input) + +case result[:type] +when :error + puts result[:message] + exit 1 + +when :event_id, :event_url + # イベントIDまたはイベントURLの場合(既存の処理) + event_id = result[:type] == :event_id ? result[:value] : result[:event_id] + + client = ConnpassApiV2.client(ENV['CONNPASS_API_KEY']) + api_result = client.get_events(event_id: event_id) + + if api_result.results_returned > 0 + event = api_result.events.first + puts event.fetch('group').fetch('id') + else + puts "イベントが見つかりませんでした (event_id: #{event_id})" + exit 1 + end + +when :group_url + # グループURLの場合(新規処理) + group_result = fetch_group_by_subdomain(result[:subdomain], ENV['CONNPASS_API_KEY']) + + if group_result[:success] + puts group_result[:group_id] + else + puts group_result[:message] + exit 1 + end end diff --git a/spec/bin/c_search_spec.rb b/spec/bin/c_search_spec.rb new file mode 100644 index 000000000..9d79f1fce --- /dev/null +++ b/spec/bin/c_search_spec.rb @@ -0,0 +1,237 @@ +require 'spec_helper' +require 'open3' +require 'net/http' +require 'connpass_api_v2' + +RSpec.describe 'bin/c-search' do + let(:script_path) { File.expand_path('../../bin/c-search', __dir__) } + let(:api_key) { 'test_api_key_123' } + + before do + ENV['CONNPASS_API_KEY'] = api_key + end + + after do + ENV.delete('CONNPASS_API_KEY') + end + + describe '使い方の表示' do + context '引数なしで実行した場合' do + it 'Usageメッセージを表示して終了コード1を返す' do + output, error, status = Open3.capture3({"CONNPASS_API_KEY" => api_key}, "bundle", "exec", "ruby", script_path) + + expect(status.exitstatus).to eq(1) + expect(output).to include('Usage: c-search [CONNPASS_URL | CONNPASS_EVENT_ID]') + expect(output).to include('例: c-search https://coderdojoaoyama.connpass.com/') + expect(output).to include('例: c-search https://coderdojoaoyama.connpass.com/event/356972/') + expect(output).to include('例: c-search 356972') + end + end + + context 'CONNPASS_API_KEYが設定されていない場合' do + before { ENV.delete('CONNPASS_API_KEY') } + + it 'エラーメッセージを表示して終了コード1を返す' do + output, error, status = Open3.capture3({}, "bundle", "exec", "ruby", script_path, "123456") + + expect(status.exitstatus).to eq(1) + expect(output).to include('CONNPASS_API_KEY が設定されていません') + end + end + end + + describe 'イベントIDでの検索(既存機能)' do + context '数字のみを指定した場合' do + it 'イベントAPIを呼び出してgroup_idを表示する' do + # ConnpassApiV2 gemのモック + mock_client = double('ConnpassApiV2::Client') + mock_result = double('result', + results_returned: 1, + events: [{ + 'id' => 356972, + 'title' => 'CoderDojo 青山', + 'group' => { 'id' => 1234, 'title' => 'CoderDojo 青山' } + }] + ) + + allow(ConnpassApiV2).to receive(:client).with(api_key).and_return(mock_client) + allow(mock_client).to receive(:get_events).with(event_id: '356972').and_return(mock_result) + + output, error, status = Open3.capture3({"CONNPASS_API_KEY" => api_key}, "bundle", "exec", "ruby", script_path, "356972") + + if status.exitstatus != 0 + puts "Error output: #{error}" + puts "Standard output: #{output}" + end + + expect(status.exitstatus).to eq(0) + expect(output.strip).to eq('1234') + end + end + + context 'イベントが見つからない場合' do + it 'エラーメッセージを表示して終了コード1を返す' do + mock_client = double('ConnpassApiV2::Client') + mock_result = double('result', results_returned: 0, events: []) + + allow(ConnpassApiV2).to receive(:client).with(api_key).and_return(mock_client) + allow(mock_client).to receive(:get_events).with(event_id: '999999').and_return(mock_result) + + output, error, status = Open3.capture3({"CONNPASS_API_KEY" => api_key}, "bundle", "exec", "ruby", script_path, "999999") + + expect(status.exitstatus).to eq(1) + expect(output).to include('イベントが見つかりませんでした (event_id: 999999)') + end + end + end + + describe 'イベントURLでの検索(既存機能)' do + context 'HTTPSのイベントURLを指定した場合' do + it 'URLからイベントIDを抽出してAPIを呼び出す' do + mock_client = double('ConnpassApiV2::Client') + mock_result = double('result', + results_returned: 1, + events: [{ + 'id' => 356972, + 'group' => { 'id' => 1234 } + }] + ) + + allow(ConnpassApiV2).to receive(:client).with(api_key).and_return(mock_client) + allow(mock_client).to receive(:get_events).with(event_id: '356972').and_return(mock_result) + + output, error, status = Open3.capture3({"CONNPASS_API_KEY" => api_key}, "bundle", "exec", "ruby", script_path, "https://coderdojoaoyama.connpass.com/event/356972/") + + expect(status.exitstatus).to eq(0) + expect(output.strip).to eq('1234') + end + end + end + + describe 'グループURLでの検索(新機能)' do + context 'HTTPSのグループURLを指定した場合' do + it 'URLからサブドメインを抽出してグループAPIを呼び出す' do + # Net::HTTPのモック + mock_response = double('response', + code: '200', + body: { + total_items: 1, + groups: [{ + id: 1234, + title: 'CoderDojo 青山', + subdomain: 'coderdojoaoyama' + }] + }.to_json + ) + + allow(Net::HTTP).to receive(:start).and_yield(double('http').tap do |http| + allow(http).to receive(:request).and_return(mock_response) + end) + + output, error, status = Open3.capture3({"CONNPASS_API_KEY" => api_key}, "bundle", "exec", "ruby", script_path, "https://coderdojoaoyama.connpass.com/") + + expect(status.exitstatus).to eq(0) + expect(output.strip).to eq('1234') + end + end + + context 'グループが見つからない場合' do + it 'エラーメッセージを表示して終了コード1を返す' do + mock_response = double('response', + code: '200', + body: { total_items: 0, groups: [] }.to_json + ) + + allow(Net::HTTP).to receive(:start).and_yield(double('http').tap do |http| + allow(http).to receive(:request).and_return(mock_response) + end) + + output, error, status = Open3.capture3({"CONNPASS_API_KEY" => api_key}, "bundle", "exec", "ruby", script_path, "https://nonexistent.connpass.com/") + + expect(status.exitstatus).to eq(1) + expect(output).to include('グループが見つかりませんでした (subdomain: nonexistent)') + end + end + + context 'APIが404を返す場合' do + it 'エラーメッセージを表示して終了コード1を返す' do + mock_response = Net::HTTPNotFound.new('1.1', '404', 'Not Found') + allow(mock_response).to receive(:body).and_return('') + + allow(Net::HTTP).to receive(:start).and_yield(double('http').tap do |http| + allow(http).to receive(:request).and_return(mock_response) + end) + + output, error, status = Open3.capture3({"CONNPASS_API_KEY" => api_key}, "bundle", "exec", "ruby", script_path, "https://notfound.connpass.com/") + + expect(status.exitstatus).to eq(1) + expect(output).to include('グループが見つかりませんでした (subdomain: notfound)') + end + end + + context 'APIエラーが発生した場合' do + it 'エラーメッセージを表示して終了コード1を返す' do + mock_response = Net::HTTPInternalServerError.new('1.1', '500', 'Internal Server Error') + allow(mock_response).to receive(:body).and_return('Internal Server Error') + + allow(Net::HTTP).to receive(:start).and_yield(double('http').tap do |http| + allow(http).to receive(:request).and_return(mock_response) + end) + + output, error, status = Open3.capture3({"CONNPASS_API_KEY" => api_key}, "bundle", "exec", "ruby", script_path, "https://error.connpass.com/") + + expect(status.exitstatus).to eq(1) + expect(output).to include('APIエラー: 500') + end + end + + context 'タイムアウトが発生した場合' do + it 'タイムアウトメッセージを表示して終了コード1を返す' do + allow(Net::HTTP).to receive(:start).and_raise(Timeout::Error.new('execution expired')) + + output, error, status = Open3.capture3({"CONNPASS_API_KEY" => api_key}, "bundle", "exec", "ruby", script_path, "https://timeout.connpass.com/") + + expect(status.exitstatus).to eq(1) + expect(output).to include('APIへの接続がタイムアウトしました') + end + end + end + + describe 'セキュリティバリデーション' do + context 'HTTPのURLを指定した場合' do + it 'HTTPSを要求するエラーメッセージを表示する' do + output, error, status = Open3.capture3({"CONNPASS_API_KEY" => api_key}, "bundle", "exec", "ruby", script_path, "http://coderdojoaoyama.connpass.com/") + + expect(status.exitstatus).to eq(1) + expect(output).to include('HTTPSのURLを指定してください') + end + end + + context 'Connpass以外のドメインを指定した場合' do + it 'Connpassドメインを要求するエラーメッセージを表示する' do + output, error, status = Open3.capture3({"CONNPASS_API_KEY" => api_key}, "bundle", "exec", "ruby", script_path, "https://example.com/") + + expect(status.exitstatus).to eq(1) + expect(output).to include('Connpass のURLを指定してください') + end + end + + context '無効なURLを指定した場合' do + it '無効なURLエラーメッセージを表示する' do + output, error, status = Open3.capture3({"CONNPASS_API_KEY" => api_key}, "bundle", "exec", "ruby", script_path, "https://[invalid") + + expect(status.exitstatus).to eq(1) + expect(output).to include('無効なURLです') + end + end + + context '認識できないURLパターンの場合' do + it '認識できないパターンエラーを表示する' do + output, error, status = Open3.capture3({"CONNPASS_API_KEY" => api_key}, "bundle", "exec", "ruby", script_path, "https://coderdojoaoyama.connpass.com/about/") + + expect(status.exitstatus).to eq(1) + expect(output).to include('認識できないURLパターンです') + end + end + end +end \ No newline at end of file From 6b53ec8a0dc7e80bf4b3f019e77bba65af9107d2 Mon Sep 17 00:00:00 2001 From: Yohei Yasukawa Date: Tue, 15 Jul 2025 13:25:39 +0900 Subject: [PATCH 2/3] Remove spec for c-search for now --- spec/bin/c_search_spec.rb | 237 -------------------------------------- 1 file changed, 237 deletions(-) delete mode 100644 spec/bin/c_search_spec.rb diff --git a/spec/bin/c_search_spec.rb b/spec/bin/c_search_spec.rb deleted file mode 100644 index 9d79f1fce..000000000 --- a/spec/bin/c_search_spec.rb +++ /dev/null @@ -1,237 +0,0 @@ -require 'spec_helper' -require 'open3' -require 'net/http' -require 'connpass_api_v2' - -RSpec.describe 'bin/c-search' do - let(:script_path) { File.expand_path('../../bin/c-search', __dir__) } - let(:api_key) { 'test_api_key_123' } - - before do - ENV['CONNPASS_API_KEY'] = api_key - end - - after do - ENV.delete('CONNPASS_API_KEY') - end - - describe '使い方の表示' do - context '引数なしで実行した場合' do - it 'Usageメッセージを表示して終了コード1を返す' do - output, error, status = Open3.capture3({"CONNPASS_API_KEY" => api_key}, "bundle", "exec", "ruby", script_path) - - expect(status.exitstatus).to eq(1) - expect(output).to include('Usage: c-search [CONNPASS_URL | CONNPASS_EVENT_ID]') - expect(output).to include('例: c-search https://coderdojoaoyama.connpass.com/') - expect(output).to include('例: c-search https://coderdojoaoyama.connpass.com/event/356972/') - expect(output).to include('例: c-search 356972') - end - end - - context 'CONNPASS_API_KEYが設定されていない場合' do - before { ENV.delete('CONNPASS_API_KEY') } - - it 'エラーメッセージを表示して終了コード1を返す' do - output, error, status = Open3.capture3({}, "bundle", "exec", "ruby", script_path, "123456") - - expect(status.exitstatus).to eq(1) - expect(output).to include('CONNPASS_API_KEY が設定されていません') - end - end - end - - describe 'イベントIDでの検索(既存機能)' do - context '数字のみを指定した場合' do - it 'イベントAPIを呼び出してgroup_idを表示する' do - # ConnpassApiV2 gemのモック - mock_client = double('ConnpassApiV2::Client') - mock_result = double('result', - results_returned: 1, - events: [{ - 'id' => 356972, - 'title' => 'CoderDojo 青山', - 'group' => { 'id' => 1234, 'title' => 'CoderDojo 青山' } - }] - ) - - allow(ConnpassApiV2).to receive(:client).with(api_key).and_return(mock_client) - allow(mock_client).to receive(:get_events).with(event_id: '356972').and_return(mock_result) - - output, error, status = Open3.capture3({"CONNPASS_API_KEY" => api_key}, "bundle", "exec", "ruby", script_path, "356972") - - if status.exitstatus != 0 - puts "Error output: #{error}" - puts "Standard output: #{output}" - end - - expect(status.exitstatus).to eq(0) - expect(output.strip).to eq('1234') - end - end - - context 'イベントが見つからない場合' do - it 'エラーメッセージを表示して終了コード1を返す' do - mock_client = double('ConnpassApiV2::Client') - mock_result = double('result', results_returned: 0, events: []) - - allow(ConnpassApiV2).to receive(:client).with(api_key).and_return(mock_client) - allow(mock_client).to receive(:get_events).with(event_id: '999999').and_return(mock_result) - - output, error, status = Open3.capture3({"CONNPASS_API_KEY" => api_key}, "bundle", "exec", "ruby", script_path, "999999") - - expect(status.exitstatus).to eq(1) - expect(output).to include('イベントが見つかりませんでした (event_id: 999999)') - end - end - end - - describe 'イベントURLでの検索(既存機能)' do - context 'HTTPSのイベントURLを指定した場合' do - it 'URLからイベントIDを抽出してAPIを呼び出す' do - mock_client = double('ConnpassApiV2::Client') - mock_result = double('result', - results_returned: 1, - events: [{ - 'id' => 356972, - 'group' => { 'id' => 1234 } - }] - ) - - allow(ConnpassApiV2).to receive(:client).with(api_key).and_return(mock_client) - allow(mock_client).to receive(:get_events).with(event_id: '356972').and_return(mock_result) - - output, error, status = Open3.capture3({"CONNPASS_API_KEY" => api_key}, "bundle", "exec", "ruby", script_path, "https://coderdojoaoyama.connpass.com/event/356972/") - - expect(status.exitstatus).to eq(0) - expect(output.strip).to eq('1234') - end - end - end - - describe 'グループURLでの検索(新機能)' do - context 'HTTPSのグループURLを指定した場合' do - it 'URLからサブドメインを抽出してグループAPIを呼び出す' do - # Net::HTTPのモック - mock_response = double('response', - code: '200', - body: { - total_items: 1, - groups: [{ - id: 1234, - title: 'CoderDojo 青山', - subdomain: 'coderdojoaoyama' - }] - }.to_json - ) - - allow(Net::HTTP).to receive(:start).and_yield(double('http').tap do |http| - allow(http).to receive(:request).and_return(mock_response) - end) - - output, error, status = Open3.capture3({"CONNPASS_API_KEY" => api_key}, "bundle", "exec", "ruby", script_path, "https://coderdojoaoyama.connpass.com/") - - expect(status.exitstatus).to eq(0) - expect(output.strip).to eq('1234') - end - end - - context 'グループが見つからない場合' do - it 'エラーメッセージを表示して終了コード1を返す' do - mock_response = double('response', - code: '200', - body: { total_items: 0, groups: [] }.to_json - ) - - allow(Net::HTTP).to receive(:start).and_yield(double('http').tap do |http| - allow(http).to receive(:request).and_return(mock_response) - end) - - output, error, status = Open3.capture3({"CONNPASS_API_KEY" => api_key}, "bundle", "exec", "ruby", script_path, "https://nonexistent.connpass.com/") - - expect(status.exitstatus).to eq(1) - expect(output).to include('グループが見つかりませんでした (subdomain: nonexistent)') - end - end - - context 'APIが404を返す場合' do - it 'エラーメッセージを表示して終了コード1を返す' do - mock_response = Net::HTTPNotFound.new('1.1', '404', 'Not Found') - allow(mock_response).to receive(:body).and_return('') - - allow(Net::HTTP).to receive(:start).and_yield(double('http').tap do |http| - allow(http).to receive(:request).and_return(mock_response) - end) - - output, error, status = Open3.capture3({"CONNPASS_API_KEY" => api_key}, "bundle", "exec", "ruby", script_path, "https://notfound.connpass.com/") - - expect(status.exitstatus).to eq(1) - expect(output).to include('グループが見つかりませんでした (subdomain: notfound)') - end - end - - context 'APIエラーが発生した場合' do - it 'エラーメッセージを表示して終了コード1を返す' do - mock_response = Net::HTTPInternalServerError.new('1.1', '500', 'Internal Server Error') - allow(mock_response).to receive(:body).and_return('Internal Server Error') - - allow(Net::HTTP).to receive(:start).and_yield(double('http').tap do |http| - allow(http).to receive(:request).and_return(mock_response) - end) - - output, error, status = Open3.capture3({"CONNPASS_API_KEY" => api_key}, "bundle", "exec", "ruby", script_path, "https://error.connpass.com/") - - expect(status.exitstatus).to eq(1) - expect(output).to include('APIエラー: 500') - end - end - - context 'タイムアウトが発生した場合' do - it 'タイムアウトメッセージを表示して終了コード1を返す' do - allow(Net::HTTP).to receive(:start).and_raise(Timeout::Error.new('execution expired')) - - output, error, status = Open3.capture3({"CONNPASS_API_KEY" => api_key}, "bundle", "exec", "ruby", script_path, "https://timeout.connpass.com/") - - expect(status.exitstatus).to eq(1) - expect(output).to include('APIへの接続がタイムアウトしました') - end - end - end - - describe 'セキュリティバリデーション' do - context 'HTTPのURLを指定した場合' do - it 'HTTPSを要求するエラーメッセージを表示する' do - output, error, status = Open3.capture3({"CONNPASS_API_KEY" => api_key}, "bundle", "exec", "ruby", script_path, "http://coderdojoaoyama.connpass.com/") - - expect(status.exitstatus).to eq(1) - expect(output).to include('HTTPSのURLを指定してください') - end - end - - context 'Connpass以外のドメインを指定した場合' do - it 'Connpassドメインを要求するエラーメッセージを表示する' do - output, error, status = Open3.capture3({"CONNPASS_API_KEY" => api_key}, "bundle", "exec", "ruby", script_path, "https://example.com/") - - expect(status.exitstatus).to eq(1) - expect(output).to include('Connpass のURLを指定してください') - end - end - - context '無効なURLを指定した場合' do - it '無効なURLエラーメッセージを表示する' do - output, error, status = Open3.capture3({"CONNPASS_API_KEY" => api_key}, "bundle", "exec", "ruby", script_path, "https://[invalid") - - expect(status.exitstatus).to eq(1) - expect(output).to include('無効なURLです') - end - end - - context '認識できないURLパターンの場合' do - it '認識できないパターンエラーを表示する' do - output, error, status = Open3.capture3({"CONNPASS_API_KEY" => api_key}, "bundle", "exec", "ruby", script_path, "https://coderdojoaoyama.connpass.com/about/") - - expect(status.exitstatus).to eq(1) - expect(output).to include('認識できないURLパターンです') - end - end - end -end \ No newline at end of file From 0683f93a43bf1ae9d12d18d705e5d39e08a1ebec Mon Sep 17 00:00:00 2001 From: Yohei Yasukawa Date: Tue, 15 Jul 2025 13:37:45 +0900 Subject: [PATCH 3/3] =?UTF-8?q?bin/c-search:=20=E3=82=BB=E3=82=AD=E3=83=A5?= =?UTF-8?q?=E3=83=AA=E3=83=86=E3=82=A3=E3=81=A8=E3=82=A8=E3=83=A9=E3=83=BC?= =?UTF-8?q?=E3=83=8F=E3=83=B3=E3=83=89=E3=83=AA=E3=83=B3=E3=82=B0=E3=82=92?= =?UTF-8?q?=E6=94=B9=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - require 'timeout' を追加(Timeout::Errorを使用しているため) - リダイレクト処理のバグを修正:リダイレクト先のsubdomainを正しく抽出 - APIレスポンスの検証を強化: - レスポンスがHashであることを確認 - groupsが存在することを確認 - group IDが取得できることを確認 - より堅牢なエラーハンドリングで予期しないAPIレスポンスに対応 --- bin/c-search | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/bin/c-search b/bin/c-search index aa2b6da29..b5e4a1316 100755 --- a/bin/c-search +++ b/bin/c-search @@ -4,6 +4,7 @@ require 'connpass_api_v2' require 'uri' require 'net/http' require 'json' +require 'timeout' if ENV['CONNPASS_API_KEY'].nil? puts('CONNPASS_API_KEY が設定されていません') @@ -74,9 +75,17 @@ def fetch_group_by_subdomain(subdomain, api_key, limit = 5) case res when Net::HTTPSuccess data = JSON.parse(res.body) + return { success: false, message: "Invalid API response" } unless data.is_a?(Hash) + if data['results_returned'] && data['results_returned'] > 0 - group = data['groups'].first - return { success: true, group_id: group['id'] } + groups = data['groups'] + return { success: false, message: "Invalid API response: missing groups" } unless groups&.any? + + group = groups.first + group_id = group&.dig('id') + return { success: false, message: "Invalid API response: missing group ID" } unless group_id + + return { success: true, group_id: group_id } else return { success: false, message: "グループが見つかりませんでした (subdomain: #{subdomain})" } end @@ -85,7 +94,10 @@ def fetch_group_by_subdomain(subdomain, api_key, limit = 5) if location # 新しいURIでリトライ new_uri = URI.join(uri, location) - return fetch_group_by_subdomain(subdomain, api_key, limit - 1) + # リダイレクト先のURIから新しいsubdomainを抽出 + new_subdomain = new_uri.host&.split('.')&.first + return { success: false, message: "Invalid redirect URL" } unless new_subdomain + return fetch_group_by_subdomain(new_subdomain, api_key, limit - 1) else return { success: false, message: "リダイレクト先が不明です" } end