From cdcd574d02abf3a8e075928e818b9f060995aadf Mon Sep 17 00:00:00 2001 From: Dylan Knutson Date: Sat, 16 Aug 2025 21:27:55 +0000 Subject: [PATCH] monitor bsky user button --- app/controllers/domain/users_controller.rb | 20 +++- .../domain/user/bluesky_user_policy.rb | 9 ++ .../users/bsky/_overview_details.html.erb | 18 +++ app/views/layouts/application.html.erb | 10 +- app/views/shared/_flash_message.html.erb | 10 ++ config/routes.rb | 1 + .../domain/users_controller_spec.rb | 108 ++++++++++++++++++ 7 files changed, 170 insertions(+), 6 deletions(-) create mode 100644 app/views/shared/_flash_message.html.erb diff --git a/app/controllers/domain/users_controller.rb b/app/controllers/domain/users_controller.rb index 97330236..4e4b0c11 100644 --- a/app/controllers/domain/users_controller.rb +++ b/app/controllers/domain/users_controller.rb @@ -3,7 +3,8 @@ class Domain::UsersController < DomainController extend T::Sig extend T::Helpers - before_action :set_user!, only: %i[show followed_by following] + before_action :set_user!, + only: %i[show followed_by following monitor_bluesky_user] before_action :set_post!, only: %i[users_faving_post] skip_before_action :authenticate_user!, only: %i[ @@ -167,6 +168,23 @@ class Domain::UsersController < DomainController } end + sig { void } + def monitor_bluesky_user + user = T.cast(@user, Domain::User::BlueskyUser) + authorize user + monitor = Domain::Bluesky::MonitoredObject.build_for_user(user) + if monitor.save + Domain::Bluesky::Job::ScanUserJob.perform_later(user:) + Domain::Bluesky::Job::ScanPostsJob.perform_later(user:) + flash[:notice] = "User is now being monitored" + else + flash[ + :alert + ] = "Error monitoring user: #{monitor.errors.full_messages.join(", ")}" + end + redirect_to domain_user_path(user) + end + private sig { override.returns(DomainController::DomainParamConfig) } diff --git a/app/policies/domain/user/bluesky_user_policy.rb b/app/policies/domain/user/bluesky_user_policy.rb index 8b37293a..f6516a3f 100644 --- a/app/policies/domain/user/bluesky_user_policy.rb +++ b/app/policies/domain/user/bluesky_user_policy.rb @@ -1,3 +1,12 @@ # typed: strict class Domain::User::BlueskyUserPolicy < Domain::UserPolicy + sig { returns(T::Boolean) } + def view_is_bluesky_user_monitored? + is_role_admin? || is_role_moderator? + end + + sig { returns(T::Boolean) } + def monitor_bluesky_user? + is_role_admin? + end end diff --git a/app/views/domain/users/bsky/_overview_details.html.erb b/app/views/domain/users/bsky/_overview_details.html.erb index a32ce93a..3e2f2d89 100644 --- a/app/views/domain/users/bsky/_overview_details.html.erb +++ b/app/views/domain/users/bsky/_overview_details.html.erb @@ -3,4 +3,22 @@ State <%= user.account_state_for_view %> + <% if policy(user).view_is_bluesky_user_monitored? %> +
+ Monitored + <% if Domain::Bluesky::MonitoredObject.exists?(kind: :user_did, value: user.did!) %> + Yes + <% else %> + <% if policy(user).monitor_bluesky_user? %> + <%= button_to( + "Monitor", + monitor_bluesky_user_domain_user_path(user), + class: "inline-block text-blue-400 border border-blue-400 hover:bg-blue-600 px-1 hover:text-white rounded-md transition-colors duration-200" + ) %> + <% else %> + No + <% end %> + <% end %> +
+ <% end %> <% end %> diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index ae399bbf..466f3875 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -180,11 +180,11 @@ <% end %>
- <% if notice %> - - <% end %> - <% if alert %> - + <% if notice || alert %> + + <%= render "shared/flash_message", value: notice, class: "bg-green-100 border border-green-400 text-green-700" %> + <%= render "shared/flash_message", value: alert, class: "bg-red-100 border border-red-400 text-red-700" %> + <% end %> <%= yield %>
diff --git a/app/views/shared/_flash_message.html.erb b/app/views/shared/_flash_message.html.erb new file mode 100644 index 00000000..a6d0767a --- /dev/null +++ b/app/views/shared/_flash_message.html.erb @@ -0,0 +1,10 @@ +<% if (value = local_assigns[:value]) && value.present? %> +
+

