Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/controllers/training_completion_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def create
user = current_user
current_user.cancel_participation_from_regular_events
current_user.delete_and_assign_new_organizer
Newspaper.publish(:training_completion_create, { user: })
ActiveSupport::Notifications.instrument('training_completion.create', user:)
user.clear_github_data
notify_to_user(user)
notify_to_admins(user)
Expand Down
6 changes: 3 additions & 3 deletions app/models/retirement.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def execute
remove_as_event_organizer
clear_github_info
destroy_cards
publish_to_newspaper
publish
notify
true
rescue ActiveRecord::RecordInvalid => e
Expand Down Expand Up @@ -82,8 +82,8 @@ def remove_as_event_organizer
@user.delete_and_assign_new_organizer
end

def publish_to_newspaper
Newspaper.publish(:retirement_create, { user: @user })
def publish
ActiveSupport::Notifications.instrument('retirement.create', user: @user)
end

def notify
Expand Down
2 changes: 1 addition & 1 deletion app/models/times_channel_destroyer.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

class TimesChannelDestroyer
def call(payload)
def call(_name, _started, _finished, _id, payload)
user = payload[:user]
return unless user.discord_profile.times_id

Expand Down
2 changes: 1 addition & 1 deletion app/models/unfinished_data_destroyer.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

class UnfinishedDataDestroyer
def call(payload)
def call(_name, _started, _finished, _id, payload)
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Sep 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

ActiveSupport::Notifications購読者の署名に正しく適合しています。

メソッド署名が ActiveSupport::Notifications のコールバック署名 (name, started, finished, id, payload) に更新されています。未使用の4つのパラメータにアンダースコアプレフィックスが付けられており、適切です。メソッド本体のロジック(payload[:user]の抽出とクリーンアップ処理)は変更されていません。

コーディングガイドラインに基づき、モデルメソッドには対応するユニットテストが必要です。以下のスクリプトを実行して、UnfinishedDataDestroyerのテストが存在し、新しい署名に対応していることを確認してください:


🏁 Script executed:

#!/bin/bash
# Description: Verify UnfinishedDataDestroyer tests exist and match new signature

echo "=== Searching for UnfinishedDataDestroyer test file ==="
fd -t f "unfinished_data_destroyer.*test" test/

echo -e "\n=== Checking test calls to UnfinishedDataDestroyer#call ==="
rg -n -A 2 "UnfinishedDataDestroyer.*\.call" test/ --type=ruby

Length of output: 116


以下のスクリプトで spec/ ディレクトリ内のテストファイルと UnfinishedDataDestroyer#call 呼び出しを確認してください。


🏁 Script executed:

#!/bin/bash
echo "=== specディレクトリでのテストファイル検索 ==="
fd -t f "unfinished_data_destroyer.*_spec\.rb" spec/

echo -e "\n=== specディレクトリでのUnfinishedDataDestroyer#call呼び出し検索 ==="
rg -n -A 2 "UnfinishedDataDestroyer.*\.call" spec/ --type=ruby

Length of output: 224


UnfinishedDataDestroyerに対応するテストが存在しません。
ActiveSupport::Notificationsのコールバック署名変更(call(name, started, finished, id, payload))を検証するユニットテストが test/ および spec/ 内に見当たりません。新しい署名に対応したテストを追加してください。

🤖 Prompt for AI Agents
In app/models/unfinished_data_destroyer.rb around line 4, there is no unit test
verifying the ActiveSupport::Notifications callback uses the new signature
call(name, started, finished, id, payload); add tests under test/ (Minitest)
and/or spec/ (RSpec) that subscribe or instrument the notification and assert
UnfinishedDataDestroyer#call is invoked with five arguments: name, started (a
Time), finished (a Time), id, and payload (a Hash). In the tests, use
ActiveSupport::Notifications.instrument to emit a sample event, stub or spy the
UnfinishedDataDestroyer instance (or use allow/expect_any_instance_of) to
capture the received args, and assert the arity and values/types match the new
signature.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

テストの追加しました。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the test file exists and review its content
echo "=== Checking if test file exists ==="
fd -t f "unfinished_data_destroyer_test.rb" test/

echo -e "\n=== Content of the test file ==="
cat test/models/unfinished_data_destroyer_test.rb

Length of output: 659


@matuaya さん、テストの追加をありがとうございます!確認しました。

test/models/unfinished_data_destroyer_test.rb のテストは適切に実装されています:

新しい5パラメータ署名を正しく使用

  • call(nil, nil, nil, nil, payload) で ActiveSupport::Notifications のコールバック署名に対応

