Files
redux-scraper/spec/controllers/domain/users_controller_spec.rb
2025-08-20 22:10:57 +00:00

430 lines
14 KiB
Ruby

# typed: false
require "rails_helper"
RSpec.describe Domain::UsersController, type: :controller do
render_views
# Create a real user with admin role for tests that require authentication
let(:admin_user) { create(:user, :admin) }
# Shared examples for common show behavior across all user types
shared_examples "show action for user type" do |user_factory, param_prefix, param_attr|
let(:domain_user) { create(user_factory) }
let(:composite_param) { "#{param_prefix}@#{domain_user.send(param_attr)}" }
context "when user exists" do
it "returns a successful response" do
get :show, params: { id: composite_param }
expect(response).to be_successful
end
it "renders the show template" do
get :show, params: { id: composite_param }
expect(response).to render_template(:show)
end
it "sets the @user instance variable" do
get :show, params: { id: composite_param }
expect(assigns(:user)).to eq(domain_user)
end
it "authorizes the user" do
expect(controller).to receive(:authorize).with(domain_user)
get :show, params: { id: composite_param }
end
it "does not require authentication" do
# Ensure no user is signed in
sign_out :user
get :show, params: { id: composite_param }
expect(response).to be_successful
end
end
context "when user does not exist" do
let(:invalid_param) { "#{param_prefix}@nonexistent" }
it "raises ActiveRecord::RecordNotFound" do
expect { get :show, params: { id: invalid_param } }.to raise_error(
ActiveRecord::RecordNotFound,
)
end
end
context "with invalid composite parameter format" do
it "raises ActionController::BadRequest for malformed param" do
expect { get :show, params: { id: "invalid_format" } }.to raise_error(
ActionController::BadRequest,
/invalid id/,
)
end
it "raises ActionController::BadRequest for unknown model type" do
expect { get :show, params: { id: "unknown@test" } }.to raise_error(
ActionController::BadRequest,
/unknown model type/,
)
end
end
end
describe "GET #show" do
before do
# Mock authorization to allow all actions for these tests
allow(controller).to receive(:authorize).and_return(true)
end
context "for Domain::User::FaUser" do
include_examples "show action for user type",
:domain_user_fa_user,
"fa",
:url_name
end
context "for Domain::User::E621User" do
include_examples "show action for user type",
:domain_user_e621_user,
"e621",
:name
end
context "for Domain::User::InkbunnyUser" do
include_examples "show action for user type",
:domain_user_inkbunny_user,
"ib",
:name
end
context "param configuration" do
it "uses the correct param config for user_id_param" do
param_config = described_class.param_config
expect(param_config.user_id_param).to eq(:id)
end
end
end
describe "GET #users_faving_post" do
before do
# Mock authorization to allow all actions for these tests
allow(controller).to receive(:authorize).and_return(true)
end
# Shared examples for common users_faving_post behavior across all post types
shared_examples "users_faving_post action for post type" do |post_factory, param_prefix, param_attr|
let(:domain_post) { create(post_factory) }
let(:composite_param) do
"#{param_prefix}@#{domain_post.send(param_attr)}"
end
context "when post exists" do
context "with no faving users" do
it "returns a successful response" do
get :users_faving_post, params: { domain_post_id: composite_param }
expect(response).to be_successful
end
it "renders the index template" do
get :users_faving_post, params: { domain_post_id: composite_param }
expect(response).to render_template(:index)
end
it "sets the @post instance variable" do
get :users_faving_post, params: { domain_post_id: composite_param }
expect(assigns(:post)).to eq(domain_post)
end
it "sets the @index_type to users_faving_post" do
get :users_faving_post, params: { domain_post_id: composite_param }
expect(assigns(:index_type)).to eq(:users_faving_post)
end
it "sets empty @users collection" do
get :users_faving_post, params: { domain_post_id: composite_param }
expect(assigns(:users)).to be_empty
end
it "authorizes the post" do
expect(controller).to receive(:authorize).with(domain_post)
get :users_faving_post, params: { domain_post_id: composite_param }
end
end
context "with faving users" do
before { setup_faving_users }
it "returns users who have faved the post" do
get :users_faving_post, params: { domain_post_id: composite_param }
expect(assigns(:users)).to match_array(expected_faving_users)
end
it "includes avatar associations" do
get :users_faving_post, params: { domain_post_id: composite_param }
# Test that the query includes avatar associations (won't cause N+1)
expect(assigns(:users).first.association(:avatar)).to be_loaded
end
end
end
context "when post does not exist" do
let(:invalid_param) { "#{param_prefix}@999999" }
it "raises ActiveRecord::RecordNotFound" do
expect {
get :users_faving_post, params: { domain_post_id: invalid_param }
}.to raise_error(ActiveRecord::RecordNotFound)
end
end
context "with invalid composite parameter format" do
it "raises ActionController::BadRequest for malformed param" do
expect {
get :users_faving_post, params: { domain_post_id: "invalid_format" }
}.to raise_error(ActionController::BadRequest, /invalid id/)
end
it "raises ActionController::BadRequest for unknown model type" do
expect {
get :users_faving_post, params: { domain_post_id: "unknown@test" }
}.to raise_error(ActionController::BadRequest, /unknown model type/)
end
end
end
context "for Domain::Post::FaPost" do
let(:faving_user1) { create(:domain_user_fa_user) }
let(:faving_user2) { create(:domain_user_fa_user) }
let(:expected_faving_users) { [faving_user1, faving_user2] }
def setup_faving_users
# Create FA-specific user-post-fav relationships
Domain::UserPostFav::FaUserPostFav.create!(
user: faving_user1,
post: domain_post,
)
Domain::UserPostFav::FaUserPostFav.create!(
user: faving_user2,
post: domain_post,
)
end
include_examples "users_faving_post action for post type",
:domain_post_fa_post,
"fa",
:fa_id
end
context "for Domain::Post::E621Post" do
let(:faving_user1) { create(:domain_user_e621_user) }
let(:faving_user2) { create(:domain_user_e621_user) }
let(:expected_faving_users) { [faving_user1, faving_user2] }
def setup_faving_users
# Create E621-specific user-post-fav relationships
Domain::UserPostFav::E621UserPostFav.create!(
user: faving_user1,
post: domain_post,
)
Domain::UserPostFav::E621UserPostFav.create!(
user: faving_user2,
post: domain_post,
)
end
include_examples "users_faving_post action for post type",
:domain_post_e621_post,
"e621",
:e621_id
end
context "for Domain::Post::InkbunnyPost" do
let(:faving_user1) { create(:domain_user_inkbunny_user) }
let(:faving_user2) { create(:domain_user_inkbunny_user) }
let(:expected_faving_users) { [faving_user1, faving_user2] }
def setup_faving_users
# Create Inkbunny-specific user-post-fav relationships
Domain::UserPostFav.create!(user: faving_user1, post: domain_post)
Domain::UserPostFav.create!(user: faving_user2, post: domain_post)
end
include_examples "users_faving_post action for post type",
:domain_post_inkbunny_post,
"ib",
:ib_id
end
context "for Domain::Post::BlueskyPost" do
let(:faving_user_with_display_name) do
create(:domain_user_bluesky_user, display_name: "Cool Artist")
end
let(:faving_user_without_display_name) do
create(:domain_user_bluesky_user, display_name: nil)
end
let(:expected_faving_users) do
[faving_user_with_display_name, faving_user_without_display_name]
end
def setup_faving_users
# Create Bluesky-specific user-post-fav relationships
Domain::UserPostFav.create!(
user: faving_user_with_display_name,
post: domain_post,
)
Domain::UserPostFav.create!(
user: faving_user_without_display_name,
post: domain_post,
)
end
include_examples "users_faving_post action for post type",
:domain_post_bluesky_post,
"bsky",
:rkey
context "with display name variations" do
before do
# Create Bluesky-specific user-post-fav relationships
Domain::UserPostFav.create!(
user: faving_user_with_display_name,
post: domain_post,
)
Domain::UserPostFav.create!(
user: faving_user_without_display_name,
post: domain_post,
)
end
it "includes users with display names" do
get :users_faving_post,
params: {
domain_post_id: "bsky@#{domain_post.rkey}",
}
users = assigns(:users)
user_with_display = users.find { |u| u.display_name.present? }
expect(user_with_display.display_name).to eq("Cool Artist")
end
it "includes users without display names" do
get :users_faving_post,
params: {
domain_post_id: "bsky@#{domain_post.rkey}",
}
users = assigns(:users)
user_without_display = users.find { |u| u.display_name.blank? }
expect(user_without_display.display_name).to be_blank
expect(user_without_display.handle).to be_present
end
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