<%= value %>

+ +
+<% end %> diff --git a/config/routes.rb b/config/routes.rb index 170796f8..f773a4c8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -33,6 +33,7 @@ Rails.application.routes.draw do get "followed_by", on: :member, action: :followed_by get "following", on: :member, action: :following + post "monitor_bluesky_user", on: :member, action: :monitor_bluesky_user resources :job_events, only: [], controller: "domain/user_job_events" do get "tracked_objects", diff --git a/spec/controllers/domain/users_controller_spec.rb b/spec/controllers/domain/users_controller_spec.rb index 8fb3e3ad..825e9db7 100644 --- a/spec/controllers/domain/users_controller_spec.rb +++ b/spec/controllers/domain/users_controller_spec.rb @@ -312,4 +312,112 @@ RSpec.describe Domain::UsersController, type: :controller do end end end + + describe "POST #monitor_bluesky_user" do + let(:bluesky_user) { create(:domain_user_bluesky_user) } + let(:composite_param) { "bsky@#{bluesky_user.handle}" } + + before do + # Sign in admin user for authentication + sign_in admin_user + + # Mock authorization to allow the action + allow(controller).to receive(:authorize).and_return(true) + + # Set @user instance variable as the controller does in find_user + controller.instance_variable_set(:@user, bluesky_user) + end + + context "when monitoring succeeds" do + it "creates a monitored object" do + expect { + post :monitor_bluesky_user, params: { id: composite_param } + }.to change(Domain::Bluesky::MonitoredObject, :count).by(1) + + monitor = Domain::Bluesky::MonitoredObject.last + expect(monitor.value).to eq(bluesky_user.did) + expect(monitor.kind).to eq("user_did") + end + + it "enqueues scan user job" do + post :monitor_bluesky_user, params: { id: composite_param } + + enqueued_jobs = + SpecUtil.enqueued_job_args(Domain::Bluesky::Job::ScanUserJob) + expect(enqueued_jobs).to contain_exactly( + hash_including(user: bluesky_user), + ) + end + + it "enqueues scan posts job" do + post :monitor_bluesky_user, params: { id: composite_param } + + enqueued_jobs = + SpecUtil.enqueued_job_args(Domain::Bluesky::Job::ScanPostsJob) + expect(enqueued_jobs).to contain_exactly( + hash_including(user: bluesky_user), + ) + end + + it "sets success flash message" do + post :monitor_bluesky_user, params: { id: composite_param } + expect(flash[:notice]).to eq("User is now being monitored") + end + + it "redirects to user page" do + post :monitor_bluesky_user, params: { id: composite_param } + expect(response).to redirect_to(domain_user_path(bluesky_user)) + end + + it "authorizes the user" do + expect(controller).to receive(:authorize).with(bluesky_user) + post :monitor_bluesky_user, params: { id: composite_param } + end + end + + context "when monitoring fails due to validation errors" do + before do + # Create existing monitor to trigger uniqueness validation failure + Domain::Bluesky::MonitoredObject.create!( + value: bluesky_user.did, + kind: :user_did, + ) + end + + it "does not create a new monitored object" do + expect { + post :monitor_bluesky_user, params: { id: composite_param } + }.not_to change(Domain::Bluesky::MonitoredObject, :count) + end + + it "fails to save the monitor due to uniqueness" do + monitor = Domain::Bluesky::MonitoredObject.build_for_user(bluesky_user) + expect(monitor.save).to be_falsey + expect(monitor.errors[:value]).to include("has already been taken") + end + + it "does not enqueue any jobs" do + SpecUtil.clear_enqueued_jobs! + post :monitor_bluesky_user, params: { id: composite_param } + expect( + SpecUtil.enqueued_job_args(Domain::Bluesky::Job::ScanUserJob), + ).to be_empty + expect( + SpecUtil.enqueued_job_args(Domain::Bluesky::Job::ScanPostsJob), + ).to be_empty + end + + it "sets error flash message with validation errors" do + post :monitor_bluesky_user, params: { id: composite_param } + expect(flash[:alert]).to match( + /Error monitoring user: .*has already been taken/, + ) + end + + it "redirects to user page" do + post :monitor_bluesky_user, params: { id: composite_param } + expect(response).to redirect_to(domain_user_path(bluesky_user)) + end + end + end end