すべての処理を検証

  • WIP日報の削除
  • 未チェック提出物の削除
  • career_path のリセット

テストケースが網羅的で、実装の変更を適切に検証しています。


🧬 Code Graph Analysis Results

<possibly_relevant_code_snippets>

app/controllers/reports_controller.rb (lines 24-30)

def unchecked
  return redirect_to reports_path unless admin_or_mentor_login?

  @reports = Report.list.page(params[:page]).per(PAGER_NUMBER)
  @reports = @reports.unchecked.not_wip
  render :index
end

app/models/times_channel_destroyer.rb (lines 3-16)

class TimesChannelDestroyer
  def call(_name, _started, _finished, _id, payload)
    user = payload[:user]
    return unless user.discord_profile.times_id

    if Discord::Server.delete_text_channel(user.discord_profile.times_id)
      user.discord_profile.times_id = nil
      user.discord_profile.times_url = nil
      user.discord_profile.save!(context: :retirement)
    else
      Rails.logger.warn "[Discord API] #{user.login_name}の分報チャンネルが削除できませんでした。"
    end
  end
end

🧠 Learnings used
Learnt from: matuaya
PR: fjordllc/bootcamp#9108
File: app/models/after_user_retirement.rb:0-0
Timestamp: 2025-09-01T22:31:57.345Z
Learning: fjordllc/bootcampプロジェクトにおいて、`Newspaper.publish(:retirement_create, { user: user })`はUnfinishedDataDestroyer(未チェックProduct・WIP Report削除、career_pathリセット)とTimesChannelDestroyer(Discord分報チャンネル削除)による重要なデータクリーンアップ処理を実行するため、退会のトリガーが何であろうと(admin、hibernation、user)必ず実行する必要がある。

Learnt from: matuaya
PR: fjordllc/bootcamp#9108
File: app/models/after_user_retirement.rb:0-0
Timestamp: 2025-09-01T22:31:57.345Z
Learning: fjordllc/bootcampプロジェクトにおいて、`Newspaper.publish(:retirement_create, { user: user })`は通知処理ではなく、UnfinishedDataDestroyerやTimesChannelDestroyer等による重要なデータクリーンアップ処理を実行するため、管理者による退会(triggered_by: 'admin')の場合でも必ず実行する必要がある。

Learnt from: jun-kondo
PR: fjordllc/bootcamp#0
File: :0-0
Timestamp: 2025-07-23T21:11:21.826Z
Learning: fjordllc/bootcampプロジェクトの`app/models/work_notification_destroyer.rb`において、このクラスは`app/controllers/works_controller.rb`の`destroy`アクションから呼び出され、`before_action :set_my_work`でIDの妥当性チェックが事前に行われるため、payloadのworkオブジェクトは常に有効であることが保証されている。そのためエラーハンドリングは不要。

Learnt from: reckyy
PR: fjordllc/bootcamp#8356
File: app/models/notification.rb:45-47
Timestamp: 2025-07-15T12:58:02.735Z
Learning: fjordllc/bootcampプロジェクトの`app/models/notification.rb`において、`training_completed`通知種別は`TARGETS_TO_KINDS`ハッシュに含めない設計になっている。これは`training_completed`が通知一覧のフィルタ対象ではないためで、意図的な設計判断である。

If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

user = payload[:user]
Product.where(user:).unchecked.destroy_all
Report.where(user:).wip.destroy_all
Expand Down
8 changes: 8 additions & 0 deletions config/initializers/active_support_notifications.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,12 @@
question_notifier = QuestionNotifier.new
ActiveSupport::Notifications.subscribe('question.create', question_notifier)
ActiveSupport::Notifications.subscribe('question.update', question_notifier)

unfinished_data_destroyer = UnfinishedDataDestroyer.new
ActiveSupport::Notifications.subscribe('retirement.create', unfinished_data_destroyer)
ActiveSupport::Notifications.subscribe('training_completion.create', unfinished_data_destroyer)

times_channel_destroyer = TimesChannelDestroyer.new
ActiveSupport::Notifications.subscribe('retirement.create', times_channel_destroyer)
ActiveSupport::Notifications.subscribe('training_completion.create', times_channel_destroyer)
end
8 changes: 0 additions & 8 deletions config/initializers/newspaper.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,5 @@
# frozen_string_literal: true

Rails.configuration.after_initialize do
unfinished_data_destroyer = UnfinishedDataDestroyer.new
Newspaper.subscribe(:retirement_create, unfinished_data_destroyer)
Newspaper.subscribe(:training_completion_create, unfinished_data_destroyer)

times_channel_destroyer = TimesChannelDestroyer.new
Newspaper.subscribe(:retirement_create, times_channel_destroyer)
Newspaper.subscribe(:training_completion_create, times_channel_destroyer)

Newspaper.subscribe(:came_comment_in_talk, CommentNotifierForAdmin.new)
end
2 changes: 1 addition & 1 deletion test/models/retirement_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class RetirementTest < ActiveSupport::TestCase
remove_as_event_organizer
clear_github_info
destroy_cards
publish_to_newspaper
publish
notify
]

Expand Down
4 changes: 2 additions & 2 deletions test/models/times_channel_destroyer_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class TimesChannelDestroyerTest < ActiveSupport::TestCase

Rails.logger.stub(:warn, ->(message) { logs << message }) do
Discord::Server.stub(:delete_text_channel, true) do
TimesChannelDestroyer.new.call({ user: })
TimesChannelDestroyer.new.call(nil, nil, nil, nil, { user: })
end
assert_nil user.discord_profile.times_id
assert_nil user.discord_profile.times_url
Expand All @@ -26,7 +26,7 @@ class TimesChannelDestroyerTest < ActiveSupport::TestCase
user.discord_profile.update!(times_id: '987654321987654321')
Rails.logger.stub(:warn, ->(message) { logs << message }) do
Discord::Server.stub(:delete_text_channel, nil) do
TimesChannelDestroyer.new.call({ user: })
TimesChannelDestroyer.new.call(nil, nil, nil, nil, { user: })
end
assert_equal '987654321987654321', user.discord_profile.times_id
assert_equal "[Discord API] #{user.login_name}の分報チャンネルが削除できませんでした。", logs.last
Expand Down
60 changes: 60 additions & 0 deletions test/models/unfinished_data_destroyer_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# frozen_string_literal: true

require 'test_helper'

class UnfinishedDataDestroyerTest < ActiveSupport::TestCase
setup do
@payload = { user: users(:kimura) }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setupがシンプルで、@payloadが外部から渡ってくるデータというのがわかりやすくて良いなと思いました!🙌

end

test '#call deletes user wip reports and unchecked products and resets career_path' do
user = @payload[:user]
user.update!(career_path: 1)

3.times do |i|
Report.create!(
user:,
title: "wipの日報#{i}",
description: 'テスト日報',
wip: true,
reported_on: Date.current - i
)
end

practices = Practice.where.not(id: user.products.pluck(:practice_id)).take(3)
practices.each do |practice|
Product.create!(
user:,
practice:,
body: '提出物',
wip: false
)
end
Comment on lines +24 to +32
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where.not(...).take(3) で既存データとの重複を避けつつテストデータを生成しているのがとても綺麗&わかりやすくてすごく勉強になりました🙌
今後必要なデータを取得する際に参考にさせていただきます!!


UnfinishedDataDestroyer.new.call(nil, nil, nil, nil, @payload)

assert_equal 0, Product.unchecked.where(user:).count
assert_equal 0, Report.wip.where(user:).count
assert_equal 'unset', user.reload.career_path
end
Comment on lines +6 to +39
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UnfinishedDataDestroyerの主要な責務は次の3点という認識なので、
#callのテストで全件(複数件)削除も含めて網羅されていると、テストを読んだときに処理内容をより直感的に理解できそうだなと思いました 💡

  1. ユーザーに紐づくWIPの日報を全削除する
  2. ユーザーに紐づく未チェックの提出物を全削除する
  3. ステータス(career_path)の更新

現在、exists?で特定の1件分の日報や提出物の存在チェックをしてくださっていると思うのですが、
#callのテストとしては全件削除できていることを確認できた方が上記の動作を明確に担保できるかなと思いました!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#callのテストで、WIP日報と未チェック提出物の全件削除を確認するように変更しました!
また、uncheckwipスコープを使用し、assertexist?を確認するのではなくcountを確認することで全件削除が明確に分かるようにしました🙏


test 'does not delete other users wip report and unchecked product' do
other_user_unchecked_product = products(:product1)
other_user_wip_report = reports(:report9)

UnfinishedDataDestroyer.new.call(nil, nil, nil, nil, @payload)

assert Product.exists?(other_user_unchecked_product.id)
assert Report.exists?(other_user_wip_report.id)
end

test 'does not delete checked product or non-wip report' do
checked_product = products(:product2)
non_wip_report = reports(:report24)

UnfinishedDataDestroyer.new.call(nil, nil, nil, nil, @payload)

assert Product.exists?(checked_product.id)
assert Report.exists?(non_wip_report.id)
end
end